Compare commits
	
		
			13 Commits
		
	
	
		
			static-can
			...
			add-loadin
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | d3bc3a796b | ||
|   | 9e64af4793 | ||
|   | b581f539e2 | ||
|   | aac1295c10 | ||
|   | e8f2802ee0 | ||
|   | 75b3730a70 | ||
|   | af29dc9c3f | ||
|   | 181cb5f335 | ||
|   | 94e560c262 | ||
|   | f802061722 | ||
|   | 58a7f6eec6 | ||
|   | 5e11d3cc4d | ||
|   | 399fb8312a | 
| @@ -33,7 +33,32 @@ services: | |||||||
|       - '6379:6379' |       - '6379:6379' | ||||||
|     expose: |     expose: | ||||||
|       - 6379 |       - 6379 | ||||||
|  |   keycloak: | ||||||
|  |     image: quay.io/keycloak/keycloak:21.1 | ||||||
|  |     restart: always | ||||||
|  |     container_name: keycloak | ||||||
|  |     environment: | ||||||
|  |       - KEYCLOAK_ADMIN=admin | ||||||
|  |       - KEYCLOAK_ADMIN_PASSWORD=admin | ||||||
|  |       - KC_DB=postgres | ||||||
|  |       - KC_DB_URL_HOST=postgres | ||||||
|  |       - KC_DB_URL_DATABASE=keycloak | ||||||
|  |       - KC_DB_USERNAME=automatisch_user | ||||||
|  |       - KC_DB_PASSWORD=automatisch_password | ||||||
|  |       - KC_HEALTH_ENABLED=true | ||||||
|  |     ports: | ||||||
|  |       - "8080:8080" | ||||||
|  |     command: start-dev | ||||||
|  |     depends_on: | ||||||
|  |       - postgres | ||||||
|  |     healthcheck: | ||||||
|  |       test: "curl -f http://localhost:8080/health/ready || exit 1" | ||||||
|  |     volumes: | ||||||
|  |       - keycloak:/opt/keycloak/data/ | ||||||
|  |     expose: | ||||||
|  |       - 8080 | ||||||
|  |  | ||||||
| volumes: | volumes: | ||||||
|   postgres_data: |   postgres_data: | ||||||
|   redis_data: |   redis_data: | ||||||
|  |   keycloak: | ||||||
|   | |||||||
| @@ -2,18 +2,55 @@ import appConfig from '../../src/config/app'; | |||||||
| import logger from '../../src/helpers/logger'; | import logger from '../../src/helpers/logger'; | ||||||
| import client from './client'; | import client from './client'; | ||||||
| import User from '../../src/models/user'; | import User from '../../src/models/user'; | ||||||
|  | import Role from '../../src/models/role'; | ||||||
|  | import Permission from '../../src/models/permission'; | ||||||
| import '../../src/config/orm'; | import '../../src/config/orm'; | ||||||
|  |  | ||||||
|  | async function seedPermissionsIfNeeded() { | ||||||
|  |   const existingPermissions = await Permission.query().limit(1).first(); | ||||||
|  |  | ||||||
|  |   if (!existingPermissions) return; | ||||||
|  |  | ||||||
|  |   const getPermission = (subject: string, actions: string[]) => actions.map(action => ({ subject, action })); | ||||||
|  |  | ||||||
|  |   await Permission.query().insert([ | ||||||
|  |     ...getPermission('Connection', ['create', 'read', 'delete', 'update']), | ||||||
|  |     ...getPermission('Execution', ['read']), | ||||||
|  |     ...getPermission('Flow', ['create', 'delete', 'publish', 'read', 'update']), | ||||||
|  |     ...getPermission('Role', ['create', 'delete', 'read', 'update']), | ||||||
|  |     ...getPermission('User', ['create', 'delete', 'read', 'update']), | ||||||
|  |   ]) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function createOrFetchRole() { | ||||||
|  |   const role = await Role.query().limit(1).first(); | ||||||
|  |  | ||||||
|  |   if (!role) { | ||||||
|  |     const createdRole = await Role.query().insertAndFetch({ | ||||||
|  |       name: 'Admin', | ||||||
|  |       key: 'admin', | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return createdRole; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return role; | ||||||
|  | } | ||||||
|  |  | ||||||
| export async function createUser( | export async function createUser( | ||||||
|   email = 'user@automatisch.io', |   email = 'user@automatisch.io', | ||||||
|   password = 'sample' |   password = 'sample' | ||||||
| ) { | ) { | ||||||
|   const UNIQUE_VIOLATION_CODE = '23505'; |   const UNIQUE_VIOLATION_CODE = '23505'; | ||||||
|  |  | ||||||
|  |   await seedPermissionsIfNeeded(); | ||||||
|  |  | ||||||
|  |   const role = await createOrFetchRole(); | ||||||
|   const userParams = { |   const userParams = { | ||||||
|     email, |     email, | ||||||
|     password, |     password, | ||||||
|     fullName: 'Initial admin', |     fullName: 'Initial admin', | ||||||
|     role: 'admin', |     roleId: role.id, | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   try { |   try { | ||||||
|   | |||||||
| @@ -12,6 +12,7 @@ const knexConfig = { | |||||||
|     database: appConfig.postgresDatabase, |     database: appConfig.postgresDatabase, | ||||||
|     ssl: appConfig.postgresEnableSsl, |     ssl: appConfig.postgresEnableSsl, | ||||||
|   }, |   }, | ||||||
|  |   asyncStackTraces: appConfig.isDev, | ||||||
|   searchPath: [appConfig.postgresSchema], |   searchPath: [appConfig.postgresSchema], | ||||||
|   pool: { min: 0, max: 20 }, |   pool: { min: 0, max: 20 }, | ||||||
|   migrations: { |   migrations: { | ||||||
|   | |||||||
| @@ -4,7 +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.", | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "dev": "ts-node-dev --exit-child src/server.ts", |     "dev": "ts-node-dev --watch 'src/graphql/schema.graphql' --exit-child src/server.ts", | ||||||
|     "worker": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/worker.ts", |     "worker": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/worker.ts", | ||||||
|     "build": "tsc && yarn copy-statics", |     "build": "tsc && yarn copy-statics", | ||||||
|     "build:watch": "nodemon --watch 'src/**/*.ts' --watch 'bin/**/*.ts' --exec yarn build --ext ts", |     "build:watch": "nodemon --watch 'src/**/*.ts' --watch 'bin/**/*.ts' --exec yarn build --ext ts", | ||||||
| @@ -24,12 +24,15 @@ | |||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@automatisch/web": "^0.7.1", |     "@automatisch/web": "^0.7.1", | ||||||
|     "@bull-board/express": "^3.10.1", |     "@bull-board/express": "^3.10.1", | ||||||
|  |     "@casl/ability": "^6.5.0", | ||||||
|     "@graphql-tools/graphql-file-loader": "^7.3.4", |     "@graphql-tools/graphql-file-loader": "^7.3.4", | ||||||
|     "@graphql-tools/load": "^7.5.2", |     "@graphql-tools/load": "^7.5.2", | ||||||
|  |     "@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", | ||||||
|     "@sentry/tracing": "^7.42.0", |     "@sentry/tracing": "^7.42.0", | ||||||
|     "@types/luxon": "^2.3.1", |     "@types/luxon": "^2.3.1", | ||||||
|  |     "@types/passport": "^1.0.12", | ||||||
|     "@types/xmlrpc": "^1.3.7", |     "@types/xmlrpc": "^1.3.7", | ||||||
|     "ajv-formats": "^2.1.1", |     "ajv-formats": "^2.1.1", | ||||||
|     "axios": "0.24.0", |     "axios": "0.24.0", | ||||||
| @@ -60,6 +63,7 @@ | |||||||
|     "nodemailer": "6.7.0", |     "nodemailer": "6.7.0", | ||||||
|     "oauth-1.0a": "^2.2.6", |     "oauth-1.0a": "^2.2.6", | ||||||
|     "objection": "^3.0.0", |     "objection": "^3.0.0", | ||||||
|  |     "passport": "^0.6.0", | ||||||
|     "pg": "^8.7.1", |     "pg": "^8.7.1", | ||||||
|     "php-serialize": "^4.0.2", |     "php-serialize": "^4.0.2", | ||||||
|     "stripe": "^11.13.0", |     "stripe": "^11.13.0", | ||||||
|   | |||||||
| @@ -17,6 +17,7 @@ import { | |||||||
| } from './helpers/create-bull-board-handler'; | } from './helpers/create-bull-board-handler'; | ||||||
| import injectBullBoardHandler from './helpers/inject-bull-board-handler'; | import injectBullBoardHandler from './helpers/inject-bull-board-handler'; | ||||||
| import router from './routes'; | import router from './routes'; | ||||||
|  | import configurePassport from './helpers/passport'; | ||||||
|  |  | ||||||
| createBullBoardHandler(serverAdapter); | createBullBoardHandler(serverAdapter); | ||||||
|  |  | ||||||
| @@ -50,6 +51,9 @@ app.use( | |||||||
|   }) |   }) | ||||||
| ); | ); | ||||||
| app.use(cors(corsOptions)); | app.use(cors(corsOptions)); | ||||||
|  |  | ||||||
|  | configurePassport(app); | ||||||
|  |  | ||||||
| app.use('/', router); | app.use('/', router); | ||||||
|  |  | ||||||
| webUIHandler(app); | webUIHandler(app); | ||||||
|   | |||||||
| @@ -0,0 +1,32 @@ | |||||||
|  | import { Knex } from 'knex'; | ||||||
|  | import capitalize from 'lodash/capitalize'; | ||||||
|  | import lowerCase from 'lodash/lowerCase'; | ||||||
|  |  | ||||||
|  | export async function up(knex: Knex): Promise<void> { | ||||||
|  |   await knex.schema.createTable('roles', (table) => { | ||||||
|  |     table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()')); | ||||||
|  |     table.string('name').notNullable(); | ||||||
|  |     table.string('key').notNullable(); | ||||||
|  |     table.string('description'); | ||||||
|  |  | ||||||
|  |     table.timestamps(true, true); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   const uniqueUserRoles = await knex('users') | ||||||
|  |     .select('role') | ||||||
|  |     .groupBy('role'); | ||||||
|  |  | ||||||
|  |   for (const { role } of uniqueUserRoles) { | ||||||
|  |     // skip empty roles | ||||||
|  |     if (!role) continue; | ||||||
|  |  | ||||||
|  |     await knex('roles').insert({ | ||||||
|  |       name: capitalize(role), | ||||||
|  |       key: lowerCase(role), | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export async function down(knex: Knex): Promise<void> { | ||||||
|  |   return knex.schema.dropTable('roles'); | ||||||
|  | } | ||||||
| @@ -0,0 +1,25 @@ | |||||||
|  | import { Knex } from 'knex'; | ||||||
|  |  | ||||||
|  | const getPermission = (subject: string, actions: string[]) => actions.map(action => ({ subject, action })); | ||||||
|  |  | ||||||
|  | export async function up(knex: Knex): Promise<void> { | ||||||
|  |   await knex.schema.createTable('permissions', (table) => { | ||||||
|  |     table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()')); | ||||||
|  |     table.string('action').notNullable(); | ||||||
|  |     table.string('subject').notNullable(); | ||||||
|  |  | ||||||
|  |     table.timestamps(true, true); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   await knex('permissions').insert([ | ||||||
|  |     ...getPermission('Connection', ['create', 'read', 'delete', 'update']), | ||||||
|  |     ...getPermission('Execution', ['read']), | ||||||
|  |     ...getPermission('Flow', ['create', 'delete', 'publish', 'read', 'update']), | ||||||
|  |     ...getPermission('Role', ['create', 'delete', 'read', 'update']), | ||||||
|  |     ...getPermission('User', ['create', 'delete', 'read', 'update']), | ||||||
|  |   ]); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export async function down(knex: Knex): Promise<void> { | ||||||
|  |   return knex.schema.dropTable('permissions'); | ||||||
|  | } | ||||||
| @@ -0,0 +1,25 @@ | |||||||
|  | import { Knex } from 'knex'; | ||||||
|  |  | ||||||
|  | export async function up(knex: Knex): Promise<void> { | ||||||
|  |   await knex.schema.createTable('roles_permissions', (table) => { | ||||||
|  |     table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()')); | ||||||
|  |     table.uuid('role_id').references('id').inTable('roles'); | ||||||
|  |     table.uuid('permission_id').references('id').inTable('permissions'); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   const roles = await knex('roles').select('id'); | ||||||
|  |   const permissions = await knex('permissions').select('id'); | ||||||
|  |  | ||||||
|  |   for (const role of roles) { | ||||||
|  |     for (const permission of permissions) { | ||||||
|  |       await knex('roles_permissions').insert({ | ||||||
|  |         role_id: role.id, | ||||||
|  |         permission_id: permission.id, | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export async function down(knex: Knex): Promise<void> { | ||||||
|  |   return knex.schema.dropTable('roles_permissions'); | ||||||
|  | } | ||||||
| @@ -0,0 +1,29 @@ | |||||||
|  | import { Knex } from 'knex'; | ||||||
|  |  | ||||||
|  | export async function up(knex: Knex): Promise<void> { | ||||||
|  |   await knex.schema.table('users', async (table) => { | ||||||
|  |     table.uuid('role_id').references('id').inTable('roles'); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   const theRole = await knex('roles').select('id').limit(1).first(); | ||||||
|  |   const roles = await knex('roles').select('id', 'key'); | ||||||
|  |  | ||||||
|  |   for (const role of roles) { | ||||||
|  |     await knex('users') | ||||||
|  |       .where({ | ||||||
|  |         role: role.key | ||||||
|  |       }) | ||||||
|  |       .update({ | ||||||
|  |         role_id: role.id | ||||||
|  |       }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // backfill not-migratables | ||||||
|  |   await knex('users').whereNull('role_id').update({ role_id: theRole.id }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export async function down(knex: Knex): Promise<void> { | ||||||
|  |   return await knex.schema.table('users', (table) => { | ||||||
|  |     table.dropColumn('role_id'); | ||||||
|  |   }); | ||||||
|  | } | ||||||
| @@ -0,0 +1,13 @@ | |||||||
|  | import { Knex } from 'knex'; | ||||||
|  |  | ||||||
|  | export async function up(knex: Knex): Promise<void> { | ||||||
|  |   await knex.schema.table('users', async (table) => { | ||||||
|  |     table.dropColumn('role'); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export async function down(knex: Knex): Promise<void> { | ||||||
|  |   return await knex.schema.table('users', (table) => { | ||||||
|  |     table.string('role').defaultTo('user'); | ||||||
|  |   }); | ||||||
|  | } | ||||||
| @@ -0,0 +1,23 @@ | |||||||
|  | import { Knex } from 'knex'; | ||||||
|  |  | ||||||
|  | export async function up(knex: Knex): Promise<void> { | ||||||
|  |   return knex.schema.createTable('saml_auth_providers', (table) => { | ||||||
|  |     table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()')); | ||||||
|  |     table.string('name').notNullable(); | ||||||
|  |     table.text('certificate').notNullable(); | ||||||
|  |     table.string('signature_algorithm').notNullable(); | ||||||
|  |     table.string('issuer').notNullable(); | ||||||
|  |     table.text('entry_point').notNullable(); | ||||||
|  |     table.text('firstname_attribute_name').notNullable(); | ||||||
|  |     table.text('surname_attribute_name').notNullable(); | ||||||
|  |     table.text('email_attribute_name').notNullable(); | ||||||
|  |     table.text('role_attribute_name').notNullable(); | ||||||
|  |     table.uuid('default_role_id').references('id').inTable('roles'); | ||||||
|  |  | ||||||
|  |     table.timestamps(true, true); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export async function down(knex: Knex): Promise<void> { | ||||||
|  |   return knex.schema.dropTable('saml_auth_providers'); | ||||||
|  | } | ||||||
| @@ -0,0 +1,17 @@ | |||||||
|  | import { Knex } from 'knex'; | ||||||
|  |  | ||||||
|  | export async function up(knex: Knex): Promise<void> { | ||||||
|  |   return knex.schema.createTable('identities', (table) => { | ||||||
|  |     table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()')); | ||||||
|  |     table.uuid('user_id').references('id').inTable('users'); | ||||||
|  |     table.string('remote_id').notNullable(); | ||||||
|  |     table.string('provider_id').notNullable(); | ||||||
|  |     table.string('provider_type').notNullable(); | ||||||
|  |  | ||||||
|  |     table.timestamps(true, true); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export async function down(knex: Knex): Promise<void> { | ||||||
|  |   return knex.schema.dropTable('identities'); | ||||||
|  | } | ||||||
| @@ -0,0 +1,11 @@ | |||||||
|  | import { Knex } from 'knex'; | ||||||
|  |  | ||||||
|  | export async function up(knex: Knex): Promise<void> { | ||||||
|  |   return await knex.schema.alterTable('users', (table) => { | ||||||
|  |     table.string('password').nullable().alter(); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export async function down(): Promise<void> { | ||||||
|  |   // void | ||||||
|  | } | ||||||
| @@ -0,0 +1,13 @@ | |||||||
|  | import { Knex } from 'knex'; | ||||||
|  |  | ||||||
|  | export async function up(knex: Knex): Promise<void> { | ||||||
|  |   return knex.schema.alterTable('permissions', (table) => { | ||||||
|  |     table.jsonb('conditions').notNullable().defaultTo([]); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export async function down(knex: Knex): Promise<void> { | ||||||
|  |   return knex.schema.alterTable('permissions', (table) => { | ||||||
|  |     table.dropColumn('conditions'); | ||||||
|  |   }); | ||||||
|  | } | ||||||
| @@ -1,47 +1,59 @@ | |||||||
| import createConnection from './mutations/create-connection'; | import createConnection from './mutations/create-connection'; | ||||||
| import generateAuthUrl from './mutations/generate-auth-url'; |  | ||||||
| import updateConnection from './mutations/update-connection'; |  | ||||||
| import resetConnection from './mutations/reset-connection'; |  | ||||||
| import verifyConnection from './mutations/verify-connection'; |  | ||||||
| import deleteConnection from './mutations/delete-connection'; |  | ||||||
| import createFlow from './mutations/create-flow'; | import createFlow from './mutations/create-flow'; | ||||||
|  | import createRole from './mutations/create-role.ee'; | ||||||
|  | import createStep from './mutations/create-step'; | ||||||
|  | import createUser from './mutations/create-user.ee'; | ||||||
|  | import deleteConnection from './mutations/delete-connection'; | ||||||
|  | import deleteCurrentUser from './mutations/delete-current-user.ee'; | ||||||
|  | import deleteFlow from './mutations/delete-flow'; | ||||||
|  | import deleteRole from './mutations/delete-role.ee'; | ||||||
|  | import deleteStep from './mutations/delete-step'; | ||||||
|  | import deleteUser from './mutations/delete-user.ee'; | ||||||
|  | import duplicateFlow from './mutations/duplicate-flow'; | ||||||
|  | import executeFlow from './mutations/execute-flow'; | ||||||
|  | import forgotPassword from './mutations/forgot-password.ee'; | ||||||
|  | import generateAuthUrl from './mutations/generate-auth-url'; | ||||||
|  | import login from './mutations/login'; | ||||||
|  | import registerUser from './mutations/register-user.ee'; | ||||||
|  | import resetConnection from './mutations/reset-connection'; | ||||||
|  | import resetPassword from './mutations/reset-password.ee'; | ||||||
|  | import updateConnection from './mutations/update-connection'; | ||||||
|  | import updateCurrentUser from './mutations/update-current-user'; | ||||||
| import updateFlow from './mutations/update-flow'; | import updateFlow from './mutations/update-flow'; | ||||||
| import updateFlowStatus from './mutations/update-flow-status'; | import updateFlowStatus from './mutations/update-flow-status'; | ||||||
| import executeFlow from './mutations/execute-flow'; | import updateRole from './mutations/update-role.ee'; | ||||||
| import deleteFlow from './mutations/delete-flow'; |  | ||||||
| import duplicateFlow from './mutations/duplicate-flow'; |  | ||||||
| import createStep from './mutations/create-step'; |  | ||||||
| import updateStep from './mutations/update-step'; | import updateStep from './mutations/update-step'; | ||||||
| import deleteStep from './mutations/delete-step'; | import updateUser from './mutations/update-user.ee'; | ||||||
| import createUser from './mutations/create-user.ee'; | import verifyConnection from './mutations/verify-connection'; | ||||||
| import deleteUser from './mutations/delete-user.ee'; |  | ||||||
| import updateUser from './mutations/update-user'; |  | ||||||
| import forgotPassword from './mutations/forgot-password.ee'; |  | ||||||
| import resetPassword from './mutations/reset-password.ee'; |  | ||||||
| import login from './mutations/login'; |  | ||||||
|  |  | ||||||
| const mutationResolvers = { | const mutationResolvers = { | ||||||
|   createConnection, |   createConnection, | ||||||
|   generateAuthUrl, |  | ||||||
|   updateConnection, |  | ||||||
|   resetConnection, |  | ||||||
|   verifyConnection, |  | ||||||
|   deleteConnection, |  | ||||||
|   createFlow, |   createFlow, | ||||||
|  |   createRole, | ||||||
|  |   createStep, | ||||||
|  |   createUser, | ||||||
|  |   deleteConnection, | ||||||
|  |   deleteCurrentUser, | ||||||
|  |   deleteFlow, | ||||||
|  |   deleteRole, | ||||||
|  |   deleteStep, | ||||||
|  |   deleteUser, | ||||||
|  |   duplicateFlow, | ||||||
|  |   executeFlow, | ||||||
|  |   forgotPassword, | ||||||
|  |   generateAuthUrl, | ||||||
|  |   login, | ||||||
|  |   registerUser, | ||||||
|  |   resetConnection, | ||||||
|  |   resetPassword, | ||||||
|  |   updateConnection, | ||||||
|  |   updateCurrentUser, | ||||||
|  |   updateUser, | ||||||
|   updateFlow, |   updateFlow, | ||||||
|   updateFlowStatus, |   updateFlowStatus, | ||||||
|   executeFlow, |   updateRole, | ||||||
|   deleteFlow, |  | ||||||
|   duplicateFlow, |  | ||||||
|   createStep, |  | ||||||
|   updateStep, |   updateStep, | ||||||
|   deleteStep, |   verifyConnection, | ||||||
|   createUser, |  | ||||||
|   deleteUser, |  | ||||||
|   updateUser, |  | ||||||
|   forgotPassword, |  | ||||||
|   resetPassword, |  | ||||||
|   login, |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export default mutationResolvers; | export default mutationResolvers; | ||||||
|   | |||||||
| @@ -13,6 +13,8 @@ const createConnection = async ( | |||||||
|   params: Params, |   params: Params, | ||||||
|   context: Context |   context: Context | ||||||
| ) => { | ) => { | ||||||
|  |   context.currentUser.can('create', 'Connection'); | ||||||
|  |  | ||||||
|   await App.findOneByKey(params.input.key); |   await App.findOneByKey(params.input.key); | ||||||
|  |  | ||||||
|   return await context.currentUser.$relatedQuery('connections').insert({ |   return await context.currentUser.$relatedQuery('connections').insert({ | ||||||
|   | |||||||
| @@ -14,6 +14,8 @@ const createFlow = async ( | |||||||
|   params: Params, |   params: Params, | ||||||
|   context: Context |   context: Context | ||||||
| ) => { | ) => { | ||||||
|  |   context.currentUser.can('create', 'Flow'); | ||||||
|  |  | ||||||
|   const connectionId = params?.input?.connectionId; |   const connectionId = params?.input?.connectionId; | ||||||
|   const appKey = params?.input?.triggerAppKey; |   const appKey = params?.input?.triggerAppKey; | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										32
									
								
								packages/backend/src/graphql/mutations/create-role.ee.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								packages/backend/src/graphql/mutations/create-role.ee.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | import kebabCase from 'lodash/kebabCase'; | ||||||
|  | import Role from '../../models/role'; | ||||||
|  | import Permission from '../../models/permission'; | ||||||
|  |  | ||||||
|  | type Params = { | ||||||
|  |   input: { | ||||||
|  |     name: string; | ||||||
|  |     description: string; | ||||||
|  |     permissions: Permission[]; | ||||||
|  |   }; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // TODO: access | ||||||
|  | const createRole = async (_parent: unknown, params: Params) => { | ||||||
|  |   const { name, description, permissions } = params.input; | ||||||
|  |   const key = kebabCase(name); | ||||||
|  |  | ||||||
|  |   const existingRole = await Role.query().findOne({ key }); | ||||||
|  |  | ||||||
|  |   if (existingRole) { | ||||||
|  |     throw new Error('Role already exists!'); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return await Role.query().insertGraph({ | ||||||
|  |     key, | ||||||
|  |     name, | ||||||
|  |     description, | ||||||
|  |     permissions, | ||||||
|  |   }, { relate: ['permissions'] }).returning('*'); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default createRole; | ||||||
| @@ -22,6 +22,8 @@ const createStep = async ( | |||||||
|   params: Params, |   params: Params, | ||||||
|   context: Context |   context: Context | ||||||
| ) => { | ) => { | ||||||
|  |   context.currentUser.can('update', 'Flow'); | ||||||
|  |  | ||||||
|   const { input } = params; |   const { input } = params; | ||||||
|  |  | ||||||
|   if (input.appKey && input.key) { |   if (input.appKey && input.key) { | ||||||
|   | |||||||
| @@ -5,11 +5,13 @@ type Params = { | |||||||
|     fullName: string; |     fullName: string; | ||||||
|     email: string; |     email: string; | ||||||
|     password: string; |     password: string; | ||||||
|  |     roleId: string; | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | // TODO: access | ||||||
| const createUser = async (_parent: unknown, params: Params) => { | const createUser = async (_parent: unknown, params: Params) => { | ||||||
|   const { fullName, email, password } = params.input; |   const { fullName, email, password, roleId } = params.input; | ||||||
|  |  | ||||||
|   const existingUser = await User.query().findOne({ email }); |   const existingUser = await User.query().findOne({ email }); | ||||||
|  |  | ||||||
| @@ -21,7 +23,7 @@ const createUser = async (_parent: unknown, params: Params) => { | |||||||
|     fullName, |     fullName, | ||||||
|     email, |     email, | ||||||
|     password, |     password, | ||||||
|     role: 'user', |     roleId, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   return user; |   return user; | ||||||
|   | |||||||
| @@ -11,6 +11,8 @@ const deleteConnection = async ( | |||||||
|   params: Params, |   params: Params, | ||||||
|   context: Context |   context: Context | ||||||
| ) => { | ) => { | ||||||
|  |   context.currentUser.can('delete', 'Connection'); | ||||||
|  |  | ||||||
|   await context.currentUser |   await context.currentUser | ||||||
|     .$relatedQuery('connections') |     .$relatedQuery('connections') | ||||||
|     .delete() |     .delete() | ||||||
|   | |||||||
| @@ -0,0 +1,23 @@ | |||||||
|  | import { Duration } from 'luxon'; | ||||||
|  | import Context from '../../types/express/context'; | ||||||
|  | import deleteUserQueue from '../../queues/delete-user.ee'; | ||||||
|  |  | ||||||
|  | // TODO: access | ||||||
|  | const deleteUser = async (_parent: unknown, params: never, context: Context) => { | ||||||
|  |   const id = context.currentUser.id; | ||||||
|  |  | ||||||
|  |   await context.currentUser.$query().delete(); | ||||||
|  |  | ||||||
|  |   const jobName = `Delete user - ${id}`; | ||||||
|  |   const jobPayload = { id }; | ||||||
|  |   const millisecondsFor30Days = Duration.fromObject({ days: 30 }).toMillis(); | ||||||
|  |   const jobOptions = { | ||||||
|  |     delay: millisecondsFor30Days | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   await deleteUserQueue.add(jobName, jobPayload, jobOptions); | ||||||
|  |  | ||||||
|  |   return true; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default deleteUser; | ||||||
| @@ -13,6 +13,8 @@ const deleteFlow = async ( | |||||||
|   params: Params, |   params: Params, | ||||||
|   context: Context |   context: Context | ||||||
| ) => { | ) => { | ||||||
|  |   context.currentUser.can('delete', 'Flow'); | ||||||
|  |  | ||||||
|   const flow = await context.currentUser |   const flow = await context.currentUser | ||||||
|     .$relatedQuery('flows') |     .$relatedQuery('flows') | ||||||
|     .findOne({ |     .findOne({ | ||||||
|   | |||||||
							
								
								
									
										31
									
								
								packages/backend/src/graphql/mutations/delete-role.ee.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								packages/backend/src/graphql/mutations/delete-role.ee.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | import Role from '../../models/role'; | ||||||
|  | import Context from '../../types/express/context'; | ||||||
|  |  | ||||||
|  | type Params = { | ||||||
|  |   input: { | ||||||
|  |     id: string; | ||||||
|  |   }; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // TODO: access | ||||||
|  | const deleteRole = async ( | ||||||
|  |   _parent: unknown, | ||||||
|  |   params: Params, | ||||||
|  |   context: Context | ||||||
|  | ) => { | ||||||
|  |   const role = await Role.query().findById(params.input.id).throwIfNotFound(); | ||||||
|  |  | ||||||
|  |   if (role.isAdmin) { | ||||||
|  |     throw new Error('Admin role cannot be deleted!'); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * TODO: consider migrations for users that still have the role or | ||||||
|  |    * do not let the role get deleted if there are still associated users | ||||||
|  |    */ | ||||||
|  |   await role.$query().delete(); | ||||||
|  |  | ||||||
|  |   return true; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default deleteRole; | ||||||
| @@ -11,6 +11,8 @@ const deleteStep = async ( | |||||||
|   params: Params, |   params: Params, | ||||||
|   context: Context |   context: Context | ||||||
| ) => { | ) => { | ||||||
|  |   context.currentUser.can('update', 'Flow'); | ||||||
|  |  | ||||||
|   const step = await context.currentUser |   const step = await context.currentUser | ||||||
|     .$relatedQuery('steps') |     .$relatedQuery('steps') | ||||||
|     .withGraphFetched('flow') |     .withGraphFetched('flow') | ||||||
|   | |||||||
| @@ -1,11 +1,23 @@ | |||||||
| import Context from '../../types/express/context'; |  | ||||||
| import deleteUserQueue from '../../queues/delete-user.ee'; |  | ||||||
| import { Duration } from 'luxon'; | import { Duration } from 'luxon'; | ||||||
|  | import Context from '../../types/express/context'; | ||||||
|  | import User from '../../models/user'; | ||||||
|  | import deleteUserQueue from '../../queues/delete-user.ee'; | ||||||
|  |  | ||||||
| const deleteUser = async (_parent: unknown, params: never, context: Context) => { | type Params = { | ||||||
|   const id = context.currentUser.id; |   input: { | ||||||
|  |     id: string; | ||||||
|  |   }; | ||||||
|  | }; | ||||||
|  |  | ||||||
|   await context.currentUser.$query().delete(); | // TODO: access | ||||||
|  | const deleteUser = async ( | ||||||
|  |   _parent: unknown, | ||||||
|  |   params: Params, | ||||||
|  |   context: Context | ||||||
|  | ) => { | ||||||
|  |   const id = params.input.id; | ||||||
|  |  | ||||||
|  |   await User.query().deleteById(id); | ||||||
|  |  | ||||||
|   const jobName = `Delete user - ${id}`; |   const jobName = `Delete user - ${id}`; | ||||||
|   const jobPayload = { id }; |   const jobPayload = { id }; | ||||||
|   | |||||||
| @@ -53,6 +53,8 @@ const duplicateFlow = async ( | |||||||
|   params: Params, |   params: Params, | ||||||
|   context: Context |   context: Context | ||||||
| ) => { | ) => { | ||||||
|  |   context.currentUser.can('create', 'Flow'); | ||||||
|  |  | ||||||
|   const flow = await context.currentUser |   const flow = await context.currentUser | ||||||
|     .$relatedQuery('flows') |     .$relatedQuery('flows') | ||||||
|     .withGraphJoined('[steps]') |     .withGraphJoined('[steps]') | ||||||
|   | |||||||
| @@ -12,6 +12,8 @@ const executeFlow = async ( | |||||||
|   params: Params, |   params: Params, | ||||||
|   context: Context |   context: Context | ||||||
| ) => { | ) => { | ||||||
|  |   context.currentUser.can('update', 'Flow'); | ||||||
|  |  | ||||||
|   const { stepId } = params.input; |   const { stepId } = params.input; | ||||||
|  |  | ||||||
|   const untilStep = await context.currentUser |   const untilStep = await context.currentUser | ||||||
|   | |||||||
| @@ -13,6 +13,8 @@ const generateAuthUrl = async ( | |||||||
|   params: Params, |   params: Params, | ||||||
|   context: Context |   context: Context | ||||||
| ) => { | ) => { | ||||||
|  |   context.currentUser.can('create', 'Connection'); | ||||||
|  |  | ||||||
|   const connection = await context.currentUser |   const connection = await context.currentUser | ||||||
|     .$relatedQuery('connections') |     .$relatedQuery('connections') | ||||||
|     .findOne({ |     .findOne({ | ||||||
|   | |||||||
							
								
								
									
										33
									
								
								packages/backend/src/graphql/mutations/register-user.ee.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								packages/backend/src/graphql/mutations/register-user.ee.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | |||||||
|  | import User from '../../models/user'; | ||||||
|  | import Role from '../../models/role'; | ||||||
|  |  | ||||||
|  | type Params = { | ||||||
|  |   input: { | ||||||
|  |     fullName: string; | ||||||
|  |     email: string; | ||||||
|  |     password: string; | ||||||
|  |   }; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const registerUser = async (_parent: unknown, params: Params) => { | ||||||
|  |   const { fullName, email, password } = params.input; | ||||||
|  |  | ||||||
|  |   const existingUser = await User.query().findOne({ email }); | ||||||
|  |  | ||||||
|  |   if (existingUser) { | ||||||
|  |     throw new Error('User already exists!'); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const role = await Role.query().findOne({ key: 'user' }); | ||||||
|  |  | ||||||
|  |   const user = await User.query().insert({ | ||||||
|  |     fullName, | ||||||
|  |     email, | ||||||
|  |     password, | ||||||
|  |     roleId: role.id, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   return user; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default registerUser; | ||||||
| @@ -11,6 +11,8 @@ const resetConnection = async ( | |||||||
|   params: Params, |   params: Params, | ||||||
|   context: Context |   context: Context | ||||||
| ) => { | ) => { | ||||||
|  |   context.currentUser.can('create', 'Connection'); | ||||||
|  |  | ||||||
|   let connection = await context.currentUser |   let connection = await context.currentUser | ||||||
|     .$relatedQuery('connections') |     .$relatedQuery('connections') | ||||||
|     .findOne({ |     .findOne({ | ||||||
|   | |||||||
| @@ -13,6 +13,8 @@ const updateConnection = async ( | |||||||
|   params: Params, |   params: Params, | ||||||
|   context: Context |   context: Context | ||||||
| ) => { | ) => { | ||||||
|  |   context.currentUser.can('create', 'Connection'); | ||||||
|  |  | ||||||
|   let connection = await context.currentUser |   let connection = await context.currentUser | ||||||
|     .$relatedQuery('connections') |     .$relatedQuery('connections') | ||||||
|     .findOne({ |     .findOne({ | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ type Params = { | |||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const updateUser = async ( | const updateCurrentUser = async ( | ||||||
|   _parent: unknown, |   _parent: unknown, | ||||||
|   params: Params, |   params: Params, | ||||||
|   context: Context |   context: Context | ||||||
| @@ -22,4 +22,4 @@ const updateUser = async ( | |||||||
|   return user; |   return user; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export default updateUser; | export default updateCurrentUser; | ||||||
| @@ -18,6 +18,8 @@ const updateFlowStatus = async ( | |||||||
|   params: Params, |   params: Params, | ||||||
|   context: Context |   context: Context | ||||||
| ) => { | ) => { | ||||||
|  |   context.currentUser.can('publish', 'Flow'); | ||||||
|  |  | ||||||
|   let flow = await context.currentUser |   let flow = await context.currentUser | ||||||
|     .$relatedQuery('flows') |     .$relatedQuery('flows') | ||||||
|     .findOne({ |     .findOne({ | ||||||
| @@ -55,7 +57,7 @@ const updateFlowStatus = async ( | |||||||
|   } else { |   } else { | ||||||
|     if (newActiveValue) { |     if (newActiveValue) { | ||||||
|       flow = await flow.$query().patchAndFetch({ |       flow = await flow.$query().patchAndFetch({ | ||||||
|         published_at: new Date().toISOString(), |         publishedAt: new Date().toISOString(), | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|       const jobName = `${JOB_NAME}-${flow.id}`; |       const jobName = `${JOB_NAME}-${flow.id}`; | ||||||
| @@ -78,9 +80,12 @@ const updateFlowStatus = async ( | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   flow = await flow.$query().withGraphFetched('steps').patchAndFetch({ |   flow = await flow | ||||||
|     active: newActiveValue, |     .$query() | ||||||
|   }); |     .withGraphFetched('steps') | ||||||
|  |     .patchAndFetch({ | ||||||
|  |       active: newActiveValue, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|   return flow; |   return flow; | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -12,6 +12,8 @@ const updateFlow = async ( | |||||||
|   params: Params, |   params: Params, | ||||||
|   context: Context |   context: Context | ||||||
| ) => { | ) => { | ||||||
|  |   context.currentUser.can('update', 'Flow'); | ||||||
|  |  | ||||||
|   let flow = await context.currentUser |   let flow = await context.currentUser | ||||||
|     .$relatedQuery('flows') |     .$relatedQuery('flows') | ||||||
|     .findOne({ |     .findOne({ | ||||||
|   | |||||||
							
								
								
									
										44
									
								
								packages/backend/src/graphql/mutations/update-role.ee.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								packages/backend/src/graphql/mutations/update-role.ee.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | |||||||
|  | import Context from '../../types/express/context'; | ||||||
|  | import Role from '../../models/role'; | ||||||
|  | import Permission from '../../models/permission'; | ||||||
|  |  | ||||||
|  | type Params = { | ||||||
|  |   input: { | ||||||
|  |     id: string; | ||||||
|  |     name: string; | ||||||
|  |     description: string; | ||||||
|  |     permissions: Permission[]; | ||||||
|  |   }; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // TODO: access | ||||||
|  | const updateUser = async ( | ||||||
|  |   _parent: unknown, | ||||||
|  |   params: Params, | ||||||
|  |   context: Context | ||||||
|  | ) => { | ||||||
|  |   const { | ||||||
|  |     id, | ||||||
|  |     name, | ||||||
|  |     description, | ||||||
|  |     permissions, | ||||||
|  |   } = params.input; | ||||||
|  |  | ||||||
|  |   const role = await Role.query().findById(id).throwIfNotFound(); | ||||||
|  |  | ||||||
|  |   // TODO: delete the unrelated items! | ||||||
|  |   await role.$relatedQuery('permissions').unrelate(); | ||||||
|  |  | ||||||
|  |   // TODO: possibly assert that given permissions do actually exist in catalog | ||||||
|  |   // TODO: possibly optimize it with patching the different permissions compared to current ones | ||||||
|  |   return await role.$query() | ||||||
|  |     .patchAndFetch( | ||||||
|  |       { | ||||||
|  |         name, | ||||||
|  |         description, | ||||||
|  |         permissions, | ||||||
|  |       } | ||||||
|  |     ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default updateUser; | ||||||
| @@ -23,6 +23,8 @@ const updateStep = async ( | |||||||
|   params: Params, |   params: Params, | ||||||
|   context: Context |   context: Context | ||||||
| ) => { | ) => { | ||||||
|  |   context.currentUser.can('update', 'Flow'); | ||||||
|  |  | ||||||
|   const { input } = params; |   const { input } = params; | ||||||
|  |  | ||||||
|   let step = await context.currentUser |   let step = await context.currentUser | ||||||
|   | |||||||
							
								
								
									
										34
									
								
								packages/backend/src/graphql/mutations/update-user.ee.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								packages/backend/src/graphql/mutations/update-user.ee.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | import Context from '../../types/express/context'; | ||||||
|  | import User from '../../models/user'; | ||||||
|  |  | ||||||
|  | type Params = { | ||||||
|  |   input: { | ||||||
|  |     id: string; | ||||||
|  |     email: string; | ||||||
|  |     fullName: string; | ||||||
|  |     role: { | ||||||
|  |       id: string; | ||||||
|  |     }; | ||||||
|  |   }; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // TODO: access | ||||||
|  | const updateUser = async ( | ||||||
|  |   _parent: unknown, | ||||||
|  |   params: Params, | ||||||
|  |   context: Context | ||||||
|  | ) => { | ||||||
|  |   const user = await User.query() | ||||||
|  |     .patchAndFetchById( | ||||||
|  |       params.input.id, | ||||||
|  |       { | ||||||
|  |         email: params.input.email, | ||||||
|  |         fullName: params.input.fullName, | ||||||
|  |         roleId: params.input.role.id, | ||||||
|  |       } | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |   return user; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default updateUser; | ||||||
| @@ -13,6 +13,8 @@ const verifyConnection = async ( | |||||||
|   params: Params, |   params: Params, | ||||||
|   context: Context |   context: Context | ||||||
| ) => { | ) => { | ||||||
|  |   context.currentUser.can('create', 'Connection'); | ||||||
|  |  | ||||||
|   let connection = await context.currentUser |   let connection = await context.currentUser | ||||||
|     .$relatedQuery('connections') |     .$relatedQuery('connections') | ||||||
|     .findOne({ |     .findOne({ | ||||||
|   | |||||||
| @@ -6,6 +6,8 @@ type Params = { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| const getApp = async (_parent: unknown, params: Params, context: Context) => { | const getApp = async (_parent: unknown, params: Params, context: Context) => { | ||||||
|  |   context.currentUser.can('read', 'Connection'); | ||||||
|  |  | ||||||
|   const app = await App.findOneByKey(params.key); |   const app = await App.findOneByKey(params.key); | ||||||
|  |  | ||||||
|   if (context.currentUser) { |   if (context.currentUser) { | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import App from '../../models/app'; |  | ||||||
| import { IApp } from '@automatisch/types'; | import { IApp } from '@automatisch/types'; | ||||||
|  | import App from '../../models/app'; | ||||||
|  |  | ||||||
| type Params = { | type Params = { | ||||||
|   name: string; |   name: string; | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
|  | import { IConnection } from '@automatisch/types'; | ||||||
| import App from '../../models/app'; | import App from '../../models/app'; | ||||||
| import Context from '../../types/express/context'; | import Context from '../../types/express/context'; | ||||||
| import { IApp, IConnection } from '@automatisch/types'; |  | ||||||
|  |  | ||||||
| type Params = { | type Params = { | ||||||
|   name: string; |   name: string; | ||||||
| @@ -11,6 +11,8 @@ const getConnectedApps = async ( | |||||||
|   params: Params, |   params: Params, | ||||||
|   context: Context |   context: Context | ||||||
| ) => { | ) => { | ||||||
|  |   context.currentUser.can('read', 'Connection'); | ||||||
|  |  | ||||||
|   let apps = await App.findAll(params.name); |   let apps = await App.findAll(params.name); | ||||||
|  |  | ||||||
|   const connections = await context.currentUser |   const connections = await context.currentUser | ||||||
|   | |||||||
| @@ -16,6 +16,8 @@ const getDynamicData = async ( | |||||||
|   params: Params, |   params: Params, | ||||||
|   context: Context |   context: Context | ||||||
| ) => { | ) => { | ||||||
|  |   context.currentUser.can('update', 'Flow'); | ||||||
|  |  | ||||||
|   const step = await context.currentUser |   const step = await context.currentUser | ||||||
|     .$relatedQuery('steps') |     .$relatedQuery('steps') | ||||||
|     .withGraphFetched({ |     .withGraphFetched({ | ||||||
|   | |||||||
| @@ -14,6 +14,8 @@ const getDynamicFields = async ( | |||||||
|   params: Params, |   params: Params, | ||||||
|   context: Context |   context: Context | ||||||
| ) => { | ) => { | ||||||
|  |   context.currentUser.can('update', 'Flow'); | ||||||
|  |  | ||||||
|   const step = await context.currentUser |   const step = await context.currentUser | ||||||
|     .$relatedQuery('steps') |     .$relatedQuery('steps') | ||||||
|     .withGraphFetched({ |     .withGraphFetched({ | ||||||
|   | |||||||
| @@ -12,6 +12,8 @@ const getExecutionSteps = async ( | |||||||
|   params: Params, |   params: Params, | ||||||
|   context: Context |   context: Context | ||||||
| ) => { | ) => { | ||||||
|  |   context.currentUser.can('read', 'Execution'); | ||||||
|  |  | ||||||
|   const execution = await context.currentUser |   const execution = await context.currentUser | ||||||
|     .$relatedQuery('executions') |     .$relatedQuery('executions') | ||||||
|     .withSoftDeleted() |     .withSoftDeleted() | ||||||
|   | |||||||
| @@ -9,6 +9,8 @@ const getExecution = async ( | |||||||
|   params: Params, |   params: Params, | ||||||
|   context: Context |   context: Context | ||||||
| ) => { | ) => { | ||||||
|  |   context.currentUser.can('read', 'Execution'); | ||||||
|  |  | ||||||
|   const execution = await context.currentUser |   const execution = await context.currentUser | ||||||
|     .$relatedQuery('executions') |     .$relatedQuery('executions') | ||||||
|     .withGraphFetched({ |     .withGraphFetched({ | ||||||
|   | |||||||
| @@ -12,6 +12,8 @@ const getExecutions = async ( | |||||||
|   params: Params, |   params: Params, | ||||||
|   context: Context |   context: Context | ||||||
| ) => { | ) => { | ||||||
|  |   context.currentUser.can('read', 'Execution'); | ||||||
|  |  | ||||||
|   const selectStatusStatement = ` |   const selectStatusStatement = ` | ||||||
|     case |     case | ||||||
|       when count(*) filter (where execution_steps.status = 'failure') > 0 |       when count(*) filter (where execution_steps.status = 'failure') > 0 | ||||||
|   | |||||||
| @@ -5,6 +5,8 @@ type Params = { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| const getFlow = async (_parent: unknown, params: Params, context: Context) => { | const getFlow = async (_parent: unknown, params: Params, context: Context) => { | ||||||
|  |   context.currentUser.can('read', 'Flow'); | ||||||
|  |  | ||||||
|   const flow = await context.currentUser |   const flow = await context.currentUser | ||||||
|     .$relatedQuery('flows') |     .$relatedQuery('flows') | ||||||
|     .withGraphJoined('[steps.[connection]]') |     .withGraphJoined('[steps.[connection]]') | ||||||
|   | |||||||
| @@ -10,6 +10,8 @@ type Params = { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| const getFlows = async (_parent: unknown, params: Params, context: Context) => { | const getFlows = async (_parent: unknown, params: Params, context: Context) => { | ||||||
|  |   context.currentUser.can('read', 'Flow'); | ||||||
|  |  | ||||||
|   const flowsQuery = context.currentUser |   const flowsQuery = context.currentUser | ||||||
|     .$relatedQuery('flows') |     .$relatedQuery('flows') | ||||||
|     .joinRelated({ |     .joinRelated({ | ||||||
|   | |||||||
							
								
								
									
										76
									
								
								packages/backend/src/graphql/queries/get-permissions.ee.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								packages/backend/src/graphql/queries/get-permissions.ee.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | |||||||
|  | const getPermissions = async () => { | ||||||
|  |   const Connection = { | ||||||
|  |     label: 'Connection', | ||||||
|  |     key: 'Connection', | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const Flow = { | ||||||
|  |     label: 'Flow', | ||||||
|  |     key: 'Flow', | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const Execution = { | ||||||
|  |     label: 'Execution', | ||||||
|  |     key: 'Execution', | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const permissions = { | ||||||
|  |     conditions: [ | ||||||
|  |       { | ||||||
|  |         key: 'isCreator', | ||||||
|  |         label: 'Is creator' | ||||||
|  |       } | ||||||
|  |     ], | ||||||
|  |     actions: [ | ||||||
|  |       { | ||||||
|  |         label: 'Create', | ||||||
|  |         action: 'create', | ||||||
|  |         subjects: [ | ||||||
|  |           Connection.key, | ||||||
|  |           Flow.key, | ||||||
|  |         ] | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         label: 'Read', | ||||||
|  |         action: 'read', | ||||||
|  |         subjects: [ | ||||||
|  |           Connection.key, | ||||||
|  |           Execution.key, | ||||||
|  |           Flow.key, | ||||||
|  |         ] | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         label: 'Update', | ||||||
|  |         action: 'update', | ||||||
|  |         subjects: [ | ||||||
|  |           Connection.key, | ||||||
|  |           Flow.key, | ||||||
|  |         ] | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         label: 'Delete', | ||||||
|  |         action: 'delete', | ||||||
|  |         subjects: [ | ||||||
|  |           Connection.key, | ||||||
|  |           Flow.key, | ||||||
|  |         ] | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         label: 'Publish', | ||||||
|  |         action: 'publish', | ||||||
|  |         subjects: [ | ||||||
|  |           Flow.key, | ||||||
|  |         ] | ||||||
|  |       } | ||||||
|  |     ], | ||||||
|  |     subjects: [ | ||||||
|  |       Connection, | ||||||
|  |       Flow, | ||||||
|  |       Execution | ||||||
|  |     ] | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return permissions; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default getPermissions; | ||||||
							
								
								
									
										13
									
								
								packages/backend/src/graphql/queries/get-role.ee.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								packages/backend/src/graphql/queries/get-role.ee.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | import Context from '../../types/express/context'; | ||||||
|  | import Role from '../../models/role'; | ||||||
|  |  | ||||||
|  | type Params = { | ||||||
|  |   id: string | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // TODO: access | ||||||
|  | const getRole = async (_parent: unknown, params: Params, context: Context) => { | ||||||
|  |   return await Role.query().findById(params.id).throwIfNotFound(); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default getRole; | ||||||
							
								
								
									
										9
									
								
								packages/backend/src/graphql/queries/get-roles.ee.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								packages/backend/src/graphql/queries/get-roles.ee.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | import Context from '../../types/express/context'; | ||||||
|  | import Role from '../../models/role'; | ||||||
|  |  | ||||||
|  | // TODO: access | ||||||
|  | const getRoles = async (_parent: unknown, params: unknown, context: Context) => { | ||||||
|  |   return await Role.query(); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default getRoles; | ||||||
| @@ -0,0 +1,9 @@ | |||||||
|  | import SamlAuthProvider from '../../models/saml-auth-provider.ee'; | ||||||
|  |  | ||||||
|  | const getSamlAuthProviders = async () => { | ||||||
|  |   const providers = await SamlAuthProvider.query(); | ||||||
|  |  | ||||||
|  |   return providers; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default getSamlAuthProviders; | ||||||
| @@ -11,6 +11,8 @@ const getStepWithTestExecutions = async ( | |||||||
|   params: Params, |   params: Params, | ||||||
|   context: Context |   context: Context | ||||||
| ) => { | ) => { | ||||||
|  |   context.currentUser.can('update', 'Flow'); | ||||||
|  |  | ||||||
|   const step = await context.currentUser |   const step = await context.currentUser | ||||||
|     .$relatedQuery('steps') |     .$relatedQuery('steps') | ||||||
|     .findOne({ 'steps.id': params.stepId }) |     .findOne({ 'steps.id': params.stepId }) | ||||||
|   | |||||||
							
								
								
									
										22
									
								
								packages/backend/src/graphql/queries/get-user.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								packages/backend/src/graphql/queries/get-user.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | |||||||
|  | import Context from '../../types/express/context'; | ||||||
|  | import User from '../../models/user'; | ||||||
|  |  | ||||||
|  | type Params = { | ||||||
|  |   id: string | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // TODO: access | ||||||
|  | const getUser = async (_parent: unknown, params: Params, context: Context) => { | ||||||
|  |   return await User | ||||||
|  |     .query() | ||||||
|  |     .leftJoinRelated({ | ||||||
|  |       role: true | ||||||
|  |     }) | ||||||
|  |     .withGraphFetched({ | ||||||
|  |       role: true | ||||||
|  |     }) | ||||||
|  |     .findById(params.id) | ||||||
|  |     .throwIfNotFound(); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default getUser; | ||||||
							
								
								
									
										25
									
								
								packages/backend/src/graphql/queries/get-users.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								packages/backend/src/graphql/queries/get-users.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | import Context from '../../types/express/context'; | ||||||
|  | import paginate from '../../helpers/pagination'; | ||||||
|  | import User from '../../models/user'; | ||||||
|  |  | ||||||
|  | type Params = { | ||||||
|  |   limit: number; | ||||||
|  |   offset: number; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // TODO: access | ||||||
|  | const getUsers = async (_parent: unknown, params: Params, context: Context) => { | ||||||
|  |   const usersQuery = User | ||||||
|  |     .query() | ||||||
|  |     .leftJoinRelated({ | ||||||
|  |       role: true | ||||||
|  |     }) | ||||||
|  |     .withGraphFetched({ | ||||||
|  |       role: true | ||||||
|  |     }) | ||||||
|  |     .orderBy('full_name', 'desc'); | ||||||
|  |  | ||||||
|  |   return paginate(usersQuery, params.limit, params.offset); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default getUsers; | ||||||
| @@ -12,6 +12,8 @@ const testConnection = async ( | |||||||
|   params: Params, |   params: Params, | ||||||
|   context: Context |   context: Context | ||||||
| ) => { | ) => { | ||||||
|  |   context.currentUser.can('update', 'Connection'); | ||||||
|  |  | ||||||
|   let connection = await context.currentUser |   let connection = await context.currentUser | ||||||
|     .$relatedQuery('connections') |     .$relatedQuery('connections') | ||||||
|     .findOne({ |     .findOne({ | ||||||
|   | |||||||
| @@ -1,47 +1,59 @@ | |||||||
| import getApps from './queries/get-apps'; |  | ||||||
| import getApp from './queries/get-app'; | import getApp from './queries/get-app'; | ||||||
|  | import getApps from './queries/get-apps'; | ||||||
|  | import getAutomatischInfo from './queries/get-automatisch-info'; | ||||||
|  | import getBillingAndUsage from './queries/get-billing-and-usage.ee'; | ||||||
| import getConnectedApps from './queries/get-connected-apps'; | import getConnectedApps from './queries/get-connected-apps'; | ||||||
| import testConnection from './queries/test-connection'; | import getCurrentUser from './queries/get-current-user'; | ||||||
| import getFlow from './queries/get-flow'; |  | ||||||
| import getFlows from './queries/get-flows'; |  | ||||||
| import getStepWithTestExecutions from './queries/get-step-with-test-executions'; |  | ||||||
| import getExecution from './queries/get-execution'; |  | ||||||
| import getExecutions from './queries/get-executions'; |  | ||||||
| import getExecutionSteps from './queries/get-execution-steps'; |  | ||||||
| import getDynamicData from './queries/get-dynamic-data'; | import getDynamicData from './queries/get-dynamic-data'; | ||||||
| import getDynamicFields from './queries/get-dynamic-fields'; | import getDynamicFields from './queries/get-dynamic-fields'; | ||||||
| import getCurrentUser from './queries/get-current-user'; | import getExecution from './queries/get-execution'; | ||||||
| import getPaymentPlans from './queries/get-payment-plans.ee'; | import getExecutionSteps from './queries/get-execution-steps'; | ||||||
| import getPaddleInfo from './queries/get-paddle-info.ee'; | import getExecutions from './queries/get-executions'; | ||||||
| import getBillingAndUsage from './queries/get-billing-and-usage.ee'; | import getFlow from './queries/get-flow'; | ||||||
|  | import getFlows from './queries/get-flows'; | ||||||
|  | import getUser from './queries/get-user'; | ||||||
|  | import getUsers from './queries/get-users'; | ||||||
| import getInvoices from './queries/get-invoices.ee'; | import getInvoices from './queries/get-invoices.ee'; | ||||||
| import getAutomatischInfo from './queries/get-automatisch-info'; | import getPaddleInfo from './queries/get-paddle-info.ee'; | ||||||
| import getTrialStatus from './queries/get-trial-status.ee'; | import getPaymentPlans from './queries/get-payment-plans.ee'; | ||||||
|  | import getPermissions from './queries/get-permissions.ee'; | ||||||
|  | import getRole from './queries/get-role.ee'; | ||||||
|  | import getRoles from './queries/get-roles.ee'; | ||||||
|  | import getSamlAuthProviders from './queries/get-saml-auth-providers.ee'; | ||||||
|  | import getStepWithTestExecutions from './queries/get-step-with-test-executions'; | ||||||
| import getSubscriptionStatus from './queries/get-subscription-status.ee'; | import getSubscriptionStatus from './queries/get-subscription-status.ee'; | ||||||
|  | import getTrialStatus from './queries/get-trial-status.ee'; | ||||||
| import healthcheck from './queries/healthcheck'; | import healthcheck from './queries/healthcheck'; | ||||||
|  | import testConnection from './queries/test-connection'; | ||||||
|  |  | ||||||
| const queryResolvers = { | const queryResolvers = { | ||||||
|   getApps, |  | ||||||
|   getApp, |   getApp, | ||||||
|  |   getApps, | ||||||
|  |   getAutomatischInfo, | ||||||
|  |   getBillingAndUsage, | ||||||
|   getConnectedApps, |   getConnectedApps, | ||||||
|   testConnection, |   getCurrentUser, | ||||||
|   getFlow, |   getDynamicData, | ||||||
|   getFlows, |   getDynamicFields, | ||||||
|   getStepWithTestExecutions, |  | ||||||
|   getExecution, |   getExecution, | ||||||
|   getExecutions, |   getExecutions, | ||||||
|   getExecutionSteps, |   getExecutionSteps, | ||||||
|   getDynamicData, |   getFlow, | ||||||
|   getDynamicFields, |   getFlows, | ||||||
|   getCurrentUser, |  | ||||||
|   getPaymentPlans, |  | ||||||
|   getPaddleInfo, |  | ||||||
|   getBillingAndUsage, |  | ||||||
|   getInvoices, |   getInvoices, | ||||||
|   getAutomatischInfo, |   getPaddleInfo, | ||||||
|   getTrialStatus, |   getPaymentPlans, | ||||||
|  |   getPermissions, | ||||||
|  |   getRole, | ||||||
|  |   getRoles, | ||||||
|  |   getSamlAuthProviders, | ||||||
|  |   getStepWithTestExecutions, | ||||||
|   getSubscriptionStatus, |   getSubscriptionStatus, | ||||||
|  |   getTrialStatus, | ||||||
|  |   getUser, | ||||||
|  |   getUsers, | ||||||
|   healthcheck, |   healthcheck, | ||||||
|  |   testConnection, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export default queryResolvers; | export default queryResolvers; | ||||||
|   | |||||||
| @@ -41,31 +41,46 @@ type Query { | |||||||
|   getAutomatischInfo: GetAutomatischInfo |   getAutomatischInfo: GetAutomatischInfo | ||||||
|   getTrialStatus: GetTrialStatus |   getTrialStatus: GetTrialStatus | ||||||
|   getSubscriptionStatus: GetSubscriptionStatus |   getSubscriptionStatus: GetSubscriptionStatus | ||||||
|  |   getSamlAuthProviders: [GetSamlAuthProviders] | ||||||
|  |   getUsers( | ||||||
|  |     limit: Int! | ||||||
|  |     offset: Int! | ||||||
|  |   ): UserConnection | ||||||
|  |   getUser(id: String!): User | ||||||
|  |   getRoles: [Role] | ||||||
|  |   getRole(id: String!): Role | ||||||
|  |   getPermissions: Permissions | ||||||
|   healthcheck: AppHealth |   healthcheck: AppHealth | ||||||
| } | } | ||||||
|  |  | ||||||
| type Mutation { | type Mutation { | ||||||
|   createConnection(input: CreateConnectionInput): Connection |   createConnection(input: CreateConnectionInput): Connection | ||||||
|   generateAuthUrl(input: GenerateAuthUrlInput): AuthLink |  | ||||||
|   updateConnection(input: UpdateConnectionInput): Connection |  | ||||||
|   resetConnection(input: ResetConnectionInput): Connection |  | ||||||
|   verifyConnection(input: VerifyConnectionInput): Connection |  | ||||||
|   deleteConnection(input: DeleteConnectionInput): Boolean |  | ||||||
|   createFlow(input: CreateFlowInput): Flow |   createFlow(input: CreateFlowInput): Flow | ||||||
|  |   createRole(input: CreateRoleInput): Role | ||||||
|  |   createStep(input: CreateStepInput): Step | ||||||
|  |   createUser(input: CreateUserInput): User | ||||||
|  |   deleteConnection(input: DeleteConnectionInput): Boolean | ||||||
|  |   deleteCurrentUser: Boolean | ||||||
|  |   deleteFlow(input: DeleteFlowInput): Boolean | ||||||
|  |   deleteRole(input: DeleteRoleInput): Boolean | ||||||
|  |   deleteStep(input: DeleteStepInput): Step | ||||||
|  |   deleteUser(input: DeleteUserInput): Boolean | ||||||
|  |   duplicateFlow(input: DuplicateFlowInput): Flow | ||||||
|  |   executeFlow(input: ExecuteFlowInput): executeFlowType | ||||||
|  |   forgotPassword(input: ForgotPasswordInput): Boolean | ||||||
|  |   generateAuthUrl(input: GenerateAuthUrlInput): AuthLink | ||||||
|  |   login(input: LoginInput): Auth | ||||||
|  |   registerUser(input: RegisterUserInput): User | ||||||
|  |   resetConnection(input: ResetConnectionInput): Connection | ||||||
|  |   resetPassword(input: ResetPasswordInput): Boolean | ||||||
|  |   updateConnection(input: UpdateConnectionInput): Connection | ||||||
|  |   updateCurrentUser(input: UpdateCurrentUserInput): User | ||||||
|   updateFlow(input: UpdateFlowInput): Flow |   updateFlow(input: UpdateFlowInput): Flow | ||||||
|   updateFlowStatus(input: UpdateFlowStatusInput): Flow |   updateFlowStatus(input: UpdateFlowStatusInput): Flow | ||||||
|   executeFlow(input: ExecuteFlowInput): executeFlowType |   updateRole(input: UpdateRoleInput): Role | ||||||
|   deleteFlow(input: DeleteFlowInput): Boolean |  | ||||||
|   duplicateFlow(input: DuplicateFlowInput): Flow |  | ||||||
|   createStep(input: CreateStepInput): Step |  | ||||||
|   updateStep(input: UpdateStepInput): Step |   updateStep(input: UpdateStepInput): Step | ||||||
|   deleteStep(input: DeleteStepInput): Step |  | ||||||
|   createUser(input: CreateUserInput): User |  | ||||||
|   deleteUser: Boolean |  | ||||||
|   updateUser(input: UpdateUserInput): User |   updateUser(input: UpdateUserInput): User | ||||||
|   forgotPassword(input: ForgotPasswordInput): Boolean |   verifyConnection(input: VerifyConnectionInput): Connection | ||||||
|   resetPassword(input: ResetPasswordInput): Boolean |  | ||||||
|   login(input: LoginInput): Auth |  | ||||||
| } | } | ||||||
|  |  | ||||||
| """ | """ | ||||||
| @@ -277,6 +292,15 @@ type Execution { | |||||||
|   flow: Flow |   flow: Flow | ||||||
| } | } | ||||||
|  |  | ||||||
|  | type UserConnection { | ||||||
|  |   edges: [UserEdge] | ||||||
|  |   pageInfo: PageInfo | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type UserEdge { | ||||||
|  |   node: User | ||||||
|  | } | ||||||
|  |  | ||||||
| input CreateConnectionInput { | input CreateConnectionInput { | ||||||
|   key: String! |   key: String! | ||||||
|   formattedData: JSONObject! |   formattedData: JSONObject! | ||||||
| @@ -360,9 +384,31 @@ input CreateUserInput { | |||||||
|   fullName: String! |   fullName: String! | ||||||
|   email: String! |   email: String! | ||||||
|   password: String! |   password: String! | ||||||
|  |   role: UserRoleInput! | ||||||
|  | } | ||||||
|  |  | ||||||
|  | input UserRoleInput { | ||||||
|  |   id: String | ||||||
| } | } | ||||||
|  |  | ||||||
| input UpdateUserInput { | input UpdateUserInput { | ||||||
|  |   id: String! | ||||||
|  |   fullName: String | ||||||
|  |   email: String | ||||||
|  |   role: UserRoleInput | ||||||
|  | } | ||||||
|  |  | ||||||
|  | input DeleteUserInput { | ||||||
|  |   id: String! | ||||||
|  | } | ||||||
|  |  | ||||||
|  | input RegisterUserInput { | ||||||
|  |   fullName: String! | ||||||
|  |   email: String! | ||||||
|  |   password: String! | ||||||
|  | } | ||||||
|  |  | ||||||
|  | input UpdateCurrentUserInput { | ||||||
|   email: String |   email: String | ||||||
|   password: String |   password: String | ||||||
|   fullName: String |   fullName: String | ||||||
| @@ -382,6 +428,29 @@ input LoginInput { | |||||||
|   password: String! |   password: String! | ||||||
| } | } | ||||||
|  |  | ||||||
|  | input PermissionInput { | ||||||
|  |   action: String! | ||||||
|  |   subject: String! | ||||||
|  |   conditions: [String] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | input CreateRoleInput { | ||||||
|  |   name: String! | ||||||
|  |   description: String | ||||||
|  |   permissions: [PermissionInput] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | input UpdateRoleInput { | ||||||
|  |   id: String! | ||||||
|  |   name: String! | ||||||
|  |   description: String | ||||||
|  |   permissions: [PermissionInput] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | input DeleteRoleInput { | ||||||
|  |   id: String! | ||||||
|  | } | ||||||
|  |  | ||||||
| """ | """ | ||||||
| The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). | The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). | ||||||
| """ | """ | ||||||
| @@ -453,11 +522,21 @@ type User { | |||||||
|   id: String |   id: String | ||||||
|   fullName: String |   fullName: String | ||||||
|   email: String |   email: String | ||||||
|   role: String |   role: Role | ||||||
|  |   permissions: [Permission] | ||||||
|   createdAt: String |   createdAt: String | ||||||
|   updatedAt: String |   updatedAt: String | ||||||
| } | } | ||||||
|  |  | ||||||
|  | type Role { | ||||||
|  |   id: String | ||||||
|  |   name: String | ||||||
|  |   key: String | ||||||
|  |   description: String | ||||||
|  |   isAdmin: Boolean | ||||||
|  |   permissions: [Permission] | ||||||
|  | } | ||||||
|  |  | ||||||
| type PageInfo { | type PageInfo { | ||||||
|   currentPage: Int! |   currentPage: Int! | ||||||
|   totalPages: Int! |   totalPages: Int! | ||||||
| @@ -554,6 +633,41 @@ type PaymentPlan { | |||||||
|   productId: String |   productId: String | ||||||
| } | } | ||||||
|  |  | ||||||
|  | type GetSamlAuthProviders { | ||||||
|  |   id: String | ||||||
|  |   name: String | ||||||
|  |   issuer: String | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Permission { | ||||||
|  |   action: String | ||||||
|  |   subject: String | ||||||
|  |   conditions: [String] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | # TODO: emphasize it's a catalog item | ||||||
|  | type Permissions { | ||||||
|  |   actions: [Action] | ||||||
|  |   subjects: [Subject] | ||||||
|  |   conditions: [Condition] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Action { | ||||||
|  |   label: String | ||||||
|  |   action: String | ||||||
|  |   subjects: [String] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Condition { | ||||||
|  |   key: String | ||||||
|  |   label: String | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Subject { | ||||||
|  |   label: String | ||||||
|  |   key: String | ||||||
|  | } | ||||||
|  |  | ||||||
| schema { | schema { | ||||||
|   query: Query |   query: Query | ||||||
|   mutation: Mutation |   mutation: Mutation | ||||||
|   | |||||||
| @@ -12,7 +12,17 @@ const isAuthenticated = rule()(async (_parent, _args, req) => { | |||||||
|     const { userId } = jwt.verify(token, appConfig.appSecretKey) as { |     const { userId } = jwt.verify(token, appConfig.appSecretKey) as { | ||||||
|       userId: string; |       userId: string; | ||||||
|     }; |     }; | ||||||
|     req.currentUser = await User.query().findById(userId).throwIfNotFound(); |     req.currentUser = await User | ||||||
|  |       .query() | ||||||
|  |       .findById(userId) | ||||||
|  |       .leftJoinRelated({ | ||||||
|  |         role: true, | ||||||
|  |         permissions: true, | ||||||
|  |       }) | ||||||
|  |       .withGraphFetched({ | ||||||
|  |         role: true, | ||||||
|  |         permissions: true, | ||||||
|  |       }); | ||||||
|  |  | ||||||
|     return true; |     return true; | ||||||
|   } catch (error) { |   } catch (error) { | ||||||
| @@ -25,13 +35,14 @@ const authentication = shield( | |||||||
|     Query: { |     Query: { | ||||||
|       '*': isAuthenticated, |       '*': isAuthenticated, | ||||||
|       getAutomatischInfo: allow, |       getAutomatischInfo: allow, | ||||||
|  |       getSamlAuthProviders: allow, | ||||||
|       healthcheck: allow, |       healthcheck: allow, | ||||||
|     }, |     }, | ||||||
|     Mutation: { |     Mutation: { | ||||||
|       '*': isAuthenticated, |       '*': isAuthenticated, | ||||||
|       login: allow, |       registerUser: allow, | ||||||
|       createUser: allow, |  | ||||||
|       forgotPassword: allow, |       forgotPassword: allow, | ||||||
|  |       login: allow, | ||||||
|       resetPassword: allow, |       resetPassword: allow, | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|   | |||||||
							
								
								
									
										14
									
								
								packages/backend/src/helpers/create-auth-token-by-user-id.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								packages/backend/src/helpers/create-auth-token-by-user-id.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | import jwt from 'jsonwebtoken'; | ||||||
|  | import appConfig from '../config/app'; | ||||||
|  |  | ||||||
|  | const TOKEN_EXPIRES_IN = '14d'; | ||||||
|  |  | ||||||
|  | const createAuthTokenByUserId = (userId: string) => { | ||||||
|  |   const token = jwt.sign({ userId }, appConfig.appSecretKey, { | ||||||
|  |     expiresIn: TOKEN_EXPIRES_IN, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   return token; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default createAuthTokenByUserId; | ||||||
| @@ -0,0 +1,48 @@ | |||||||
|  | import SamlAuthProvider from '../models/saml-auth-provider.ee'; | ||||||
|  | import User from '../models/user'; | ||||||
|  | import Identity from '../models/identity.ee'; | ||||||
|  |  | ||||||
|  | const getUser = (user: Record<string, unknown>, providerConfig: SamlAuthProvider) => ({ | ||||||
|  |   name: user[providerConfig.firstnameAttributeName], | ||||||
|  |   surname: user[providerConfig.surnameAttributeName], | ||||||
|  |   id: user.nameID, | ||||||
|  |   email: user[providerConfig.emailAttributeName], | ||||||
|  |   role: user[providerConfig.roleAttributeName], | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | const findOrCreateUserBySamlIdentity = async (userIdentity: Record<string, unknown>, samlAuthProvider: SamlAuthProvider) => { | ||||||
|  |   const mappedUser = getUser(userIdentity, samlAuthProvider); | ||||||
|  |   const identity = await Identity.query().findOne({ | ||||||
|  |     remote_id: mappedUser.id, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   if (identity) { | ||||||
|  |     const user = await identity.$relatedQuery('user'); | ||||||
|  |  | ||||||
|  |     return user; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const createdUser = await User.query().insertGraph({ | ||||||
|  |     fullName: [ | ||||||
|  |       mappedUser.name, | ||||||
|  |       mappedUser.surname | ||||||
|  |     ] | ||||||
|  |       .filter(Boolean) | ||||||
|  |       .join(' '), | ||||||
|  |     email: mappedUser.email as string, | ||||||
|  |     roleId: samlAuthProvider.defaultRoleId, | ||||||
|  |     identities: [ | ||||||
|  |       { | ||||||
|  |         remoteId: mappedUser.id as string, | ||||||
|  |         providerId: samlAuthProvider.id, | ||||||
|  |         providerType: 'saml' | ||||||
|  |       } | ||||||
|  |     ] | ||||||
|  |   }, { | ||||||
|  |     relate: ['identities'] | ||||||
|  |   }).returning('*'); | ||||||
|  |  | ||||||
|  |   return createdUser; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default findOrCreateUserBySamlIdentity; | ||||||
| @@ -1,10 +1,11 @@ | |||||||
| import { Model } from 'objection'; | import { Model } from 'objection'; | ||||||
| import ExtendedQueryBuilder from '../models/query-builder'; | import ExtendedQueryBuilder from '../models/query-builder'; | ||||||
|  | import type Base from '../models/base'; | ||||||
|  |  | ||||||
| const paginate = async ( | const paginate = async ( | ||||||
|   query: ExtendedQueryBuilder<Model, Model[]>, |   query: ExtendedQueryBuilder<Model, Model[]>, | ||||||
|   limit: number, |   limit: number, | ||||||
|   offset: number |   offset: number, | ||||||
| ) => { | ) => { | ||||||
|   if (limit < 1 || limit > 100) { |   if (limit < 1 || limit > 100) { | ||||||
|     throw new Error('Limit must be between 1 and 100'); |     throw new Error('Limit must be between 1 and 100'); | ||||||
| @@ -20,11 +21,9 @@ const paginate = async ( | |||||||
|       currentPage: Math.ceil(offset / limit + 1), |       currentPage: Math.ceil(offset / limit + 1), | ||||||
|       totalPages: Math.ceil(count / limit), |       totalPages: Math.ceil(count / limit), | ||||||
|     }, |     }, | ||||||
|     edges: records.map((record: Model) => { |     edges: records.map((record: Base) => ({ | ||||||
|       return { |       node: record, | ||||||
|         node: record, |     })), | ||||||
|       }; |  | ||||||
|     }), |  | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										84
									
								
								packages/backend/src/helpers/passport.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								packages/backend/src/helpers/passport.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,84 @@ | |||||||
|  | import { URL } from 'node:url'; | ||||||
|  | import { IRequest } from '@automatisch/types'; | ||||||
|  | import { MultiSamlStrategy } from '@node-saml/passport-saml'; | ||||||
|  | import { Express } from 'express'; | ||||||
|  | import passport from 'passport'; | ||||||
|  |  | ||||||
|  | import appConfig from '../config/app'; | ||||||
|  | import createAuthTokenByUserId from '../helpers/create-auth-token-by-user-id'; | ||||||
|  | import SamlAuthProvider from '../models/saml-auth-provider.ee'; | ||||||
|  | import findOrCreateUserBySamlIdentity from './find-or-create-user-by-saml-identity.ee' | ||||||
|  |  | ||||||
|  | export default function configurePassport(app: Express) { | ||||||
|  |   app.use(passport.initialize({ | ||||||
|  |     userProperty: 'currentUser', | ||||||
|  |   })); | ||||||
|  |  | ||||||
|  |   passport.use(new MultiSamlStrategy( | ||||||
|  |     { | ||||||
|  |       passReqToCallback: true, | ||||||
|  |       getSamlOptions: async function (request, done) { | ||||||
|  |         const { issuer } = request.params; | ||||||
|  |         const notFoundIssuer = new Error('Issuer cannot be found!'); | ||||||
|  |  | ||||||
|  |         if (!issuer) return done(notFoundIssuer); | ||||||
|  |  | ||||||
|  |         const authProvider = await SamlAuthProvider.query().findOne({ | ||||||
|  |           issuer: request.params.issuer as string, | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         if (!authProvider) { | ||||||
|  |           return done(notFoundIssuer); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return done(null, authProvider.config); | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |     async function (request, user: Record<string, unknown>, done) { | ||||||
|  |       const { issuer } = request.params; | ||||||
|  |       const notFoundIssuer = new Error('Issuer cannot be found!'); | ||||||
|  |  | ||||||
|  |       if (!issuer) return done(notFoundIssuer); | ||||||
|  |  | ||||||
|  |       const authProvider = await SamlAuthProvider.query().findOne({ | ||||||
|  |         issuer: request.params.issuer as string, | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       if (!authProvider) { | ||||||
|  |         return done(notFoundIssuer); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       const foundUserWithIdentity = await findOrCreateUserBySamlIdentity(user, authProvider); | ||||||
|  |       return done(null, foundUserWithIdentity as unknown as Record<string, unknown>); | ||||||
|  |     }, | ||||||
|  |     function (request, user: Record<string, unknown>, done: (error: any, user: Record<string, unknown>) => void) { | ||||||
|  |       return done(null, null); | ||||||
|  |     } | ||||||
|  |   )); | ||||||
|  |  | ||||||
|  |   app.get('/login/saml/:issuer', | ||||||
|  |     passport.authenticate('saml', | ||||||
|  |       { | ||||||
|  |         session: false, | ||||||
|  |         successRedirect: '/', | ||||||
|  |       }) | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   app.post( | ||||||
|  |     '/login/saml/:issuer/callback', | ||||||
|  |     passport.authenticate('saml', { | ||||||
|  |       session: false, | ||||||
|  |       failureRedirect: '/', | ||||||
|  |       failureFlash: true, | ||||||
|  |     }), | ||||||
|  |     (req: IRequest, res) => { | ||||||
|  |       const token = createAuthTokenByUserId(req.currentUser.id); | ||||||
|  |  | ||||||
|  |       const redirectUrl = new URL( | ||||||
|  |         `/login/callback?token=${token}`, | ||||||
|  |         appConfig.webAppUrl, | ||||||
|  |       ).toString(); | ||||||
|  |       res.redirect(redirectUrl); | ||||||
|  |     } | ||||||
|  |   ); | ||||||
|  | }; | ||||||
| @@ -40,6 +40,9 @@ class Connection extends Base { | |||||||
|       userId: { type: 'string', format: 'uuid' }, |       userId: { type: 'string', format: 'uuid' }, | ||||||
|       verified: { type: 'boolean', default: false }, |       verified: { type: 'boolean', default: false }, | ||||||
|       draft: { type: 'boolean' }, |       draft: { type: 'boolean' }, | ||||||
|  |       deletedAt: { type: 'string' }, | ||||||
|  |       createdAt: { type: 'string' }, | ||||||
|  |       updatedAt: { type: 'string' }, | ||||||
|     }, |     }, | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -31,6 +31,9 @@ class ExecutionStep extends Base { | |||||||
|       dataOut: { type: ['object', 'null'] }, |       dataOut: { type: ['object', 'null'] }, | ||||||
|       status: { type: 'string', enum: ['success', 'failure'] }, |       status: { type: 'string', enum: ['success', 'failure'] }, | ||||||
|       errorDetails: { type: ['object', 'null'] }, |       errorDetails: { type: ['object', 'null'] }, | ||||||
|  |       deletedAt: { type: 'string' }, | ||||||
|  |       createdAt: { type: 'string' }, | ||||||
|  |       updatedAt: { type: 'string' }, | ||||||
|     }, |     }, | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -22,6 +22,9 @@ class Execution extends Base { | |||||||
|       flowId: { type: 'string', format: 'uuid' }, |       flowId: { type: 'string', format: 'uuid' }, | ||||||
|       testRun: { type: 'boolean', default: false }, |       testRun: { type: 'boolean', default: false }, | ||||||
|       internalId: { type: 'string' }, |       internalId: { type: 'string' }, | ||||||
|  |       deletedAt: { type: 'string' }, | ||||||
|  |       createdAt: { type: 'string' }, | ||||||
|  |       updatedAt: { type: 'string' }, | ||||||
|     }, |     }, | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -19,7 +19,7 @@ class Flow extends Base { | |||||||
|   status: 'paused' | 'published' | 'draft'; |   status: 'paused' | 'published' | 'draft'; | ||||||
|   steps: Step[]; |   steps: Step[]; | ||||||
|   triggerStep: Step; |   triggerStep: Step; | ||||||
|   published_at: string; |   publishedAt: string; | ||||||
|   remoteWebhookId: string; |   remoteWebhookId: string; | ||||||
|   executions?: Execution[]; |   executions?: Execution[]; | ||||||
|   lastExecution?: Execution; |   lastExecution?: Execution; | ||||||
| @@ -37,6 +37,10 @@ class Flow extends Base { | |||||||
|       userId: { type: 'string', format: 'uuid' }, |       userId: { type: 'string', format: 'uuid' }, | ||||||
|       remoteWebhookId: { type: 'string' }, |       remoteWebhookId: { type: 'string' }, | ||||||
|       active: { type: 'boolean' }, |       active: { type: 'boolean' }, | ||||||
|  |       publishedAt: { type: 'string' }, | ||||||
|  |       deletedAt: { type: 'string' }, | ||||||
|  |       createdAt: { type: 'string' }, | ||||||
|  |       updatedAt: { type: 'string' }, | ||||||
|     }, |     }, | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										53
									
								
								packages/backend/src/models/identity.ee.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								packages/backend/src/models/identity.ee.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | |||||||
|  | import Base from './base'; | ||||||
|  | import SamlAuthProvider from './saml-auth-provider.ee'; | ||||||
|  | import User from './user'; | ||||||
|  |  | ||||||
|  | class Identity extends Base { | ||||||
|  |   id!: string; | ||||||
|  |   remoteId!: string; | ||||||
|  |   userId!: string; | ||||||
|  |   providerId!: string; | ||||||
|  |   providerType!: 'saml'; | ||||||
|  |  | ||||||
|  |   static tableName = 'identities'; | ||||||
|  |  | ||||||
|  |   static jsonSchema = { | ||||||
|  |     type: 'object', | ||||||
|  |     required: [ | ||||||
|  |       'providerId', | ||||||
|  |       'remoteId', | ||||||
|  |       'userId', | ||||||
|  |       'providerType', | ||||||
|  |     ], | ||||||
|  |  | ||||||
|  |     properties: { | ||||||
|  |       id: { type: 'string', format: 'uuid' }, | ||||||
|  |       userId: { type: 'string', format: 'uuid' }, | ||||||
|  |       remoteId: { type: 'string', minLength: 1 }, | ||||||
|  |       providerId: { type: 'string', format: 'uuid' }, | ||||||
|  |       providerType: { type: 'string', enum: ['saml'] }, | ||||||
|  |     }, | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   static relationMappings = () => ({ | ||||||
|  |     user: { | ||||||
|  |       relation: Base.BelongsToOneRelation, | ||||||
|  |       modelClass: User, | ||||||
|  |       join: { | ||||||
|  |         from: 'users.id', | ||||||
|  |         to: 'identities.user_id', | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |     samlAuthProvider: { | ||||||
|  |       relation: Base.BelongsToOneRelation, | ||||||
|  |       modelClass: SamlAuthProvider, | ||||||
|  |       join: { | ||||||
|  |         from: 'saml_auth_providers.id', | ||||||
|  |         to: 'identities.provider_id' | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default Identity; | ||||||
							
								
								
									
										26
									
								
								packages/backend/src/models/permission.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								packages/backend/src/models/permission.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | import Base from './base'; | ||||||
|  |  | ||||||
|  | class Permission extends Base { | ||||||
|  |   id: string; | ||||||
|  |   action: string; | ||||||
|  |   subject: string; | ||||||
|  |   conditions: string[]; | ||||||
|  |  | ||||||
|  |   static tableName = 'permissions'; | ||||||
|  |  | ||||||
|  |   static jsonSchema = { | ||||||
|  |     type: 'object', | ||||||
|  |     required: ['action', 'subject'], | ||||||
|  |  | ||||||
|  |     properties: { | ||||||
|  |       id: { type: 'string', format: 'uuid' }, | ||||||
|  |       action: { type: 'string', minLength: 1 }, | ||||||
|  |       subject: { type: 'string', minLength: 1 }, | ||||||
|  |       conditions: { type: 'array', items: { type: 'string' } }, | ||||||
|  |       createdAt: { type: 'string' }, | ||||||
|  |       updatedAt: { type: 'string' }, | ||||||
|  |     }, | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default Permission; | ||||||
| @@ -1,6 +1,7 @@ | |||||||
| import { | import { | ||||||
|   Model, |   Model, | ||||||
|   Page, |   Page, | ||||||
|  |   ModelClass, | ||||||
|   PartialModelObject, |   PartialModelObject, | ||||||
|   ForClassMethod, |   ForClassMethod, | ||||||
|   AnyQueryBuilder, |   AnyQueryBuilder, | ||||||
| @@ -8,6 +9,10 @@ import { | |||||||
|  |  | ||||||
| const DELETED_COLUMN_NAME = 'deleted_at'; | const DELETED_COLUMN_NAME = 'deleted_at'; | ||||||
|  |  | ||||||
|  | const supportsSoftDeletion = (modelClass: ModelClass<any>) => { | ||||||
|  |   return modelClass.jsonSchema.properties.deletedAt; | ||||||
|  | } | ||||||
|  |  | ||||||
| const buildQueryBuidlerForClass = (): ForClassMethod => { | const buildQueryBuidlerForClass = (): ForClassMethod => { | ||||||
|   return (modelClass) => { |   return (modelClass) => { | ||||||
|     const qb: AnyQueryBuilder = Model.QueryBuilder.forClass.call( |     const qb: AnyQueryBuilder = Model.QueryBuilder.forClass.call( | ||||||
| @@ -15,7 +20,7 @@ const buildQueryBuidlerForClass = (): ForClassMethod => { | |||||||
|       modelClass |       modelClass | ||||||
|     ); |     ); | ||||||
|     qb.onBuild((builder) => { |     qb.onBuild((builder) => { | ||||||
|       if (!builder.context().withSoftDeleted) { |       if (!builder.context().withSoftDeleted && supportsSoftDeletion(qb.modelClass())) { | ||||||
|         builder.whereNull( |         builder.whereNull( | ||||||
|           `${qb.modelClass().tableName}.${DELETED_COLUMN_NAME}` |           `${qb.modelClass().tableName}.${DELETED_COLUMN_NAME}` | ||||||
|         ); |         ); | ||||||
| @@ -38,9 +43,13 @@ class ExtendedQueryBuilder<M extends Model, R = M[]> extends Model.QueryBuilder< | |||||||
|   static forClass: ForClassMethod = buildQueryBuidlerForClass(); |   static forClass: ForClassMethod = buildQueryBuidlerForClass(); | ||||||
|  |  | ||||||
|   delete() { |   delete() { | ||||||
|     return this.patch({ |     if (supportsSoftDeletion(this.modelClass())) { | ||||||
|       [DELETED_COLUMN_NAME]: new Date().toISOString(), |       return this.patch({ | ||||||
|     } as unknown as PartialModelObject<M>); |         [DELETED_COLUMN_NAME]: new Date().toISOString(), | ||||||
|  |       } as unknown as PartialModelObject<M>); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return super.delete(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   hardDelete() { |   hardDelete() { | ||||||
|   | |||||||
							
								
								
									
										61
									
								
								packages/backend/src/models/role.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								packages/backend/src/models/role.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | |||||||
|  | import Base from './base'; | ||||||
|  | import Permission from './permission'; | ||||||
|  | import User from './user'; | ||||||
|  |  | ||||||
|  | class Role extends Base { | ||||||
|  |   id!: string; | ||||||
|  |   name!: string; | ||||||
|  |   key: string; | ||||||
|  |   description: string; | ||||||
|  |   users?: User[]; | ||||||
|  |   permissions?: Permission[]; | ||||||
|  |  | ||||||
|  |   static tableName = 'roles'; | ||||||
|  |  | ||||||
|  |   static jsonSchema = { | ||||||
|  |     type: 'object', | ||||||
|  |     required: ['name', 'key'], | ||||||
|  |  | ||||||
|  |     properties: { | ||||||
|  |       id: { type: 'string', format: 'uuid' }, | ||||||
|  |       name: { type: 'string', minLength: 1 }, | ||||||
|  |       key: { type: 'string', minLength: 1 }, | ||||||
|  |       description: { type: ['string', 'null'], minLength: 1, maxLength: 255 }, | ||||||
|  |       createdAt: { type: 'string' }, | ||||||
|  |       updatedAt: { type: 'string' }, | ||||||
|  |     }, | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   static get virtualAttributes() { | ||||||
|  |     return ['isAdmin']; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static relationMappings = () => ({ | ||||||
|  |     users: { | ||||||
|  |       relation: Base.HasManyRelation, | ||||||
|  |       modelClass: User, | ||||||
|  |       join: { | ||||||
|  |         from: 'roles.id', | ||||||
|  |         to: 'users.role_id', | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |     permissions: { | ||||||
|  |       relation: Base.ManyToManyRelation, | ||||||
|  |       modelClass: Permission, | ||||||
|  |       join: { | ||||||
|  |         from: 'roles.id', | ||||||
|  |         through: { | ||||||
|  |           from: 'roles_permissions.role_id', | ||||||
|  |           to: 'roles_permissions.permission_id', | ||||||
|  |         }, | ||||||
|  |         to: 'permissions.id', | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   get isAdmin() { | ||||||
|  |     return this.key === 'admin'; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default Role; | ||||||
							
								
								
									
										79
									
								
								packages/backend/src/models/saml-auth-provider.ee.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								packages/backend/src/models/saml-auth-provider.ee.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | |||||||
|  | import { URL } from 'node:url'; | ||||||
|  | import type { SamlConfig } from '@node-saml/passport-saml'; | ||||||
|  | import appConfig from '../config/app'; | ||||||
|  | import Base from './base'; | ||||||
|  | import Identity from './identity.ee'; | ||||||
|  |  | ||||||
|  | class SamlAuthProvider extends Base { | ||||||
|  |   id!: string; | ||||||
|  |   name: string; | ||||||
|  |   certificate: string; | ||||||
|  |   signatureAlgorithm: SamlConfig["signatureAlgorithm"]; | ||||||
|  |   issuer: string; | ||||||
|  |   entryPoint: string; | ||||||
|  |   firstnameAttributeName: string; | ||||||
|  |   surnameAttributeName: string; | ||||||
|  |   emailAttributeName: string; | ||||||
|  |   roleAttributeName: string; | ||||||
|  |   defaultRoleId: string; | ||||||
|  |  | ||||||
|  |   static tableName = 'saml_auth_providers'; | ||||||
|  |  | ||||||
|  |   static jsonSchema = { | ||||||
|  |     type: 'object', | ||||||
|  |     required: [ | ||||||
|  |       'name', | ||||||
|  |       'certificate', | ||||||
|  |       'signatureAlgorithm', | ||||||
|  |       'entryPoint', | ||||||
|  |       'issuer', | ||||||
|  |       'firstnameAttributeName', | ||||||
|  |       'surnameAttributeName', | ||||||
|  |       'emailAttributeName', | ||||||
|  |       'roleAttributeName', | ||||||
|  |       'defaultRoleId', | ||||||
|  |     ], | ||||||
|  |  | ||||||
|  |     properties: { | ||||||
|  |       id: { type: 'string', format: 'uuid' }, | ||||||
|  |       name: { type: 'string', minLength: 1 }, | ||||||
|  |       certificate: { type: 'string', minLength: 1 }, | ||||||
|  |       signatureAlgorithm: { type: 'string', enum: ['sha1', 'sha256', 'sha512'] }, | ||||||
|  |       issuer: { type: 'string', minLength: 1 }, | ||||||
|  |       entryPoint: { type: 'string', minLength: 1 }, | ||||||
|  |       firstnameAttributeName: { type: 'string', minLength: 1 }, | ||||||
|  |       surnameAttributeName: { type: 'string', minLength: 1 }, | ||||||
|  |       emailAttributeName: { type: 'string', minLength: 1 }, | ||||||
|  |       roleAttributeName: { type: 'string', minLength: 1 }, | ||||||
|  |       defaultRoleId: { type: 'string', format: 'uuid' } | ||||||
|  |     }, | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   static relationMappings = () => ({ | ||||||
|  |     identities: { | ||||||
|  |       relation: Base.HasOneRelation, | ||||||
|  |       modelClass: Identity, | ||||||
|  |       join: { | ||||||
|  |         from: 'identities.provider_id', | ||||||
|  |         to: 'saml_auth_providers.id', | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   get config(): SamlConfig { | ||||||
|  |     const callbackUrl = new URL( | ||||||
|  |       `/login/saml/${this.issuer}/callback`, | ||||||
|  |       appConfig.baseUrl | ||||||
|  |     ).toString(); | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |       callbackUrl, | ||||||
|  |       cert: this.certificate, | ||||||
|  |       entryPoint: this.entryPoint, | ||||||
|  |       issuer: this.issuer, | ||||||
|  |       signatureAlgorithm: this.signatureAlgorithm, | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default SamlAuthProvider; | ||||||
| @@ -46,6 +46,9 @@ class Step extends Base { | |||||||
|       position: { type: 'integer' }, |       position: { type: 'integer' }, | ||||||
|       parameters: { type: 'object' }, |       parameters: { type: 'object' }, | ||||||
|       webhookPath: { type: ['string', 'null'] }, |       webhookPath: { type: ['string', 'null'] }, | ||||||
|  |       deletedAt: { type: 'string' }, | ||||||
|  |       createdAt: { type: 'string' }, | ||||||
|  |       updatedAt: { type: 'string' }, | ||||||
|     }, |     }, | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -46,6 +46,9 @@ class Subscription extends Base { | |||||||
|       nextBillDate: { type: 'string' }, |       nextBillDate: { type: 'string' }, | ||||||
|       lastBillDate: { type: 'string' }, |       lastBillDate: { type: 'string' }, | ||||||
|       cancellationEffectiveDate: { type: 'string' }, |       cancellationEffectiveDate: { type: 'string' }, | ||||||
|  |       deletedAt: { type: 'string' }, | ||||||
|  |       createdAt: { type: 'string' }, | ||||||
|  |       updatedAt: { type: 'string' }, | ||||||
|     }, |     }, | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
| @@ -84,7 +87,7 @@ class Subscription extends Base { | |||||||
|     return ( |     return ( | ||||||
|       this.status === 'deleted' && |       this.status === 'deleted' && | ||||||
|       Number(this.cancellationEffectiveDate) > |       Number(this.cancellationEffectiveDate) > | ||||||
|         DateTime.now().startOf('day').toMillis() |       DateTime.now().startOf('day').toMillis() | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -24,6 +24,9 @@ class UsageData extends Base { | |||||||
|       subscriptionId: { type: 'string', format: 'uuid' }, |       subscriptionId: { type: 'string', format: 'uuid' }, | ||||||
|       consumedTaskCount: { type: 'integer' }, |       consumedTaskCount: { type: 'integer' }, | ||||||
|       nextResetAt: { type: 'string' }, |       nextResetAt: { type: 'string' }, | ||||||
|  |       deletedAt: { type: 'string' }, | ||||||
|  |       createdAt: { type: 'string' }, | ||||||
|  |       updatedAt: { type: 'string' }, | ||||||
|     }, |     }, | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,14 +1,20 @@ | |||||||
|  | import crypto from 'node:crypto'; | ||||||
| import { QueryContext, ModelOptions } from 'objection'; | import { QueryContext, ModelOptions } from 'objection'; | ||||||
| import bcrypt from 'bcrypt'; | import bcrypt from 'bcrypt'; | ||||||
| import crypto from 'crypto'; |  | ||||||
| import { DateTime } from 'luxon'; | import { DateTime } from 'luxon'; | ||||||
|  | import { PureAbility, fieldPatternMatcher, mongoQueryMatcher } from '@casl/ability'; | ||||||
|  | import type { Subject } from '@casl/ability'; | ||||||
|  |  | ||||||
| import appConfig from '../config/app'; | import appConfig from '../config/app'; | ||||||
| import Base from './base'; | import Base from './base'; | ||||||
| import ExtendedQueryBuilder from './query-builder'; | import ExtendedQueryBuilder from './query-builder'; | ||||||
| import Connection from './connection'; | import Connection from './connection'; | ||||||
| import Flow from './flow'; | import Flow from './flow'; | ||||||
| import Step from './step'; | import Step from './step'; | ||||||
|  | import Role from './role'; | ||||||
|  | import Permission from './permission'; | ||||||
| import Execution from './execution'; | import Execution from './execution'; | ||||||
|  | import Identity from './identity.ee'; | ||||||
| import UsageData from './usage-data.ee'; | import UsageData from './usage-data.ee'; | ||||||
| import Subscription from './subscription.ee'; | import Subscription from './subscription.ee'; | ||||||
|  |  | ||||||
| @@ -16,8 +22,8 @@ class User extends Base { | |||||||
|   id!: string; |   id!: string; | ||||||
|   fullName!: string; |   fullName!: string; | ||||||
|   email!: string; |   email!: string; | ||||||
|  |   roleId: string; | ||||||
|   password!: string; |   password!: string; | ||||||
|   role: string; |  | ||||||
|   resetPasswordToken: string; |   resetPasswordToken: string; | ||||||
|   resetPasswordTokenSentAt: string; |   resetPasswordTokenSentAt: string; | ||||||
|   trialExpiryDate: string; |   trialExpiryDate: string; | ||||||
| @@ -29,19 +35,28 @@ class User extends Base { | |||||||
|   currentUsageData?: UsageData; |   currentUsageData?: UsageData; | ||||||
|   subscriptions?: Subscription[]; |   subscriptions?: Subscription[]; | ||||||
|   currentSubscription?: Subscription; |   currentSubscription?: Subscription; | ||||||
|  |   role: Role; | ||||||
|  |   permissions: Permission[]; | ||||||
|  |   identities: Identity[]; | ||||||
|  |  | ||||||
|   static tableName = 'users'; |   static tableName = 'users'; | ||||||
|  |  | ||||||
|   static jsonSchema = { |   static jsonSchema = { | ||||||
|     type: 'object', |     type: 'object', | ||||||
|     required: ['fullName', 'email', 'password'], |     required: ['fullName', 'email'], | ||||||
|  |  | ||||||
|     properties: { |     properties: { | ||||||
|       id: { type: 'string', format: 'uuid' }, |       id: { type: 'string', format: 'uuid' }, | ||||||
|       fullName: { type: 'string', minLength: 1 }, |       fullName: { type: 'string', minLength: 1 }, | ||||||
|       email: { type: 'string', format: 'email', minLength: 1, maxLength: 255 }, |       email: { type: 'string', format: 'email', minLength: 1, maxLength: 255 }, | ||||||
|       password: { type: 'string', minLength: 1, maxLength: 255 }, |       password: { type: 'string' }, | ||||||
|       role: { type: 'string', enum: ['admin', 'user'] }, |       resetPasswordToken: { type: 'string' }, | ||||||
|  |       resetPasswordTokenSentAt: { type: 'string' }, | ||||||
|  |       trialExpiryDate: { type: 'string' }, | ||||||
|  |       roleId: { type: 'string', format: 'uuid' }, | ||||||
|  |       deletedAt: { type: 'string' }, | ||||||
|  |       createdAt: { type: 'string' }, | ||||||
|  |       updatedAt: { type: 'string' }, | ||||||
|     }, |     }, | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
| @@ -124,6 +139,34 @@ class User extends Base { | |||||||
|         builder.orderBy('created_at', 'desc').limit(1).first(); |         builder.orderBy('created_at', 'desc').limit(1).first(); | ||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|  |     role: { | ||||||
|  |       relation: Base.HasOneRelation, | ||||||
|  |       modelClass: Role, | ||||||
|  |       join: { | ||||||
|  |         from: 'roles.id', | ||||||
|  |         to: 'users.role_id', | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |     permissions: { | ||||||
|  |       relation: Base.ManyToManyRelation, | ||||||
|  |       modelClass: Permission, | ||||||
|  |       join: { | ||||||
|  |         from: 'users.role_id', | ||||||
|  |         through: { | ||||||
|  |           from: 'roles_permissions.role_id', | ||||||
|  |           to: 'roles_permissions.permission_id', | ||||||
|  |         }, | ||||||
|  |         to: 'permissions.id', | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |     identities: { | ||||||
|  |       relation: Base.HasManyRelation, | ||||||
|  |       modelClass: Identity, | ||||||
|  |       join: { | ||||||
|  |         from: 'identities.user_id', | ||||||
|  |         to: 'users.id', | ||||||
|  |       } | ||||||
|  |     } | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   login(password: string) { |   login(password: string) { | ||||||
| @@ -158,7 +201,9 @@ class User extends Base { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   async generateHash() { |   async generateHash() { | ||||||
|     this.password = await bcrypt.hash(this.password, 10); |     if (this.password) { | ||||||
|  |       this.password = await bcrypt.hash(this.password, 10); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async startTrialPeriod() { |   async startTrialPeriod() { | ||||||
| @@ -232,9 +277,7 @@ class User extends Base { | |||||||
|   async $beforeUpdate(opt: ModelOptions, queryContext: QueryContext) { |   async $beforeUpdate(opt: ModelOptions, queryContext: QueryContext) { | ||||||
|     await super.$beforeUpdate(opt, queryContext); |     await super.$beforeUpdate(opt, queryContext); | ||||||
|  |  | ||||||
|     if (this.password) { |     await this.generateHash(); | ||||||
|       await this.generateHash(); |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async $afterInsert(queryContext: QueryContext) { |   async $afterInsert(queryContext: QueryContext) { | ||||||
| @@ -248,6 +291,34 @@ class User extends Base { | |||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   get ability() { | ||||||
|  |     if (!this.permissions) { | ||||||
|  |       throw new Error('User.permissions must be fetched!'); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // We're not using mongo, but our fields, conditions match | ||||||
|  |     return new PureAbility(this.permissions, { | ||||||
|  |       conditionsMatcher: mongoQueryMatcher, | ||||||
|  |       fieldMatcher: fieldPatternMatcher | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   can(action: string, subject: Subject) { | ||||||
|  |     const can = this.ability.can(action, subject); | ||||||
|  |  | ||||||
|  |     if (!can) throw new Error('Not authorized!'); | ||||||
|  |  | ||||||
|  |     return can; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   cannot(action: string, subject: Subject) { | ||||||
|  |     const cannot = this.ability.cannot(action, subject); | ||||||
|  |  | ||||||
|  |     if (cannot) throw new Error('Not authorized!'); | ||||||
|  |  | ||||||
|  |     return cannot; | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| export default User; | export default User; | ||||||
|   | |||||||
							
								
								
									
										23
									
								
								packages/types/index.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										23
									
								
								packages/types/index.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -95,6 +95,15 @@ export interface IUser { | |||||||
|   connections: IConnection[]; |   connections: IConnection[]; | ||||||
|   flows: IFlow[]; |   flows: IFlow[]; | ||||||
|   steps: IStep[]; |   steps: IStep[]; | ||||||
|  |   role: IRole; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface IRole { | ||||||
|  |   id: string; | ||||||
|  |   key: string; | ||||||
|  |   name: string; | ||||||
|  |   description: string; | ||||||
|  |   isAdmin: boolean; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface IFieldDropdown { | export interface IFieldDropdown { | ||||||
| @@ -386,6 +395,20 @@ type TInvoice = { | |||||||
|   receipt_url: string; |   receipt_url: string; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | type TSamlAuthProvider = { | ||||||
|  |   id: string; | ||||||
|  |   name: string; | ||||||
|  |   certificate: string; | ||||||
|  |   signatureAlgorithm: "sha1" | "sha256" | "sha512"; | ||||||
|  |   issuer: string; | ||||||
|  |   entryPoint: string; | ||||||
|  |   firstnameAttributeName: string; | ||||||
|  |   surnameAttributeName: string; | ||||||
|  |   emailAttributeName: string; | ||||||
|  |   roleAttributeName: string; | ||||||
|  |   defaultRoleId: string; | ||||||
|  | } | ||||||
|  |  | ||||||
| declare module 'axios' { | declare module 'axios' { | ||||||
|   interface AxiosResponse { |   interface AxiosResponse { | ||||||
|     httpError?: IJSONObject; |     httpError?: IJSONObject; | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| PORT=3001 | PORT=3001 | ||||||
|  | REACT_APP_API_URL=http://localhost:3000 | ||||||
| REACT_APP_GRAPHQL_URL=http://localhost:3000/graphql | REACT_APP_GRAPHQL_URL=http://localhost:3000/graphql | ||||||
| # HTTPS=true | # HTTPS=true | ||||||
| REACT_APP_BASE_URL=http://localhost:3001 | REACT_APP_BASE_URL=http://localhost:3001 | ||||||
|   | |||||||
							
								
								
									
										73
									
								
								packages/web/src/adminSettingsRoutes.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								packages/web/src/adminSettingsRoutes.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | |||||||
|  | import { Route, Navigate } from 'react-router-dom'; | ||||||
|  | import AdminSettingsLayout from 'components/AdminSettingsLayout'; | ||||||
|  | import Users from 'pages/Users'; | ||||||
|  | import EditUser from 'pages/EditUser'; | ||||||
|  | import CreateUser from 'pages/CreateUser'; | ||||||
|  | import Roles from 'pages/Roles/index.ee'; | ||||||
|  | import CreateRole from 'pages/CreateRole/index.ee'; | ||||||
|  | import EditRole from 'pages/EditRole/index.ee'; | ||||||
|  |  | ||||||
|  | import * as URLS from 'config/urls'; | ||||||
|  |  | ||||||
|  | export default ( | ||||||
|  |   <> | ||||||
|  |     <Route | ||||||
|  |       path={URLS.USERS} | ||||||
|  |       element={ | ||||||
|  |         <AdminSettingsLayout> | ||||||
|  |           <Users /> | ||||||
|  |         </AdminSettingsLayout> | ||||||
|  |       } | ||||||
|  |     /> | ||||||
|  |  | ||||||
|  |     <Route | ||||||
|  |       path={URLS.CREATE_USER} | ||||||
|  |       element={ | ||||||
|  |         <AdminSettingsLayout> | ||||||
|  |           <CreateUser /> | ||||||
|  |         </AdminSettingsLayout> | ||||||
|  |       } | ||||||
|  |     /> | ||||||
|  |  | ||||||
|  |     <Route | ||||||
|  |       path={URLS.USER_PATTERN} | ||||||
|  |       element={ | ||||||
|  |         <AdminSettingsLayout> | ||||||
|  |           <EditUser /> | ||||||
|  |         </AdminSettingsLayout> | ||||||
|  |       } | ||||||
|  |     /> | ||||||
|  |  | ||||||
|  |     <Route | ||||||
|  |       path={URLS.ROLES} | ||||||
|  |       element={ | ||||||
|  |         <AdminSettingsLayout> | ||||||
|  |           <Roles /> | ||||||
|  |         </AdminSettingsLayout> | ||||||
|  |       } | ||||||
|  |     /> | ||||||
|  |  | ||||||
|  |     <Route | ||||||
|  |       path={URLS.CREATE_ROLE} | ||||||
|  |       element={ | ||||||
|  |         <AdminSettingsLayout> | ||||||
|  |           <CreateRole /> | ||||||
|  |         </AdminSettingsLayout> | ||||||
|  |       } | ||||||
|  |     /> | ||||||
|  |  | ||||||
|  |     <Route | ||||||
|  |       path={URLS.ROLE_PATTERN} | ||||||
|  |       element={ | ||||||
|  |         <AdminSettingsLayout> | ||||||
|  |           <EditRole /> | ||||||
|  |         </AdminSettingsLayout> | ||||||
|  |       } | ||||||
|  |     /> | ||||||
|  |  | ||||||
|  |     <Route | ||||||
|  |       path={URLS.ADMIN_SETTINGS} | ||||||
|  |       element={<Navigate to={URLS.USERS} replace />} | ||||||
|  |     /> | ||||||
|  |   </> | ||||||
|  | ); | ||||||
| @@ -54,6 +54,10 @@ function AccountDropdownMenu( | |||||||
|         {formatMessage('accountDropdownMenu.settings')} |         {formatMessage('accountDropdownMenu.settings')} | ||||||
|       </MenuItem> |       </MenuItem> | ||||||
|  |  | ||||||
|  |       <MenuItem component={Link} to={URLS.ADMIN_SETTINGS_DASHBOARD}> | ||||||
|  |         {formatMessage('accountDropdownMenu.adminSettings')} | ||||||
|  |       </MenuItem> | ||||||
|  |  | ||||||
|       <MenuItem onClick={logout} data-test="logout-item"> |       <MenuItem onClick={logout} data-test="logout-item"> | ||||||
|         {formatMessage('accountDropdownMenu.logout')} |         {formatMessage('accountDropdownMenu.logout')} | ||||||
|       </MenuItem> |       </MenuItem> | ||||||
|   | |||||||
							
								
								
									
										81
									
								
								packages/web/src/components/AdminSettingsLayout/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								packages/web/src/components/AdminSettingsLayout/index.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | |||||||
|  | import * as React from 'react'; | ||||||
|  | import Box from '@mui/material/Box'; | ||||||
|  | import Toolbar from '@mui/material/Toolbar'; | ||||||
|  | import { useTheme } from '@mui/material/styles'; | ||||||
|  | import useMediaQuery from '@mui/material/useMediaQuery'; | ||||||
|  | import GroupIcon from '@mui/icons-material/Group'; | ||||||
|  | import GroupsIcon from '@mui/icons-material/Groups'; | ||||||
|  | import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'; | ||||||
|  |  | ||||||
|  | import * as URLS from 'config/urls'; | ||||||
|  | import useAutomatischInfo from 'hooks/useAutomatischInfo'; | ||||||
|  | import AppBar from 'components/AppBar'; | ||||||
|  | import Drawer from 'components/Drawer'; | ||||||
|  |  | ||||||
|  | type SettingsLayoutProps = { | ||||||
|  |   children: React.ReactNode; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | function createDrawerLinks({ isCloud }: { isCloud: boolean }) { | ||||||
|  |   const items = [ | ||||||
|  |     { | ||||||
|  |       Icon: GroupIcon, | ||||||
|  |       primary: 'adminSettingsDrawer.users', | ||||||
|  |       to: URLS.USERS, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       Icon: GroupsIcon, | ||||||
|  |       primary: 'adminSettingsDrawer.roles', | ||||||
|  |       to: URLS.ROLES, | ||||||
|  |     } | ||||||
|  |   ] | ||||||
|  |  | ||||||
|  |   return items; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const drawerBottomLinks = [ | ||||||
|  |   { | ||||||
|  |     Icon: ArrowBackIosNewIcon, | ||||||
|  |     primary: 'adminSettingsDrawer.goBack', | ||||||
|  |     to: '/', | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | export default function SettingsLayout({ | ||||||
|  |   children, | ||||||
|  | }: SettingsLayoutProps): React.ReactElement { | ||||||
|  |   const { isCloud } = useAutomatischInfo(); | ||||||
|  |   const theme = useTheme(); | ||||||
|  |   const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg')); | ||||||
|  |   const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens); | ||||||
|  |  | ||||||
|  |   const openDrawer = () => setDrawerOpen(true); | ||||||
|  |   const closeDrawer = () => setDrawerOpen(false); | ||||||
|  |   const drawerLinks = createDrawerLinks({ isCloud }); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <AppBar | ||||||
|  |         drawerOpen={isDrawerOpen} | ||||||
|  |         onDrawerOpen={openDrawer} | ||||||
|  |         onDrawerClose={closeDrawer} | ||||||
|  |       /> | ||||||
|  |  | ||||||
|  |       <Box sx={{ display: 'flex' }}> | ||||||
|  |         <Drawer | ||||||
|  |           links={drawerLinks} | ||||||
|  |           bottomLinks={drawerBottomLinks} | ||||||
|  |           open={isDrawerOpen} | ||||||
|  |           onOpen={openDrawer} | ||||||
|  |           onClose={closeDrawer} | ||||||
|  |         /> | ||||||
|  |  | ||||||
|  |         <Box sx={{ flex: 1 }}> | ||||||
|  |           <Toolbar /> | ||||||
|  |  | ||||||
|  |           {children} | ||||||
|  |         </Box> | ||||||
|  |       </Box> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | } | ||||||
| @@ -19,6 +19,7 @@ export default function ConditionalIconButton(props: any): React.ReactElement { | |||||||
|         type={buttonProps.type} |         type={buttonProps.type} | ||||||
|         size={buttonProps.size} |         size={buttonProps.size} | ||||||
|         component={buttonProps.component} |         component={buttonProps.component} | ||||||
|  |         to={buttonProps.to} | ||||||
|       > |       > | ||||||
|         {icon} |         {icon} | ||||||
|       </IconButton> |       </IconButton> | ||||||
|   | |||||||
							
								
								
									
										58
									
								
								packages/web/src/components/ConfirmationDialog/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								packages/web/src/components/ConfirmationDialog/index.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | |||||||
|  | import * as React from 'react'; | ||||||
|  | import Button from '@mui/material/Button'; | ||||||
|  | import Dialog from '@mui/material/Dialog'; | ||||||
|  | import DialogActions from '@mui/material/DialogActions'; | ||||||
|  | import DialogContent from '@mui/material/DialogContent'; | ||||||
|  | import DialogContentText from '@mui/material/DialogContentText'; | ||||||
|  | import DialogTitle from '@mui/material/DialogTitle'; | ||||||
|  |  | ||||||
|  | type ConfirmationDialogProps = { | ||||||
|  |   onClose: () => void; | ||||||
|  |   onConfirm: () => void; | ||||||
|  |   title: React.ReactNode; | ||||||
|  |   description: React.ReactNode; | ||||||
|  |   cancelButtonChildren: React.ReactNode; | ||||||
|  |   confirmButtionChildren: React.ReactNode; | ||||||
|  |   open?: boolean; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default function ConfirmationDialog(props: ConfirmationDialogProps) { | ||||||
|  |   const { | ||||||
|  |     onClose, | ||||||
|  |     onConfirm, | ||||||
|  |     title, | ||||||
|  |     description, | ||||||
|  |     cancelButtonChildren, | ||||||
|  |     confirmButtionChildren, | ||||||
|  |     open = true, | ||||||
|  |   } = props; | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Dialog open={open} onClose={onClose}> | ||||||
|  |       {title && ( | ||||||
|  |         <DialogTitle> | ||||||
|  |           {title} | ||||||
|  |         </DialogTitle> | ||||||
|  |       )} | ||||||
|  |       {description && ( | ||||||
|  |         <DialogContent> | ||||||
|  |           <DialogContentText> | ||||||
|  |             {description} | ||||||
|  |           </DialogContentText> | ||||||
|  |         </DialogContent> | ||||||
|  |       )} | ||||||
|  |  | ||||||
|  |       <DialogActions> | ||||||
|  |         {(cancelButtonChildren && onClose) && ( | ||||||
|  |           <Button onClick={onClose}>{cancelButtonChildren}</Button> | ||||||
|  |         )} | ||||||
|  |  | ||||||
|  |         {(confirmButtionChildren && onConfirm) && ( | ||||||
|  |           <Button onClick={onConfirm} color="error"> | ||||||
|  |             {confirmButtionChildren} | ||||||
|  |           </Button> | ||||||
|  |         )} | ||||||
|  |       </DialogActions> | ||||||
|  |     </Dialog> | ||||||
|  |   ); | ||||||
|  | } | ||||||
| @@ -1,16 +1,11 @@ | |||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { useNavigate } from 'react-router-dom'; | import { useNavigate } from 'react-router-dom'; | ||||||
| import { useMutation } from '@apollo/client'; | import { useMutation } from '@apollo/client'; | ||||||
| import Button from '@mui/material/Button'; |  | ||||||
| import Dialog from '@mui/material/Dialog'; |  | ||||||
| import DialogActions from '@mui/material/DialogActions'; |  | ||||||
| import DialogContent from '@mui/material/DialogContent'; |  | ||||||
| import DialogContentText from '@mui/material/DialogContentText'; |  | ||||||
| import DialogTitle from '@mui/material/DialogTitle'; |  | ||||||
|  |  | ||||||
| import * as URLS from 'config/urls'; | import * as URLS from 'config/urls'; | ||||||
|  | import ConfirmationDialog from 'components/ConfirmationDialog'; | ||||||
| import apolloClient from 'graphql/client'; | import apolloClient from 'graphql/client'; | ||||||
| import { DELETE_USER } from 'graphql/mutations/delete-user.ee'; | import { DELETE_CURRENT_USER } from 'graphql/mutations/delete-current-user.ee'; | ||||||
| import useAuthentication from 'hooks/useAuthentication'; | import useAuthentication from 'hooks/useAuthentication'; | ||||||
| import useFormatMessage from 'hooks/useFormatMessage'; | import useFormatMessage from 'hooks/useFormatMessage'; | ||||||
| import useCurrentUser from 'hooks/useCurrentUser'; | import useCurrentUser from 'hooks/useCurrentUser'; | ||||||
| @@ -20,37 +15,29 @@ type DeleteAccountDialogProps = { | |||||||
| } | } | ||||||
|  |  | ||||||
| export default function DeleteAccountDialog(props: DeleteAccountDialogProps) { | export default function DeleteAccountDialog(props: DeleteAccountDialogProps) { | ||||||
|   const [deleteUser] = useMutation(DELETE_USER); |   const [deleteCurrentUser] = useMutation(DELETE_CURRENT_USER); | ||||||
|   const formatMessage = useFormatMessage(); |   const formatMessage = useFormatMessage(); | ||||||
|   const currentUser = useCurrentUser(); |   const currentUser = useCurrentUser(); | ||||||
|   const authentication = useAuthentication(); |   const authentication = useAuthentication(); | ||||||
|   const navigate = useNavigate(); |   const navigate = useNavigate(); | ||||||
|  |  | ||||||
|   const handleConfirm = React.useCallback(async () => { |   const handleConfirm = React.useCallback(async () => { | ||||||
|     await deleteUser(); |     await deleteCurrentUser(); | ||||||
|  |  | ||||||
|     authentication.updateToken(''); |     authentication.updateToken(''); | ||||||
|     await apolloClient.clearStore(); |     await apolloClient.clearStore(); | ||||||
|  |  | ||||||
|     navigate(URLS.LOGIN); |     navigate(URLS.LOGIN); | ||||||
|   }, [deleteUser, currentUser]); |   }, [deleteCurrentUser, currentUser]); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Dialog open onClose={props.onClose}> |     <ConfirmationDialog | ||||||
|       <DialogTitle > |       title={formatMessage('deleteAccountDialog.title')} | ||||||
|         {formatMessage('deleteAccountDialog.title')} |       description={formatMessage('deleteAccountDialog.description')} | ||||||
|       </DialogTitle> |       onClose={props.onClose} | ||||||
|       <DialogContent> |       onConfirm={handleConfirm} | ||||||
|         <DialogContentText id="alert-dialog-description"> |       cancelButtonChildren={formatMessage('deleteAccountDialog.cancel')} | ||||||
|           {formatMessage('deleteAccountDialog.description')} |       confirmButtionChildren={formatMessage('deleteAccountDialog.confirm')} | ||||||
|         </DialogContentText> |     /> | ||||||
|       </DialogContent> |  | ||||||
|       <DialogActions> |  | ||||||
|         <Button onClick={props.onClose}>{formatMessage('deleteAccountDialog.cancel')}</Button> |  | ||||||
|         <Button onClick={handleConfirm} color="error"> |  | ||||||
|           {formatMessage('deleteAccountDialog.confirm')} |  | ||||||
|         </Button> |  | ||||||
|       </DialogActions> |  | ||||||
|     </Dialog> |  | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										46
									
								
								packages/web/src/components/DeleteRoleButton/index.ee.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								packages/web/src/components/DeleteRoleButton/index.ee.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | |||||||
|  | import * as React from 'react'; | ||||||
|  | import { useMutation } from '@apollo/client'; | ||||||
|  | import IconButton from '@mui/material/IconButton'; | ||||||
|  | import DeleteIcon from '@mui/icons-material/Delete'; | ||||||
|  |  | ||||||
|  | import ConfirmationDialog from 'components/ConfirmationDialog'; | ||||||
|  | import { DELETE_ROLE } from 'graphql/mutations/delete-role.ee'; | ||||||
|  | import useFormatMessage from 'hooks/useFormatMessage'; | ||||||
|  |  | ||||||
|  | type DeleteRoleButtonProps = { | ||||||
|  |   roleId: string; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default function DeleteRoleButton(props: DeleteRoleButtonProps) { | ||||||
|  |   const { roleId } = props; | ||||||
|  |   const [showConfirmation, setShowConfirmation] = React.useState(false); | ||||||
|  |   const [deleteRole] = useMutation(DELETE_ROLE, { | ||||||
|  |     variables: { input: { id: roleId } }, | ||||||
|  |     refetchQueries: ['GetRoles'], | ||||||
|  |   }); | ||||||
|  |   const formatMessage = useFormatMessage(); | ||||||
|  |  | ||||||
|  |   const handleConfirm = React.useCallback(async () => { | ||||||
|  |     await deleteRole(); | ||||||
|  |  | ||||||
|  |     setShowConfirmation(false); | ||||||
|  |   }, [deleteRole]); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <IconButton onClick={() => setShowConfirmation(true)} size="small"> | ||||||
|  |         <DeleteIcon /> | ||||||
|  |       </IconButton> | ||||||
|  |  | ||||||
|  |       <ConfirmationDialog | ||||||
|  |         open={showConfirmation} | ||||||
|  |         title={formatMessage('deleteRoleButton.title')} | ||||||
|  |         description={formatMessage('deleteRoleButton.description')} | ||||||
|  |         onClose={() => setShowConfirmation(false)} | ||||||
|  |         onConfirm={handleConfirm} | ||||||
|  |         cancelButtonChildren={formatMessage('deleteRoleButton.cancel')} | ||||||
|  |         confirmButtionChildren={formatMessage('deleteRoleButton.confirm')} | ||||||
|  |       /> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										46
									
								
								packages/web/src/components/DeleteUserButton/index.ee.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								packages/web/src/components/DeleteUserButton/index.ee.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | |||||||
|  | import * as React from 'react'; | ||||||
|  | import { useMutation } from '@apollo/client'; | ||||||
|  | import IconButton from '@mui/material/IconButton'; | ||||||
|  | import DeleteIcon from '@mui/icons-material/Delete'; | ||||||
|  |  | ||||||
|  | import ConfirmationDialog from 'components/ConfirmationDialog'; | ||||||
|  | import { DELETE_USER } from 'graphql/mutations/delete-user.ee'; | ||||||
|  | import useFormatMessage from 'hooks/useFormatMessage'; | ||||||
|  |  | ||||||
|  | type DeleteUserButtonProps = { | ||||||
|  |   userId: string; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default function DeleteUserButton(props: DeleteUserButtonProps) { | ||||||
|  |   const { userId } = props; | ||||||
|  |   const [showConfirmation, setShowConfirmation] = React.useState(false); | ||||||
|  |   const [deleteUser] = useMutation(DELETE_USER, { | ||||||
|  |     variables: { input: { id: userId } }, | ||||||
|  |     refetchQueries: ['GetUsers'], | ||||||
|  |   }); | ||||||
|  |   const formatMessage = useFormatMessage(); | ||||||
|  |  | ||||||
|  |   const handleConfirm = React.useCallback(async () => { | ||||||
|  |     await deleteUser(); | ||||||
|  |  | ||||||
|  |     setShowConfirmation(false); | ||||||
|  |   }, [deleteUser]); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <IconButton onClick={() => setShowConfirmation(true)} size="small"> | ||||||
|  |         <DeleteIcon /> | ||||||
|  |       </IconButton> | ||||||
|  |  | ||||||
|  |       <ConfirmationDialog | ||||||
|  |         open={showConfirmation} | ||||||
|  |         title={formatMessage('deleteUserButton.title')} | ||||||
|  |         description={formatMessage('deleteUserButton.description')} | ||||||
|  |         onClose={() => setShowConfirmation(false)} | ||||||
|  |         onConfirm={handleConfirm} | ||||||
|  |         cancelButtonChildren={formatMessage('deleteUserButton.cancel')} | ||||||
|  |         confirmButtionChildren={formatMessage('deleteUserButton.confirm')} | ||||||
|  |       /> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										47
									
								
								packages/web/src/components/ListLoader/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								packages/web/src/components/ListLoader/index.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | |||||||
|  | import { | ||||||
|  |   IconButton, | ||||||
|  |   Skeleton, | ||||||
|  |   Stack, | ||||||
|  |   TableCell, | ||||||
|  |   TableRow, | ||||||
|  | } from '@mui/material'; | ||||||
|  | import EditIcon from '@mui/icons-material/Edit'; | ||||||
|  | import DeleteIcon from '@mui/icons-material/Delete'; | ||||||
|  |  | ||||||
|  | type ListLoaderProps = { | ||||||
|  |   rowsNumber: number; | ||||||
|  |   cellNumber: number; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const ListLoader = ({ rowsNumber, cellNumber }: ListLoaderProps) => { | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       {[...Array(rowsNumber)].map((row, index) => ( | ||||||
|  |         <TableRow | ||||||
|  |           key={index} | ||||||
|  |           sx={{ '&:last-child td, &:last-child th': { border: 0 } }} | ||||||
|  |         > | ||||||
|  |           {[...Array(cellNumber)].map((cell, index) => ( | ||||||
|  |             <TableCell key={index} scope="row"> | ||||||
|  |               <Skeleton /> | ||||||
|  |             </TableCell> | ||||||
|  |           ))} | ||||||
|  |  | ||||||
|  |           <TableCell> | ||||||
|  |             <Stack direction="row" gap={1} justifyContent="right"> | ||||||
|  |               <IconButton size="small"> | ||||||
|  |                 <EditIcon /> | ||||||
|  |               </IconButton> | ||||||
|  |  | ||||||
|  |               <IconButton size="small"> | ||||||
|  |                 <DeleteIcon /> | ||||||
|  |               </IconButton> | ||||||
|  |             </Stack> | ||||||
|  |           </TableCell> | ||||||
|  |         </TableRow> | ||||||
|  |       ))} | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default ListLoader; | ||||||
							
								
								
									
										91
									
								
								packages/web/src/components/RoleList/index.ee.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								packages/web/src/components/RoleList/index.ee.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,91 @@ | |||||||
|  | import * as React from 'react'; | ||||||
|  | import { Link } from 'react-router-dom'; | ||||||
|  | import Stack from '@mui/material/Stack'; | ||||||
|  | import Table from '@mui/material/Table'; | ||||||
|  | import TableBody from '@mui/material/TableBody'; | ||||||
|  | import TableCell from '@mui/material/TableCell'; | ||||||
|  | import TableContainer from '@mui/material/TableContainer'; | ||||||
|  | import TableHead from '@mui/material/TableHead'; | ||||||
|  | import TableRow from '@mui/material/TableRow'; | ||||||
|  | import Paper from '@mui/material/Paper'; | ||||||
|  | import IconButton from '@mui/material/IconButton'; | ||||||
|  | import Typography from '@mui/material/Typography'; | ||||||
|  | import EditIcon from '@mui/icons-material/Edit'; | ||||||
|  |  | ||||||
|  | import DeleteRoleButton from 'components/DeleteRoleButton/index.ee'; | ||||||
|  | import ListLoader from 'components/ListLoader'; | ||||||
|  | import useFormatMessage from 'hooks/useFormatMessage'; | ||||||
|  | import useRoles from 'hooks/useRoles.ee'; | ||||||
|  | import * as URLS from 'config/urls'; | ||||||
|  |  | ||||||
|  | // TODO: introduce interaction feedback upon deletion (successful + failure) | ||||||
|  | export default function RoleList(): React.ReactElement { | ||||||
|  |   const formatMessage = useFormatMessage(); | ||||||
|  |   const { roles, loading } = useRoles(); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <TableContainer component={Paper}> | ||||||
|  |       <Table> | ||||||
|  |         <TableHead> | ||||||
|  |           <TableRow> | ||||||
|  |             <TableCell component="th"> | ||||||
|  |               <Typography | ||||||
|  |                 variant="subtitle1" | ||||||
|  |                 sx={{ color: 'text.secondary', fontWeight: 700 }} | ||||||
|  |               > | ||||||
|  |                 {formatMessage('roleList.name')} | ||||||
|  |               </Typography> | ||||||
|  |             </TableCell> | ||||||
|  |  | ||||||
|  |             <TableCell component="th"> | ||||||
|  |               <Typography | ||||||
|  |                 variant="subtitle1" | ||||||
|  |                 sx={{ color: 'text.secondary', fontWeight: 700 }} | ||||||
|  |               > | ||||||
|  |                 {formatMessage('roleList.description')} | ||||||
|  |               </Typography> | ||||||
|  |             </TableCell> | ||||||
|  |  | ||||||
|  |             <TableCell component="th" /> | ||||||
|  |           </TableRow> | ||||||
|  |         </TableHead> | ||||||
|  |         <TableBody> | ||||||
|  |           {loading ? ( | ||||||
|  |             <ListLoader rowsNumber={3} cellNumber={2} /> | ||||||
|  |           ) : ( | ||||||
|  |             roles.map((role) => ( | ||||||
|  |               <TableRow | ||||||
|  |                 key={role.id} | ||||||
|  |                 sx={{ '&:last-child td, &:last-child th': { border: 0 } }} | ||||||
|  |               > | ||||||
|  |                 <TableCell scope="row"> | ||||||
|  |                   <Typography variant="subtitle2">{role.name}</Typography> | ||||||
|  |                 </TableCell> | ||||||
|  |  | ||||||
|  |                 <TableCell scope="row"> | ||||||
|  |                   <Typography variant="subtitle2"> | ||||||
|  |                     {role.description} | ||||||
|  |                   </Typography> | ||||||
|  |                 </TableCell> | ||||||
|  |  | ||||||
|  |                 <TableCell> | ||||||
|  |                   <Stack direction="row" gap={1} justifyContent="right"> | ||||||
|  |                     <IconButton | ||||||
|  |                       size="small" | ||||||
|  |                       component={Link} | ||||||
|  |                       to={URLS.ROLE(role.id)} | ||||||
|  |                     > | ||||||
|  |                       <EditIcon /> | ||||||
|  |                     </IconButton> | ||||||
|  |  | ||||||
|  |                     <DeleteRoleButton roleId={role.id} /> | ||||||
|  |                   </Stack> | ||||||
|  |                 </TableCell> | ||||||
|  |               </TableRow> | ||||||
|  |             )) | ||||||
|  |           )} | ||||||
|  |         </TableBody> | ||||||
|  |       </Table> | ||||||
|  |     </TableContainer> | ||||||
|  |   ); | ||||||
|  | } | ||||||
| @@ -9,7 +9,7 @@ import { yupResolver } from '@hookform/resolvers/yup'; | |||||||
|  |  | ||||||
| import useAuthentication from 'hooks/useAuthentication'; | import useAuthentication from 'hooks/useAuthentication'; | ||||||
| import * as URLS from 'config/urls'; | import * as URLS from 'config/urls'; | ||||||
| import { CREATE_USER } from 'graphql/mutations/create-user.ee'; | import { REGISTER_USER } from 'graphql/mutations/register-user.ee'; | ||||||
| import Form from 'components/Form'; | import Form from 'components/Form'; | ||||||
| import TextField from 'components/TextField'; | import TextField from 'components/TextField'; | ||||||
| import { LOGIN } from 'graphql/mutations/login'; | import { LOGIN } from 'graphql/mutations/login'; | ||||||
| @@ -40,7 +40,7 @@ function SignUpForm() { | |||||||
|   const navigate = useNavigate(); |   const navigate = useNavigate(); | ||||||
|   const authentication = useAuthentication(); |   const authentication = useAuthentication(); | ||||||
|   const formatMessage = useFormatMessage(); |   const formatMessage = useFormatMessage(); | ||||||
|   const [createUser, { loading: createUserLoading }] = useMutation(CREATE_USER); |   const [registerUser, { loading: registerUserLoading }] = useMutation(REGISTER_USER); | ||||||
|   const [login, { loading: loginLoading }] = useMutation(LOGIN); |   const [login, { loading: loginLoading }] = useMutation(LOGIN); | ||||||
|  |  | ||||||
|   React.useEffect(() => { |   React.useEffect(() => { | ||||||
| @@ -51,7 +51,7 @@ function SignUpForm() { | |||||||
|  |  | ||||||
|   const handleSubmit = async (values: any) => { |   const handleSubmit = async (values: any) => { | ||||||
|     const { fullName, email, password } = values; |     const { fullName, email, password } = values; | ||||||
|     await createUser({ |     await registerUser({ | ||||||
|       variables: { |       variables: { | ||||||
|         input: { fullName, email, password }, |         input: { fullName, email, password }, | ||||||
|       }, |       }, | ||||||
| @@ -165,7 +165,7 @@ function SignUpForm() { | |||||||
|               variant="contained" |               variant="contained" | ||||||
|               color="primary" |               color="primary" | ||||||
|               sx={{ boxShadow: 2, mt: 3 }} |               sx={{ boxShadow: 2, mt: 3 }} | ||||||
|               loading={createUserLoading || loginLoading} |               loading={registerUserLoading || loginLoading} | ||||||
|               fullWidth |               fullWidth | ||||||
|               data-test="signUp-button" |               data-test="signUp-button" | ||||||
|             > |             > | ||||||
|   | |||||||
							
								
								
									
										39
									
								
								packages/web/src/components/SsoProviders/index.ee.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								packages/web/src/components/SsoProviders/index.ee.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | |||||||
|  | import * as React from 'react'; | ||||||
|  | import Paper from '@mui/material/Paper'; | ||||||
|  | import Button from '@mui/material/Button'; | ||||||
|  | import Stack from '@mui/material/Stack'; | ||||||
|  | import Divider from '@mui/material/Divider'; | ||||||
|  |  | ||||||
|  | import appConfig from 'config/app'; | ||||||
|  | import useSamlAuthProviders from 'hooks/useSamlAuthProviders.ee'; | ||||||
|  | import useFormatMessage from 'hooks/useFormatMessage'; | ||||||
|  |  | ||||||
|  | function SsoProviders() { | ||||||
|  |   const formatMessage = useFormatMessage(); | ||||||
|  |   const { providers, loading } = useSamlAuthProviders(); | ||||||
|  |  | ||||||
|  |   if (!loading && providers.length === 0) return null; | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <Divider>{formatMessage('loginPage.divider')}</Divider> | ||||||
|  |  | ||||||
|  |       <Paper sx={{ px: 2, py: 4 }}> | ||||||
|  |         <Stack direction="column" gap={1}> | ||||||
|  |           {providers.map((provider) => ( | ||||||
|  |             <Button | ||||||
|  |               key={provider.id} | ||||||
|  |               component="a" | ||||||
|  |               href={`${appConfig.apiUrl}/login/saml/${provider.issuer}`} | ||||||
|  |               variant="outlined" | ||||||
|  |             > | ||||||
|  |               {provider.name} | ||||||
|  |             </Button> | ||||||
|  |           ))} | ||||||
|  |         </Stack> | ||||||
|  |       </Paper> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default SsoProviders; | ||||||
| @@ -58,7 +58,7 @@ export default function UpgradeFreeTrial() { | |||||||
|             alignItems="stretch" |             alignItems="stretch" | ||||||
|           > |           > | ||||||
|             <TableContainer component={Paper}> |             <TableContainer component={Paper}> | ||||||
|               <Table aria-label="simple table"> |               <Table> | ||||||
|                 <TableHead |                 <TableHead | ||||||
|                   sx={{ |                   sx={{ | ||||||
|                     backgroundColor: (theme) => |                     backgroundColor: (theme) => | ||||||
|   | |||||||
							
								
								
									
										90
									
								
								packages/web/src/components/UserList/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								packages/web/src/components/UserList/index.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | |||||||
|  | import * as React from 'react'; | ||||||
|  | import { Link } from 'react-router-dom'; | ||||||
|  | import Stack from '@mui/material/Stack'; | ||||||
|  | import Table from '@mui/material/Table'; | ||||||
|  | import TableBody from '@mui/material/TableBody'; | ||||||
|  | import TableCell from '@mui/material/TableCell'; | ||||||
|  | import TableContainer from '@mui/material/TableContainer'; | ||||||
|  | import TableHead from '@mui/material/TableHead'; | ||||||
|  | import TableRow from '@mui/material/TableRow'; | ||||||
|  | import Paper from '@mui/material/Paper'; | ||||||
|  | import IconButton from '@mui/material/IconButton'; | ||||||
|  | import Typography from '@mui/material/Typography'; | ||||||
|  | import EditIcon from '@mui/icons-material/Edit'; | ||||||
|  |  | ||||||
|  | import DeleteUserButton from 'components/DeleteUserButton/index.ee'; | ||||||
|  | import ListLoader from 'components/ListLoader'; | ||||||
|  | import useUsers from 'hooks/useUsers'; | ||||||
|  | import useFormatMessage from 'hooks/useFormatMessage'; | ||||||
|  | import * as URLS from 'config/urls'; | ||||||
|  |  | ||||||
|  | // TODO: introduce translation entries | ||||||
|  | // TODO: introduce interaction feedback upon deletion (successful + failure) | ||||||
|  | export default function UserList(): React.ReactElement { | ||||||
|  |   const formatMessage = useFormatMessage(); | ||||||
|  |   const { users, loading } = useUsers(); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <TableContainer component={Paper}> | ||||||
|  |       <Table> | ||||||
|  |         <TableHead> | ||||||
|  |           <TableRow> | ||||||
|  |             <TableCell component="th"> | ||||||
|  |               <Typography | ||||||
|  |                 variant="subtitle1" | ||||||
|  |                 sx={{ color: 'text.secondary', fontWeight: 700 }} | ||||||
|  |               > | ||||||
|  |                 {formatMessage('userList.fullName')} | ||||||
|  |               </Typography> | ||||||
|  |             </TableCell> | ||||||
|  |  | ||||||
|  |             <TableCell component="th"> | ||||||
|  |               <Typography | ||||||
|  |                 variant="subtitle1" | ||||||
|  |                 sx={{ color: 'text.secondary', fontWeight: 700 }} | ||||||
|  |               > | ||||||
|  |                 {formatMessage('userList.email')} | ||||||
|  |               </Typography> | ||||||
|  |             </TableCell> | ||||||
|  |  | ||||||
|  |             <TableCell component="th" /> | ||||||
|  |           </TableRow> | ||||||
|  |         </TableHead> | ||||||
|  |         <TableBody> | ||||||
|  |           {loading ? ( | ||||||
|  |             <ListLoader rowsNumber={3} cellNumber={2} /> | ||||||
|  |           ) : ( | ||||||
|  |             users.map((user) => ( | ||||||
|  |               <TableRow | ||||||
|  |                 key={user.id} | ||||||
|  |                 sx={{ '&:last-child td, &:last-child th': { border: 0 } }} | ||||||
|  |               > | ||||||
|  |                 <TableCell scope="row"> | ||||||
|  |                   <Typography variant="subtitle2">{user.fullName}</Typography> | ||||||
|  |                 </TableCell> | ||||||
|  |  | ||||||
|  |                 <TableCell> | ||||||
|  |                   <Typography variant="subtitle2">{user.email}</Typography> | ||||||
|  |                 </TableCell> | ||||||
|  |  | ||||||
|  |                 <TableCell> | ||||||
|  |                   <Stack direction="row" gap={1} justifyContent="right"> | ||||||
|  |                     <IconButton | ||||||
|  |                       size="small" | ||||||
|  |                       component={Link} | ||||||
|  |                       to={URLS.USER(user.id)} | ||||||
|  |                     > | ||||||
|  |                       <EditIcon /> | ||||||
|  |                     </IconButton> | ||||||
|  |  | ||||||
|  |                     <DeleteUserButton userId={user.id} /> | ||||||
|  |                   </Stack> | ||||||
|  |                 </TableCell> | ||||||
|  |               </TableRow> | ||||||
|  |             )) | ||||||
|  |           )} | ||||||
|  |         </TableBody> | ||||||
|  |       </Table> | ||||||
|  |     </TableContainer> | ||||||
|  |   ); | ||||||
|  | } | ||||||
| @@ -1,13 +1,24 @@ | |||||||
| type Config = { | type Config = { | ||||||
|   [key: string]: string; |   [key: string]: string; | ||||||
|  |   baseUrl: string; | ||||||
|  |   apiUrl: string; | ||||||
|  |   graphqlUrl: string; | ||||||
|  |   notificationsUrl: string; | ||||||
|  |   chatwootBaseUrl: string; | ||||||
|  |   supportEmailAddress: string; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const config: Config = { | const config: Config = { | ||||||
|   baseUrl: process.env.REACT_APP_BASE_URL as string, |   baseUrl: process.env.REACT_APP_BASE_URL as string, | ||||||
|  |   apiUrl: process.env.REACT_APP_API_URL as string, | ||||||
|   graphqlUrl: process.env.REACT_APP_GRAPHQL_URL as string, |   graphqlUrl: process.env.REACT_APP_GRAPHQL_URL as string, | ||||||
|   notificationsUrl: process.env.REACT_APP_NOTIFICATIONS_URL as string, |   notificationsUrl: process.env.REACT_APP_NOTIFICATIONS_URL as string, | ||||||
|   chatwootBaseUrl: 'https://app.chatwoot.com', |   chatwootBaseUrl: 'https://app.chatwoot.com', | ||||||
|   supportEmailAddress: 'support@automatisch.io' |   supportEmailAddress: 'support@automatisch.io' | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | if (!config.apiUrl) { | ||||||
|  |   config.apiUrl = (new URL(config.graphqlUrl)).origin; | ||||||
|  | } | ||||||
|  |  | ||||||
| export default config; | export default config; | ||||||
|   | |||||||
| @@ -1,35 +1,36 @@ | |||||||
| export const CONNECTIONS = '/connections'; | export const CONNECTIONS = '/connections'; | ||||||
| export const EXECUTIONS = '/executions'; | export const EXECUTIONS = '/executions'; | ||||||
| export const EXECUTION_PATTERN = '/executions/:executionId'; | export const EXECUTION_PATTERN = '/executions/:executionId'; | ||||||
| export const EXECUTION = (executionId: string): string => | export const EXECUTION = (executionId: string) => | ||||||
|   `/executions/${executionId}`; |   `/executions/${executionId}`; | ||||||
|  |  | ||||||
| export const LOGIN = '/login'; | export const LOGIN = '/login'; | ||||||
|  | export const LOGIN_CALLBACK = `${LOGIN}/callback`; | ||||||
| export const SIGNUP = '/sign-up'; | export const SIGNUP = '/sign-up'; | ||||||
| export const FORGOT_PASSWORD = '/forgot-password'; | export const FORGOT_PASSWORD = '/forgot-password'; | ||||||
| export const RESET_PASSWORD = '/reset-password'; | export const RESET_PASSWORD = '/reset-password'; | ||||||
|  |  | ||||||
| export const APPS = '/apps'; | export const APPS = '/apps'; | ||||||
| export const NEW_APP_CONNECTION = '/apps/new'; | export const NEW_APP_CONNECTION = '/apps/new'; | ||||||
| export const APP = (appKey: string): string => `/app/${appKey}`; | export const APP = (appKey: string) => `/app/${appKey}`; | ||||||
| export const APP_PATTERN = '/app/:appKey'; | export const APP_PATTERN = '/app/:appKey'; | ||||||
| export const APP_CONNECTIONS = (appKey: string): string => | export const APP_CONNECTIONS = (appKey: string) => | ||||||
|   `/app/${appKey}/connections`; |   `/app/${appKey}/connections`; | ||||||
| export const APP_CONNECTIONS_PATTERN = '/app/:appKey/connections'; | export const APP_CONNECTIONS_PATTERN = '/app/:appKey/connections'; | ||||||
| export const APP_ADD_CONNECTION = (appKey: string): string => | export const APP_ADD_CONNECTION = (appKey: string) => | ||||||
|   `/app/${appKey}/connections/add`; |   `/app/${appKey}/connections/add`; | ||||||
| export const APP_ADD_CONNECTION_PATTERN = '/app/:appKey/connections/add'; | export const APP_ADD_CONNECTION_PATTERN = '/app/:appKey/connections/add'; | ||||||
| export const APP_RECONNECT_CONNECTION = ( | export const APP_RECONNECT_CONNECTION = ( | ||||||
|   appKey: string, |   appKey: string, | ||||||
|   connectionId: string |   connectionId: string | ||||||
| ): string => `/app/${appKey}/connections/${connectionId}/reconnect`; | ) => `/app/${appKey}/connections/${connectionId}/reconnect`; | ||||||
| export const APP_RECONNECT_CONNECTION_PATTERN = | export const APP_RECONNECT_CONNECTION_PATTERN = | ||||||
|   '/app/:appKey/connections/:connectionId/reconnect'; |   '/app/:appKey/connections/:connectionId/reconnect'; | ||||||
| export const APP_FLOWS = (appKey: string): string => `/app/${appKey}/flows`; | export const APP_FLOWS = (appKey: string) => `/app/${appKey}/flows`; | ||||||
| export const APP_FLOWS_FOR_CONNECTION = ( | export const APP_FLOWS_FOR_CONNECTION = ( | ||||||
|   appKey: string, |   appKey: string, | ||||||
|   connectionId: string |   connectionId: string | ||||||
| ): string => `/app/${appKey}/flows?connectionId=${connectionId}`; | ) => `/app/${appKey}/flows?connectionId=${connectionId}`; | ||||||
| export const APP_FLOWS_PATTERN = '/app/:appKey/flows'; | export const APP_FLOWS_PATTERN = '/app/:appKey/flows'; | ||||||
|  |  | ||||||
| export const EDITOR = '/editor'; | export const EDITOR = '/editor'; | ||||||
| @@ -54,11 +55,11 @@ export const CREATE_FLOW_WITH_APP_AND_CONNECTION = ( | |||||||
|  |  | ||||||
|   return `/editor/create?${searchParams}`; |   return `/editor/create?${searchParams}`; | ||||||
| }; | }; | ||||||
| export const FLOW_EDITOR = (flowId: string): string => `/editor/${flowId}`; | export const FLOW_EDITOR = (flowId: string) => `/editor/${flowId}`; | ||||||
|  |  | ||||||
| export const FLOWS = '/flows'; | export const FLOWS = '/flows'; | ||||||
| // TODO: revert this back to /flows/:flowId once we have a proper single flow page | // TODO: revert this back to /flows/:flowId once we have a proper single flow page | ||||||
| export const FLOW = (flowId: string): string => `/editor/${flowId}`; | export const FLOW = (flowId: string) => `/editor/${flowId}`; | ||||||
| export const FLOW_PATTERN = '/flows/:flowId'; | export const FLOW_PATTERN = '/flows/:flowId'; | ||||||
|  |  | ||||||
| export const SETTINGS = '/settings'; | export const SETTINGS = '/settings'; | ||||||
| @@ -71,6 +72,17 @@ export const SETTINGS_PROFILE = `${SETTINGS}/${PROFILE}`; | |||||||
| export const SETTINGS_BILLING_AND_USAGE = `${SETTINGS}/${BILLING_AND_USAGE}`; | export const SETTINGS_BILLING_AND_USAGE = `${SETTINGS}/${BILLING_AND_USAGE}`; | ||||||
| export const SETTINGS_PLAN_UPGRADE = `${SETTINGS_BILLING_AND_USAGE}/${PLAN_UPGRADE}`; | export const SETTINGS_PLAN_UPGRADE = `${SETTINGS_BILLING_AND_USAGE}/${PLAN_UPGRADE}`; | ||||||
|  |  | ||||||
|  | export const ADMIN_SETTINGS = '/admin-settings'; | ||||||
|  | export const ADMIN_SETTINGS_DASHBOARD = ADMIN_SETTINGS; | ||||||
|  | export const USERS = `${ADMIN_SETTINGS}/users`; | ||||||
|  | export const USER = (userId: string) => `${USERS}/${userId}`; | ||||||
|  | export const USER_PATTERN = `${USERS}/:userId`; | ||||||
|  | export const CREATE_USER = `${USERS}/create`; | ||||||
|  | export const ROLES = `${ADMIN_SETTINGS}/roles`; | ||||||
|  | export const ROLE = (roleId: string) => `${ROLES}/${roleId}`; | ||||||
|  | export const ROLE_PATTERN = `${ROLES}/:roleId`; | ||||||
|  | export const CREATE_ROLE = `${ROLES}/create`; | ||||||
|  |  | ||||||
| export const DASHBOARD = FLOWS; | export const DASHBOARD = FLOWS; | ||||||
|  |  | ||||||
| // External links | // External links | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								packages/web/src/graphql/mutations/create-role.ee.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								packages/web/src/graphql/mutations/create-role.ee.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | import { gql } from '@apollo/client'; | ||||||
|  |  | ||||||
|  | export const CREATE_ROLE = gql` | ||||||
|  |   mutation CreateRole($input: CreateRoleInput) { | ||||||
|  |     createRole(input: $input) { | ||||||
|  |       id | ||||||
|  |       key | ||||||
|  |       name | ||||||
|  |       description | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | `; | ||||||
| @@ -3,8 +3,12 @@ import { gql } from '@apollo/client'; | |||||||
| export const CREATE_USER = gql` | export const CREATE_USER = gql` | ||||||
|   mutation CreateUser($input: CreateUserInput) { |   mutation CreateUser($input: CreateUserInput) { | ||||||
|     createUser(input: $input) { |     createUser(input: $input) { | ||||||
|  |       id | ||||||
|       email |       email | ||||||
|       fullName |       fullName | ||||||
|  |       role { | ||||||
|  |         id | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| `; | `; | ||||||
|   | |||||||
| @@ -0,0 +1,7 @@ | |||||||
|  | import { gql } from '@apollo/client'; | ||||||
|  |  | ||||||
|  | export const DELETE_CURRENT_USER = gql` | ||||||
|  |   mutation DeleteCurrentUser { | ||||||
|  |     deleteCurrentUser | ||||||
|  |   } | ||||||
|  | `; | ||||||
							
								
								
									
										7
									
								
								packages/web/src/graphql/mutations/delete-role.ee.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								packages/web/src/graphql/mutations/delete-role.ee.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | import { gql } from '@apollo/client'; | ||||||
|  |  | ||||||
|  | export const DELETE_ROLE = gql` | ||||||
|  |   mutation DeleteRole($input: DeleteRoleInput) { | ||||||
|  |     deleteRole(input: $input) | ||||||
|  |   } | ||||||
|  | `; | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| import { gql } from '@apollo/client'; | import { gql } from '@apollo/client'; | ||||||
|  |  | ||||||
| export const DELETE_USER = gql` | export const DELETE_USER = gql` | ||||||
|   mutation DeleteUser { |   mutation DeleteUser($input: DeleteUserInput) { | ||||||
|     deleteUser |     deleteUser(input: $input) | ||||||
|   } |   } | ||||||
| `; | `; | ||||||
|   | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user