Compare commits
	
		
			13 Commits
		
	
	
		
			temp-branc
			...
			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' | ||||
|     expose: | ||||
|       - 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: | ||||
|   postgres_data: | ||||
|   redis_data: | ||||
|   keycloak: | ||||
|   | ||||
| @@ -2,18 +2,55 @@ import appConfig from '../../src/config/app'; | ||||
| import logger from '../../src/helpers/logger'; | ||||
| import client from './client'; | ||||
| import User from '../../src/models/user'; | ||||
| import Role from '../../src/models/role'; | ||||
| import Permission from '../../src/models/permission'; | ||||
| 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( | ||||
|   email = 'user@automatisch.io', | ||||
|   password = 'sample' | ||||
| ) { | ||||
|   const UNIQUE_VIOLATION_CODE = '23505'; | ||||
|  | ||||
|   await seedPermissionsIfNeeded(); | ||||
|  | ||||
|   const role = await createOrFetchRole(); | ||||
|   const userParams = { | ||||
|     email, | ||||
|     password, | ||||
|     fullName: 'Initial admin', | ||||
|     role: 'admin', | ||||
|     roleId: role.id, | ||||
|   }; | ||||
|  | ||||
|   try { | ||||
|   | ||||
| @@ -12,6 +12,7 @@ const knexConfig = { | ||||
|     database: appConfig.postgresDatabase, | ||||
|     ssl: appConfig.postgresEnableSsl, | ||||
|   }, | ||||
|   asyncStackTraces: appConfig.isDev, | ||||
|   searchPath: [appConfig.postgresSchema], | ||||
|   pool: { min: 0, max: 20 }, | ||||
|   migrations: { | ||||
|   | ||||
| @@ -4,7 +4,7 @@ | ||||
|   "license": "See LICENSE file", | ||||
|   "description": "The open source Zapier alternative. Build workflow automation without spending time and money.", | ||||
|   "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", | ||||
|     "build": "tsc && yarn copy-statics", | ||||
|     "build:watch": "nodemon --watch 'src/**/*.ts' --watch 'bin/**/*.ts' --exec yarn build --ext ts", | ||||
| @@ -24,12 +24,15 @@ | ||||
|   "dependencies": { | ||||
|     "@automatisch/web": "^0.7.1", | ||||
|     "@bull-board/express": "^3.10.1", | ||||
|     "@casl/ability": "^6.5.0", | ||||
|     "@graphql-tools/graphql-file-loader": "^7.3.4", | ||||
|     "@graphql-tools/load": "^7.5.2", | ||||
|     "@node-saml/passport-saml": "^4.0.4", | ||||
|     "@rudderstack/rudder-sdk-node": "^1.1.2", | ||||
|     "@sentry/node": "^7.42.0", | ||||
|     "@sentry/tracing": "^7.42.0", | ||||
|     "@types/luxon": "^2.3.1", | ||||
|     "@types/passport": "^1.0.12", | ||||
|     "@types/xmlrpc": "^1.3.7", | ||||
|     "ajv-formats": "^2.1.1", | ||||
|     "axios": "0.24.0", | ||||
| @@ -60,6 +63,7 @@ | ||||
|     "nodemailer": "6.7.0", | ||||
|     "oauth-1.0a": "^2.2.6", | ||||
|     "objection": "^3.0.0", | ||||
|     "passport": "^0.6.0", | ||||
|     "pg": "^8.7.1", | ||||
|     "php-serialize": "^4.0.2", | ||||
|     "stripe": "^11.13.0", | ||||
|   | ||||
| @@ -17,6 +17,7 @@ import { | ||||
| } from './helpers/create-bull-board-handler'; | ||||
| import injectBullBoardHandler from './helpers/inject-bull-board-handler'; | ||||
| import router from './routes'; | ||||
| import configurePassport from './helpers/passport'; | ||||
|  | ||||
| createBullBoardHandler(serverAdapter); | ||||
|  | ||||
| @@ -50,6 +51,9 @@ app.use( | ||||
|   }) | ||||
| ); | ||||
| app.use(cors(corsOptions)); | ||||
|  | ||||
| configurePassport(app); | ||||
|  | ||||
| app.use('/', router); | ||||
|  | ||||
| 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 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 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 updateFlowStatus from './mutations/update-flow-status'; | ||||
| import executeFlow from './mutations/execute-flow'; | ||||
| import deleteFlow from './mutations/delete-flow'; | ||||
| import duplicateFlow from './mutations/duplicate-flow'; | ||||
| import createStep from './mutations/create-step'; | ||||
| import updateRole from './mutations/update-role.ee'; | ||||
| import updateStep from './mutations/update-step'; | ||||
| import deleteStep from './mutations/delete-step'; | ||||
| import createUser from './mutations/create-user.ee'; | ||||
| 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'; | ||||
| import updateUser from './mutations/update-user.ee'; | ||||
| import verifyConnection from './mutations/verify-connection'; | ||||
|  | ||||
| const mutationResolvers = { | ||||
|   createConnection, | ||||
|   generateAuthUrl, | ||||
|   updateConnection, | ||||
|   resetConnection, | ||||
|   verifyConnection, | ||||
|   deleteConnection, | ||||
|   createFlow, | ||||
|   createRole, | ||||
|   createStep, | ||||
|   createUser, | ||||
|   deleteConnection, | ||||
|   deleteCurrentUser, | ||||
|   deleteFlow, | ||||
|   deleteRole, | ||||
|   deleteStep, | ||||
|   deleteUser, | ||||
|   duplicateFlow, | ||||
|   executeFlow, | ||||
|   forgotPassword, | ||||
|   generateAuthUrl, | ||||
|   login, | ||||
|   registerUser, | ||||
|   resetConnection, | ||||
|   resetPassword, | ||||
|   updateConnection, | ||||
|   updateCurrentUser, | ||||
|   updateUser, | ||||
|   updateFlow, | ||||
|   updateFlowStatus, | ||||
|   executeFlow, | ||||
|   deleteFlow, | ||||
|   duplicateFlow, | ||||
|   createStep, | ||||
|   updateRole, | ||||
|   updateStep, | ||||
|   deleteStep, | ||||
|   createUser, | ||||
|   deleteUser, | ||||
|   updateUser, | ||||
|   forgotPassword, | ||||
|   resetPassword, | ||||
|   login, | ||||
|   verifyConnection, | ||||
| }; | ||||
|  | ||||
| export default mutationResolvers; | ||||
|   | ||||
| @@ -13,6 +13,8 @@ const createConnection = async ( | ||||
|   params: Params, | ||||
|   context: Context | ||||
| ) => { | ||||
|   context.currentUser.can('create', 'Connection'); | ||||
|  | ||||
|   await App.findOneByKey(params.input.key); | ||||
|  | ||||
|   return await context.currentUser.$relatedQuery('connections').insert({ | ||||
|   | ||||
| @@ -14,6 +14,8 @@ const createFlow = async ( | ||||
|   params: Params, | ||||
|   context: Context | ||||
| ) => { | ||||
|   context.currentUser.can('create', 'Flow'); | ||||
|  | ||||
|   const connectionId = params?.input?.connectionId; | ||||
|   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, | ||||
|   context: Context | ||||
| ) => { | ||||
|   context.currentUser.can('update', 'Flow'); | ||||
|  | ||||
|   const { input } = params; | ||||
|  | ||||
|   if (input.appKey && input.key) { | ||||
|   | ||||
| @@ -5,11 +5,13 @@ type Params = { | ||||
|     fullName: string; | ||||
|     email: string; | ||||
|     password: string; | ||||
|     roleId: string; | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| // TODO: access | ||||
| 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 }); | ||||
|  | ||||
| @@ -21,7 +23,7 @@ const createUser = async (_parent: unknown, params: Params) => { | ||||
|     fullName, | ||||
|     email, | ||||
|     password, | ||||
|     role: 'user', | ||||
|     roleId, | ||||
|   }); | ||||
|  | ||||
|   return user; | ||||
|   | ||||
| @@ -11,6 +11,8 @@ const deleteConnection = async ( | ||||
|   params: Params, | ||||
|   context: Context | ||||
| ) => { | ||||
|   context.currentUser.can('delete', 'Connection'); | ||||
|  | ||||
|   await context.currentUser | ||||
|     .$relatedQuery('connections') | ||||
|     .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, | ||||
|   context: Context | ||||
| ) => { | ||||
|   context.currentUser.can('delete', 'Flow'); | ||||
|  | ||||
|   const flow = await context.currentUser | ||||
|     .$relatedQuery('flows') | ||||
|     .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, | ||||
|   context: Context | ||||
| ) => { | ||||
|   context.currentUser.can('update', 'Flow'); | ||||
|  | ||||
|   const step = await context.currentUser | ||||
|     .$relatedQuery('steps') | ||||
|     .withGraphFetched('flow') | ||||
|   | ||||
| @@ -1,11 +1,23 @@ | ||||
| import Context from '../../types/express/context'; | ||||
| import deleteUserQueue from '../../queues/delete-user.ee'; | ||||
| 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) => { | ||||
|   const id = context.currentUser.id; | ||||
| type Params = { | ||||
|   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 jobPayload = { id }; | ||||
|   | ||||
| @@ -53,6 +53,8 @@ const duplicateFlow = async ( | ||||
|   params: Params, | ||||
|   context: Context | ||||
| ) => { | ||||
|   context.currentUser.can('create', 'Flow'); | ||||
|  | ||||
|   const flow = await context.currentUser | ||||
|     .$relatedQuery('flows') | ||||
|     .withGraphJoined('[steps]') | ||||
|   | ||||
| @@ -12,6 +12,8 @@ const executeFlow = async ( | ||||
|   params: Params, | ||||
|   context: Context | ||||
| ) => { | ||||
|   context.currentUser.can('update', 'Flow'); | ||||
|  | ||||
|   const { stepId } = params.input; | ||||
|  | ||||
|   const untilStep = await context.currentUser | ||||
|   | ||||
| @@ -13,6 +13,8 @@ const generateAuthUrl = async ( | ||||
|   params: Params, | ||||
|   context: Context | ||||
| ) => { | ||||
|   context.currentUser.can('create', 'Connection'); | ||||
|  | ||||
|   const connection = await context.currentUser | ||||
|     .$relatedQuery('connections') | ||||
|     .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, | ||||
|   context: Context | ||||
| ) => { | ||||
|   context.currentUser.can('create', 'Connection'); | ||||
|  | ||||
|   let connection = await context.currentUser | ||||
|     .$relatedQuery('connections') | ||||
|     .findOne({ | ||||
|   | ||||
| @@ -13,6 +13,8 @@ const updateConnection = async ( | ||||
|   params: Params, | ||||
|   context: Context | ||||
| ) => { | ||||
|   context.currentUser.can('create', 'Connection'); | ||||
|  | ||||
|   let connection = await context.currentUser | ||||
|     .$relatedQuery('connections') | ||||
|     .findOne({ | ||||
|   | ||||
| @@ -8,7 +8,7 @@ type Params = { | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| const updateUser = async ( | ||||
| const updateCurrentUser = async ( | ||||
|   _parent: unknown, | ||||
|   params: Params, | ||||
|   context: Context | ||||
| @@ -22,4 +22,4 @@ const updateUser = async ( | ||||
|   return user; | ||||
| }; | ||||
| 
 | ||||
| export default updateUser; | ||||
| export default updateCurrentUser; | ||||
| @@ -18,6 +18,8 @@ const updateFlowStatus = async ( | ||||
|   params: Params, | ||||
|   context: Context | ||||
| ) => { | ||||
|   context.currentUser.can('publish', 'Flow'); | ||||
|  | ||||
|   let flow = await context.currentUser | ||||
|     .$relatedQuery('flows') | ||||
|     .findOne({ | ||||
| @@ -55,7 +57,7 @@ const updateFlowStatus = async ( | ||||
|   } else { | ||||
|     if (newActiveValue) { | ||||
|       flow = await flow.$query().patchAndFetch({ | ||||
|         published_at: new Date().toISOString(), | ||||
|         publishedAt: new Date().toISOString(), | ||||
|       }); | ||||
|  | ||||
|       const jobName = `${JOB_NAME}-${flow.id}`; | ||||
| @@ -78,7 +80,10 @@ const updateFlowStatus = async ( | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   flow = await flow.$query().withGraphFetched('steps').patchAndFetch({ | ||||
|   flow = await flow | ||||
|     .$query() | ||||
|     .withGraphFetched('steps') | ||||
|     .patchAndFetch({ | ||||
|       active: newActiveValue, | ||||
|     }); | ||||
|  | ||||
|   | ||||
| @@ -12,6 +12,8 @@ const updateFlow = async ( | ||||
|   params: Params, | ||||
|   context: Context | ||||
| ) => { | ||||
|   context.currentUser.can('update', 'Flow'); | ||||
|  | ||||
|   let flow = await context.currentUser | ||||
|     .$relatedQuery('flows') | ||||
|     .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, | ||||
|   context: Context | ||||
| ) => { | ||||
|   context.currentUser.can('update', 'Flow'); | ||||
|  | ||||
|   const { input } = params; | ||||
|  | ||||
|   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, | ||||
|   context: Context | ||||
| ) => { | ||||
|   context.currentUser.can('create', 'Connection'); | ||||
|  | ||||
|   let connection = await context.currentUser | ||||
|     .$relatedQuery('connections') | ||||
|     .findOne({ | ||||
|   | ||||
| @@ -6,6 +6,8 @@ type Params = { | ||||
| }; | ||||
|  | ||||
| const getApp = async (_parent: unknown, params: Params, context: Context) => { | ||||
|   context.currentUser.can('read', 'Connection'); | ||||
|  | ||||
|   const app = await App.findOneByKey(params.key); | ||||
|  | ||||
|   if (context.currentUser) { | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import App from '../../models/app'; | ||||
| import { IApp } from '@automatisch/types'; | ||||
| import App from '../../models/app'; | ||||
|  | ||||
| type Params = { | ||||
|   name: string; | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { IConnection } from '@automatisch/types'; | ||||
| import App from '../../models/app'; | ||||
| import Context from '../../types/express/context'; | ||||
| import { IApp, IConnection } from '@automatisch/types'; | ||||
|  | ||||
| type Params = { | ||||
|   name: string; | ||||
| @@ -11,6 +11,8 @@ const getConnectedApps = async ( | ||||
|   params: Params, | ||||
|   context: Context | ||||
| ) => { | ||||
|   context.currentUser.can('read', 'Connection'); | ||||
|  | ||||
|   let apps = await App.findAll(params.name); | ||||
|  | ||||
|   const connections = await context.currentUser | ||||
|   | ||||
| @@ -16,6 +16,8 @@ const getDynamicData = async ( | ||||
|   params: Params, | ||||
|   context: Context | ||||
| ) => { | ||||
|   context.currentUser.can('update', 'Flow'); | ||||
|  | ||||
|   const step = await context.currentUser | ||||
|     .$relatedQuery('steps') | ||||
|     .withGraphFetched({ | ||||
|   | ||||
| @@ -14,6 +14,8 @@ const getDynamicFields = async ( | ||||
|   params: Params, | ||||
|   context: Context | ||||
| ) => { | ||||
|   context.currentUser.can('update', 'Flow'); | ||||
|  | ||||
|   const step = await context.currentUser | ||||
|     .$relatedQuery('steps') | ||||
|     .withGraphFetched({ | ||||
|   | ||||
| @@ -12,6 +12,8 @@ const getExecutionSteps = async ( | ||||
|   params: Params, | ||||
|   context: Context | ||||
| ) => { | ||||
|   context.currentUser.can('read', 'Execution'); | ||||
|  | ||||
|   const execution = await context.currentUser | ||||
|     .$relatedQuery('executions') | ||||
|     .withSoftDeleted() | ||||
|   | ||||
| @@ -9,6 +9,8 @@ const getExecution = async ( | ||||
|   params: Params, | ||||
|   context: Context | ||||
| ) => { | ||||
|   context.currentUser.can('read', 'Execution'); | ||||
|  | ||||
|   const execution = await context.currentUser | ||||
|     .$relatedQuery('executions') | ||||
|     .withGraphFetched({ | ||||
|   | ||||
| @@ -12,6 +12,8 @@ const getExecutions = async ( | ||||
|   params: Params, | ||||
|   context: Context | ||||
| ) => { | ||||
|   context.currentUser.can('read', 'Execution'); | ||||
|  | ||||
|   const selectStatusStatement = ` | ||||
|     case | ||||
|       when count(*) filter (where execution_steps.status = 'failure') > 0 | ||||
|   | ||||
| @@ -5,6 +5,8 @@ type Params = { | ||||
| }; | ||||
|  | ||||
| const getFlow = async (_parent: unknown, params: Params, context: Context) => { | ||||
|   context.currentUser.can('read', 'Flow'); | ||||
|  | ||||
|   const flow = await context.currentUser | ||||
|     .$relatedQuery('flows') | ||||
|     .withGraphJoined('[steps.[connection]]') | ||||
|   | ||||
| @@ -10,6 +10,8 @@ type Params = { | ||||
| }; | ||||
|  | ||||
| const getFlows = async (_parent: unknown, params: Params, context: Context) => { | ||||
|   context.currentUser.can('read', 'Flow'); | ||||
|  | ||||
|   const flowsQuery = context.currentUser | ||||
|     .$relatedQuery('flows') | ||||
|     .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, | ||||
|   context: Context | ||||
| ) => { | ||||
|   context.currentUser.can('update', 'Flow'); | ||||
|  | ||||
|   const step = await context.currentUser | ||||
|     .$relatedQuery('steps') | ||||
|     .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, | ||||
|   context: Context | ||||
| ) => { | ||||
|   context.currentUser.can('update', 'Connection'); | ||||
|  | ||||
|   let connection = await context.currentUser | ||||
|     .$relatedQuery('connections') | ||||
|     .findOne({ | ||||
|   | ||||
| @@ -1,47 +1,59 @@ | ||||
| import getApps from './queries/get-apps'; | ||||
| 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 testConnection from './queries/test-connection'; | ||||
| 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 getCurrentUser from './queries/get-current-user'; | ||||
| import getDynamicData from './queries/get-dynamic-data'; | ||||
| import getDynamicFields from './queries/get-dynamic-fields'; | ||||
| import getCurrentUser from './queries/get-current-user'; | ||||
| import getPaymentPlans from './queries/get-payment-plans.ee'; | ||||
| import getPaddleInfo from './queries/get-paddle-info.ee'; | ||||
| import getBillingAndUsage from './queries/get-billing-and-usage.ee'; | ||||
| import getExecution from './queries/get-execution'; | ||||
| import getExecutionSteps from './queries/get-execution-steps'; | ||||
| import getExecutions from './queries/get-executions'; | ||||
| 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 getAutomatischInfo from './queries/get-automatisch-info'; | ||||
| import getTrialStatus from './queries/get-trial-status.ee'; | ||||
| import getPaddleInfo from './queries/get-paddle-info.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 getTrialStatus from './queries/get-trial-status.ee'; | ||||
| import healthcheck from './queries/healthcheck'; | ||||
| import testConnection from './queries/test-connection'; | ||||
|  | ||||
| const queryResolvers = { | ||||
|   getApps, | ||||
|   getApp, | ||||
|   getApps, | ||||
|   getAutomatischInfo, | ||||
|   getBillingAndUsage, | ||||
|   getConnectedApps, | ||||
|   testConnection, | ||||
|   getFlow, | ||||
|   getFlows, | ||||
|   getStepWithTestExecutions, | ||||
|   getCurrentUser, | ||||
|   getDynamicData, | ||||
|   getDynamicFields, | ||||
|   getExecution, | ||||
|   getExecutions, | ||||
|   getExecutionSteps, | ||||
|   getDynamicData, | ||||
|   getDynamicFields, | ||||
|   getCurrentUser, | ||||
|   getPaymentPlans, | ||||
|   getPaddleInfo, | ||||
|   getBillingAndUsage, | ||||
|   getFlow, | ||||
|   getFlows, | ||||
|   getInvoices, | ||||
|   getAutomatischInfo, | ||||
|   getTrialStatus, | ||||
|   getPaddleInfo, | ||||
|   getPaymentPlans, | ||||
|   getPermissions, | ||||
|   getRole, | ||||
|   getRoles, | ||||
|   getSamlAuthProviders, | ||||
|   getStepWithTestExecutions, | ||||
|   getSubscriptionStatus, | ||||
|   getTrialStatus, | ||||
|   getUser, | ||||
|   getUsers, | ||||
|   healthcheck, | ||||
|   testConnection, | ||||
| }; | ||||
|  | ||||
| export default queryResolvers; | ||||
|   | ||||
| @@ -41,31 +41,46 @@ type Query { | ||||
|   getAutomatischInfo: GetAutomatischInfo | ||||
|   getTrialStatus: GetTrialStatus | ||||
|   getSubscriptionStatus: GetSubscriptionStatus | ||||
|   getSamlAuthProviders: [GetSamlAuthProviders] | ||||
|   getUsers( | ||||
|     limit: Int! | ||||
|     offset: Int! | ||||
|   ): UserConnection | ||||
|   getUser(id: String!): User | ||||
|   getRoles: [Role] | ||||
|   getRole(id: String!): Role | ||||
|   getPermissions: Permissions | ||||
|   healthcheck: AppHealth | ||||
| } | ||||
|  | ||||
| type Mutation { | ||||
|   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 | ||||
|   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 | ||||
|   updateFlowStatus(input: UpdateFlowStatusInput): Flow | ||||
|   executeFlow(input: ExecuteFlowInput): executeFlowType | ||||
|   deleteFlow(input: DeleteFlowInput): Boolean | ||||
|   duplicateFlow(input: DuplicateFlowInput): Flow | ||||
|   createStep(input: CreateStepInput): Step | ||||
|   updateRole(input: UpdateRoleInput): Role | ||||
|   updateStep(input: UpdateStepInput): Step | ||||
|   deleteStep(input: DeleteStepInput): Step | ||||
|   createUser(input: CreateUserInput): User | ||||
|   deleteUser: Boolean | ||||
|   updateUser(input: UpdateUserInput): User | ||||
|   forgotPassword(input: ForgotPasswordInput): Boolean | ||||
|   resetPassword(input: ResetPasswordInput): Boolean | ||||
|   login(input: LoginInput): Auth | ||||
|   verifyConnection(input: VerifyConnectionInput): Connection | ||||
| } | ||||
|  | ||||
| """ | ||||
| @@ -277,6 +292,15 @@ type Execution { | ||||
|   flow: Flow | ||||
| } | ||||
|  | ||||
| type UserConnection { | ||||
|   edges: [UserEdge] | ||||
|   pageInfo: PageInfo | ||||
| } | ||||
|  | ||||
| type UserEdge { | ||||
|   node: User | ||||
| } | ||||
|  | ||||
| input CreateConnectionInput { | ||||
|   key: String! | ||||
|   formattedData: JSONObject! | ||||
| @@ -360,9 +384,31 @@ input CreateUserInput { | ||||
|   fullName: String! | ||||
|   email: String! | ||||
|   password: String! | ||||
|   role: UserRoleInput! | ||||
| } | ||||
|  | ||||
| input UserRoleInput { | ||||
|   id: String | ||||
| } | ||||
|  | ||||
| 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 | ||||
|   password: String | ||||
|   fullName: String | ||||
| @@ -382,6 +428,29 @@ input LoginInput { | ||||
|   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). | ||||
| """ | ||||
| @@ -453,11 +522,21 @@ type User { | ||||
|   id: String | ||||
|   fullName: String | ||||
|   email: String | ||||
|   role: String | ||||
|   role: Role | ||||
|   permissions: [Permission] | ||||
|   createdAt: String | ||||
|   updatedAt: String | ||||
| } | ||||
|  | ||||
| type Role { | ||||
|   id: String | ||||
|   name: String | ||||
|   key: String | ||||
|   description: String | ||||
|   isAdmin: Boolean | ||||
|   permissions: [Permission] | ||||
| } | ||||
|  | ||||
| type PageInfo { | ||||
|   currentPage: Int! | ||||
|   totalPages: Int! | ||||
| @@ -554,6 +633,41 @@ type PaymentPlan { | ||||
|   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 { | ||||
|   query: Query | ||||
|   mutation: Mutation | ||||
|   | ||||
| @@ -12,7 +12,17 @@ const isAuthenticated = rule()(async (_parent, _args, req) => { | ||||
|     const { userId } = jwt.verify(token, appConfig.appSecretKey) as { | ||||
|       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; | ||||
|   } catch (error) { | ||||
| @@ -25,13 +35,14 @@ const authentication = shield( | ||||
|     Query: { | ||||
|       '*': isAuthenticated, | ||||
|       getAutomatischInfo: allow, | ||||
|       getSamlAuthProviders: allow, | ||||
|       healthcheck: allow, | ||||
|     }, | ||||
|     Mutation: { | ||||
|       '*': isAuthenticated, | ||||
|       login: allow, | ||||
|       createUser: allow, | ||||
|       registerUser: allow, | ||||
|       forgotPassword: allow, | ||||
|       login: 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 ExtendedQueryBuilder from '../models/query-builder'; | ||||
| import type Base from '../models/base'; | ||||
|  | ||||
| const paginate = async ( | ||||
|   query: ExtendedQueryBuilder<Model, Model[]>, | ||||
|   limit: number, | ||||
|   offset: number | ||||
|   offset: number, | ||||
| ) => { | ||||
|   if (limit < 1 || limit > 100) { | ||||
|     throw new Error('Limit must be between 1 and 100'); | ||||
| @@ -20,11 +21,9 @@ const paginate = async ( | ||||
|       currentPage: Math.ceil(offset / limit + 1), | ||||
|       totalPages: Math.ceil(count / limit), | ||||
|     }, | ||||
|     edges: records.map((record: Model) => { | ||||
|       return { | ||||
|     edges: records.map((record: Base) => ({ | ||||
|       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' }, | ||||
|       verified: { type: 'boolean', default: false }, | ||||
|       draft: { type: 'boolean' }, | ||||
|       deletedAt: { type: 'string' }, | ||||
|       createdAt: { type: 'string' }, | ||||
|       updatedAt: { type: 'string' }, | ||||
|     }, | ||||
|   }; | ||||
|  | ||||
|   | ||||
| @@ -31,6 +31,9 @@ class ExecutionStep extends Base { | ||||
|       dataOut: { type: ['object', 'null'] }, | ||||
|       status: { type: 'string', enum: ['success', 'failure'] }, | ||||
|       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' }, | ||||
|       testRun: { type: 'boolean', default: false }, | ||||
|       internalId: { type: 'string' }, | ||||
|       deletedAt: { type: 'string' }, | ||||
|       createdAt: { type: 'string' }, | ||||
|       updatedAt: { type: 'string' }, | ||||
|     }, | ||||
|   }; | ||||
|  | ||||
|   | ||||
| @@ -19,7 +19,7 @@ class Flow extends Base { | ||||
|   status: 'paused' | 'published' | 'draft'; | ||||
|   steps: Step[]; | ||||
|   triggerStep: Step; | ||||
|   published_at: string; | ||||
|   publishedAt: string; | ||||
|   remoteWebhookId: string; | ||||
|   executions?: Execution[]; | ||||
|   lastExecution?: Execution; | ||||
| @@ -37,6 +37,10 @@ class Flow extends Base { | ||||
|       userId: { type: 'string', format: 'uuid' }, | ||||
|       remoteWebhookId: { type: 'string' }, | ||||
|       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 { | ||||
|   Model, | ||||
|   Page, | ||||
|   ModelClass, | ||||
|   PartialModelObject, | ||||
|   ForClassMethod, | ||||
|   AnyQueryBuilder, | ||||
| @@ -8,6 +9,10 @@ import { | ||||
|  | ||||
| const DELETED_COLUMN_NAME = 'deleted_at'; | ||||
|  | ||||
| const supportsSoftDeletion = (modelClass: ModelClass<any>) => { | ||||
|   return modelClass.jsonSchema.properties.deletedAt; | ||||
| } | ||||
|  | ||||
| const buildQueryBuidlerForClass = (): ForClassMethod => { | ||||
|   return (modelClass) => { | ||||
|     const qb: AnyQueryBuilder = Model.QueryBuilder.forClass.call( | ||||
| @@ -15,7 +20,7 @@ const buildQueryBuidlerForClass = (): ForClassMethod => { | ||||
|       modelClass | ||||
|     ); | ||||
|     qb.onBuild((builder) => { | ||||
|       if (!builder.context().withSoftDeleted) { | ||||
|       if (!builder.context().withSoftDeleted && supportsSoftDeletion(qb.modelClass())) { | ||||
|         builder.whereNull( | ||||
|           `${qb.modelClass().tableName}.${DELETED_COLUMN_NAME}` | ||||
|         ); | ||||
| @@ -38,11 +43,15 @@ class ExtendedQueryBuilder<M extends Model, R = M[]> extends Model.QueryBuilder< | ||||
|   static forClass: ForClassMethod = buildQueryBuidlerForClass(); | ||||
|  | ||||
|   delete() { | ||||
|     if (supportsSoftDeletion(this.modelClass())) { | ||||
|       return this.patch({ | ||||
|         [DELETED_COLUMN_NAME]: new Date().toISOString(), | ||||
|       } as unknown as PartialModelObject<M>); | ||||
|     } | ||||
|  | ||||
|     return super.delete(); | ||||
|   } | ||||
|  | ||||
|   hardDelete() { | ||||
|     return super.delete(); | ||||
|   } | ||||
|   | ||||
							
								
								
									
										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' }, | ||||
|       parameters: { type: 'object' }, | ||||
|       webhookPath: { type: ['string', 'null'] }, | ||||
|       deletedAt: { type: 'string' }, | ||||
|       createdAt: { type: 'string' }, | ||||
|       updatedAt: { type: 'string' }, | ||||
|     }, | ||||
|   }; | ||||
|  | ||||
|   | ||||
| @@ -46,6 +46,9 @@ class Subscription extends Base { | ||||
|       nextBillDate: { type: 'string' }, | ||||
|       lastBillDate: { type: 'string' }, | ||||
|       cancellationEffectiveDate: { type: 'string' }, | ||||
|       deletedAt: { type: 'string' }, | ||||
|       createdAt: { type: 'string' }, | ||||
|       updatedAt: { type: 'string' }, | ||||
|     }, | ||||
|   }; | ||||
|  | ||||
|   | ||||
| @@ -24,6 +24,9 @@ class UsageData extends Base { | ||||
|       subscriptionId: { type: 'string', format: 'uuid' }, | ||||
|       consumedTaskCount: { type: 'integer' }, | ||||
|       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 bcrypt from 'bcrypt'; | ||||
| import crypto from 'crypto'; | ||||
| import { DateTime } from 'luxon'; | ||||
| import { PureAbility, fieldPatternMatcher, mongoQueryMatcher } from '@casl/ability'; | ||||
| import type { Subject } from '@casl/ability'; | ||||
|  | ||||
| import appConfig from '../config/app'; | ||||
| import Base from './base'; | ||||
| import ExtendedQueryBuilder from './query-builder'; | ||||
| import Connection from './connection'; | ||||
| import Flow from './flow'; | ||||
| import Step from './step'; | ||||
| import Role from './role'; | ||||
| import Permission from './permission'; | ||||
| import Execution from './execution'; | ||||
| import Identity from './identity.ee'; | ||||
| import UsageData from './usage-data.ee'; | ||||
| import Subscription from './subscription.ee'; | ||||
|  | ||||
| @@ -16,8 +22,8 @@ class User extends Base { | ||||
|   id!: string; | ||||
|   fullName!: string; | ||||
|   email!: string; | ||||
|   roleId: string; | ||||
|   password!: string; | ||||
|   role: string; | ||||
|   resetPasswordToken: string; | ||||
|   resetPasswordTokenSentAt: string; | ||||
|   trialExpiryDate: string; | ||||
| @@ -29,19 +35,28 @@ class User extends Base { | ||||
|   currentUsageData?: UsageData; | ||||
|   subscriptions?: Subscription[]; | ||||
|   currentSubscription?: Subscription; | ||||
|   role: Role; | ||||
|   permissions: Permission[]; | ||||
|   identities: Identity[]; | ||||
|  | ||||
|   static tableName = 'users'; | ||||
|  | ||||
|   static jsonSchema = { | ||||
|     type: 'object', | ||||
|     required: ['fullName', 'email', 'password'], | ||||
|     required: ['fullName', 'email'], | ||||
|  | ||||
|     properties: { | ||||
|       id: { type: 'string', format: 'uuid' }, | ||||
|       fullName: { type: 'string', minLength: 1 }, | ||||
|       email: { type: 'string', format: 'email', minLength: 1, maxLength: 255 }, | ||||
|       password: { type: 'string', minLength: 1, maxLength: 255 }, | ||||
|       role: { type: 'string', enum: ['admin', 'user'] }, | ||||
|       password: { type: 'string' }, | ||||
|       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(); | ||||
|       }, | ||||
|     }, | ||||
|     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) { | ||||
| @@ -158,8 +201,10 @@ class User extends Base { | ||||
|   } | ||||
|  | ||||
|   async generateHash() { | ||||
|     if (this.password) { | ||||
|       this.password = await bcrypt.hash(this.password, 10); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   async startTrialPeriod() { | ||||
|     this.trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate(); | ||||
| @@ -232,10 +277,8 @@ class User extends Base { | ||||
|   async $beforeUpdate(opt: ModelOptions, queryContext: QueryContext) { | ||||
|     await super.$beforeUpdate(opt, queryContext); | ||||
|  | ||||
|     if (this.password) { | ||||
|     await this.generateHash(); | ||||
|   } | ||||
|   } | ||||
|  | ||||
|   async $afterInsert(queryContext: QueryContext) { | ||||
|     await super.$afterInsert(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; | ||||
|   | ||||
							
								
								
									
										23
									
								
								packages/types/index.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										23
									
								
								packages/types/index.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -95,6 +95,15 @@ export interface IUser { | ||||
|   connections: IConnection[]; | ||||
|   flows: IFlow[]; | ||||
|   steps: IStep[]; | ||||
|   role: IRole; | ||||
| } | ||||
|  | ||||
| export interface IRole { | ||||
|   id: string; | ||||
|   key: string; | ||||
|   name: string; | ||||
|   description: string; | ||||
|   isAdmin: boolean; | ||||
| } | ||||
|  | ||||
| export interface IFieldDropdown { | ||||
| @@ -386,6 +395,20 @@ type TInvoice = { | ||||
|   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' { | ||||
|   interface AxiosResponse { | ||||
|     httpError?: IJSONObject; | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| PORT=3001 | ||||
| REACT_APP_API_URL=http://localhost:3000 | ||||
| REACT_APP_GRAPHQL_URL=http://localhost:3000/graphql | ||||
| # HTTPS=true | ||||
| 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')} | ||||
|       </MenuItem> | ||||
|  | ||||
|       <MenuItem component={Link} to={URLS.ADMIN_SETTINGS_DASHBOARD}> | ||||
|         {formatMessage('accountDropdownMenu.adminSettings')} | ||||
|       </MenuItem> | ||||
|  | ||||
|       <MenuItem onClick={logout} data-test="logout-item"> | ||||
|         {formatMessage('accountDropdownMenu.logout')} | ||||
|       </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} | ||||
|         size={buttonProps.size} | ||||
|         component={buttonProps.component} | ||||
|         to={buttonProps.to} | ||||
|       > | ||||
|         {icon} | ||||
|       </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 { useNavigate } from 'react-router-dom'; | ||||
| 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 ConfirmationDialog from 'components/ConfirmationDialog'; | ||||
| 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 useFormatMessage from 'hooks/useFormatMessage'; | ||||
| import useCurrentUser from 'hooks/useCurrentUser'; | ||||
| @@ -20,37 +15,29 @@ type DeleteAccountDialogProps = { | ||||
| } | ||||
|  | ||||
| export default function DeleteAccountDialog(props: DeleteAccountDialogProps) { | ||||
|   const [deleteUser] = useMutation(DELETE_USER); | ||||
|   const [deleteCurrentUser] = useMutation(DELETE_CURRENT_USER); | ||||
|   const formatMessage = useFormatMessage(); | ||||
|   const currentUser = useCurrentUser(); | ||||
|   const authentication = useAuthentication(); | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   const handleConfirm = React.useCallback(async () => { | ||||
|     await deleteUser(); | ||||
|     await deleteCurrentUser(); | ||||
|  | ||||
|     authentication.updateToken(''); | ||||
|     await apolloClient.clearStore(); | ||||
|  | ||||
|     navigate(URLS.LOGIN); | ||||
|   }, [deleteUser, currentUser]); | ||||
|   }, [deleteCurrentUser, currentUser]); | ||||
|  | ||||
|   return ( | ||||
|     <Dialog open onClose={props.onClose}> | ||||
|       <DialogTitle > | ||||
|         {formatMessage('deleteAccountDialog.title')} | ||||
|       </DialogTitle> | ||||
|       <DialogContent> | ||||
|         <DialogContentText id="alert-dialog-description"> | ||||
|           {formatMessage('deleteAccountDialog.description')} | ||||
|         </DialogContentText> | ||||
|       </DialogContent> | ||||
|       <DialogActions> | ||||
|         <Button onClick={props.onClose}>{formatMessage('deleteAccountDialog.cancel')}</Button> | ||||
|         <Button onClick={handleConfirm} color="error"> | ||||
|           {formatMessage('deleteAccountDialog.confirm')} | ||||
|         </Button> | ||||
|       </DialogActions> | ||||
|     </Dialog> | ||||
|     <ConfirmationDialog | ||||
|       title={formatMessage('deleteAccountDialog.title')} | ||||
|       description={formatMessage('deleteAccountDialog.description')} | ||||
|       onClose={props.onClose} | ||||
|       onConfirm={handleConfirm} | ||||
|       cancelButtonChildren={formatMessage('deleteAccountDialog.cancel')} | ||||
|       confirmButtionChildren={formatMessage('deleteAccountDialog.confirm')} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|   | ||||
							
								
								
									
										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 * 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 TextField from 'components/TextField'; | ||||
| import { LOGIN } from 'graphql/mutations/login'; | ||||
| @@ -40,7 +40,7 @@ function SignUpForm() { | ||||
|   const navigate = useNavigate(); | ||||
|   const authentication = useAuthentication(); | ||||
|   const formatMessage = useFormatMessage(); | ||||
|   const [createUser, { loading: createUserLoading }] = useMutation(CREATE_USER); | ||||
|   const [registerUser, { loading: registerUserLoading }] = useMutation(REGISTER_USER); | ||||
|   const [login, { loading: loginLoading }] = useMutation(LOGIN); | ||||
|  | ||||
|   React.useEffect(() => { | ||||
| @@ -51,7 +51,7 @@ function SignUpForm() { | ||||
|  | ||||
|   const handleSubmit = async (values: any) => { | ||||
|     const { fullName, email, password } = values; | ||||
|     await createUser({ | ||||
|     await registerUser({ | ||||
|       variables: { | ||||
|         input: { fullName, email, password }, | ||||
|       }, | ||||
| @@ -165,7 +165,7 @@ function SignUpForm() { | ||||
|               variant="contained" | ||||
|               color="primary" | ||||
|               sx={{ boxShadow: 2, mt: 3 }} | ||||
|               loading={createUserLoading || loginLoading} | ||||
|               loading={registerUserLoading || loginLoading} | ||||
|               fullWidth | ||||
|               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" | ||||
|           > | ||||
|             <TableContainer component={Paper}> | ||||
|               <Table aria-label="simple table"> | ||||
|               <Table> | ||||
|                 <TableHead | ||||
|                   sx={{ | ||||
|                     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 = { | ||||
|   [key: string]: string; | ||||
|   baseUrl: string; | ||||
|   apiUrl: string; | ||||
|   graphqlUrl: string; | ||||
|   notificationsUrl: string; | ||||
|   chatwootBaseUrl: string; | ||||
|   supportEmailAddress: string; | ||||
| }; | ||||
|  | ||||
| const config: Config = { | ||||
|   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, | ||||
|   notificationsUrl: process.env.REACT_APP_NOTIFICATIONS_URL as string, | ||||
|   chatwootBaseUrl: 'https://app.chatwoot.com', | ||||
|   supportEmailAddress: 'support@automatisch.io' | ||||
| }; | ||||
|  | ||||
| if (!config.apiUrl) { | ||||
|   config.apiUrl = (new URL(config.graphqlUrl)).origin; | ||||
| } | ||||
|  | ||||
| export default config; | ||||
|   | ||||
| @@ -1,35 +1,36 @@ | ||||
| export const CONNECTIONS = '/connections'; | ||||
| export const EXECUTIONS = '/executions'; | ||||
| export const EXECUTION_PATTERN = '/executions/:executionId'; | ||||
| export const EXECUTION = (executionId: string): string => | ||||
| export const EXECUTION = (executionId: string) => | ||||
|   `/executions/${executionId}`; | ||||
|  | ||||
| export const LOGIN = '/login'; | ||||
| export const LOGIN_CALLBACK = `${LOGIN}/callback`; | ||||
| export const SIGNUP = '/sign-up'; | ||||
| export const FORGOT_PASSWORD = '/forgot-password'; | ||||
| export const RESET_PASSWORD = '/reset-password'; | ||||
|  | ||||
| export const APPS = '/apps'; | ||||
| 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_CONNECTIONS = (appKey: string): string => | ||||
| export const APP_CONNECTIONS = (appKey: string) => | ||||
|   `/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`; | ||||
| export const APP_ADD_CONNECTION_PATTERN = '/app/:appKey/connections/add'; | ||||
| export const APP_RECONNECT_CONNECTION = ( | ||||
|   appKey: string, | ||||
|   connectionId: string | ||||
| ): string => `/app/${appKey}/connections/${connectionId}/reconnect`; | ||||
| ) => `/app/${appKey}/connections/${connectionId}/reconnect`; | ||||
| export const APP_RECONNECT_CONNECTION_PATTERN = | ||||
|   '/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 = ( | ||||
|   appKey: string, | ||||
|   connectionId: string | ||||
| ): string => `/app/${appKey}/flows?connectionId=${connectionId}`; | ||||
| ) => `/app/${appKey}/flows?connectionId=${connectionId}`; | ||||
| export const APP_FLOWS_PATTERN = '/app/:appKey/flows'; | ||||
|  | ||||
| export const EDITOR = '/editor'; | ||||
| @@ -54,11 +55,11 @@ export const CREATE_FLOW_WITH_APP_AND_CONNECTION = ( | ||||
|  | ||||
|   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'; | ||||
| // 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 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_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; | ||||
|  | ||||
| // 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` | ||||
|   mutation CreateUser($input: CreateUserInput) { | ||||
|     createUser(input: $input) { | ||||
|       id | ||||
|       email | ||||
|       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'; | ||||
|  | ||||
| export const DELETE_USER = gql` | ||||
|   mutation DeleteUser { | ||||
|     deleteUser | ||||
|   mutation DeleteUser($input: DeleteUserInput) { | ||||
|     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