Compare commits
	
		
			4 Commits
		
	
	
		
			dependabot
			...
			shared-con
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 8f09681771 | ||
|   | 3801f9cfa0 | ||
|   | b6eea0b5fc | ||
|   | 099a8ea2cf | 
| @@ -59,8 +59,8 @@ | |||||||
|     "http-proxy-agent": "^7.0.0", |     "http-proxy-agent": "^7.0.0", | ||||||
|     "https-proxy-agent": "^7.0.1", |     "https-proxy-agent": "^7.0.1", | ||||||
|     "jsonwebtoken": "^9.0.0", |     "jsonwebtoken": "^9.0.0", | ||||||
|     "knex": "^2.4.0", |  | ||||||
|     "libphonenumber-js": "^1.10.48", |     "libphonenumber-js": "^1.10.48", | ||||||
|  |     "knex": "^2.5.1", | ||||||
|     "lodash.get": "^4.4.2", |     "lodash.get": "^4.4.2", | ||||||
|     "luxon": "2.5.2", |     "luxon": "2.5.2", | ||||||
|     "memory-cache": "^0.2.0", |     "memory-cache": "^0.2.0", | ||||||
| @@ -69,7 +69,7 @@ | |||||||
|     "node-html-markdown": "^1.3.0", |     "node-html-markdown": "^1.3.0", | ||||||
|     "nodemailer": "6.7.0", |     "nodemailer": "6.7.0", | ||||||
|     "oauth-1.0a": "^2.2.6", |     "oauth-1.0a": "^2.2.6", | ||||||
|     "objection": "^3.0.0", |     "objection": "^3.1.1", | ||||||
|     "passport": "^0.6.0", |     "passport": "^0.6.0", | ||||||
|     "pg": "^8.7.1", |     "pg": "^8.7.1", | ||||||
|     "php-serialize": "^4.0.2", |     "php-serialize": "^4.0.2", | ||||||
|   | |||||||
| @@ -0,0 +1,15 @@ | |||||||
|  | import { Knex } from 'knex'; | ||||||
|  |  | ||||||
|  | export async function up(knex: Knex): Promise<void> { | ||||||
|  |   return knex.schema.createTable('shared_connections', (table) => { | ||||||
|  |     table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()')); | ||||||
|  |     table.uuid('connection_id').notNullable().references('id').inTable('connections'); | ||||||
|  |     table.uuid('role_id').notNullable().references('id').inTable('roles'); | ||||||
|  |  | ||||||
|  |     table.timestamps(true, true); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export async function down(knex: Knex): Promise<void> { | ||||||
|  |   return knex.schema.dropTable('shared_connections'); | ||||||
|  | } | ||||||
| @@ -19,6 +19,7 @@ import login from './mutations/login'; | |||||||
| import registerUser from './mutations/register-user.ee'; | import registerUser from './mutations/register-user.ee'; | ||||||
| import resetConnection from './mutations/reset-connection'; | import resetConnection from './mutations/reset-connection'; | ||||||
| import resetPassword from './mutations/reset-password.ee'; | import resetPassword from './mutations/reset-password.ee'; | ||||||
|  | import shareConnection from './mutations/share-connection.ee'; | ||||||
| import updateAppAuthClient from './mutations/update-app-auth-client.ee'; | import updateAppAuthClient from './mutations/update-app-auth-client.ee'; | ||||||
| import updateAppConfig from './mutations/update-app-config.ee'; | import updateAppConfig from './mutations/update-app-config.ee'; | ||||||
| import updateConfig from './mutations/update-config.ee'; | import updateConfig from './mutations/update-config.ee'; | ||||||
| @@ -55,6 +56,7 @@ const mutationResolvers = { | |||||||
|   registerUser, |   registerUser, | ||||||
|   resetConnection, |   resetConnection, | ||||||
|   resetPassword, |   resetPassword, | ||||||
|  |   shareConnection, | ||||||
|   updateAppAuthClient, |   updateAppAuthClient, | ||||||
|   updateAppConfig, |   updateAppConfig, | ||||||
|   updateConfig, |   updateConfig, | ||||||
|   | |||||||
| @@ -28,11 +28,11 @@ const createFlow = async ( | |||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   if (connectionId) { |   if (connectionId) { | ||||||
|     const hasConnection = await context.currentUser |     const connection = await context.currentUser | ||||||
|       .$relatedQuery('connections') |       .relatedConnectionsQuery() | ||||||
|       .findById(connectionId); |       .findById(connectionId); | ||||||
|  |  | ||||||
|     if (!hasConnection) { |     if (!connection) { | ||||||
|       throw new Error('The connection does not exist!'); |       throw new Error('The connection does not exist!'); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import Context from '../../types/express/context'; | import Context from '../../types/express/context'; | ||||||
|  | import Connection from '../../models/connection'; | ||||||
|  |  | ||||||
| type Params = { | type Params = { | ||||||
|   input: { |   input: { | ||||||
| @@ -11,10 +12,13 @@ const deleteConnection = async ( | |||||||
|   params: Params, |   params: Params, | ||||||
|   context: Context |   context: Context | ||||||
| ) => { | ) => { | ||||||
|   context.currentUser.can('delete', 'Connection'); |   const conditions = context.currentUser.can('delete', 'Connection'); | ||||||
|  |   const userConnections = context.currentUser.$relatedQuery('connections'); | ||||||
|  |   const allConnections = Connection.query(); | ||||||
|  |   const baseQuery = conditions.isCreator ? userConnections : allConnections; | ||||||
|  |  | ||||||
|   await context.currentUser |   await baseQuery | ||||||
|     .$relatedQuery('connections') |     .clone() | ||||||
|     .delete() |     .delete() | ||||||
|     .findOne({ |     .findOne({ | ||||||
|       id: params.input.id, |       id: params.input.id, | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ import deleteUserQueue from '../../queues/delete-user.ee'; | |||||||
| import flowQueue from '../../queues/flow'; | import flowQueue from '../../queues/flow'; | ||||||
| import Flow from '../../models/flow'; | import Flow from '../../models/flow'; | ||||||
| import Execution from '../../models/execution'; | import Execution from '../../models/execution'; | ||||||
|  | import User from '../../models/user'; | ||||||
| import ExecutionStep from '../../models/execution-step'; | import ExecutionStep from '../../models/execution-step'; | ||||||
| import appConfig from '../../config/app'; | import appConfig from '../../config/app'; | ||||||
|  |  | ||||||
| @@ -14,10 +15,66 @@ const deleteCurrentUser = async ( | |||||||
| ) => { | ) => { | ||||||
|   const id = context.currentUser.id; |   const id = context.currentUser.id; | ||||||
|  |  | ||||||
|   const flows = await context.currentUser.$relatedQuery('flows').where({ |   try { | ||||||
|  |     await User.transaction(async (trx) => { | ||||||
|  |       const flows = await context.currentUser | ||||||
|  |         .$relatedQuery('flows', trx) | ||||||
|  |         .where({ | ||||||
|           active: true, |           active: true, | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|  |       const { count } = await context.currentUser | ||||||
|  |         .$relatedQuery('connections', trx) | ||||||
|  |         .joinRelated('sharedConnections') | ||||||
|  |         .joinRelated('steps') | ||||||
|  |         .join('flows', function () { | ||||||
|  |           this | ||||||
|  |             .on( | ||||||
|  |               'flows.id', '=', 'steps.flow_id' | ||||||
|  |             ) | ||||||
|  |             .andOnVal( | ||||||
|  |               'flows.user_id', '<>', id | ||||||
|  |             ) | ||||||
|  |             .andOnVal( | ||||||
|  |               'flows.active', '=', true | ||||||
|  |             ) | ||||||
|  |         }) | ||||||
|  |         .count() | ||||||
|  |         .first(); | ||||||
|  |  | ||||||
|  |       if (count) { | ||||||
|  |         throw new Error('The shared connections must be removed first!'); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       const executionIds = ( | ||||||
|  |         await context.currentUser | ||||||
|  |           .$relatedQuery('executions', trx) | ||||||
|  |           .select('executions.id') | ||||||
|  |       ).map((execution: Execution) => execution.id); | ||||||
|  |       const flowIds = flows.map((flow) => flow.id); | ||||||
|  |  | ||||||
|  |       await ExecutionStep.query(trx).delete().whereIn('execution_id', executionIds); | ||||||
|  |       await context.currentUser.$relatedQuery('executions', trx).delete(); | ||||||
|  |       await context.currentUser.$relatedQuery('steps', trx).delete(); | ||||||
|  |       await Flow.query(trx).whereIn('id', flowIds).delete(); | ||||||
|  |       await context.currentUser.$relatedQuery('connections', trx).delete(); | ||||||
|  |       await context.currentUser.$relatedQuery('identities', trx).delete(); | ||||||
|  |  | ||||||
|  |       if (appConfig.isCloud) { | ||||||
|  |         await context.currentUser.$relatedQuery('subscriptions', trx).delete(); | ||||||
|  |         await context.currentUser.$relatedQuery('usageData', trx).delete(); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       await context.currentUser.$query(trx).delete(); | ||||||
|  |  | ||||||
|  |       const jobName = `Delete user - ${id}`; | ||||||
|  |       const jobPayload = { id }; | ||||||
|  |       const millisecondsFor30Days = Duration.fromObject({ days: 30 }).toMillis(); | ||||||
|  |       const jobOptions = { | ||||||
|  |         delay: millisecondsFor30Days, | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       // must be done as the last action as this cannot be reverted via the transaction! | ||||||
|       const repeatableJobs = await flowQueue.getRepeatableJobs(); |       const repeatableJobs = await flowQueue.getRepeatableJobs(); | ||||||
|  |  | ||||||
|       for (const flow of flows) { |       for (const flow of flows) { | ||||||
| @@ -28,37 +85,17 @@ const deleteCurrentUser = async ( | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|  |  | ||||||
|   const executionIds = ( |  | ||||||
|     await context.currentUser |  | ||||||
|       .$relatedQuery('executions') |  | ||||||
|       .select('executions.id') |  | ||||||
|   ).map((execution: Execution) => execution.id); |  | ||||||
|   const flowIds = flows.map((flow) => flow.id); |  | ||||||
|  |  | ||||||
|   await ExecutionStep.query().delete().whereIn('execution_id', executionIds); |  | ||||||
|   await context.currentUser.$relatedQuery('executions').delete(); |  | ||||||
|   await context.currentUser.$relatedQuery('steps').delete(); |  | ||||||
|   await Flow.query().whereIn('id', flowIds).delete(); |  | ||||||
|   await context.currentUser.$relatedQuery('connections').delete(); |  | ||||||
|   await context.currentUser.$relatedQuery('identities').delete(); |  | ||||||
|  |  | ||||||
|   if (appConfig.isCloud) { |  | ||||||
|     await context.currentUser.$relatedQuery('subscriptions').delete(); |  | ||||||
|     await context.currentUser.$relatedQuery('usageData').delete(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   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); |       await deleteUserQueue.add(jobName, jobPayload, jobOptions); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|     return true; |     return true; | ||||||
|  |   } catch (err) { | ||||||
|  |     if (err instanceof Error) { | ||||||
|  |       throw err; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     throw new Error('The user deletion has failed!'); | ||||||
|  |   } | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export default deleteCurrentUser; | export default deleteCurrentUser; | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import Context from '../../types/express/context'; | import Context from '../../types/express/context'; | ||||||
|  | import Connection from '../../models/connection'; | ||||||
|  |  | ||||||
| type Params = { | type Params = { | ||||||
|   input: { |   input: { | ||||||
| @@ -11,10 +12,13 @@ const resetConnection = async ( | |||||||
|   params: Params, |   params: Params, | ||||||
|   context: Context |   context: Context | ||||||
| ) => { | ) => { | ||||||
|   context.currentUser.can('create', 'Connection'); |   const conditions = context.currentUser.can('update', 'Connection'); | ||||||
|  |   const userConnections = context.currentUser.$relatedQuery('connections'); | ||||||
|  |   const allConnections = Connection.query(); | ||||||
|  |   const baseQuery = conditions.isCreator ? userConnections : allConnections; | ||||||
|  |  | ||||||
|   let connection = await context.currentUser |   let connection = await baseQuery | ||||||
|     .$relatedQuery('connections') |     .clone() | ||||||
|     .findOne({ |     .findOne({ | ||||||
|       id: params.input.id, |       id: params.input.id, | ||||||
|     }) |     }) | ||||||
|   | |||||||
| @@ -0,0 +1,55 @@ | |||||||
|  | import Context from '../../types/express/context'; | ||||||
|  | import Connection from '../../models/connection'; | ||||||
|  | import SharedConnection from '../../models/shared-connection'; | ||||||
|  |  | ||||||
|  | type Params = { | ||||||
|  |   input: { | ||||||
|  |     id: string; | ||||||
|  |     roleIds: string[]; | ||||||
|  |   }; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const shareConnection = async ( | ||||||
|  |   _parent: unknown, | ||||||
|  |   params: Params, | ||||||
|  |   context: Context | ||||||
|  | ) => { | ||||||
|  |   const conditions = context.currentUser.can('update', 'Connection'); | ||||||
|  |  | ||||||
|  |   if (conditions.isCreator) return; | ||||||
|  |  | ||||||
|  |   const { | ||||||
|  |     id, | ||||||
|  |     roleIds, | ||||||
|  |   } = params.input; | ||||||
|  |  | ||||||
|  |   const connection = await Connection | ||||||
|  |     .query() | ||||||
|  |     .findById(id) | ||||||
|  |     .throwIfNotFound(); | ||||||
|  |  | ||||||
|  |   try { | ||||||
|  |     const updatedConnection = await Connection.transaction(async (trx) => { | ||||||
|  |       await connection.$relatedQuery('sharedConnections', trx).delete(); | ||||||
|  |  | ||||||
|  |       if (roleIds?.length) { | ||||||
|  |         const sharedConnections = roleIds.map((roleId) => ({ | ||||||
|  |           roleId, | ||||||
|  |           connectionId: connection.id, | ||||||
|  |         })); | ||||||
|  |  | ||||||
|  |         await SharedConnection.query().insert(sharedConnections); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       return await Connection | ||||||
|  |         .query(trx) | ||||||
|  |         .findById(id); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return updatedConnection; | ||||||
|  |   } catch (err) { | ||||||
|  |     throw new Error('The connection sharing preferences could not be updated!'); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default shareConnection; | ||||||
| @@ -1,6 +1,7 @@ | |||||||
| import { IJSONObject } from '@automatisch/types'; | import { IJSONObject } from '@automatisch/types'; | ||||||
| import Context from '../../types/express/context'; | import Context from '../../types/express/context'; | ||||||
| import AppAuthClient from '../../models/app-auth-client'; | import AppAuthClient from '../../models/app-auth-client'; | ||||||
|  | import Connection from '../../models/connection'; | ||||||
|  |  | ||||||
| type Params = { | type Params = { | ||||||
|   input: { |   input: { | ||||||
| @@ -15,10 +16,13 @@ const updateConnection = async ( | |||||||
|   params: Params, |   params: Params, | ||||||
|   context: Context |   context: Context | ||||||
| ) => { | ) => { | ||||||
|   context.currentUser.can('create', 'Connection'); |   const conditions = context.currentUser.can('update', 'Connection'); | ||||||
|  |   const userConnections = context.currentUser.$relatedQuery('connections'); | ||||||
|  |   const allConnections = Connection.query(); | ||||||
|  |   const baseQuery = conditions.isCreator ? userConnections : allConnections; | ||||||
|  |  | ||||||
|   let connection = await context.currentUser |   let connection = await baseQuery | ||||||
|     .$relatedQuery('connections') |     .clone() | ||||||
|     .findOne({ |     .findOne({ | ||||||
|       id: params.input.id, |       id: params.input.id, | ||||||
|     }) |     }) | ||||||
|   | |||||||
| @@ -45,10 +45,11 @@ const updateStep = async ( | |||||||
|  |  | ||||||
|       canSeeAllConnections = !conditions.isCreator; |       canSeeAllConnections = !conditions.isCreator; | ||||||
|     } catch { |     } catch { | ||||||
|       // void |       // The user does not have permission to read any connections! | ||||||
|  |       throw new Error('The connection does not exist!'); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const userConnections = context.currentUser.$relatedQuery('connections'); |     const userConnections = context.currentUser.relatedConnectionsQuery(); | ||||||
|     const allConnections = Connection.query(); |     const allConnections = Connection.query(); | ||||||
|     const baseConnectionsQuery = canSeeAllConnections ? allConnections : userConnections; |     const baseConnectionsQuery = canSeeAllConnections ? allConnections : userConnections; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -9,28 +9,55 @@ type Params = { | |||||||
| const getApp = async (_parent: unknown, params: Params, context: Context) => { | const getApp = async (_parent: unknown, params: Params, context: Context) => { | ||||||
|   const conditions = context.currentUser.can('read', 'Connection'); |   const conditions = context.currentUser.can('read', 'Connection'); | ||||||
|  |  | ||||||
|   const userConnections = context.currentUser.$relatedQuery('connections'); |  | ||||||
|   const allConnections = Connection.query(); |  | ||||||
|   const connectionBaseQuery = conditions.isCreator ? userConnections : allConnections; |  | ||||||
|  |  | ||||||
|   const app = await App.findOneByKey(params.key); |   const app = await App.findOneByKey(params.key); | ||||||
|  |  | ||||||
|   if (context.currentUser) { |   if (context.currentUser) { | ||||||
|     const connections = await connectionBaseQuery |     const userConnections = context.currentUser.relatedConnectionsQuery(); | ||||||
|       .clone() |     const allConnections = Connection.query(); | ||||||
|       .select('connections.*') |     const connectionBaseQuery = conditions.isCreator ? userConnections : allConnections; | ||||||
|       .withGraphFetched({ |  | ||||||
|         appConfig: true, |     const connections = await Connection.query() | ||||||
|         appAuthClient: true |       .with('connections', connectionBaseQuery) | ||||||
|  |       .with( | ||||||
|  |         'connections_with_flow_count', | ||||||
|  |         Connection.query() | ||||||
|  |           .clearSelect() | ||||||
|  |           .select('connections.id') | ||||||
|  |           .leftJoinRelated('steps') | ||||||
|  |           .leftJoin('flows', function () { | ||||||
|  |             this | ||||||
|  |               .on( | ||||||
|  |                 'flows.id', | ||||||
|  |                 '=', | ||||||
|  |                 'steps.flow_id', | ||||||
|  |               ) | ||||||
|  |  | ||||||
|  |             if (conditions.isCreator) { | ||||||
|  |               this.andOnVal( | ||||||
|  |                 'flows.user_id', | ||||||
|  |                 '=', | ||||||
|  |                 context.currentUser.id | ||||||
|  |               ) | ||||||
|  |             } | ||||||
|           }) |           }) | ||||||
|       .fullOuterJoinRelated('steps') |  | ||||||
|           .where({ |           .where({ | ||||||
|             'connections.key': params.key, |             'connections.key': params.key, | ||||||
|             'connections.draft': false, |             'connections.draft': false, | ||||||
|           }) |           }) | ||||||
|           .countDistinct('steps.flow_id as flowCount') |           .countDistinct('steps.flow_id as flowCount') | ||||||
|           .groupBy('connections.id') |           .groupBy('connections.id') | ||||||
|       .orderBy('created_at', 'desc'); |       ) | ||||||
|  |       .select( | ||||||
|  |         'connections.*', | ||||||
|  |         'connections_with_flow_count.flowCount as flowCount' | ||||||
|  |       ) | ||||||
|  |       .from('connections') | ||||||
|  |       .withGraphFetched({ | ||||||
|  |         appConfig: true, | ||||||
|  |         appAuthClient: true | ||||||
|  |       }) | ||||||
|  |       .joinRaw('join connections_with_flow_count on connections.id = connections_with_flow_count.id') | ||||||
|  |       .orderBy('connections.created_at', 'desc'); | ||||||
|  |  | ||||||
|     return { |     return { | ||||||
|       ...app, |       ...app, | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ const getConnectedApps = async ( | |||||||
| ) => { | ) => { | ||||||
|   const conditions = context.currentUser.can('read', 'Connection'); |   const conditions = context.currentUser.can('read', 'Connection'); | ||||||
|  |  | ||||||
|   const userConnections = context.currentUser.$relatedQuery('connections'); |   const userConnections = context.currentUser.relatedConnectionsQuery(); | ||||||
|   const allConnections = Connection.query(); |   const allConnections = Connection.query(); | ||||||
|   const connectionBaseQuery = conditions.isCreator ? userConnections : allConnections; |   const connectionBaseQuery = conditions.isCreator ? userConnections : allConnections; | ||||||
|  |  | ||||||
| @@ -25,8 +25,9 @@ const getConnectedApps = async ( | |||||||
|  |  | ||||||
|   let apps = await App.findAll(params.name); |   let apps = await App.findAll(params.name); | ||||||
|  |  | ||||||
|   const connections = await connectionBaseQuery |   const connections = await Connection | ||||||
|     .clone() |     .query() | ||||||
|  |     .with('connections', connectionBaseQuery) | ||||||
|     .select('connections.key') |     .select('connections.key') | ||||||
|     .where({ draft: false }) |     .where({ draft: false }) | ||||||
|     .count('connections.id as count') |     .count('connections.id as count') | ||||||
|   | |||||||
| @@ -0,0 +1,29 @@ | |||||||
|  | import Context from '../../types/express/context'; | ||||||
|  | import Connection from '../../models/connection'; | ||||||
|  |  | ||||||
|  | type Params = { | ||||||
|  |   id: string; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const getSharedConnectionRoleIds = async ( | ||||||
|  |   _parent: unknown, | ||||||
|  |   params: Params, | ||||||
|  |   context: Context | ||||||
|  | ) => { | ||||||
|  |   const conditions = context.currentUser.can('update', 'Connection'); | ||||||
|  |  | ||||||
|  |   if (conditions.isCreator) return; | ||||||
|  |  | ||||||
|  |   const connection = await Connection | ||||||
|  |     .query() | ||||||
|  |     .findById(params.id) | ||||||
|  |     .throwIfNotFound(); | ||||||
|  |  | ||||||
|  |   const sharedConnections = await connection.$relatedQuery('sharedConnections'); | ||||||
|  |  | ||||||
|  |   const roleIds = sharedConnections.map(({ roleId }) => roleId); | ||||||
|  |  | ||||||
|  |   return roleIds; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default getSharedConnectionRoleIds; | ||||||
| @@ -13,15 +13,15 @@ const testConnection = async ( | |||||||
|   params: Params, |   params: Params, | ||||||
|   context: Context |   context: Context | ||||||
| ) => { | ) => { | ||||||
|   const conditions = context.currentUser.can('update', 'Connection'); |   const conditions = context.currentUser.can('read', 'Connection'); | ||||||
|   const userConnections = context.currentUser.$relatedQuery('connections'); |   const userConnections = context.currentUser.relatedConnectionsQuery(); | ||||||
|   const allConnections = Connection.query(); |   const allConnections = Connection.query(); | ||||||
|   const connectionBaseQuery = conditions.isCreator ? userConnections : allConnections; |   const connectionBaseQuery = conditions.isCreator ? userConnections : allConnections; | ||||||
|  |  | ||||||
|   let connection = await connectionBaseQuery |   let connection = await connectionBaseQuery | ||||||
|     .clone() |     .clone() | ||||||
|     .findOne({ |     .findOne({ | ||||||
|       id: params.id, |       'connections.id': params.id, | ||||||
|     }) |     }) | ||||||
|     .throwIfNotFound(); |     .throwIfNotFound(); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -24,6 +24,7 @@ import getRole from './queries/get-role.ee'; | |||||||
| import getRoles from './queries/get-roles.ee'; | import getRoles from './queries/get-roles.ee'; | ||||||
| import getSamlAuthProviderRoleMappings from './queries/get-saml-auth-provider-role-mappings.ee'; | import getSamlAuthProviderRoleMappings from './queries/get-saml-auth-provider-role-mappings.ee'; | ||||||
| import getSamlAuthProvider from './queries/get-saml-auth-provider.ee'; | import getSamlAuthProvider from './queries/get-saml-auth-provider.ee'; | ||||||
|  | import getSharedConnectionRoleIds from './queries/get-shared-connection-role-ids.ee'; | ||||||
| import getStepWithTestExecutions from './queries/get-step-with-test-executions'; | import getStepWithTestExecutions from './queries/get-step-with-test-executions'; | ||||||
| import getSubscriptionStatus from './queries/get-subscription-status.ee'; | import getSubscriptionStatus from './queries/get-subscription-status.ee'; | ||||||
| import getTrialStatus from './queries/get-trial-status.ee'; | import getTrialStatus from './queries/get-trial-status.ee'; | ||||||
| @@ -60,6 +61,7 @@ const queryResolvers = { | |||||||
|   getRoles, |   getRoles, | ||||||
|   getSamlAuthProvider, |   getSamlAuthProvider, | ||||||
|   getSamlAuthProviderRoleMappings, |   getSamlAuthProviderRoleMappings, | ||||||
|  |   getSharedConnectionRoleIds, | ||||||
|   getStepWithTestExecutions, |   getStepWithTestExecutions, | ||||||
|   getSubscriptionStatus, |   getSubscriptionStatus, | ||||||
|   getTrialStatus, |   getTrialStatus, | ||||||
|   | |||||||
| @@ -53,6 +53,7 @@ type Query { | |||||||
|   getNotifications: [Notification] |   getNotifications: [Notification] | ||||||
|   getSamlAuthProvider: SamlAuthProvider |   getSamlAuthProvider: SamlAuthProvider | ||||||
|   getSamlAuthProviderRoleMappings(id: String!): [SamlAuthProvidersRoleMapping] |   getSamlAuthProviderRoleMappings(id: String!): [SamlAuthProvidersRoleMapping] | ||||||
|  |   getSharedConnectionRoleIds(id: String!): [String] | ||||||
|   getSubscriptionStatus: GetSubscriptionStatus |   getSubscriptionStatus: GetSubscriptionStatus | ||||||
|   getTrialStatus: GetTrialStatus |   getTrialStatus: GetTrialStatus | ||||||
|   getUser(id: String!): User |   getUser(id: String!): User | ||||||
| @@ -83,6 +84,7 @@ type Mutation { | |||||||
|   registerUser(input: RegisterUserInput): User |   registerUser(input: RegisterUserInput): User | ||||||
|   resetConnection(input: ResetConnectionInput): Connection |   resetConnection(input: ResetConnectionInput): Connection | ||||||
|   resetPassword(input: ResetPasswordInput): Boolean |   resetPassword(input: ResetPasswordInput): Boolean | ||||||
|  |   shareConnection(input: ShareConnectionInput): Connection | ||||||
|   updateAppAuthClient(input: UpdateAppAuthClientInput): AppAuthClient |   updateAppAuthClient(input: UpdateAppAuthClientInput): AppAuthClient | ||||||
|   updateAppConfig(input: UpdateAppConfigInput): AppConfig |   updateAppConfig(input: UpdateAppConfigInput): AppConfig | ||||||
|   updateConfig(input: JSONObject): JSONObject |   updateConfig(input: JSONObject): JSONObject | ||||||
| @@ -244,6 +246,7 @@ type AuthLink { | |||||||
| type Connection { | type Connection { | ||||||
|   id: String |   id: String | ||||||
|   key: String |   key: String | ||||||
|  |   shared: Boolean | ||||||
|   reconnectable: Boolean |   reconnectable: Boolean | ||||||
|   appAuthClientId: String |   appAuthClientId: String | ||||||
|   formattedData: ConnectionData |   formattedData: ConnectionData | ||||||
| @@ -810,6 +813,11 @@ input ExecutionFiltersInput { | |||||||
|   status: String |   status: String | ||||||
| } | } | ||||||
|  |  | ||||||
|  | input ShareConnectionInput { | ||||||
|  |   id: String! | ||||||
|  |   roleIds: [String] | ||||||
|  | } | ||||||
|  |  | ||||||
| schema { | schema { | ||||||
|   query: Query |   query: Query | ||||||
|   mutation: Mutation |   mutation: Mutation | ||||||
|   | |||||||
| @@ -1,18 +1,18 @@ | |||||||
| import { QueryContext, ModelOptions } from 'objection'; | import { IJSONObject, IRequest } from '@automatisch/types'; | ||||||
| import type { RelationMappings } from 'objection'; |  | ||||||
| import { AES, enc } from 'crypto-js'; | import { AES, enc } from 'crypto-js'; | ||||||
| import { IRequest } from '@automatisch/types'; | import type { RelationMappings } from 'objection'; | ||||||
| import App from './app'; | import { ModelOptions, QueryContext } from 'objection'; | ||||||
| import AppConfig from './app-config'; |  | ||||||
| import AppAuthClient from './app-auth-client'; |  | ||||||
| import Base from './base'; |  | ||||||
| import User from './user'; |  | ||||||
| import Step from './step'; |  | ||||||
| import ExtendedQueryBuilder from './query-builder'; |  | ||||||
| import appConfig from '../config/app'; | import appConfig from '../config/app'; | ||||||
| import { IJSONObject } from '@automatisch/types'; |  | ||||||
| import Telemetry from '../helpers/telemetry'; |  | ||||||
| import globalVariable from '../helpers/global-variable'; | import globalVariable from '../helpers/global-variable'; | ||||||
|  | import Telemetry from '../helpers/telemetry'; | ||||||
|  | import App from './app'; | ||||||
|  | import AppAuthClient from './app-auth-client'; | ||||||
|  | import AppConfig from './app-config'; | ||||||
|  | import Base from './base'; | ||||||
|  | import ExtendedQueryBuilder from './query-builder'; | ||||||
|  | import SharedConnection from './shared-connection'; | ||||||
|  | import Step from './step'; | ||||||
|  | import User from './user'; | ||||||
|  |  | ||||||
| class Connection extends Base { | class Connection extends Base { | ||||||
|   id!: string; |   id!: string; | ||||||
| @@ -24,6 +24,9 @@ class Connection extends Base { | |||||||
|   draft: boolean; |   draft: boolean; | ||||||
|   count?: number; |   count?: number; | ||||||
|   flowCount?: number; |   flowCount?: number; | ||||||
|  |   sharedConnections?: SharedConnection[]; | ||||||
|  |   // computed via `User.relevantConnectionsQuery` | ||||||
|  |   shared?: boolean; | ||||||
|   user?: User; |   user?: User; | ||||||
|   steps?: Step[]; |   steps?: Step[]; | ||||||
|   triggerSteps?: Step[]; |   triggerSteps?: Step[]; | ||||||
| @@ -46,6 +49,7 @@ class Connection extends Base { | |||||||
|       appAuthClientId: { type: 'string', format: 'uuid' }, |       appAuthClientId: { type: 'string', format: 'uuid' }, | ||||||
|       verified: { type: 'boolean', default: false }, |       verified: { type: 'boolean', default: false }, | ||||||
|       draft: { type: 'boolean' }, |       draft: { type: 'boolean' }, | ||||||
|  |       shared: { type: 'boolean', readOnly: true, }, | ||||||
|       deletedAt: { type: 'string' }, |       deletedAt: { type: 'string' }, | ||||||
|       createdAt: { type: 'string' }, |       createdAt: { type: 'string' }, | ||||||
|       updatedAt: { type: 'string' }, |       updatedAt: { type: 'string' }, | ||||||
| @@ -100,6 +104,14 @@ class Connection extends Base { | |||||||
|         to: 'app_auth_clients.id', |         to: 'app_auth_clients.id', | ||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|  |     sharedConnections: { | ||||||
|  |       relation: Base.HasManyRelation, | ||||||
|  |       modelClass: SharedConnection, | ||||||
|  |       join: { | ||||||
|  |         from: 'connections.id', | ||||||
|  |         to: 'shared_connections.connection_id', | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   get reconnectable() { |   get reconnectable() { | ||||||
|   | |||||||
							
								
								
									
										45
									
								
								packages/backend/src/models/shared-connection.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								packages/backend/src/models/shared-connection.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | |||||||
|  | import Base from './base'; | ||||||
|  | import Role from './role'; | ||||||
|  | import User from './user'; | ||||||
|  |  | ||||||
|  | class SharedConnection extends Base { | ||||||
|  |   id!: string; | ||||||
|  |   roleId!: string; | ||||||
|  |   connectionId!: string; | ||||||
|  |  | ||||||
|  |   static tableName = 'shared_connections'; | ||||||
|  |  | ||||||
|  |   static jsonSchema = { | ||||||
|  |     type: 'object', | ||||||
|  |     required: ['roleId', 'connectionId'], | ||||||
|  |  | ||||||
|  |     properties: { | ||||||
|  |       id: { type: 'string', format: 'uuid' }, | ||||||
|  |       roleId: { type: 'string', format: 'uuid' }, | ||||||
|  |       connectionId: { type: 'string', format: 'uuid' }, | ||||||
|  |       createdAt: { type: 'string' }, | ||||||
|  |       updatedAt: { type: 'string' }, | ||||||
|  |     }, | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   static relationMappings = () => ({ | ||||||
|  |     roles: { | ||||||
|  |       relation: Base.HasManyRelation, | ||||||
|  |       modelClass: Role, | ||||||
|  |       join: { | ||||||
|  |         from: 'shared_connections.role_id', | ||||||
|  |         to: 'roles.id', | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |     users: { | ||||||
|  |       relation: Base.HasManyRelation, | ||||||
|  |       modelClass: User, | ||||||
|  |       join: { | ||||||
|  |         from: 'shared_connections.role_id', | ||||||
|  |         to: 'users.role_id', | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default SharedConnection; | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| import bcrypt from 'bcrypt'; | import bcrypt from 'bcrypt'; | ||||||
| import { DateTime } from 'luxon'; | import { DateTime } from 'luxon'; | ||||||
| import crypto from 'node:crypto'; | import crypto from 'node:crypto'; | ||||||
| import { ModelOptions, QueryContext } from 'objection'; | import { raw, ModelOptions, QueryContext } from 'objection'; | ||||||
|  |  | ||||||
| import appConfig from '../config/app'; | import appConfig from '../config/app'; | ||||||
| import { hasValidLicense } from '../helpers/license.ee'; | import { hasValidLicense } from '../helpers/license.ee'; | ||||||
| @@ -28,6 +28,7 @@ class User extends Base { | |||||||
|   resetPasswordTokenSentAt: string; |   resetPasswordTokenSentAt: string; | ||||||
|   trialExpiryDate: string; |   trialExpiryDate: string; | ||||||
|   connections?: Connection[]; |   connections?: Connection[]; | ||||||
|  |   sharedConnections?: Connection[]; | ||||||
|   flows?: Flow[]; |   flows?: Flow[]; | ||||||
|   steps?: Step[]; |   steps?: Step[]; | ||||||
|   executions?: Execution[]; |   executions?: Execution[]; | ||||||
| @@ -69,6 +70,18 @@ class User extends Base { | |||||||
|         to: 'connections.user_id', |         to: 'connections.user_id', | ||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|  |     sharedConnections: { | ||||||
|  |       relation: Base.ManyToManyRelation, | ||||||
|  |       modelClass: Connection, | ||||||
|  |       join: { | ||||||
|  |         from: 'users.role_id', | ||||||
|  |         through: { | ||||||
|  |           from: 'shared_connections.role_id', | ||||||
|  |           to: 'shared_connections.connection_id', | ||||||
|  |         }, | ||||||
|  |         to: 'connections.id', | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|     flows: { |     flows: { | ||||||
|       relation: Base.HasManyRelation, |       relation: Base.HasManyRelation, | ||||||
|       modelClass: Flow, |       modelClass: Flow, | ||||||
| @@ -165,6 +178,40 @@ class User extends Base { | |||||||
|     }, |     }, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|  |   relatedConnectionsQuery() { | ||||||
|  |     return Connection | ||||||
|  |       .query() | ||||||
|  |       .select('connections.*', raw('shared_connections.role_id IS NOT NULL as shared')) | ||||||
|  |       .leftJoin( | ||||||
|  |         'shared_connections', | ||||||
|  |         'connections.id', | ||||||
|  |         '=', | ||||||
|  |         'shared_connections.connection_id' | ||||||
|  |       ) | ||||||
|  |       .join( | ||||||
|  |         'users', | ||||||
|  |         function () { | ||||||
|  |           this | ||||||
|  |             .on( | ||||||
|  |               'users.id', | ||||||
|  |               '=', | ||||||
|  |               'connections.user_id', | ||||||
|  |             ) | ||||||
|  |             .orOn( | ||||||
|  |               'users.role_id', | ||||||
|  |               '=', | ||||||
|  |               'shared_connections.role_id' | ||||||
|  |             ) | ||||||
|  |         }, | ||||||
|  |       ) | ||||||
|  |       .where( | ||||||
|  |         'users.id', | ||||||
|  |         '=', | ||||||
|  |         this.id | ||||||
|  |       ) | ||||||
|  |       .groupBy('connections.id', 'shared_connections.role_id'); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   login(password: string) { |   login(password: string) { | ||||||
|     return bcrypt.compare(password, this.password); |     return bcrypt.compare(password, this.password); | ||||||
|   } |   } | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								packages/types/index.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								packages/types/index.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -23,6 +23,7 @@ export interface IConnection { | |||||||
|   formattedData?: IJSONObject; |   formattedData?: IJSONObject; | ||||||
|   userId: string; |   userId: string; | ||||||
|   verified: boolean; |   verified: boolean; | ||||||
|  |   shared?: boolean; | ||||||
|   count?: number; |   count?: number; | ||||||
|   flowCount?: number; |   flowCount?: number; | ||||||
|   appData?: IApp; |   appData?: IApp; | ||||||
|   | |||||||
| @@ -0,0 +1,168 @@ | |||||||
|  | import type { IApp, IField, IJSONObject } from '@automatisch/types'; | ||||||
|  | import LoadingButton from '@mui/lab/LoadingButton'; | ||||||
|  | import Alert from '@mui/material/Alert'; | ||||||
|  | import Dialog from '@mui/material/Dialog'; | ||||||
|  | import DialogContent from '@mui/material/DialogContent'; | ||||||
|  | import DialogContentText from '@mui/material/DialogContentText'; | ||||||
|  | import DialogTitle from '@mui/material/DialogTitle'; | ||||||
|  | import * as React from 'react'; | ||||||
|  | import { FieldValues, SubmitHandler } from 'react-hook-form'; | ||||||
|  | import { useNavigate, useSearchParams } from 'react-router-dom'; | ||||||
|  |  | ||||||
|  | import AppAuthClientsDialog from 'components/AppAuthClientsDialog/index.ee'; | ||||||
|  | import InputCreator from 'components/InputCreator'; | ||||||
|  | import * as URLS from 'config/urls'; | ||||||
|  | import useAuthenticateApp from 'hooks/useAuthenticateApp.ee'; | ||||||
|  | import useFormatMessage from 'hooks/useFormatMessage'; | ||||||
|  | import { generateExternalLink } from '../../helpers/translationValues'; | ||||||
|  | import { Form } from './style'; | ||||||
|  |  | ||||||
|  | type AdminApplicationConnectionCreateProps = { | ||||||
|  |   onClose: (response: Record<string, unknown>) => void; | ||||||
|  |   application: IApp; | ||||||
|  |   connectionId?: string; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default function AdminApplicationConnectionCreate( | ||||||
|  |   props: AdminApplicationConnectionCreateProps | ||||||
|  | ): React.ReactElement { | ||||||
|  |   const { application, connectionId, onClose } = props; | ||||||
|  |   const { name, authDocUrl, key, auth } = application; | ||||||
|  |   const navigate = useNavigate(); | ||||||
|  |   const [searchParams] = useSearchParams(); | ||||||
|  |   const formatMessage = useFormatMessage(); | ||||||
|  |   const [error, setError] = React.useState<IJSONObject | null>(null); | ||||||
|  |   const [inProgress, setInProgress] = React.useState(false); | ||||||
|  |   const hasConnection = Boolean(connectionId); | ||||||
|  |   const useShared = searchParams.get('shared') === 'true'; | ||||||
|  |   const appAuthClientId = searchParams.get('appAuthClientId') || undefined; | ||||||
|  |   const { authenticate } = useAuthenticateApp({ | ||||||
|  |     appKey: key, | ||||||
|  |     connectionId, | ||||||
|  |     appAuthClientId, | ||||||
|  |     useShared: !!appAuthClientId, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   React.useEffect(function relayProviderData() { | ||||||
|  |     if (window.opener) { | ||||||
|  |       window.opener.postMessage({ | ||||||
|  |         source: 'automatisch', | ||||||
|  |         payload: { search: window.location.search, hash: window.location.hash }, | ||||||
|  |       }); | ||||||
|  |       window.close(); | ||||||
|  |     } | ||||||
|  |   }, []); | ||||||
|  |  | ||||||
|  |   React.useEffect( | ||||||
|  |     function initiateSharedAuthenticationForGivenAuthClient() { | ||||||
|  |       if (!appAuthClientId) return; | ||||||
|  |       if (!authenticate) return; | ||||||
|  |  | ||||||
|  |       const asyncAuthenticate = async () => { | ||||||
|  |         await authenticate(); | ||||||
|  |  | ||||||
|  |         navigate(URLS.ADMIN_APP_CONNECTIONS(key)); | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       asyncAuthenticate(); | ||||||
|  |     }, | ||||||
|  |     [appAuthClientId, authenticate] | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   const handleClientClick = (appAuthClientId: string) => | ||||||
|  |     navigate( | ||||||
|  |       URLS.ADMIN_APP_CONNECTIONS_CREATE_WITH_AUTH_CLIENT_ID( | ||||||
|  |         key, | ||||||
|  |         appAuthClientId | ||||||
|  |       ) | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |   const handleAuthClientsDialogClose = () => | ||||||
|  |     navigate(URLS.ADMIN_APP_CONNECTIONS(key)); | ||||||
|  |  | ||||||
|  |   const submitHandler: SubmitHandler<FieldValues> = React.useCallback( | ||||||
|  |     async (data) => { | ||||||
|  |       if (!authenticate) return; | ||||||
|  |  | ||||||
|  |       setInProgress(true); | ||||||
|  |  | ||||||
|  |       try { | ||||||
|  |         const response = await authenticate({ | ||||||
|  |           fields: data, | ||||||
|  |         }); | ||||||
|  |         onClose(response as Record<string, unknown>); | ||||||
|  |       } catch (err) { | ||||||
|  |         const error = err as IJSONObject; | ||||||
|  |         console.log(error); | ||||||
|  |         setError((error.graphQLErrors as IJSONObject[])?.[0]); | ||||||
|  |       } finally { | ||||||
|  |         setInProgress(false); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     [authenticate] | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   if (useShared) | ||||||
|  |     return ( | ||||||
|  |       <AppAuthClientsDialog | ||||||
|  |         appKey={key} | ||||||
|  |         onClose={handleAuthClientsDialogClose} | ||||||
|  |         onClientClick={handleClientClick} | ||||||
|  |       /> | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |   if (appAuthClientId) return <React.Fragment />; | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Dialog open={true} onClose={onClose}> | ||||||
|  |       <DialogTitle> | ||||||
|  |         {hasConnection | ||||||
|  |           ? formatMessage('adminAppsConnections.reconnectConnection') | ||||||
|  |           : formatMessage('adminAppsConnections.createConnection')} | ||||||
|  |       </DialogTitle> | ||||||
|  |  | ||||||
|  |       {authDocUrl && ( | ||||||
|  |         <Alert severity="info" sx={{ fontWeight: 300 }}> | ||||||
|  |           {formatMessage('adminAppsConnections.callToDocs', { | ||||||
|  |             appName: name, | ||||||
|  |             docsLink: generateExternalLink(authDocUrl), | ||||||
|  |           })} | ||||||
|  |         </Alert> | ||||||
|  |       )} | ||||||
|  |  | ||||||
|  |       {error && ( | ||||||
|  |         <Alert | ||||||
|  |           severity="error" | ||||||
|  |           sx={{ mt: 1, fontWeight: 500, wordBreak: 'break-all' }} | ||||||
|  |         > | ||||||
|  |           {error.message} | ||||||
|  |           {error.details && ( | ||||||
|  |             <pre style={{ whiteSpace: 'pre-wrap' }}> | ||||||
|  |               {JSON.stringify(error.details, null, 2)} | ||||||
|  |             </pre> | ||||||
|  |           )} | ||||||
|  |         </Alert> | ||||||
|  |       )} | ||||||
|  |  | ||||||
|  |       <DialogContent> | ||||||
|  |         <DialogContentText tabIndex={-1} component="div"> | ||||||
|  |           <Form onSubmit={submitHandler}> | ||||||
|  |             {auth?.fields?.map((field: IField) => ( | ||||||
|  |               <InputCreator key={field.key} schema={field} /> | ||||||
|  |             ))} | ||||||
|  |  | ||||||
|  |             <LoadingButton | ||||||
|  |               type="submit" | ||||||
|  |               variant="contained" | ||||||
|  |               color="primary" | ||||||
|  |               sx={{ boxShadow: 2 }} | ||||||
|  |               loading={inProgress} | ||||||
|  |             > | ||||||
|  |               {formatMessage('adminAppsConnections.submit')} | ||||||
|  |             </LoadingButton> | ||||||
|  |           </Form> | ||||||
|  |         </DialogContentText> | ||||||
|  |       </DialogContent> | ||||||
|  |     </Dialog> | ||||||
|  |   ); | ||||||
|  | } | ||||||
| @@ -0,0 +1,9 @@ | |||||||
|  | import { styled } from '@mui/material/styles'; | ||||||
|  | import BaseForm from 'components/Form'; | ||||||
|  |  | ||||||
|  | export const Form = styled(BaseForm)(({ theme }) => ({ | ||||||
|  |   display: 'flex', | ||||||
|  |   flexDirection: 'column', | ||||||
|  |   gap: theme.spacing(2), | ||||||
|  |   paddingTop: theme.spacing(1), | ||||||
|  | })); | ||||||
| @@ -0,0 +1,70 @@ | |||||||
|  | import { useFieldArray, useFormContext } from 'react-hook-form'; | ||||||
|  | import FormControlLabel from '@mui/material/FormControlLabel'; | ||||||
|  | import Divider from '@mui/material/Divider'; | ||||||
|  | import Checkbox from '@mui/material/Checkbox'; | ||||||
|  |  | ||||||
|  | import useFormatMessage from 'hooks/useFormatMessage'; | ||||||
|  | import ControlledCheckbox from 'components/ControlledCheckbox'; | ||||||
|  | import { Stack } from '@mui/material'; | ||||||
|  |  | ||||||
|  | type Roles = { id: string; name: string; checked: boolean }[]; | ||||||
|  |  | ||||||
|  | function RolesFieldArray() { | ||||||
|  |   const formatMessage = useFormatMessage(); | ||||||
|  |   const { control, watch, setValue } = useFormContext(); | ||||||
|  |   const fieldArrayData = useFieldArray({ | ||||||
|  |     control, | ||||||
|  |     name: 'roles', | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   const fields = fieldArrayData.fields as Roles; | ||||||
|  |   const watchedFields = watch('roles') as Roles; | ||||||
|  |   const allFieldsSelected = watchedFields.every((field) => field.checked); | ||||||
|  |   const allFieldsDeselected = watchedFields.every((field) => !field.checked); | ||||||
|  |  | ||||||
|  |   const handleSelectAllClick = () => { | ||||||
|  |     setValue( | ||||||
|  |       'roles', | ||||||
|  |       watchedFields.map((field) => ({ ...field, checked: !allFieldsSelected })), | ||||||
|  |       { shouldDirty: true } | ||||||
|  |     ); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Stack direction="column" spacing={1}> | ||||||
|  |       <FormControlLabel | ||||||
|  |         control={ | ||||||
|  |           <Checkbox | ||||||
|  |             color="primary" | ||||||
|  |             indeterminate={!(allFieldsSelected || allFieldsDeselected)} | ||||||
|  |             checked={allFieldsSelected} | ||||||
|  |             onChange={handleSelectAllClick} | ||||||
|  |           /> | ||||||
|  |         } | ||||||
|  |         label={ | ||||||
|  |           allFieldsSelected | ||||||
|  |             ? formatMessage('adminAppsConnections.deselectAll') | ||||||
|  |             : formatMessage('adminAppsConnections.selectAll') | ||||||
|  |         } | ||||||
|  |         sx={{ margin: 0 }} | ||||||
|  |       /> | ||||||
|  |       <Divider /> | ||||||
|  |       {fields.map((role, index) => { | ||||||
|  |         return ( | ||||||
|  |           <FormControlLabel | ||||||
|  |             key={role.id} | ||||||
|  |             control={ | ||||||
|  |               <ControlledCheckbox | ||||||
|  |                 name={`roles.${index}.checked`} | ||||||
|  |                 defaultValue={role.checked} | ||||||
|  |               /> | ||||||
|  |             } | ||||||
|  |             label={role.name} | ||||||
|  |           /> | ||||||
|  |         ); | ||||||
|  |       })} | ||||||
|  |     </Stack> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default RolesFieldArray; | ||||||
| @@ -0,0 +1,140 @@ | |||||||
|  | import * as React from 'react'; | ||||||
|  | import { useParams } from 'react-router-dom'; | ||||||
|  | import Dialog from '@mui/material/Dialog'; | ||||||
|  | import DialogTitle from '@mui/material/DialogTitle'; | ||||||
|  | import DialogContent from '@mui/material/DialogContent'; | ||||||
|  | import DialogContentText from '@mui/material/DialogContentText'; | ||||||
|  | import LoadingButton from '@mui/lab/LoadingButton'; | ||||||
|  | import Stack from '@mui/material/Stack'; | ||||||
|  | import Alert from '@mui/material/Alert'; | ||||||
|  | import { CircularProgress } from '@mui/material'; | ||||||
|  | import { useMutation } from '@apollo/client'; | ||||||
|  | import { IApp, IRole } from '@automatisch/types'; | ||||||
|  | import { FieldValues, SubmitHandler } from 'react-hook-form'; | ||||||
|  |  | ||||||
|  | import { SHARE_CONNECTION } from 'graphql/mutations/share-connection'; | ||||||
|  | import useFormatMessage from 'hooks/useFormatMessage'; | ||||||
|  | import useSharedConnectionRoleIds from 'hooks/useSharedConnectionRoleIds'; | ||||||
|  | import useRoles from 'hooks/useRoles.ee'; | ||||||
|  |  | ||||||
|  | import RolesFieldArray from './RolesFieldArray'; | ||||||
|  | import { Form } from './style'; | ||||||
|  |  | ||||||
|  | type AdminApplicationConnectionShareProps = { | ||||||
|  |   onClose: (response: Record<string, unknown>) => void; | ||||||
|  |   application: IApp; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | type Params = { | ||||||
|  |   connectionId: string; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | function generateRolesData(roles: IRole[], roleIds: string[]) { | ||||||
|  |   return roles.map(({ id, name }) => ({ | ||||||
|  |     id, | ||||||
|  |     name, | ||||||
|  |     checked: roleIds.includes(id), | ||||||
|  |   })); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default function AdminApplicationConnectionShare( | ||||||
|  |   props: AdminApplicationConnectionShareProps | ||||||
|  | ): React.ReactElement { | ||||||
|  |   const { onClose } = props; | ||||||
|  |   const { connectionId } = useParams() as Params; | ||||||
|  |   const formatMessage = useFormatMessage(); | ||||||
|  |   const [ | ||||||
|  |     shareConnection, | ||||||
|  |     { loading: loadingShareConnection, error: shareConnectionError }, | ||||||
|  |   ] = useMutation(SHARE_CONNECTION, { | ||||||
|  |     context: { autoSnackbar: false }, | ||||||
|  |   }); | ||||||
|  |   const { | ||||||
|  |     roleIds, | ||||||
|  |     loading: roleIdsLoading, | ||||||
|  |     error: roleIdsError, | ||||||
|  |   } = useSharedConnectionRoleIds(connectionId, { | ||||||
|  |     context: { autoSnackbar: false }, | ||||||
|  |   }); | ||||||
|  |   const { roles, loading: rolesLoading, error: rolesError } = useRoles(); | ||||||
|  |  | ||||||
|  |   const error = shareConnectionError || roleIdsError || rolesError; | ||||||
|  |   const showDialogContent = | ||||||
|  |     !roleIdsLoading && !rolesLoading && !roleIdsError && !rolesError; | ||||||
|  |  | ||||||
|  |   const submitHandler: SubmitHandler<FieldValues> = React.useCallback( | ||||||
|  |     async (data) => { | ||||||
|  |       const roles = data.roles as { | ||||||
|  |         id: string; | ||||||
|  |         name: string; | ||||||
|  |         checked: boolean; | ||||||
|  |       }[]; | ||||||
|  |  | ||||||
|  |       const response = await shareConnection({ | ||||||
|  |         variables: { | ||||||
|  |           input: { | ||||||
|  |             id: connectionId, | ||||||
|  |             roleIds: roles | ||||||
|  |               .filter((role) => role.checked) | ||||||
|  |               .map((role) => role.id), | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |       }); | ||||||
|  |       onClose(response as Record<string, unknown>); | ||||||
|  |     }, | ||||||
|  |     [] | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   const defaultValues = React.useMemo( | ||||||
|  |     () => ({ | ||||||
|  |       roles: generateRolesData(roles, roleIds), | ||||||
|  |     }), | ||||||
|  |     [roles, roleIds] | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Dialog open={true} onClose={onClose}> | ||||||
|  |       <DialogTitle> | ||||||
|  |         {formatMessage('adminAppsConnections.shareConnection')} | ||||||
|  |       </DialogTitle> | ||||||
|  |       {error && ( | ||||||
|  |         <Alert | ||||||
|  |           severity="error" | ||||||
|  |           sx={{ mt: 1, fontWeight: 500, wordBreak: 'break-all' }} | ||||||
|  |         > | ||||||
|  |           {error.message} | ||||||
|  |         </Alert> | ||||||
|  |       )} | ||||||
|  |       {(roleIdsLoading || rolesLoading) && ( | ||||||
|  |         <CircularProgress sx={{ display: 'block', margin: '20px auto' }} /> | ||||||
|  |       )} | ||||||
|  |       {showDialogContent && ( | ||||||
|  |         <DialogContent sx={{ pt: '0px !important' }}> | ||||||
|  |           <DialogContentText tabIndex={-1} component="div"> | ||||||
|  |             <Form | ||||||
|  |               defaultValues={defaultValues} | ||||||
|  |               onSubmit={submitHandler} | ||||||
|  |               render={({ formState: { isDirty } }) => { | ||||||
|  |                 return ( | ||||||
|  |                   <Stack direction="column"> | ||||||
|  |                     <RolesFieldArray /> | ||||||
|  |                     <LoadingButton | ||||||
|  |                       type="submit" | ||||||
|  |                       variant="contained" | ||||||
|  |                       color="primary" | ||||||
|  |                       sx={{ boxShadow: 2, mt: 5 }} | ||||||
|  |                       disabled={!isDirty} | ||||||
|  |                       loading={loadingShareConnection} | ||||||
|  |                     > | ||||||
|  |                       {formatMessage('adminAppsConnections.submit')} | ||||||
|  |                     </LoadingButton> | ||||||
|  |                   </Stack> | ||||||
|  |                 ); | ||||||
|  |               }} | ||||||
|  |             ></Form> | ||||||
|  |           </DialogContentText> | ||||||
|  |         </DialogContent> | ||||||
|  |       )} | ||||||
|  |     </Dialog> | ||||||
|  |   ); | ||||||
|  | } | ||||||
| @@ -0,0 +1,9 @@ | |||||||
|  | import { styled } from '@mui/material/styles'; | ||||||
|  | import BaseForm from 'components/Form'; | ||||||
|  |  | ||||||
|  | export const Form = styled(BaseForm)(({ theme }) => ({ | ||||||
|  |   display: 'flex', | ||||||
|  |   flexDirection: 'column', | ||||||
|  |   gap: theme.spacing(2), | ||||||
|  |   paddingTop: theme.spacing(1), | ||||||
|  | })); | ||||||
| @@ -0,0 +1,85 @@ | |||||||
|  | import * as React from 'react'; | ||||||
|  | import { Link } from 'react-router-dom'; | ||||||
|  | import Menu from '@mui/material/Menu'; | ||||||
|  | import type { PopoverProps } from '@mui/material/Popover'; | ||||||
|  | import MenuItem from '@mui/material/MenuItem'; | ||||||
|  | import type { IConnection } from '@automatisch/types'; | ||||||
|  |  | ||||||
|  | import * as URLS from 'config/urls'; | ||||||
|  | import useFormatMessage from 'hooks/useFormatMessage'; | ||||||
|  |  | ||||||
|  | type Action = { | ||||||
|  |   type: 'test' | 'reconnect' | 'delete' | 'shareConnection'; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | type ContextMenuProps = { | ||||||
|  |   appKey: string; | ||||||
|  |   connection: IConnection; | ||||||
|  |   onClose: () => void; | ||||||
|  |   onMenuItemClick: (event: React.MouseEvent, action: Action) => void; | ||||||
|  |   anchorEl: PopoverProps['anchorEl']; | ||||||
|  |   disableReconnection: boolean; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default function ContextMenu( | ||||||
|  |   props: ContextMenuProps | ||||||
|  | ): React.ReactElement { | ||||||
|  |   const { | ||||||
|  |     appKey, | ||||||
|  |     connection, | ||||||
|  |     onClose, | ||||||
|  |     onMenuItemClick, | ||||||
|  |     anchorEl, | ||||||
|  |     disableReconnection, | ||||||
|  |   } = props; | ||||||
|  |   const formatMessage = useFormatMessage(); | ||||||
|  |  | ||||||
|  |   const createActionHandler = React.useCallback( | ||||||
|  |     (action: Action) => { | ||||||
|  |       return function clickHandler(event: React.MouseEvent) { | ||||||
|  |         onMenuItemClick(event, action); | ||||||
|  |  | ||||||
|  |         onClose(); | ||||||
|  |       }; | ||||||
|  |     }, | ||||||
|  |     [onMenuItemClick, onClose] | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Menu | ||||||
|  |       open={true} | ||||||
|  |       onClose={onClose} | ||||||
|  |       hideBackdrop={false} | ||||||
|  |       anchorEl={anchorEl} | ||||||
|  |     > | ||||||
|  |       <MenuItem onClick={createActionHandler({ type: 'test' })}> | ||||||
|  |         {formatMessage('adminAppsConnections.testConnection')} | ||||||
|  |       </MenuItem> | ||||||
|  |  | ||||||
|  |       <MenuItem | ||||||
|  |         component={Link} | ||||||
|  |         disabled={disableReconnection} | ||||||
|  |         to={URLS.ADMIN_APP_RECONNECT_CONNECTION( | ||||||
|  |           appKey, | ||||||
|  |           connection.id, | ||||||
|  |           connection.appAuthClientId | ||||||
|  |         )} | ||||||
|  |         onClick={createActionHandler({ type: 'reconnect' })} | ||||||
|  |       > | ||||||
|  |         {formatMessage('adminAppsConnections.reconnect')} | ||||||
|  |       </MenuItem> | ||||||
|  |  | ||||||
|  |       <MenuItem | ||||||
|  |         component={Link} | ||||||
|  |         to={URLS.ADMIN_APP_SHARE_CONNECTION(appKey, connection.id)} | ||||||
|  |         onClick={createActionHandler({ type: 'shareConnection' })} | ||||||
|  |       > | ||||||
|  |         {formatMessage('adminAppsConnections.shareConnection')} | ||||||
|  |       </MenuItem> | ||||||
|  |  | ||||||
|  |       <MenuItem onClick={createActionHandler({ type: 'delete' })}> | ||||||
|  |         {formatMessage('adminAppsConnections.delete')} | ||||||
|  |       </MenuItem> | ||||||
|  |     </Menu> | ||||||
|  |   ); | ||||||
|  | } | ||||||
| @@ -0,0 +1,155 @@ | |||||||
|  | import type { IConnection } from '@automatisch/types'; | ||||||
|  | import { useLazyQuery, useMutation } from '@apollo/client'; | ||||||
|  | import CheckCircleIcon from '@mui/icons-material/CheckCircle'; | ||||||
|  | import ErrorIcon from '@mui/icons-material/Error'; | ||||||
|  | import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; | ||||||
|  | import Box from '@mui/material/Box'; | ||||||
|  | import Card from '@mui/material/Card'; | ||||||
|  | import CardActionArea from '@mui/material/CardActionArea'; | ||||||
|  | import CircularProgress from '@mui/material/CircularProgress'; | ||||||
|  | import Stack from '@mui/material/Stack'; | ||||||
|  | import { DateTime } from 'luxon'; | ||||||
|  | import * as React from 'react'; | ||||||
|  |  | ||||||
|  | import { DELETE_CONNECTION } from 'graphql/mutations/delete-connection'; | ||||||
|  | import { TEST_CONNECTION } from 'graphql/queries/test-connection'; | ||||||
|  | import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; | ||||||
|  | import useFormatMessage from 'hooks/useFormatMessage'; | ||||||
|  |  | ||||||
|  | import ConnectionContextMenu from '../AppConnectionContextMenu'; | ||||||
|  | import { CardContent, Typography } from './style'; | ||||||
|  |  | ||||||
|  | type AppConnectionRowProps = { | ||||||
|  |   connection: IConnection; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement { | ||||||
|  |   const enqueueSnackbar = useEnqueueSnackbar(); | ||||||
|  |   const [verificationVisible, setVerificationVisible] = React.useState(false); | ||||||
|  |   const [testConnection, { called: testCalled, loading: testLoading }] = | ||||||
|  |     useLazyQuery(TEST_CONNECTION, { | ||||||
|  |       fetchPolicy: 'network-only', | ||||||
|  |       onCompleted: () => { | ||||||
|  |         setTimeout(() => setVerificationVisible(false), 3000); | ||||||
|  |       }, | ||||||
|  |       onError: () => { | ||||||
|  |         setTimeout(() => setVerificationVisible(false), 3000); | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |   const [deleteConnection] = useMutation(DELETE_CONNECTION); | ||||||
|  |  | ||||||
|  |   const formatMessage = useFormatMessage(); | ||||||
|  |   const { id, key, formattedData, verified, createdAt, reconnectable, shared } = | ||||||
|  |     props.connection; | ||||||
|  |  | ||||||
|  |   const contextButtonRef = React.useRef<SVGSVGElement | null>(null); | ||||||
|  |   const [anchorEl, setAnchorEl] = React.useState<SVGSVGElement | null>(null); | ||||||
|  |  | ||||||
|  |   const handleClose = () => { | ||||||
|  |     setAnchorEl(null); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const onContextMenuClick = () => setAnchorEl(contextButtonRef.current); | ||||||
|  |   const onContextMenuAction = React.useCallback( | ||||||
|  |     async (event, action: { [key: string]: string }) => { | ||||||
|  |       if (action.type === 'delete') { | ||||||
|  |         await deleteConnection({ | ||||||
|  |           variables: { input: { id } }, | ||||||
|  |           update: (cache) => { | ||||||
|  |             const connectionCacheId = cache.identify({ | ||||||
|  |               __typename: 'Connection', | ||||||
|  |               id, | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |             cache.evict({ | ||||||
|  |               id: connectionCacheId, | ||||||
|  |             }); | ||||||
|  |           }, | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         enqueueSnackbar(formatMessage('adminAppsConnections.deletedMessage'), { | ||||||
|  |           variant: 'success', | ||||||
|  |         }); | ||||||
|  |       } else if (action.type === 'test') { | ||||||
|  |         setVerificationVisible(true); | ||||||
|  |         testConnection({ variables: { id } }); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     [deleteConnection, id, testConnection, formatMessage, enqueueSnackbar] | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   const relativeCreatedAt = DateTime.fromMillis( | ||||||
|  |     parseInt(createdAt, 10) | ||||||
|  |   ).toRelative(); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <Card sx={{ my: 2 }}> | ||||||
|  |         <CardActionArea onClick={onContextMenuClick}> | ||||||
|  |           <CardContent> | ||||||
|  |             <Stack justifyContent="center" alignItems="flex-start" spacing={1}> | ||||||
|  |               <Typography variant="h6" sx={{ textAlign: 'left' }}> | ||||||
|  |                 {formattedData?.screenName} {shared && 'shared'} | ||||||
|  |               </Typography> | ||||||
|  |  | ||||||
|  |               <Typography variant="caption"> | ||||||
|  |                 {formatMessage('adminAppsConnections.addedAt', { | ||||||
|  |                   datetime: relativeCreatedAt, | ||||||
|  |                 })} | ||||||
|  |               </Typography> | ||||||
|  |             </Stack> | ||||||
|  |  | ||||||
|  |             <Box> | ||||||
|  |               <Stack direction="row" alignItems="center" spacing={1}> | ||||||
|  |                 {verificationVisible && testCalled && testLoading && ( | ||||||
|  |                   <> | ||||||
|  |                     <CircularProgress size={16} /> | ||||||
|  |                     <Typography variant="caption"> | ||||||
|  |                       {formatMessage('adminAppsConnections.testing')} | ||||||
|  |                     </Typography> | ||||||
|  |                   </> | ||||||
|  |                 )} | ||||||
|  |                 {verificationVisible && testCalled && !testLoading && verified && ( | ||||||
|  |                   <> | ||||||
|  |                     <CheckCircleIcon fontSize="small" color="success" /> | ||||||
|  |                     <Typography variant="caption"> | ||||||
|  |                       {formatMessage('adminAppsConnections.testSuccessful')} | ||||||
|  |                     </Typography> | ||||||
|  |                   </> | ||||||
|  |                 )} | ||||||
|  |                 {verificationVisible && | ||||||
|  |                   testCalled && | ||||||
|  |                   !testLoading && | ||||||
|  |                   !verified && ( | ||||||
|  |                     <> | ||||||
|  |                       <ErrorIcon fontSize="small" color="error" /> | ||||||
|  |                       <Typography variant="caption"> | ||||||
|  |                         {formatMessage('adminAppsConnections.testFailed')} | ||||||
|  |                       </Typography> | ||||||
|  |                     </> | ||||||
|  |                   )} | ||||||
|  |               </Stack> | ||||||
|  |             </Box> | ||||||
|  |  | ||||||
|  |             <Box> | ||||||
|  |               <MoreHorizIcon ref={contextButtonRef} /> | ||||||
|  |             </Box> | ||||||
|  |           </CardContent> | ||||||
|  |         </CardActionArea> | ||||||
|  |       </Card> | ||||||
|  |  | ||||||
|  |       {anchorEl && ( | ||||||
|  |         <ConnectionContextMenu | ||||||
|  |           appKey={key} | ||||||
|  |           connection={props.connection} | ||||||
|  |           disableReconnection={!reconnectable} | ||||||
|  |           onClose={handleClose} | ||||||
|  |           onMenuItemClick={onContextMenuAction} | ||||||
|  |           anchorEl={anchorEl} | ||||||
|  |         /> | ||||||
|  |       )} | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default AppConnectionRow; | ||||||
| @@ -0,0 +1,16 @@ | |||||||
|  | import { styled } from '@mui/material/styles'; | ||||||
|  | import MuiCardContent from '@mui/material/CardContent'; | ||||||
|  | import MuiTypography from '@mui/material/Typography'; | ||||||
|  |  | ||||||
|  | export const CardContent = styled(MuiCardContent)(({ theme }) => ({ | ||||||
|  |   display: 'grid', | ||||||
|  |   gridTemplateRows: 'auto', | ||||||
|  |   gridTemplateColumns: '1fr auto auto auto', | ||||||
|  |   gridColumnGap: theme.spacing(2), | ||||||
|  |   alignItems: 'center', | ||||||
|  | })); | ||||||
|  |  | ||||||
|  | export const Typography = styled(MuiTypography)(() => ({ | ||||||
|  |   textAlign: 'center', | ||||||
|  |   display: 'inline-block', | ||||||
|  | })); | ||||||
| @@ -0,0 +1,55 @@ | |||||||
|  | import { Link } from 'react-router-dom'; | ||||||
|  | import { useQuery } from '@apollo/client'; | ||||||
|  | import CircularProgress from '@mui/material/CircularProgress'; | ||||||
|  | import Stack from '@mui/material/Stack'; | ||||||
|  | import Button from '@mui/material/Button'; | ||||||
|  | import type { IConnection } from '@automatisch/types'; | ||||||
|  |  | ||||||
|  | import { GET_APP_CONNECTIONS } from 'graphql/queries/get-app-connections'; | ||||||
|  | import * as URLS from 'config/urls'; | ||||||
|  | import useFormatMessage from 'hooks/useFormatMessage'; | ||||||
|  | import NoResultFound from 'components/NoResultFound'; | ||||||
|  |  | ||||||
|  | import AppConnectionRow from './AppConnectionRow'; | ||||||
|  |  | ||||||
|  | type AdminApplicationConnectionsProps = { appKey: string }; | ||||||
|  |  | ||||||
|  | function AdminApplicationConnections( | ||||||
|  |   props: AdminApplicationConnectionsProps | ||||||
|  | ): React.ReactElement { | ||||||
|  |   const { appKey } = props; | ||||||
|  |   const formatMessage = useFormatMessage(); | ||||||
|  |   const { data, loading } = useQuery(GET_APP_CONNECTIONS, { | ||||||
|  |     variables: { key: appKey }, | ||||||
|  |   }); | ||||||
|  |   const appConnections: IConnection[] = data?.getApp?.connections || []; | ||||||
|  |  | ||||||
|  |   if (loading) | ||||||
|  |     return <CircularProgress sx={{ display: 'block', margin: '20px auto' }} />; | ||||||
|  |  | ||||||
|  |   if (appConnections.length === 0) { | ||||||
|  |     return ( | ||||||
|  |       <NoResultFound | ||||||
|  |         to={URLS.ADMIN_APP_CONNECTIONS_CREATE(appKey)} | ||||||
|  |         text={formatMessage('adminAppsConnections.noConnections')} | ||||||
|  |       /> | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <div> | ||||||
|  |       {appConnections.map((appConnection) => ( | ||||||
|  |         <AppConnectionRow key={appConnection.id} connection={appConnection} /> | ||||||
|  |       ))} | ||||||
|  |       <Stack justifyContent="flex-end" direction="row"> | ||||||
|  |         <Link to={URLS.ADMIN_APP_CONNECTIONS_CREATE(appKey)}> | ||||||
|  |           <Button variant="contained" sx={{ mt: 2 }} component="div"> | ||||||
|  |             {formatMessage('adminAppsConnections.createConnection')} | ||||||
|  |           </Button> | ||||||
|  |         </Link> | ||||||
|  |       </Stack> | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default AdminApplicationConnections; | ||||||
| @@ -53,6 +53,7 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement { | |||||||
|     createdAt, |     createdAt, | ||||||
|     flowCount, |     flowCount, | ||||||
|     reconnectable, |     reconnectable, | ||||||
|  |     shared, | ||||||
|   } = props.connection; |   } = props.connection; | ||||||
|  |  | ||||||
|   const contextButtonRef = React.useRef<SVGSVGElement | null>(null); |   const contextButtonRef = React.useRef<SVGSVGElement | null>(null); | ||||||
| @@ -105,7 +106,7 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement { | |||||||
|           <CardContent> |           <CardContent> | ||||||
|             <Stack justifyContent="center" alignItems="flex-start" spacing={1}> |             <Stack justifyContent="center" alignItems="flex-start" spacing={1}> | ||||||
|               <Typography variant="h6" sx={{ textAlign: 'left' }}> |               <Typography variant="h6" sx={{ textAlign: 'left' }}> | ||||||
|                 {formattedData?.screenName} |                 {formattedData?.screenName} {shared && 'shared'} | ||||||
|               </Typography> |               </Typography> | ||||||
|  |  | ||||||
|               <Typography variant="caption"> |               <Typography variant="caption"> | ||||||
|   | |||||||
| @@ -103,6 +103,13 @@ export const ADMIN_APP_AUTH_CLIENTS_PATTERN = `${ADMIN_SETTINGS}/apps/:appKey/au | |||||||
| export const ADMIN_APP_CONNECTIONS_PATTERN = `${ADMIN_SETTINGS}/apps/:appKey/connections`; | export const ADMIN_APP_CONNECTIONS_PATTERN = `${ADMIN_SETTINGS}/apps/:appKey/connections`; | ||||||
| export const ADMIN_APP_CONNECTIONS = (appKey: string) => | export const ADMIN_APP_CONNECTIONS = (appKey: string) => | ||||||
|   `${ADMIN_SETTINGS}/apps/${appKey}/connections`; |   `${ADMIN_SETTINGS}/apps/${appKey}/connections`; | ||||||
|  | export const ADMIN_APP_CONNECTIONS_CREATE = (appKey: string, shared = false) => | ||||||
|  |   `${ADMIN_SETTINGS}/apps/${appKey}/connections/create?shared=${shared}`; | ||||||
|  | export const ADMIN_APP_CONNECTIONS_CREATE_WITH_AUTH_CLIENT_ID = ( | ||||||
|  |   appKey: string, | ||||||
|  |   appAuthClientId: string | ||||||
|  | ) => | ||||||
|  |   `${ADMIN_SETTINGS}/apps/${appKey}/connections/create?appAuthClientId=${appAuthClientId}`; | ||||||
| export const ADMIN_APP_SETTINGS = (appKey: string) => | export const ADMIN_APP_SETTINGS = (appKey: string) => | ||||||
|   `${ADMIN_SETTINGS}/apps/${appKey}/settings`; |   `${ADMIN_SETTINGS}/apps/${appKey}/settings`; | ||||||
| export const ADMIN_APP_AUTH_CLIENTS = (appKey: string) => | export const ADMIN_APP_AUTH_CLIENTS = (appKey: string) => | ||||||
| @@ -111,6 +118,23 @@ export const ADMIN_APP_AUTH_CLIENT = (appKey: string, id: string) => | |||||||
|   `${ADMIN_SETTINGS}/apps/${appKey}/auth-clients/${id}`; |   `${ADMIN_SETTINGS}/apps/${appKey}/auth-clients/${id}`; | ||||||
| export const ADMIN_APP_AUTH_CLIENTS_CREATE = (appKey: string) => | export const ADMIN_APP_AUTH_CLIENTS_CREATE = (appKey: string) => | ||||||
|   `${ADMIN_SETTINGS}/apps/${appKey}/auth-clients/create`; |   `${ADMIN_SETTINGS}/apps/${appKey}/auth-clients/create`; | ||||||
|  | export const ADMIN_APP_RECONNECT_CONNECTION = ( | ||||||
|  |   appKey: string, | ||||||
|  |   connectionId: string, | ||||||
|  |   appAuthClientId?: string | ||||||
|  | ) => { | ||||||
|  |   const path = `${ADMIN_SETTINGS}/apps/${appKey}/connections/${connectionId}/reconnect`; | ||||||
|  |  | ||||||
|  |   if (appAuthClientId) { | ||||||
|  |     return `${path}?appAuthClientId=${appAuthClientId}`; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return path; | ||||||
|  | }; | ||||||
|  | export const ADMIN_APP_SHARE_CONNECTION = ( | ||||||
|  |   appKey: string, | ||||||
|  |   connectionId: string | ||||||
|  | ) => `${ADMIN_SETTINGS}/apps/${appKey}/connections/${connectionId}/share`; | ||||||
|  |  | ||||||
| export const DASHBOARD = FLOWS; | export const DASHBOARD = FLOWS; | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										9
									
								
								packages/web/src/graphql/mutations/share-connection.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								packages/web/src/graphql/mutations/share-connection.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | import { gql } from '@apollo/client'; | ||||||
|  |  | ||||||
|  | export const SHARE_CONNECTION = gql` | ||||||
|  |   mutation ShareConnection($input: ShareConnectionInput) { | ||||||
|  |     shareConnection(input: $input) { | ||||||
|  |       id | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | `; | ||||||
| @@ -7,6 +7,7 @@ export const GET_APP_CONNECTIONS = gql` | |||||||
|       connections { |       connections { | ||||||
|         id |         id | ||||||
|         key |         key | ||||||
|  |         shared | ||||||
|         reconnectable |         reconnectable | ||||||
|         appAuthClientId |         appAuthClientId | ||||||
|         verified |         verified | ||||||
|   | |||||||
| @@ -0,0 +1,7 @@ | |||||||
|  | import { gql } from '@apollo/client'; | ||||||
|  |  | ||||||
|  | export const GET_SHARED_CONNECTION_ROLE_IDS = gql` | ||||||
|  |   query GetSharedConnectionRoleIds($id: String!) { | ||||||
|  |     getSharedConnectionRoleIds(id: $id) | ||||||
|  |   } | ||||||
|  | `; | ||||||
| @@ -5,13 +5,16 @@ import { GET_ROLES } from 'graphql/queries/get-roles.ee'; | |||||||
|  |  | ||||||
| type QueryResponse = { | type QueryResponse = { | ||||||
|   getRoles: IRole[]; |   getRoles: IRole[]; | ||||||
| } | }; | ||||||
|  |  | ||||||
| export default function useRoles() { | export default function useRoles() { | ||||||
|   const { data, loading } = useQuery<QueryResponse>(GET_ROLES, { context: { autoSnackbar: false } }); |   const { data, loading, error } = useQuery<QueryResponse>(GET_ROLES, { | ||||||
|  |     context: { autoSnackbar: false }, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|   return { |   return { | ||||||
|     roles: data?.getRoles || [], |     roles: data?.getRoles || [], | ||||||
|     loading |     loading, | ||||||
|  |     error, | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										32
									
								
								packages/web/src/hooks/useSharedConnectionRoleIds.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								packages/web/src/hooks/useSharedConnectionRoleIds.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | import * as React from 'react'; | ||||||
|  | import { LazyQueryHookOptions, useLazyQuery } from '@apollo/client'; | ||||||
|  |  | ||||||
|  | import { GET_SHARED_CONNECTION_ROLE_IDS } from 'graphql/queries/get-shared-connection-role-ids'; | ||||||
|  |  | ||||||
|  | type QueryResponse = { | ||||||
|  |   getSharedConnectionRoleIds: string[]; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default function useSharedConnectionRoleIds( | ||||||
|  |   connectionId: string, | ||||||
|  |   options?: LazyQueryHookOptions | ||||||
|  | ) { | ||||||
|  |   const [getSharedConnectionRoleIds, { data, loading, error }] = | ||||||
|  |     useLazyQuery<QueryResponse>(GET_SHARED_CONNECTION_ROLE_IDS, options); | ||||||
|  |  | ||||||
|  |   React.useEffect(() => { | ||||||
|  |     if (connectionId) { | ||||||
|  |       getSharedConnectionRoleIds({ | ||||||
|  |         variables: { | ||||||
|  |           id: connectionId, | ||||||
|  |         }, | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   }, [connectionId]); | ||||||
|  |  | ||||||
|  |   return { | ||||||
|  |     roleIds: data?.getSharedConnectionRoleIds || [], | ||||||
|  |     loading, | ||||||
|  |     error, | ||||||
|  |   }; | ||||||
|  | } | ||||||
| @@ -265,5 +265,21 @@ | |||||||
|   "authClient.buttonSubmit": "Submit", |   "authClient.buttonSubmit": "Submit", | ||||||
|   "authClient.inputName": "Name", |   "authClient.inputName": "Name", | ||||||
|   "authClient.inputActive": "Active", |   "authClient.inputActive": "Active", | ||||||
|   "updateAuthClient.title": "Update auth client" |   "updateAuthClient.title": "Update auth client", | ||||||
|  |   "adminAppsConnections.noConnections": "You don't have any connections yet.", | ||||||
|  |   "adminAppsConnections.createConnection": "Create connection", | ||||||
|  |   "adminAppsConnections.deletedMessage": "The connection has been deleted.", | ||||||
|  |   "adminAppsConnections.addedAt": "added {datetime}", | ||||||
|  |   "adminAppsConnections.testing": "Testing...", | ||||||
|  |   "adminAppsConnections.testSuccessful": "Test successful", | ||||||
|  |   "adminAppsConnections.testFailed": "Test failed", | ||||||
|  |   "adminAppsConnections.testConnection": "Test connection", | ||||||
|  |   "adminAppsConnections.delete": "Delete", | ||||||
|  |   "adminAppsConnections.reconnect": "Reconnect", | ||||||
|  |   "adminAppsConnections.shareConnection": "Share connection", | ||||||
|  |   "adminAppsConnections.reconnectConnection": "Reconnect connection", | ||||||
|  |   "adminAppsConnections.callToDocs": "Visit <docsLink>our documentation</docsLink> to see how to add connection for {appName}.", | ||||||
|  |   "adminAppsConnections.submit": "Submit", | ||||||
|  |   "adminAppsConnections.selectAll": "Select all roles", | ||||||
|  |   "adminAppsConnections.deselectAll": "Deselect all roles" | ||||||
| } | } | ||||||
|   | |||||||
| @@ -27,9 +27,26 @@ import AdminApplicationSettings from 'components/AdminApplicationSettings'; | |||||||
| import AdminApplicationAuthClients from 'components/AdminApplicationAuthClients'; | import AdminApplicationAuthClients from 'components/AdminApplicationAuthClients'; | ||||||
| import AdminApplicationCreateAuthClient from 'components/AdminApplicationCreateAuthClient'; | import AdminApplicationCreateAuthClient from 'components/AdminApplicationCreateAuthClient'; | ||||||
| import AdminApplicationUpdateAuthClient from 'components/AdminApplicationUpdateAuthClient'; | import AdminApplicationUpdateAuthClient from 'components/AdminApplicationUpdateAuthClient'; | ||||||
|  | import AdminApplicationConnections from 'components/AdminApplicationConnections'; | ||||||
|  | import AdminApplicationConnectionCreate from 'components/AdminApplicationConnectionCreate'; | ||||||
|  | import AdminApplicationConnectionShare from 'components/AdminApplicationConnectionShare'; | ||||||
|  |  | ||||||
| type AdminApplicationParams = { | type AdminApplicationParams = { | ||||||
|   appKey: string; |   appKey: string; | ||||||
|  |   connectionId?: string; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const ReconnectConnection = (props: any): React.ReactElement => { | ||||||
|  |   const { application, onClose } = props; | ||||||
|  |   const { connectionId } = useParams() as AdminApplicationParams; | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <AdminApplicationConnectionCreate | ||||||
|  |       onClose={onClose} | ||||||
|  |       application={application} | ||||||
|  |       connectionId={connectionId} | ||||||
|  |     /> | ||||||
|  |   ); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export default function AdminApplication(): React.ReactElement | null { | export default function AdminApplication(): React.ReactElement | null { | ||||||
| @@ -57,6 +74,7 @@ export default function AdminApplication(): React.ReactElement | null { | |||||||
|   const app = data?.getApp || {}; |   const app = data?.getApp || {}; | ||||||
|  |  | ||||||
|   const goToAuthClientsPage = () => navigate('auth-clients'); |   const goToAuthClientsPage = () => navigate('auth-clients'); | ||||||
|  |   const goToConnectionsPage = () => navigate('connections'); | ||||||
|  |  | ||||||
|   if (loading) return null; |   if (loading) return null; | ||||||
|  |  | ||||||
| @@ -120,7 +138,7 @@ export default function AdminApplication(): React.ReactElement | null { | |||||||
|                 /> |                 /> | ||||||
|                 <Route |                 <Route | ||||||
|                   path={`/connections/*`} |                   path={`/connections/*`} | ||||||
|                   element={<div>App connections</div>} |                   element={<AdminApplicationConnections appKey={appKey} />} | ||||||
|                 /> |                 /> | ||||||
|                 <Route |                 <Route | ||||||
|                   path="/" |                   path="/" | ||||||
| @@ -153,6 +171,33 @@ export default function AdminApplication(): React.ReactElement | null { | |||||||
|             /> |             /> | ||||||
|           } |           } | ||||||
|         /> |         /> | ||||||
|  |         <Route | ||||||
|  |           path="/connections/create" | ||||||
|  |           element={ | ||||||
|  |             <AdminApplicationConnectionCreate | ||||||
|  |               onClose={goToConnectionsPage} | ||||||
|  |               application={app} | ||||||
|  |             /> | ||||||
|  |           } | ||||||
|  |         /> | ||||||
|  |         <Route | ||||||
|  |           path="/connections/:connectionId/reconnect" | ||||||
|  |           element={ | ||||||
|  |             <ReconnectConnection | ||||||
|  |               application={app} | ||||||
|  |               onClose={goToConnectionsPage} | ||||||
|  |             /> | ||||||
|  |           } | ||||||
|  |         /> | ||||||
|  |         <Route | ||||||
|  |           path="/connections/:connectionId/share" | ||||||
|  |           element={ | ||||||
|  |             <AdminApplicationConnectionShare | ||||||
|  |               onClose={goToConnectionsPage} | ||||||
|  |               application={app} | ||||||
|  |             /> | ||||||
|  |           } | ||||||
|  |         /> | ||||||
|       </Routes> |       </Routes> | ||||||
|     </> |     </> | ||||||
|   ); |   ); | ||||||
|   | |||||||
| @@ -65,8 +65,8 @@ function RoleMappings({ provider, providerLoading }: RoleMappingsProps) { | |||||||
|         enqueueSnackbar(formatMessage('roleMappingsForm.successfullySaved'), { |         enqueueSnackbar(formatMessage('roleMappingsForm.successfullySaved'), { | ||||||
|           variant: 'success', |           variant: 'success', | ||||||
|           SnackbarProps: { |           SnackbarProps: { | ||||||
|             'data-test': 'snackbar-update-role-mappings-success' |             'data-test': 'snackbar-update-role-mappings-success', | ||||||
|           } |           }, | ||||||
|         }); |         }); | ||||||
|       } |       } | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|   | |||||||
| @@ -5,13 +5,12 @@ import Stack from '@mui/material/Stack'; | |||||||
| import DeleteIcon from '@mui/icons-material/Delete'; | import DeleteIcon from '@mui/icons-material/Delete'; | ||||||
| import IconButton from '@mui/material/IconButton'; | import IconButton from '@mui/material/IconButton'; | ||||||
| import Button from '@mui/material/Button'; | import Button from '@mui/material/Button'; | ||||||
|  | import { Divider, Typography } from '@mui/material'; | ||||||
|  |  | ||||||
| import useRoles from 'hooks/useRoles.ee'; | import useRoles from 'hooks/useRoles.ee'; | ||||||
| import useFormatMessage from 'hooks/useFormatMessage'; | import useFormatMessage from 'hooks/useFormatMessage'; | ||||||
|  |  | ||||||
| import ControlledAutocomplete from 'components/ControlledAutocomplete'; | import ControlledAutocomplete from 'components/ControlledAutocomplete'; | ||||||
| import TextField from 'components/TextField'; | import TextField from 'components/TextField'; | ||||||
| import { Divider, Typography } from '@mui/material'; |  | ||||||
|  |  | ||||||
| function generateRoleOptions(roles: IRole[]) { | function generateRoleOptions(roles: IRole[]) { | ||||||
|   return roles?.map(({ name: label, id: value }) => ({ label, value })); |   return roles?.map(({ name: label, id: value }) => ({ label, value })); | ||||||
|   | |||||||
							
								
								
									
										35
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										35
									
								
								yarn.lock
									
									
									
									
									
								
							| @@ -7106,6 +7106,11 @@ commander@7.1.0: | |||||||
|   resolved "https://registry.npmjs.org/commander/-/commander-7.1.0.tgz" |   resolved "https://registry.npmjs.org/commander/-/commander-7.1.0.tgz" | ||||||
|   integrity sha512-pRxBna3MJe6HKnBGsDyMv8ETbptw3axEdYHoqNh7gu5oDcew8fs0xnivZGm06Ogk8zGAJ9VX+OPEr2GXEQK4dg== |   integrity sha512-pRxBna3MJe6HKnBGsDyMv8ETbptw3axEdYHoqNh7gu5oDcew8fs0xnivZGm06Ogk8zGAJ9VX+OPEr2GXEQK4dg== | ||||||
|  |  | ||||||
|  | commander@^10.0.0: | ||||||
|  |   version "10.0.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" | ||||||
|  |   integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== | ||||||
|  |  | ||||||
| commander@^2.20.0: | commander@^2.20.0: | ||||||
|   version "2.20.3" |   version "2.20.3" | ||||||
|   resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" |   resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" | ||||||
| @@ -7121,7 +7126,7 @@ commander@^8.3.0: | |||||||
|   resolved "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz" |   resolved "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz" | ||||||
|   integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== |   integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== | ||||||
|  |  | ||||||
| commander@^9.0.0, commander@^9.1.0: | commander@^9.0.0: | ||||||
|   version "9.5.0" |   version "9.5.0" | ||||||
|   resolved "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz" |   resolved "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz" | ||||||
|   integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== |   integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== | ||||||
| @@ -12261,13 +12266,13 @@ klona@^2.0.4, klona@^2.0.5: | |||||||
|   resolved "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz" |   resolved "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz" | ||||||
|   integrity sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ== |   integrity sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ== | ||||||
|  |  | ||||||
| knex@^2.4.0: | knex@^2.5.1: | ||||||
|   version "2.4.0" |   version "2.5.1" | ||||||
|   resolved "https://registry.npmjs.org/knex/-/knex-2.4.0.tgz" |   resolved "https://registry.yarnpkg.com/knex/-/knex-2.5.1.tgz#a6c6b449866cf4229f070c17411f23871ba52ef9" | ||||||
|   integrity sha512-i0GWwqYp1Hs2yvc2rlDO6nzzkLhwdyOZKRdsMTB8ZxOs2IXQyL5rBjSbS1krowCh6V65T4X9CJaKtuIfkaPGSA== |   integrity sha512-z78DgGKUr4SE/6cm7ku+jHvFT0X97aERh/f0MUKAKgFnwCYBEW4TFBqtHWFYiJFid7fMrtpZ/gxJthvz5mEByA== | ||||||
|   dependencies: |   dependencies: | ||||||
|     colorette "2.0.19" |     colorette "2.0.19" | ||||||
|     commander "^9.1.0" |     commander "^10.0.0" | ||||||
|     debug "4.3.4" |     debug "4.3.4" | ||||||
|     escalade "^3.1.1" |     escalade "^3.1.1" | ||||||
|     esm "^3.2.25" |     esm "^3.2.25" | ||||||
| @@ -12275,7 +12280,7 @@ knex@^2.4.0: | |||||||
|     getopts "2.3.0" |     getopts "2.3.0" | ||||||
|     interpret "^2.2.0" |     interpret "^2.2.0" | ||||||
|     lodash "^4.17.21" |     lodash "^4.17.21" | ||||||
|     pg-connection-string "2.5.0" |     pg-connection-string "2.6.1" | ||||||
|     rechoir "^0.8.0" |     rechoir "^0.8.0" | ||||||
|     resolve-from "^5.0.0" |     resolve-from "^5.0.0" | ||||||
|     tarn "^3.0.2" |     tarn "^3.0.2" | ||||||
| @@ -13859,12 +13864,13 @@ object.values@^1.1.0, object.values@^1.1.5: | |||||||
|     define-properties "^1.1.3" |     define-properties "^1.1.3" | ||||||
|     es-abstract "^1.19.1" |     es-abstract "^1.19.1" | ||||||
|  |  | ||||||
| objection@^3.0.0: | objection@^3.1.1: | ||||||
|   version "3.0.1" |   version "3.1.1" | ||||||
|   resolved "https://registry.npmjs.org/objection/-/objection-3.0.1.tgz" |   resolved "https://registry.yarnpkg.com/objection/-/objection-3.1.1.tgz#b744d4ff13c01863d6edec773f1315c964442510" | ||||||
|   integrity sha512-rqNnyQE+C55UHjdpTOJEKQHJGZ/BGtBBtgxdUpKG4DQXRUmqxfmgS/MhPWxB9Pw0mLSVLEltr6soD4c0Sddy0Q== |   integrity sha512-v8dqQrFwZm9gRN3ZF4abF+hL6Jm5EbcUjOxVDan0lheOev0sggGGHBP8jgesZ68I0XXBjDFjGXCjTPZsWDu49A== | ||||||
|   dependencies: |   dependencies: | ||||||
|     ajv "^8.6.2" |     ajv "^8.6.2" | ||||||
|  |     ajv-formats "^2.1.1" | ||||||
|     db-errors "^0.2.3" |     db-errors "^0.2.3" | ||||||
|  |  | ||||||
| obuf@^1.0.0, obuf@^1.1.2: | obuf@^1.0.0, obuf@^1.1.2: | ||||||
| @@ -14385,7 +14391,12 @@ performance-now@^2.1.0: | |||||||
|   resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" |   resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" | ||||||
|   integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= |   integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= | ||||||
|  |  | ||||||
| pg-connection-string@2.5.0, pg-connection-string@^2.5.0: | pg-connection-string@2.6.1: | ||||||
|  |   version "2.6.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.1.tgz#78c23c21a35dd116f48e12e23c0965e8d9e2cbfb" | ||||||
|  |   integrity sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg== | ||||||
|  |  | ||||||
|  | pg-connection-string@^2.5.0: | ||||||
|   version "2.5.0" |   version "2.5.0" | ||||||
|   resolved "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz" |   resolved "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz" | ||||||
|   integrity sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ== |   integrity sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ== | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user