feat: introduce app configs with shared auth clients (#1213)
This commit is contained in:
@@ -25,6 +25,12 @@ const verifyCredentials = async ($: IGlobalVariable) => {
|
|||||||
$.auth.data.accessToken = data.access_token;
|
$.auth.data.accessToken = data.access_token;
|
||||||
|
|
||||||
const currentUser = await getCurrentUser($);
|
const currentUser = await getCurrentUser($);
|
||||||
|
const screenName = [
|
||||||
|
currentUser.username,
|
||||||
|
$.auth.data.instanceUrl,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' @ ');
|
||||||
|
|
||||||
await $.auth.set({
|
await $.auth.set({
|
||||||
clientId: $.auth.data.clientId,
|
clientId: $.auth.data.clientId,
|
||||||
@@ -34,7 +40,7 @@ const verifyCredentials = async ($: IGlobalVariable) => {
|
|||||||
scope: data.scope,
|
scope: data.scope,
|
||||||
tokenType: data.token_type,
|
tokenType: data.token_type,
|
||||||
userId: currentUser.id,
|
userId: currentUser.id,
|
||||||
screenName: `${currentUser.username} @ ${$.auth.data.instanceUrl}`,
|
screenName,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -0,0 +1,17 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
return knex.schema.createTable('app_configs', (table) => {
|
||||||
|
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
||||||
|
table.string('key').unique().notNullable();
|
||||||
|
table.boolean('allow_custom_connection').notNullable().defaultTo(false);
|
||||||
|
table.boolean('shared').notNullable().defaultTo(false);
|
||||||
|
table.boolean('disabled').notNullable().defaultTo(false);
|
||||||
|
|
||||||
|
table.timestamps(true, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
return knex.schema.dropTable('app_configs');
|
||||||
|
}
|
@@ -0,0 +1,17 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
return knex.schema.createTable('app_auth_clients', (table) => {
|
||||||
|
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
||||||
|
table.string('name').unique().notNullable();
|
||||||
|
table.uuid('app_config_id').notNullable().references('id').inTable('app_configs');
|
||||||
|
table.text('auth_defaults').notNullable();
|
||||||
|
table.boolean('active').notNullable().defaultTo(false);
|
||||||
|
|
||||||
|
table.timestamps(true, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
return knex.schema.dropTable('app_auth_clients');
|
||||||
|
}
|
@@ -0,0 +1,13 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.table('connections', async (table) => {
|
||||||
|
table.uuid('app_auth_client_id').references('id').inTable('app_auth_clients');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
return await knex.schema.table('connections', (table) => {
|
||||||
|
table.dropColumn('app_auth_client_id');
|
||||||
|
});
|
||||||
|
}
|
@@ -0,0 +1,33 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
|
||||||
|
const getPermissionForRole = (
|
||||||
|
roleId: string,
|
||||||
|
subject: string,
|
||||||
|
actions: string[]
|
||||||
|
) =>
|
||||||
|
actions.map((action) => ({
|
||||||
|
role_id: roleId,
|
||||||
|
subject,
|
||||||
|
action,
|
||||||
|
conditions: [],
|
||||||
|
}));
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const role = (await knex('roles')
|
||||||
|
.first(['id', 'key'])
|
||||||
|
.where({ key: 'admin' })
|
||||||
|
.limit(1)) as { id: string; key: string };
|
||||||
|
|
||||||
|
await knex('permissions').insert(
|
||||||
|
getPermissionForRole(role.id, 'App', [
|
||||||
|
'create',
|
||||||
|
'read',
|
||||||
|
'delete',
|
||||||
|
'update',
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex('permissions').where({ subject: 'App' }).delete();
|
||||||
|
}
|
@@ -1,3 +1,5 @@
|
|||||||
|
import createAppAuthClient from './mutations/create-app-auth-client.ee';
|
||||||
|
import createAppConfig from './mutations/create-app-config.ee';
|
||||||
import createConnection from './mutations/create-connection';
|
import createConnection from './mutations/create-connection';
|
||||||
import createFlow from './mutations/create-flow';
|
import createFlow from './mutations/create-flow';
|
||||||
import createRole from './mutations/create-role.ee';
|
import createRole from './mutations/create-role.ee';
|
||||||
@@ -17,6 +19,8 @@ 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 updateAppAuthClient from './mutations/update-app-auth-client.ee';
|
||||||
|
import updateAppConfig from './mutations/update-app-config.ee';
|
||||||
import updateConfig from './mutations/update-config.ee';
|
import updateConfig from './mutations/update-config.ee';
|
||||||
import updateConnection from './mutations/update-connection';
|
import updateConnection from './mutations/update-connection';
|
||||||
import updateCurrentUser from './mutations/update-current-user';
|
import updateCurrentUser from './mutations/update-current-user';
|
||||||
@@ -30,6 +34,8 @@ import upsertSamlAuthProvidersRoleMappings from './mutations/upsert-saml-auth-pr
|
|||||||
import verifyConnection from './mutations/verify-connection';
|
import verifyConnection from './mutations/verify-connection';
|
||||||
|
|
||||||
const mutationResolvers = {
|
const mutationResolvers = {
|
||||||
|
createAppAuthClient,
|
||||||
|
createAppConfig,
|
||||||
createConnection,
|
createConnection,
|
||||||
createFlow,
|
createFlow,
|
||||||
createRole,
|
createRole,
|
||||||
@@ -49,6 +55,8 @@ const mutationResolvers = {
|
|||||||
registerUser,
|
registerUser,
|
||||||
resetConnection,
|
resetConnection,
|
||||||
resetPassword,
|
resetPassword,
|
||||||
|
updateAppAuthClient,
|
||||||
|
updateAppConfig,
|
||||||
updateConfig,
|
updateConfig,
|
||||||
updateConnection,
|
updateConnection,
|
||||||
updateCurrentUser,
|
updateCurrentUser,
|
||||||
|
@@ -0,0 +1,35 @@
|
|||||||
|
import { IJSONObject } from '@automatisch/types';
|
||||||
|
import AppConfig from '../../models/app-config';
|
||||||
|
import Context from '../../types/express/context';
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
input: {
|
||||||
|
appConfigId: string;
|
||||||
|
name: string;
|
||||||
|
formattedAuthDefaults?: IJSONObject;
|
||||||
|
active?: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createAppAuthClient = async (
|
||||||
|
_parent: unknown,
|
||||||
|
params: Params,
|
||||||
|
context: Context
|
||||||
|
) => {
|
||||||
|
context.currentUser.can('update', 'App');
|
||||||
|
|
||||||
|
const appConfig = await AppConfig
|
||||||
|
.query()
|
||||||
|
.findById(params.input.appConfigId)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
const appAuthClient = await appConfig
|
||||||
|
.$relatedQuery('appAuthClients')
|
||||||
|
.insert(
|
||||||
|
params.input
|
||||||
|
);
|
||||||
|
|
||||||
|
return appAuthClient;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createAppAuthClient;
|
@@ -0,0 +1,36 @@
|
|||||||
|
import App from '../../models/app';
|
||||||
|
import AppConfig from '../../models/app-config';
|
||||||
|
import Context from '../../types/express/context';
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
input: {
|
||||||
|
key: string;
|
||||||
|
allowCustomConnection?: boolean;
|
||||||
|
shared?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createAppConfig = async (
|
||||||
|
_parent: unknown,
|
||||||
|
params: Params,
|
||||||
|
context: Context
|
||||||
|
) => {
|
||||||
|
context.currentUser.can('update', 'App');
|
||||||
|
|
||||||
|
const key = params.input.key;
|
||||||
|
|
||||||
|
const app = await App.findOneByKey(key);
|
||||||
|
|
||||||
|
if (!app) throw new Error('The app cannot be found!');
|
||||||
|
|
||||||
|
const appConfig = await AppConfig
|
||||||
|
.query()
|
||||||
|
.insert(
|
||||||
|
params.input
|
||||||
|
);
|
||||||
|
|
||||||
|
return appConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createAppConfig;
|
@@ -1,13 +1,16 @@
|
|||||||
import App from '../../models/app';
|
|
||||||
import Context from '../../types/express/context';
|
|
||||||
import { IJSONObject } from '@automatisch/types';
|
import { IJSONObject } from '@automatisch/types';
|
||||||
|
import App from '../../models/app';
|
||||||
|
import AppConfig from '../../models/app-config';
|
||||||
|
import Context from '../../types/express/context';
|
||||||
|
|
||||||
type Params = {
|
type Params = {
|
||||||
input: {
|
input: {
|
||||||
key: string;
|
key: string;
|
||||||
|
appAuthClientId: string;
|
||||||
formattedData: IJSONObject;
|
formattedData: IJSONObject;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const createConnection = async (
|
const createConnection = async (
|
||||||
_parent: unknown,
|
_parent: unknown,
|
||||||
params: Params,
|
params: Params,
|
||||||
@@ -15,13 +18,42 @@ const createConnection = async (
|
|||||||
) => {
|
) => {
|
||||||
context.currentUser.can('create', 'Connection');
|
context.currentUser.can('create', 'Connection');
|
||||||
|
|
||||||
await App.findOneByKey(params.input.key);
|
const { key, appAuthClientId } = params.input;
|
||||||
|
|
||||||
return await context.currentUser.$relatedQuery('connections').insert({
|
const app = await App.findOneByKey(key);
|
||||||
key: params.input.key,
|
|
||||||
formattedData: params.input.formattedData,
|
const appConfig = await AppConfig.query().findOne({ key });
|
||||||
|
|
||||||
|
let formattedData = params.input.formattedData;
|
||||||
|
if (appConfig) {
|
||||||
|
if (appConfig.disabled) throw new Error('This application has been disabled for new connections!');
|
||||||
|
|
||||||
|
if (!appConfig.allowCustomConnection && formattedData) throw new Error(`Custom connections cannot be created for ${app.name}!`);
|
||||||
|
|
||||||
|
if (appConfig.shared && !formattedData) {
|
||||||
|
const authClient = await appConfig
|
||||||
|
.$relatedQuery('appAuthClients')
|
||||||
|
.findById(appAuthClientId)
|
||||||
|
.where({
|
||||||
|
active: true
|
||||||
|
})
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
formattedData = authClient.formattedAuthDefaults;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdConnection = await context
|
||||||
|
.currentUser
|
||||||
|
.$relatedQuery('connections')
|
||||||
|
.insert({
|
||||||
|
key,
|
||||||
|
appAuthClientId,
|
||||||
|
formattedData,
|
||||||
verified: false,
|
verified: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return createdConnection;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default createConnection;
|
export default createConnection;
|
||||||
|
@@ -0,0 +1,28 @@
|
|||||||
|
import Context from '../../types/express/context';
|
||||||
|
import AppAuthClient from '../../models/app-auth-client';
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
input: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteAppAuthClient = async (
|
||||||
|
_parent: unknown,
|
||||||
|
params: Params,
|
||||||
|
context: Context
|
||||||
|
) => {
|
||||||
|
context.currentUser.can('delete', 'App');
|
||||||
|
|
||||||
|
await AppAuthClient
|
||||||
|
.query()
|
||||||
|
.delete()
|
||||||
|
.findOne({
|
||||||
|
id: params.input.id,
|
||||||
|
})
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default deleteAppAuthClient;
|
@@ -0,0 +1,38 @@
|
|||||||
|
import { IJSONObject } from '@automatisch/types';
|
||||||
|
import AppAuthClient from '../../models/app-auth-client';
|
||||||
|
import Context from '../../types/express/context';
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
input: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
formattedAuthDefaults?: IJSONObject;
|
||||||
|
active?: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateAppAuthClient = async (
|
||||||
|
_parent: unknown,
|
||||||
|
params: Params,
|
||||||
|
context: Context
|
||||||
|
) => {
|
||||||
|
context.currentUser.can('update', 'App');
|
||||||
|
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
...appAuthClientData
|
||||||
|
} = params.input;
|
||||||
|
|
||||||
|
const appAuthClient = await AppAuthClient
|
||||||
|
.query()
|
||||||
|
.findById(id)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
await appAuthClient
|
||||||
|
.$query()
|
||||||
|
.patch(appAuthClientData);
|
||||||
|
|
||||||
|
return appAuthClient;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default updateAppAuthClient;
|
@@ -0,0 +1,39 @@
|
|||||||
|
import AppConfig from '../../models/app-config';
|
||||||
|
import Context from '../../types/express/context';
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
input: {
|
||||||
|
id: string;
|
||||||
|
allowCustomConnection?: boolean;
|
||||||
|
shared?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateAppConfig = async (
|
||||||
|
_parent: unknown,
|
||||||
|
params: Params,
|
||||||
|
context: Context
|
||||||
|
) => {
|
||||||
|
context.currentUser.can('update', 'App');
|
||||||
|
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
...appConfigToUpdate
|
||||||
|
} = params.input;
|
||||||
|
|
||||||
|
const appConfig = await AppConfig
|
||||||
|
.query()
|
||||||
|
.findById(id)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
await appConfig
|
||||||
|
.$query()
|
||||||
|
.patch(
|
||||||
|
appConfigToUpdate
|
||||||
|
);
|
||||||
|
|
||||||
|
return appConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default updateAppConfig;
|
@@ -1,10 +1,12 @@
|
|||||||
import Context from '../../types/express/context';
|
|
||||||
import { IJSONObject } from '@automatisch/types';
|
import { IJSONObject } from '@automatisch/types';
|
||||||
|
import Context from '../../types/express/context';
|
||||||
|
import AppAuthClient from '../../models/app-auth-client';
|
||||||
|
|
||||||
type Params = {
|
type Params = {
|
||||||
input: {
|
input: {
|
||||||
id: string;
|
id: string;
|
||||||
formattedData: IJSONObject;
|
formattedData?: IJSONObject;
|
||||||
|
appAuthClientId?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -22,10 +24,21 @@ const updateConnection = async (
|
|||||||
})
|
})
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
let formattedData = params.input.formattedData;
|
||||||
|
|
||||||
|
if (params.input.appAuthClientId) {
|
||||||
|
const appAuthClient = await AppAuthClient
|
||||||
|
.query()
|
||||||
|
.findById(params.input.appAuthClientId)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
formattedData = appAuthClient.formattedAuthDefaults;
|
||||||
|
}
|
||||||
|
|
||||||
connection = await connection.$query().patchAndFetch({
|
connection = await connection.$query().patchAndFetch({
|
||||||
formattedData: {
|
formattedData: {
|
||||||
...connection.formattedData,
|
...connection.formattedData,
|
||||||
...params.input.formattedData,
|
...formattedData,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -0,0 +1,30 @@
|
|||||||
|
import AppAuthClient from '../../models/app-auth-client';
|
||||||
|
import Context from '../../types/express/context';
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAppAuthClient = async (_parent: unknown, params: Params, context: Context) => {
|
||||||
|
let canSeeAllClients = false;
|
||||||
|
try {
|
||||||
|
context.currentUser.can('read', 'App');
|
||||||
|
|
||||||
|
canSeeAllClients = true;
|
||||||
|
} catch {
|
||||||
|
// void
|
||||||
|
}
|
||||||
|
|
||||||
|
const appAuthClient = AppAuthClient
|
||||||
|
.query()
|
||||||
|
.findById(params.id)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
if (!canSeeAllClients) {
|
||||||
|
appAuthClient.where({ active: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
return await appAuthClient;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getAppAuthClient;
|
@@ -0,0 +1,40 @@
|
|||||||
|
import AppConfig from '../../models/app-config';
|
||||||
|
import Context from '../../types/express/context';
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
appKey: string;
|
||||||
|
active: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAppAuthClients = async (_parent: unknown, params: Params, context: Context) => {
|
||||||
|
let canSeeAllClients = false;
|
||||||
|
try {
|
||||||
|
context.currentUser.can('read', 'App');
|
||||||
|
|
||||||
|
canSeeAllClients = true;
|
||||||
|
} catch {
|
||||||
|
// void
|
||||||
|
}
|
||||||
|
|
||||||
|
const appConfig = await AppConfig
|
||||||
|
.query()
|
||||||
|
.findOne({
|
||||||
|
key: params.appKey,
|
||||||
|
})
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
const appAuthClients = appConfig
|
||||||
|
.$relatedQuery('appAuthClients')
|
||||||
|
.where({ active: params.active })
|
||||||
|
.skipUndefined();
|
||||||
|
|
||||||
|
if (!canSeeAllClients) {
|
||||||
|
appAuthClients.where({
|
||||||
|
active: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return await appAuthClients;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getAppAuthClients;
|
23
packages/backend/src/graphql/queries/get-app-config.ee.ts
Normal file
23
packages/backend/src/graphql/queries/get-app-config.ee.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import AppConfig from '../../models/app-config';
|
||||||
|
import Context from '../../types/express/context';
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
key: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAppConfig = async (_parent: unknown, params: Params, context: Context) => {
|
||||||
|
context.currentUser.can('create', 'Connection');
|
||||||
|
|
||||||
|
const appConfig = await AppConfig
|
||||||
|
.query()
|
||||||
|
.withGraphFetched({
|
||||||
|
appAuthClients: true
|
||||||
|
})
|
||||||
|
.findOne({
|
||||||
|
key: params.key
|
||||||
|
});
|
||||||
|
|
||||||
|
return appConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getAppConfig;
|
@@ -19,6 +19,10 @@ const getApp = async (_parent: unknown, params: Params, context: Context) => {
|
|||||||
const connections = await connectionBaseQuery
|
const connections = await connectionBaseQuery
|
||||||
.clone()
|
.clone()
|
||||||
.select('connections.*')
|
.select('connections.*')
|
||||||
|
.withGraphFetched({
|
||||||
|
appConfig: true,
|
||||||
|
appAuthClient: true
|
||||||
|
})
|
||||||
.fullOuterJoinRelated('steps')
|
.fullOuterJoinRelated('steps')
|
||||||
.where({
|
.where({
|
||||||
'connections.key': params.key,
|
'connections.key': params.key,
|
||||||
|
@@ -1,9 +1,12 @@
|
|||||||
import getApp from './queries/get-app';
|
import getApp from './queries/get-app';
|
||||||
|
import getAppAuthClient from './queries/get-app-auth-client.ee';
|
||||||
|
import getAppAuthClients from './queries/get-app-auth-clients.ee';
|
||||||
|
import getAppConfig from './queries/get-app-config.ee';
|
||||||
import getApps from './queries/get-apps';
|
import getApps from './queries/get-apps';
|
||||||
import getAutomatischInfo from './queries/get-automatisch-info';
|
import getAutomatischInfo from './queries/get-automatisch-info';
|
||||||
import getBillingAndUsage from './queries/get-billing-and-usage.ee';
|
import getBillingAndUsage from './queries/get-billing-and-usage.ee';
|
||||||
import getConnectedApps from './queries/get-connected-apps';
|
|
||||||
import getConfig from './queries/get-config.ee';
|
import getConfig from './queries/get-config.ee';
|
||||||
|
import getConnectedApps from './queries/get-connected-apps';
|
||||||
import getCurrentUser from './queries/get-current-user';
|
import getCurrentUser from './queries/get-current-user';
|
||||||
import getDynamicData from './queries/get-dynamic-data';
|
import getDynamicData from './queries/get-dynamic-data';
|
||||||
import getDynamicFields from './queries/get-dynamic-fields';
|
import getDynamicFields from './queries/get-dynamic-fields';
|
||||||
@@ -30,6 +33,9 @@ import testConnection from './queries/test-connection';
|
|||||||
|
|
||||||
const queryResolvers = {
|
const queryResolvers = {
|
||||||
getApp,
|
getApp,
|
||||||
|
getAppAuthClient,
|
||||||
|
getAppAuthClients,
|
||||||
|
getAppConfig,
|
||||||
getApps,
|
getApps,
|
||||||
getAutomatischInfo,
|
getAutomatischInfo,
|
||||||
getBillingAndUsage,
|
getBillingAndUsage,
|
||||||
|
@@ -5,6 +5,9 @@ type Query {
|
|||||||
onlyWithActions: Boolean
|
onlyWithActions: Boolean
|
||||||
): [App]
|
): [App]
|
||||||
getApp(key: String!): App
|
getApp(key: String!): App
|
||||||
|
getAppConfig(key: String!): AppConfig
|
||||||
|
getAppAuthClient(id: String!): AppAuthClient
|
||||||
|
getAppAuthClients(appKey: String!, active: Boolean): [AppAuthClient]
|
||||||
getConnectedApps(name: String): [App]
|
getConnectedApps(name: String): [App]
|
||||||
testConnection(id: String!): Connection
|
testConnection(id: String!): Connection
|
||||||
getFlow(id: String!): Flow
|
getFlow(id: String!): Flow
|
||||||
@@ -49,10 +52,12 @@ type Query {
|
|||||||
getUser(id: String!): User
|
getUser(id: String!): User
|
||||||
getUsers(limit: Int!, offset: Int!): UserConnection
|
getUsers(limit: Int!, offset: Int!): UserConnection
|
||||||
healthcheck: AppHealth
|
healthcheck: AppHealth
|
||||||
listSamlAuthProviders: [ListSamlAuthProviders]
|
listSamlAuthProviders: [ListSamlAuthProvider]
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
|
createAppConfig(input: CreateAppConfigInput): AppConfig
|
||||||
|
createAppAuthClient(input: CreateAppAuthClientInput): AppAuthClient
|
||||||
createConnection(input: CreateConnectionInput): Connection
|
createConnection(input: CreateConnectionInput): Connection
|
||||||
createFlow(input: CreateFlowInput): Flow
|
createFlow(input: CreateFlowInput): Flow
|
||||||
createRole(input: CreateRoleInput): Role
|
createRole(input: CreateRoleInput): Role
|
||||||
@@ -72,6 +77,8 @@ 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
|
||||||
|
updateAppAuthClient(input: UpdateAppAuthClientInput): AppAuthClient
|
||||||
|
updateAppConfig(input: UpdateAppConfigInput): AppConfig
|
||||||
updateConfig(input: JSONObject): JSONObject
|
updateConfig(input: JSONObject): JSONObject
|
||||||
updateConnection(input: UpdateConnectionInput): Connection
|
updateConnection(input: UpdateConnectionInput): Connection
|
||||||
updateCurrentUser(input: UpdateCurrentUserInput): User
|
updateCurrentUser(input: UpdateCurrentUserInput): User
|
||||||
@@ -162,6 +169,16 @@ type SubstepArgumentAdditionalFieldsArgument {
|
|||||||
value: String
|
value: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AppConfig {
|
||||||
|
id: String
|
||||||
|
key: String
|
||||||
|
allowCustomConnection: Boolean
|
||||||
|
canConnect: Boolean
|
||||||
|
canCustomConnect: Boolean
|
||||||
|
shared: Boolean
|
||||||
|
disabled: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
type App {
|
type App {
|
||||||
name: String
|
name: String
|
||||||
key: String
|
key: String
|
||||||
@@ -181,7 +198,9 @@ type App {
|
|||||||
type AppAuth {
|
type AppAuth {
|
||||||
fields: [Field]
|
fields: [Field]
|
||||||
authenticationSteps: [AuthenticationStep]
|
authenticationSteps: [AuthenticationStep]
|
||||||
|
sharedAuthenticationSteps: [AuthenticationStep]
|
||||||
reconnectionSteps: [ReconnectionStep]
|
reconnectionSteps: [ReconnectionStep]
|
||||||
|
sharedReconnectionSteps: [ReconnectionStep]
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ArgumentEnumType {
|
enum ArgumentEnumType {
|
||||||
@@ -219,6 +238,8 @@ type AuthLink {
|
|||||||
type Connection {
|
type Connection {
|
||||||
id: String
|
id: String
|
||||||
key: String
|
key: String
|
||||||
|
reconnectable: Boolean
|
||||||
|
appAuthClientId: String
|
||||||
formattedData: ConnectionData
|
formattedData: ConnectionData
|
||||||
verified: Boolean
|
verified: Boolean
|
||||||
app: App
|
app: App
|
||||||
@@ -328,7 +349,8 @@ type UserEdge {
|
|||||||
|
|
||||||
input CreateConnectionInput {
|
input CreateConnectionInput {
|
||||||
key: String!
|
key: String!
|
||||||
formattedData: JSONObject!
|
appAuthClientId: String
|
||||||
|
formattedData: JSONObject
|
||||||
}
|
}
|
||||||
|
|
||||||
input GenerateAuthUrlInput {
|
input GenerateAuthUrlInput {
|
||||||
@@ -337,7 +359,8 @@ input GenerateAuthUrlInput {
|
|||||||
|
|
||||||
input UpdateConnectionInput {
|
input UpdateConnectionInput {
|
||||||
id: String!
|
id: String!
|
||||||
formattedData: JSONObject!
|
formattedData: JSONObject
|
||||||
|
appAuthClientId: String
|
||||||
}
|
}
|
||||||
|
|
||||||
input ResetConnectionInput {
|
input ResetConnectionInput {
|
||||||
@@ -690,7 +713,7 @@ type PaymentPlan {
|
|||||||
productId: String
|
productId: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListSamlAuthProviders {
|
type ListSamlAuthProvider {
|
||||||
id: String
|
id: String
|
||||||
name: String
|
name: String
|
||||||
issuer: String
|
issuer: String
|
||||||
@@ -725,6 +748,41 @@ type Subject {
|
|||||||
key: String
|
key: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input CreateAppConfigInput {
|
||||||
|
key: String
|
||||||
|
allowCustomConnection: Boolean
|
||||||
|
shared: Boolean
|
||||||
|
disabled: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
input UpdateAppConfigInput {
|
||||||
|
id: String
|
||||||
|
allowCustomConnection: Boolean
|
||||||
|
shared: Boolean
|
||||||
|
disabled: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppAuthClient {
|
||||||
|
id: String
|
||||||
|
appConfigId: String
|
||||||
|
name: String
|
||||||
|
active: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
input CreateAppAuthClientInput {
|
||||||
|
appConfigId: String
|
||||||
|
name: String
|
||||||
|
formattedAuthDefaults: JSONObject
|
||||||
|
active: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
input UpdateAppAuthClientInput {
|
||||||
|
id: String
|
||||||
|
name: String
|
||||||
|
formattedAuthDefaults: JSONObject
|
||||||
|
active: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
schema {
|
schema {
|
||||||
query: Query
|
query: Query
|
||||||
mutation: Mutation
|
mutation: Mutation
|
||||||
|
@@ -3,6 +3,7 @@ import { IApp } from '@automatisch/types';
|
|||||||
function addAuthenticationSteps(app: IApp): IApp {
|
function addAuthenticationSteps(app: IApp): IApp {
|
||||||
if (app.auth.generateAuthUrl) {
|
if (app.auth.generateAuthUrl) {
|
||||||
app.auth.authenticationSteps = authenticationStepsWithAuthUrl;
|
app.auth.authenticationSteps = authenticationStepsWithAuthUrl;
|
||||||
|
app.auth.sharedAuthenticationSteps = sharedAuthenticationStepsWithAuthUrl;
|
||||||
} else {
|
} else {
|
||||||
app.auth.authenticationSteps = authenticationStepsWithoutAuthUrl;
|
app.auth.authenticationSteps = authenticationStepsWithoutAuthUrl;
|
||||||
}
|
}
|
||||||
@@ -98,4 +99,65 @@ const authenticationStepsWithAuthUrl = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const sharedAuthenticationStepsWithAuthUrl = [
|
||||||
|
{
|
||||||
|
type: 'mutation' as const,
|
||||||
|
name: 'createConnection',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
name: 'key',
|
||||||
|
value: '{key}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'appAuthClientId',
|
||||||
|
value: '{appAuthClientId}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'mutation' as const,
|
||||||
|
name: 'generateAuthUrl',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
value: '{createConnection.id}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'openWithPopup' as const,
|
||||||
|
name: 'openAuthPopup',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
name: 'url',
|
||||||
|
value: '{generateAuthUrl.url}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'mutation' as const,
|
||||||
|
name: 'updateConnection',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
value: '{createConnection.id}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'formattedData',
|
||||||
|
value: '{openAuthPopup.all}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'mutation' as const,
|
||||||
|
name: 'verifyConnection',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
value: '{createConnection.id}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export default addAuthenticationSteps;
|
export default addAuthenticationSteps;
|
||||||
|
@@ -67,11 +67,21 @@ function addReconnectionSteps(app: IApp): IApp {
|
|||||||
|
|
||||||
if (hasReconnectionSteps) return app;
|
if (hasReconnectionSteps) return app;
|
||||||
|
|
||||||
|
if (app.auth.authenticationSteps) {
|
||||||
const updatedSteps = replaceCreateConnectionsWithUpdate(
|
const updatedSteps = replaceCreateConnectionsWithUpdate(
|
||||||
app.auth.authenticationSteps
|
app.auth.authenticationSteps
|
||||||
);
|
);
|
||||||
|
|
||||||
app.auth.reconnectionSteps = [resetConnectionStep, ...updatedSteps];
|
app.auth.reconnectionSteps = [resetConnectionStep, ...updatedSteps];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (app.auth.sharedAuthenticationSteps) {
|
||||||
|
const updatedStepsWithEmbeddedDefaults = replaceCreateConnectionsWithUpdate(
|
||||||
|
app.auth.sharedAuthenticationSteps
|
||||||
|
);
|
||||||
|
|
||||||
|
app.auth.sharedReconnectionSteps = [resetConnectionStep, ...updatedStepsWithEmbeddedDefaults];
|
||||||
|
}
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
91
packages/backend/src/models/app-auth-client.ts
Normal file
91
packages/backend/src/models/app-auth-client.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { IJSONObject } from '@automatisch/types';
|
||||||
|
import { AES, enc } from 'crypto-js';
|
||||||
|
import { ModelOptions, QueryContext } from 'objection';
|
||||||
|
import appConfig from '../config/app';
|
||||||
|
import AppConfig from './app-config';
|
||||||
|
import Base from './base';
|
||||||
|
|
||||||
|
class AppAuthClient extends Base {
|
||||||
|
id!: string;
|
||||||
|
name: string;
|
||||||
|
active: boolean;
|
||||||
|
appConfigId!: string;
|
||||||
|
authDefaults: string;
|
||||||
|
formattedAuthDefaults?: IJSONObject;
|
||||||
|
appConfig?: AppConfig;
|
||||||
|
|
||||||
|
static tableName = 'app_auth_clients';
|
||||||
|
|
||||||
|
static jsonSchema = {
|
||||||
|
type: 'object',
|
||||||
|
required: ['name', 'appConfigId', 'formattedAuthDefaults'],
|
||||||
|
|
||||||
|
properties: {
|
||||||
|
id: { type: 'string', format: 'uuid' },
|
||||||
|
appConfigId: { type: 'string', format: 'uuid' },
|
||||||
|
active: { type: 'boolean' },
|
||||||
|
authDefaults: { type: ['string', 'null'] },
|
||||||
|
formattedAuthDefaults: { type: 'object' },
|
||||||
|
createdAt: { type: 'string' },
|
||||||
|
updatedAt: { type: 'string' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
static relationMappings = () => ({
|
||||||
|
appConfig: {
|
||||||
|
relation: Base.BelongsToOneRelation,
|
||||||
|
modelClass: AppConfig,
|
||||||
|
join: {
|
||||||
|
from: 'app_auth_clients.app_config_id',
|
||||||
|
to: 'app_configs.id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
encryptData(): void {
|
||||||
|
if (!this.eligibleForEncryption()) return;
|
||||||
|
|
||||||
|
this.authDefaults = AES.encrypt(
|
||||||
|
JSON.stringify(this.formattedAuthDefaults),
|
||||||
|
appConfig.encryptionKey
|
||||||
|
).toString();
|
||||||
|
|
||||||
|
delete this.formattedAuthDefaults;
|
||||||
|
}
|
||||||
|
decryptData(): void {
|
||||||
|
if (!this.eligibleForDecryption()) return;
|
||||||
|
|
||||||
|
this.formattedAuthDefaults = JSON.parse(
|
||||||
|
AES.decrypt(this.authDefaults, appConfig.encryptionKey).toString(enc.Utf8)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
eligibleForEncryption(): boolean {
|
||||||
|
return this.formattedAuthDefaults ? true : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
eligibleForDecryption(): boolean {
|
||||||
|
return this.authDefaults ? true : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Make another abstraction like beforeSave instead of using
|
||||||
|
// beforeInsert and beforeUpdate separately for the same operation.
|
||||||
|
async $beforeInsert(queryContext: QueryContext): Promise<void> {
|
||||||
|
await super.$beforeInsert(queryContext);
|
||||||
|
this.encryptData();
|
||||||
|
}
|
||||||
|
|
||||||
|
async $beforeUpdate(
|
||||||
|
opt: ModelOptions,
|
||||||
|
queryContext: QueryContext
|
||||||
|
): Promise<void> {
|
||||||
|
await super.$beforeUpdate(opt, queryContext);
|
||||||
|
this.encryptData();
|
||||||
|
}
|
||||||
|
|
||||||
|
async $afterFind(): Promise<void> {
|
||||||
|
this.decryptData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppAuthClient;
|
70
packages/backend/src/models/app-config.ts
Normal file
70
packages/backend/src/models/app-config.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import App from './app';
|
||||||
|
import Base from './base';
|
||||||
|
import AppAuthClient from './app-auth-client';
|
||||||
|
|
||||||
|
class AppConfig extends Base {
|
||||||
|
id!: string;
|
||||||
|
key!: string;
|
||||||
|
allowCustomConnection: boolean;
|
||||||
|
shared: boolean;
|
||||||
|
disabled: boolean;
|
||||||
|
app?: App;
|
||||||
|
appAuthClients?: AppAuthClient[];
|
||||||
|
|
||||||
|
static tableName = 'app_configs';
|
||||||
|
|
||||||
|
static jsonSchema = {
|
||||||
|
type: 'object',
|
||||||
|
required: ['key'],
|
||||||
|
|
||||||
|
properties: {
|
||||||
|
id: { type: 'string', format: 'uuid' },
|
||||||
|
key: { type: 'string' },
|
||||||
|
allowCustomConnection: { type: 'boolean', default: false },
|
||||||
|
shared: { type: 'boolean', default: false },
|
||||||
|
disabled: { type: 'boolean', default: false },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
static get virtualAttributes() {
|
||||||
|
return ['canConnect', 'canCustomConnect'];
|
||||||
|
}
|
||||||
|
|
||||||
|
static relationMappings = () => ({
|
||||||
|
appAuthClients: {
|
||||||
|
relation: Base.HasManyRelation,
|
||||||
|
modelClass: AppAuthClient,
|
||||||
|
join: {
|
||||||
|
from: 'app_configs.id',
|
||||||
|
to: 'app_auth_clients.app_config_id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
get canCustomConnect() {
|
||||||
|
return !this.disabled && this.allowCustomConnection;
|
||||||
|
}
|
||||||
|
|
||||||
|
get canConnect() {
|
||||||
|
const hasSomeActiveAppAuthClients = !!this.appAuthClients
|
||||||
|
?.some(appAuthClient => appAuthClient.active);
|
||||||
|
const shared = this.shared;
|
||||||
|
const active = this.disabled === false;
|
||||||
|
|
||||||
|
const conditions = [
|
||||||
|
hasSomeActiveAppAuthClients,
|
||||||
|
shared,
|
||||||
|
active
|
||||||
|
];
|
||||||
|
|
||||||
|
return conditions.every(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getApp() {
|
||||||
|
if (!this.key) return null;
|
||||||
|
|
||||||
|
return await App.findOneByKey(this.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppConfig;
|
@@ -3,6 +3,8 @@ import type { RelationMappings } from 'objection';
|
|||||||
import { AES, enc } from 'crypto-js';
|
import { AES, enc } from 'crypto-js';
|
||||||
import { IRequest } from '@automatisch/types';
|
import { IRequest } from '@automatisch/types';
|
||||||
import App from './app';
|
import App from './app';
|
||||||
|
import AppConfig from './app-config';
|
||||||
|
import AppAuthClient from './app-auth-client';
|
||||||
import Base from './base';
|
import Base from './base';
|
||||||
import User from './user';
|
import User from './user';
|
||||||
import Step from './step';
|
import Step from './step';
|
||||||
@@ -25,6 +27,9 @@ class Connection extends Base {
|
|||||||
user?: User;
|
user?: User;
|
||||||
steps?: Step[];
|
steps?: Step[];
|
||||||
triggerSteps?: Step[];
|
triggerSteps?: Step[];
|
||||||
|
appAuthClientId?: string;
|
||||||
|
appAuthClient?: AppAuthClient;
|
||||||
|
appConfig?: AppConfig;
|
||||||
|
|
||||||
static tableName = 'connections';
|
static tableName = 'connections';
|
||||||
|
|
||||||
@@ -38,6 +43,7 @@ class Connection extends Base {
|
|||||||
data: { type: 'string' },
|
data: { type: 'string' },
|
||||||
formattedData: { type: 'object' },
|
formattedData: { type: 'object' },
|
||||||
userId: { type: 'string', format: 'uuid' },
|
userId: { 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' },
|
||||||
deletedAt: { type: 'string' },
|
deletedAt: { type: 'string' },
|
||||||
@@ -46,6 +52,10 @@ class Connection extends Base {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static get virtualAttributes() {
|
||||||
|
return ['reconnectable'];
|
||||||
|
}
|
||||||
|
|
||||||
static relationMappings = (): RelationMappings => ({
|
static relationMappings = (): RelationMappings => ({
|
||||||
user: {
|
user: {
|
||||||
relation: Base.BelongsToOneRelation,
|
relation: Base.BelongsToOneRelation,
|
||||||
@@ -74,8 +84,36 @@ class Connection extends Base {
|
|||||||
builder.where('type', '=', 'trigger');
|
builder.where('type', '=', 'trigger');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
appConfig: {
|
||||||
|
relation: Base.BelongsToOneRelation,
|
||||||
|
modelClass: AppConfig,
|
||||||
|
join: {
|
||||||
|
from: 'connections.key',
|
||||||
|
to: 'app_configs.key',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
appAuthClient: {
|
||||||
|
relation: Base.BelongsToOneRelation,
|
||||||
|
modelClass: AppAuthClient,
|
||||||
|
join: {
|
||||||
|
from: 'connections.app_auth_client_id',
|
||||||
|
to: 'app_auth_clients.id',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
get reconnectable() {
|
||||||
|
if (this.appAuthClientId) {
|
||||||
|
return this.appAuthClient.active;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.appConfig) {
|
||||||
|
return !this.appConfig.disabled && this.appConfig.allowCustomConnection;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
encryptData(): void {
|
encryptData(): void {
|
||||||
if (!this.eligibleForEncryption()) return;
|
if (!this.eligibleForEncryption()) return;
|
||||||
|
|
||||||
|
@@ -294,6 +294,7 @@ class User extends Base {
|
|||||||
if (Array.isArray(this.permissions)) {
|
if (Array.isArray(this.permissions)) {
|
||||||
this.permissions = this.permissions.filter((permission) => {
|
this.permissions = this.permissions.filter((permission) => {
|
||||||
const restrictedSubjects = [
|
const restrictedSubjects = [
|
||||||
|
'App',
|
||||||
'Role',
|
'Role',
|
||||||
'SamlAuthProvider',
|
'SamlAuthProvider',
|
||||||
'Config',
|
'Config',
|
||||||
|
22
packages/types/index.d.ts
vendored
22
packages/types/index.d.ts
vendored
@@ -27,6 +27,8 @@ export interface IConnection {
|
|||||||
flowCount?: number;
|
flowCount?: number;
|
||||||
appData?: IApp;
|
appData?: IApp;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
reconnectable?: boolean;
|
||||||
|
appAuthClientId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IExecutionStep {
|
export interface IExecutionStep {
|
||||||
@@ -247,6 +249,8 @@ export interface IAuth {
|
|||||||
fields?: IField[];
|
fields?: IField[];
|
||||||
authenticationSteps?: IAuthenticationStep[];
|
authenticationSteps?: IAuthenticationStep[];
|
||||||
reconnectionSteps?: IAuthenticationStep[];
|
reconnectionSteps?: IAuthenticationStep[];
|
||||||
|
sharedAuthenticationSteps?: IAuthenticationStep[];
|
||||||
|
sharedReconnectionSteps?: IAuthenticationStep[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ITriggerOutput {
|
export interface ITriggerOutput {
|
||||||
@@ -424,6 +428,24 @@ type TSamlAuthProvider = {
|
|||||||
defaultRoleId: string;
|
defaultRoleId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AppConfig = {
|
||||||
|
id: string;
|
||||||
|
key: string;
|
||||||
|
allowCustomConnection: boolean;
|
||||||
|
canConnect: boolean;
|
||||||
|
canCustomConnect: boolean;
|
||||||
|
shared: boolean;
|
||||||
|
disabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppAuthClient = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
appConfigId: string;
|
||||||
|
authDefaults: string;
|
||||||
|
formattedAuthDefaults: IJSONObject;
|
||||||
|
}
|
||||||
|
|
||||||
declare module 'axios' {
|
declare module 'axios' {
|
||||||
interface AxiosResponse {
|
interface AxiosResponse {
|
||||||
httpError?: IJSONObject;
|
httpError?: IJSONObject;
|
||||||
|
@@ -1,17 +1,19 @@
|
|||||||
import * as React from 'react';
|
import type { IApp, IField, IJSONObject } from '@automatisch/types';
|
||||||
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
import Alert from '@mui/material/Alert';
|
import Alert from '@mui/material/Alert';
|
||||||
import DialogTitle from '@mui/material/DialogTitle';
|
import Dialog from '@mui/material/Dialog';
|
||||||
import DialogContent from '@mui/material/DialogContent';
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
import DialogContentText from '@mui/material/DialogContentText';
|
import DialogContentText from '@mui/material/DialogContentText';
|
||||||
import Dialog from '@mui/material/Dialog';
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
import * as React from 'react';
|
||||||
import { FieldValues, SubmitHandler } from 'react-hook-form';
|
import { FieldValues, SubmitHandler } from 'react-hook-form';
|
||||||
import type { IApp, IJSONObject, IField } from '@automatisch/types';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import AppAuthClientsDialog from 'components/AppAuthClientsDialog/index.ee';
|
||||||
import computeAuthStepVariables from 'helpers/computeAuthStepVariables';
|
|
||||||
import { processStep } from 'helpers/authenticationSteps';
|
|
||||||
import InputCreator from 'components/InputCreator';
|
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 { generateExternalLink } from '../../helpers/translationValues';
|
||||||
import { Form } from './style';
|
import { Form } from './style';
|
||||||
|
|
||||||
@@ -21,24 +23,27 @@ type AddAppConnectionProps = {
|
|||||||
connectionId?: string;
|
connectionId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Response = {
|
|
||||||
[key: string]: any;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function AddAppConnection(
|
export default function AddAppConnection(
|
||||||
props: AddAppConnectionProps
|
props: AddAppConnectionProps
|
||||||
): React.ReactElement {
|
): React.ReactElement {
|
||||||
const { application, connectionId, onClose } = props;
|
const { application, connectionId, onClose } = props;
|
||||||
const { name, authDocUrl, key, auth } = application;
|
const { name, authDocUrl, key, auth } = application;
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const [error, setError] = React.useState<IJSONObject | null>(null);
|
const [error, setError] = React.useState<IJSONObject | null>(null);
|
||||||
const [inProgress, setInProgress] = React.useState(false);
|
const [inProgress, setInProgress] = React.useState(false);
|
||||||
const hasConnection = Boolean(connectionId);
|
const hasConnection = Boolean(connectionId);
|
||||||
const steps = hasConnection
|
const useShared = searchParams.get('shared') === 'true';
|
||||||
? auth?.reconnectionSteps
|
const appAuthClientId = searchParams.get('appAuthClientId') || undefined;
|
||||||
: auth?.authenticationSteps;
|
const { authenticate } = useAuthenticateApp({
|
||||||
|
appKey: key,
|
||||||
|
connectionId,
|
||||||
|
appAuthClientId,
|
||||||
|
useShared: !!appAuthClientId,
|
||||||
|
});
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(function relayProviderData() {
|
||||||
if (window.opener) {
|
if (window.opener) {
|
||||||
window.opener.postMessage({
|
window.opener.postMessage({
|
||||||
source: 'automatisch',
|
source: 'automatisch',
|
||||||
@@ -48,51 +53,61 @@ export default function AddAppConnection(
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const submitHandler: SubmitHandler<FieldValues> = React.useCallback(
|
React.useEffect(
|
||||||
async (data) => {
|
function initiateSharedAuthenticationForGivenAuthClient() {
|
||||||
if (!steps) return;
|
if (!appAuthClientId) return;
|
||||||
|
if (!authenticate) return;
|
||||||
|
|
||||||
setInProgress(true);
|
const asyncAuthenticate = async () => {
|
||||||
setError(null);
|
await authenticate();
|
||||||
|
|
||||||
const response: Response = {
|
navigate(URLS.APP_CONNECTIONS(key));
|
||||||
key,
|
|
||||||
connection: {
|
|
||||||
id: connectionId,
|
|
||||||
},
|
|
||||||
fields: data,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let stepIndex = 0;
|
asyncAuthenticate();
|
||||||
while (stepIndex < steps.length) {
|
},
|
||||||
const step = steps[stepIndex];
|
[appAuthClientId, authenticate]
|
||||||
const variables = computeAuthStepVariables(step.arguments, response);
|
);
|
||||||
|
|
||||||
|
const handleClientClick = (appAuthClientId: string) =>
|
||||||
|
navigate(URLS.APP_ADD_CONNECTION_WITH_AUTH_CLIENT_ID(key, appAuthClientId));
|
||||||
|
|
||||||
|
const handleAuthClientsDialogClose = () =>
|
||||||
|
navigate(URLS.APP_CONNECTIONS(key));
|
||||||
|
|
||||||
|
const submitHandler: SubmitHandler<FieldValues> = React.useCallback(
|
||||||
|
async (data) => {
|
||||||
|
if (!authenticate) return;
|
||||||
|
|
||||||
|
setInProgress(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stepResponse = await processStep(step, variables);
|
const response = await authenticate({
|
||||||
|
fields: data,
|
||||||
response[step.name] = stepResponse;
|
});
|
||||||
|
onClose(response as Record<string, unknown>);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = err as IJSONObject;
|
const error = err as IJSONObject;
|
||||||
console.log(error);
|
console.log(error);
|
||||||
setError((error.graphQLErrors as IJSONObject[])?.[0]);
|
setError((error.graphQLErrors as IJSONObject[])?.[0]);
|
||||||
|
} finally {
|
||||||
setInProgress(false);
|
setInProgress(false);
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stepIndex++;
|
|
||||||
|
|
||||||
if (stepIndex === steps.length) {
|
|
||||||
onClose(response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setInProgress(false);
|
|
||||||
},
|
},
|
||||||
[connectionId, key, steps, onClose]
|
[authenticate]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (useShared)
|
||||||
|
return (
|
||||||
|
<AppAuthClientsDialog
|
||||||
|
appKey={key}
|
||||||
|
onClose={handleAuthClientsDialogClose}
|
||||||
|
onClientClick={handleClientClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (appAuthClientId) return <React.Fragment />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={true} onClose={onClose} data-test="add-app-connection-dialog">
|
<Dialog open={true} onClose={onClose} data-test="add-app-connection-dialog">
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
|
@@ -0,0 +1,50 @@
|
|||||||
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
|
import List from '@mui/material/List';
|
||||||
|
import ListItem from '@mui/material/ListItem';
|
||||||
|
import ListItemButton from '@mui/material/ListItemButton';
|
||||||
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import useAppAuthClients from 'hooks/useAppAuthClients.ee';
|
||||||
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
|
||||||
|
type AppAuthClientsDialogProps = {
|
||||||
|
appKey: string;
|
||||||
|
onClientClick: (appAuthClientId: string) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AppAuthClientsDialog(props: AppAuthClientsDialogProps) {
|
||||||
|
const { appKey, onClientClick, onClose } = props;
|
||||||
|
const { appAuthClients } = useAppAuthClients(appKey);
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
|
|
||||||
|
React.useEffect(
|
||||||
|
function autoAuthenticateSingleClient() {
|
||||||
|
if (appAuthClients?.length === 1) {
|
||||||
|
onClientClick(appAuthClients[0].id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[appAuthClients]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!appAuthClients?.length || appAuthClients?.length === 1)
|
||||||
|
return <React.Fragment />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog onClose={onClose} open={true}>
|
||||||
|
<DialogTitle>{formatMessage('appAuthClientsDialog.title')}</DialogTitle>
|
||||||
|
|
||||||
|
<List sx={{ pt: 0 }}>
|
||||||
|
{appAuthClients.map((appAuthClient) => (
|
||||||
|
<ListItem disableGutters key={appAuthClient.id}>
|
||||||
|
<ListItemButton onClick={() => onClientClick(appAuthClient.id)}>
|
||||||
|
<ListItemText primary={appAuthClient.name} />
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
@@ -3,6 +3,7 @@ import { Link } from 'react-router-dom';
|
|||||||
import Menu from '@mui/material/Menu';
|
import Menu from '@mui/material/Menu';
|
||||||
import type { PopoverProps } from '@mui/material/Popover';
|
import type { PopoverProps } from '@mui/material/Popover';
|
||||||
import MenuItem from '@mui/material/MenuItem';
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import type { IConnection } from '@automatisch/types';
|
||||||
|
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
@@ -13,16 +14,24 @@ type Action = {
|
|||||||
|
|
||||||
type ContextMenuProps = {
|
type ContextMenuProps = {
|
||||||
appKey: string;
|
appKey: string;
|
||||||
connectionId: string;
|
connection: IConnection;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onMenuItemClick: (event: React.MouseEvent, action: Action) => void;
|
onMenuItemClick: (event: React.MouseEvent, action: Action) => void;
|
||||||
anchorEl: PopoverProps['anchorEl'];
|
anchorEl: PopoverProps['anchorEl'];
|
||||||
|
disableReconnection: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ContextMenu(
|
export default function ContextMenu(
|
||||||
props: ContextMenuProps
|
props: ContextMenuProps
|
||||||
): React.ReactElement {
|
): React.ReactElement {
|
||||||
const { appKey, connectionId, onClose, onMenuItemClick, anchorEl } = props;
|
const {
|
||||||
|
appKey,
|
||||||
|
connection,
|
||||||
|
onClose,
|
||||||
|
onMenuItemClick,
|
||||||
|
anchorEl,
|
||||||
|
disableReconnection,
|
||||||
|
} = props;
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
|
|
||||||
const createActionHandler = React.useCallback(
|
const createActionHandler = React.useCallback(
|
||||||
@@ -45,7 +54,7 @@ export default function ContextMenu(
|
|||||||
>
|
>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
component={Link}
|
component={Link}
|
||||||
to={URLS.APP_FLOWS_FOR_CONNECTION(appKey, connectionId)}
|
to={URLS.APP_FLOWS_FOR_CONNECTION(appKey, connection.id)}
|
||||||
onClick={createActionHandler({ type: 'viewFlows' })}
|
onClick={createActionHandler({ type: 'viewFlows' })}
|
||||||
>
|
>
|
||||||
{formatMessage('connection.viewFlows')}
|
{formatMessage('connection.viewFlows')}
|
||||||
@@ -57,7 +66,12 @@ export default function ContextMenu(
|
|||||||
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
component={Link}
|
component={Link}
|
||||||
to={URLS.APP_RECONNECT_CONNECTION(appKey, connectionId)}
|
disabled={disableReconnection}
|
||||||
|
to={URLS.APP_RECONNECT_CONNECTION(
|
||||||
|
appKey,
|
||||||
|
connection.id,
|
||||||
|
connection.appAuthClientId
|
||||||
|
)}
|
||||||
onClick={createActionHandler({ type: 'reconnect' })}
|
onClick={createActionHandler({ type: 'reconnect' })}
|
||||||
>
|
>
|
||||||
{formatMessage('connection.reconnect')}
|
{formatMessage('connection.reconnect')}
|
||||||
|
@@ -45,8 +45,15 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
|
|||||||
const [deleteConnection] = useMutation(DELETE_CONNECTION);
|
const [deleteConnection] = useMutation(DELETE_CONNECTION);
|
||||||
|
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const { id, key, formattedData, verified, createdAt, flowCount } =
|
const {
|
||||||
props.connection;
|
id,
|
||||||
|
key,
|
||||||
|
formattedData,
|
||||||
|
verified,
|
||||||
|
createdAt,
|
||||||
|
flowCount,
|
||||||
|
reconnectable,
|
||||||
|
} = props.connection;
|
||||||
|
|
||||||
const contextButtonRef = React.useRef<SVGSVGElement | null>(null);
|
const contextButtonRef = React.useRef<SVGSVGElement | null>(null);
|
||||||
const [anchorEl, setAnchorEl] = React.useState<SVGSVGElement | null>(null);
|
const [anchorEl, setAnchorEl] = React.useState<SVGSVGElement | null>(null);
|
||||||
@@ -159,7 +166,8 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
|
|||||||
{anchorEl && (
|
{anchorEl && (
|
||||||
<ConnectionContextMenu
|
<ConnectionContextMenu
|
||||||
appKey={key}
|
appKey={key}
|
||||||
connectionId={id}
|
connection={props.connection}
|
||||||
|
disableReconnection={!reconnectable}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
onMenuItemClick={onContextMenuAction}
|
onMenuItemClick={onContextMenuAction}
|
||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
|
@@ -1,18 +1,21 @@
|
|||||||
import * as React from 'react';
|
import { useLazyQuery, useQuery } from '@apollo/client';
|
||||||
import { useQuery, useLazyQuery } from '@apollo/client';
|
import Autocomplete from '@mui/material/Autocomplete';
|
||||||
import TextField from '@mui/material/TextField';
|
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import Collapse from '@mui/material/Collapse';
|
import Collapse from '@mui/material/Collapse';
|
||||||
import ListItem from '@mui/material/ListItem';
|
import ListItem from '@mui/material/ListItem';
|
||||||
import Autocomplete from '@mui/material/Autocomplete';
|
import TextField from '@mui/material/TextField';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
import type { IApp, IConnection, IStep, ISubstep } from '@automatisch/types';
|
import type { IApp, IConnection, IStep, ISubstep } from '@automatisch/types';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
|
||||||
import { EditorContext } from 'contexts/Editor';
|
|
||||||
import FlowSubstepTitle from 'components/FlowSubstepTitle';
|
|
||||||
import AddAppConnection from 'components/AddAppConnection';
|
import AddAppConnection from 'components/AddAppConnection';
|
||||||
|
import AppAuthClientsDialog from 'components/AppAuthClientsDialog/index.ee';
|
||||||
|
import FlowSubstepTitle from 'components/FlowSubstepTitle';
|
||||||
|
import useAppConfig from 'hooks/useAppConfig.ee';
|
||||||
|
import { EditorContext } from 'contexts/Editor';
|
||||||
import { GET_APP_CONNECTIONS } from 'graphql/queries/get-app-connections';
|
import { GET_APP_CONNECTIONS } from 'graphql/queries/get-app-connections';
|
||||||
import { TEST_CONNECTION } from 'graphql/queries/test-connection';
|
import { TEST_CONNECTION } from 'graphql/queries/test-connection';
|
||||||
|
import useAuthenticateApp from 'hooks/useAuthenticateApp.ee';
|
||||||
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
|
||||||
type ChooseConnectionSubstepProps = {
|
type ChooseConnectionSubstepProps = {
|
||||||
application: IApp;
|
application: IApp;
|
||||||
@@ -26,6 +29,7 @@ type ChooseConnectionSubstepProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ADD_CONNECTION_VALUE = 'ADD_CONNECTION';
|
const ADD_CONNECTION_VALUE = 'ADD_CONNECTION';
|
||||||
|
const ADD_SHARED_CONNECTION_VALUE = 'ADD_SHARED_CONNECTION';
|
||||||
|
|
||||||
const optionGenerator = (
|
const optionGenerator = (
|
||||||
connection: IConnection
|
connection: IConnection
|
||||||
@@ -53,11 +57,18 @@ function ChooseConnectionSubstep(
|
|||||||
const { connection, appKey } = step;
|
const { connection, appKey } = step;
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const editorContext = React.useContext(EditorContext);
|
const editorContext = React.useContext(EditorContext);
|
||||||
|
const { authenticate } = useAuthenticateApp({
|
||||||
|
appKey: application.key,
|
||||||
|
useShared: true,
|
||||||
|
});
|
||||||
const [showAddConnectionDialog, setShowAddConnectionDialog] =
|
const [showAddConnectionDialog, setShowAddConnectionDialog] =
|
||||||
React.useState(false);
|
React.useState(false);
|
||||||
|
const [showAddSharedConnectionDialog, setShowAddSharedConnectionDialog] =
|
||||||
|
React.useState(false);
|
||||||
const { data, loading, refetch } = useQuery(GET_APP_CONNECTIONS, {
|
const { data, loading, refetch } = useQuery(GET_APP_CONNECTIONS, {
|
||||||
variables: { key: appKey },
|
variables: { key: appKey },
|
||||||
});
|
});
|
||||||
|
const { appConfig } = useAppConfig(application.key);
|
||||||
// TODO: show detailed error when connection test/verification fails
|
// TODO: show detailed error when connection test/verification fails
|
||||||
const [
|
const [
|
||||||
testConnection,
|
testConnection,
|
||||||
@@ -86,13 +97,49 @@ function ChooseConnectionSubstep(
|
|||||||
optionGenerator(connection)
|
optionGenerator(connection)
|
||||||
) || [];
|
) || [];
|
||||||
|
|
||||||
|
if (!appConfig || appConfig.canCustomConnect) {
|
||||||
options.push({
|
options.push({
|
||||||
label: formatMessage('chooseConnectionSubstep.addNewConnection'),
|
label: formatMessage('chooseConnectionSubstep.addNewConnection'),
|
||||||
value: ADD_CONNECTION_VALUE,
|
value: ADD_CONNECTION_VALUE,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appConfig?.canConnect) {
|
||||||
|
options.push({
|
||||||
|
label: formatMessage('chooseConnectionSubstep.addNewSharedConnection'),
|
||||||
|
value: ADD_SHARED_CONNECTION_VALUE,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}, [data, formatMessage]);
|
}, [data, formatMessage, appConfig]);
|
||||||
|
|
||||||
|
const handleClientClick = async (appAuthClientId: string) => {
|
||||||
|
try {
|
||||||
|
const response = await authenticate?.({
|
||||||
|
appAuthClientId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const connectionId = response?.createConnection.id;
|
||||||
|
|
||||||
|
if (connectionId) {
|
||||||
|
await refetch();
|
||||||
|
|
||||||
|
onChange({
|
||||||
|
step: {
|
||||||
|
...step,
|
||||||
|
connection: {
|
||||||
|
id: connectionId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// void
|
||||||
|
} finally {
|
||||||
|
setShowAddSharedConnectionDialog(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const { name } = substep;
|
const { name } = substep;
|
||||||
|
|
||||||
@@ -131,6 +178,11 @@ function ChooseConnectionSubstep(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (connectionId === ADD_SHARED_CONNECTION_VALUE) {
|
||||||
|
setShowAddSharedConnectionDialog(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (connectionId !== step.connection?.id) {
|
if (connectionId !== step.connection?.id) {
|
||||||
onChange({
|
onChange({
|
||||||
step: {
|
step: {
|
||||||
@@ -216,6 +268,14 @@ function ChooseConnectionSubstep(
|
|||||||
application={application}
|
application={application}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{application && showAddSharedConnectionDialog && (
|
||||||
|
<AppAuthClientsDialog
|
||||||
|
appKey={application.key}
|
||||||
|
onClose={() => setShowAddSharedConnectionDialog(false)}
|
||||||
|
onClientClick={handleClientClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
116
packages/web/src/components/SplitButton/index.tsx
Normal file
116
packages/web/src/components/SplitButton/index.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import ButtonGroup from '@mui/material/ButtonGroup';
|
||||||
|
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||||
|
import Grow from '@mui/material/Grow';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import MenuList from '@mui/material/MenuList';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import Popper from '@mui/material/Popper';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
type SplitButtonProps = {
|
||||||
|
options: {
|
||||||
|
key: string;
|
||||||
|
'data-test'?: string;
|
||||||
|
label: React.ReactNode;
|
||||||
|
to: string;
|
||||||
|
}[];
|
||||||
|
disabled?: boolean;
|
||||||
|
defaultActionIndex?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SplitButton(props: SplitButtonProps) {
|
||||||
|
const { options, disabled, defaultActionIndex = 0 } = props;
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
const anchorRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const multiOptions = options.length > 1;
|
||||||
|
const selectedOption = options[defaultActionIndex];
|
||||||
|
|
||||||
|
const handleToggle = () => {
|
||||||
|
setOpen((prevOpen) => !prevOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = (event: Event) => {
|
||||||
|
if (
|
||||||
|
anchorRef.current &&
|
||||||
|
anchorRef.current.contains(event.target as HTMLElement)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<ButtonGroup
|
||||||
|
variant="contained"
|
||||||
|
ref={anchorRef}
|
||||||
|
aria-label="split button"
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
data-test={selectedOption['data-test']}
|
||||||
|
component={Link}
|
||||||
|
to={selectedOption.to}
|
||||||
|
sx={{
|
||||||
|
// Link component causes style loss in ButtonGroup
|
||||||
|
borderRadius: 0,
|
||||||
|
borderRight: '1px solid #bdbdbd',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selectedOption.label}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{multiOptions && (
|
||||||
|
<Button size="small" onClick={handleToggle} sx={{ borderRadius: 0 }}>
|
||||||
|
<ArrowDropDownIcon />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ButtonGroup>
|
||||||
|
|
||||||
|
{multiOptions && (
|
||||||
|
<Popper
|
||||||
|
sx={{
|
||||||
|
zIndex: 1,
|
||||||
|
}}
|
||||||
|
open={open}
|
||||||
|
anchorEl={anchorRef.current}
|
||||||
|
transition
|
||||||
|
disablePortal
|
||||||
|
>
|
||||||
|
{({ TransitionProps, placement }) => (
|
||||||
|
<Grow
|
||||||
|
{...TransitionProps}
|
||||||
|
style={{
|
||||||
|
transformOrigin:
|
||||||
|
placement === 'bottom' ? 'center top' : 'center bottom',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Paper>
|
||||||
|
<ClickAwayListener onClickAway={handleClose}>
|
||||||
|
<MenuList autoFocusItem>
|
||||||
|
{options.map((option, index) => (
|
||||||
|
<MenuItem
|
||||||
|
key={option.key}
|
||||||
|
selected={index === defaultActionIndex}
|
||||||
|
component={Link}
|
||||||
|
to={option.to}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</MenuList>
|
||||||
|
</ClickAwayListener>
|
||||||
|
</Paper>
|
||||||
|
</Grow>
|
||||||
|
)}
|
||||||
|
</Popper>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
@@ -20,13 +20,24 @@ export const APP_PATTERN = '/app/:appKey';
|
|||||||
export const APP_CONNECTIONS = (appKey: string) =>
|
export const APP_CONNECTIONS = (appKey: string) =>
|
||||||
`/app/${appKey}/connections`;
|
`/app/${appKey}/connections`;
|
||||||
export const APP_CONNECTIONS_PATTERN = '/app/:appKey/connections';
|
export const APP_CONNECTIONS_PATTERN = '/app/:appKey/connections';
|
||||||
export const APP_ADD_CONNECTION = (appKey: string) =>
|
export const APP_ADD_CONNECTION = (appKey: string, shared = false) =>
|
||||||
`/app/${appKey}/connections/add`;
|
`/app/${appKey}/connections/add?shared=${shared}`;
|
||||||
|
export const APP_ADD_CONNECTION_WITH_AUTH_CLIENT_ID = (appKey: string, appAuthClientId: string) =>
|
||||||
|
`/app/${appKey}/connections/add?appAuthClientId=${appAuthClientId}`;
|
||||||
export const APP_ADD_CONNECTION_PATTERN = '/app/:appKey/connections/add';
|
export const APP_ADD_CONNECTION_PATTERN = '/app/:appKey/connections/add';
|
||||||
export const APP_RECONNECT_CONNECTION = (
|
export const APP_RECONNECT_CONNECTION = (
|
||||||
appKey: string,
|
appKey: string,
|
||||||
connectionId: string
|
connectionId: string,
|
||||||
) => `/app/${appKey}/connections/${connectionId}/reconnect`;
|
appAuthClientId?: string,
|
||||||
|
) => {
|
||||||
|
const path = `/app/${appKey}/connections/${connectionId}/reconnect`;
|
||||||
|
|
||||||
|
if (appAuthClientId) {
|
||||||
|
return `${path}?appAuthClientId=${appAuthClientId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
};
|
||||||
export const APP_RECONNECT_CONNECTION_PATTERN =
|
export const APP_RECONNECT_CONNECTION_PATTERN =
|
||||||
'/app/:appKey/connections/:connectionId/reconnect';
|
'/app/:appKey/connections/:connectionId/reconnect';
|
||||||
export const APP_FLOWS = (appKey: string) => `/app/${appKey}/flows`;
|
export const APP_FLOWS = (appKey: string) => `/app/${appKey}/flows`;
|
||||||
|
13
packages/web/src/graphql/queries/get-app-auth-client.ee.ts
Normal file
13
packages/web/src/graphql/queries/get-app-auth-client.ee.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const GET_APP_AUTH_CLIENT = gql`
|
||||||
|
query GetAppAuthClient($id: String!) {
|
||||||
|
getAppAuthClient(id: $id) {
|
||||||
|
id
|
||||||
|
appConfigId
|
||||||
|
name
|
||||||
|
active
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
13
packages/web/src/graphql/queries/get-app-auth-clients.ee.ts
Normal file
13
packages/web/src/graphql/queries/get-app-auth-clients.ee.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const GET_APP_AUTH_CLIENTS = gql`
|
||||||
|
query GetAppAuthClients($appKey: String!, $active: Boolean) {
|
||||||
|
getAppAuthClients(appKey: $appKey, active: $active) {
|
||||||
|
id
|
||||||
|
appConfigId
|
||||||
|
name
|
||||||
|
active
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
16
packages/web/src/graphql/queries/get-app-config.ee.ts
Normal file
16
packages/web/src/graphql/queries/get-app-config.ee.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const GET_APP_CONFIG = gql`
|
||||||
|
query GetAppConfig($key: String!) {
|
||||||
|
getAppConfig(key: $key) {
|
||||||
|
id
|
||||||
|
key
|
||||||
|
allowCustomConnection
|
||||||
|
canConnect
|
||||||
|
canCustomConnect
|
||||||
|
shared
|
||||||
|
disabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
@@ -7,6 +7,8 @@ export const GET_APP_CONNECTIONS = gql`
|
|||||||
connections {
|
connections {
|
||||||
id
|
id
|
||||||
key
|
key
|
||||||
|
reconnectable
|
||||||
|
appAuthClientId
|
||||||
verified
|
verified
|
||||||
flowCount
|
flowCount
|
||||||
formattedData {
|
formattedData {
|
||||||
|
@@ -39,6 +39,19 @@ export const GET_APP = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
sharedAuthenticationSteps {
|
||||||
|
type
|
||||||
|
name
|
||||||
|
arguments {
|
||||||
|
name
|
||||||
|
value
|
||||||
|
type
|
||||||
|
properties {
|
||||||
|
name
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
reconnectionSteps {
|
reconnectionSteps {
|
||||||
type
|
type
|
||||||
name
|
name
|
||||||
@@ -52,6 +65,19 @@ export const GET_APP = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
sharedReconnectionSteps {
|
||||||
|
type
|
||||||
|
name
|
||||||
|
arguments {
|
||||||
|
name
|
||||||
|
value
|
||||||
|
type
|
||||||
|
properties {
|
||||||
|
name
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
connections {
|
connections {
|
||||||
id
|
id
|
||||||
|
@@ -49,6 +49,19 @@ export const GET_APPS = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
sharedAuthenticationSteps {
|
||||||
|
type
|
||||||
|
name
|
||||||
|
arguments {
|
||||||
|
name
|
||||||
|
value
|
||||||
|
type
|
||||||
|
properties {
|
||||||
|
name
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
reconnectionSteps {
|
reconnectionSteps {
|
||||||
type
|
type
|
||||||
name
|
name
|
||||||
@@ -62,6 +75,19 @@ export const GET_APPS = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
sharedReconnectionSteps {
|
||||||
|
type
|
||||||
|
name
|
||||||
|
arguments {
|
||||||
|
name
|
||||||
|
value
|
||||||
|
type
|
||||||
|
properties {
|
||||||
|
name
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
triggers {
|
triggers {
|
||||||
name
|
name
|
||||||
|
@@ -57,7 +57,9 @@ const processOpenWithPopup = (
|
|||||||
popup?.focus();
|
popup?.focus();
|
||||||
|
|
||||||
const closeCheckIntervalId = setInterval(() => {
|
const closeCheckIntervalId = setInterval(() => {
|
||||||
if (popup.closed) {
|
if (!popup) return;
|
||||||
|
|
||||||
|
if (popup?.closed) {
|
||||||
clearInterval(closeCheckIntervalId);
|
clearInterval(closeCheckIntervalId);
|
||||||
reject({ message: 'Error occured while verifying credentials!' });
|
reject({ message: 'Error occured while verifying credentials!' });
|
||||||
}
|
}
|
||||||
|
26
packages/web/src/hooks/useApp.ts
Normal file
26
packages/web/src/hooks/useApp.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { useQuery } from '@apollo/client';
|
||||||
|
import { IApp } from '@automatisch/types';
|
||||||
|
|
||||||
|
import { GET_APP } from 'graphql/queries/get-app';
|
||||||
|
|
||||||
|
type QueryResponse = {
|
||||||
|
getApp: IApp;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useApp(key: string) {
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
loading
|
||||||
|
} = useQuery<QueryResponse>(
|
||||||
|
GET_APP,
|
||||||
|
{
|
||||||
|
variables: { key }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const app = data?.getApp;
|
||||||
|
|
||||||
|
return {
|
||||||
|
app,
|
||||||
|
loading,
|
||||||
|
};
|
||||||
|
}
|
31
packages/web/src/hooks/useAppAuthClient.ee.ts
Normal file
31
packages/web/src/hooks/useAppAuthClient.ee.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { useLazyQuery } from '@apollo/client';
|
||||||
|
import { AppConfig } from '@automatisch/types';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { GET_APP_AUTH_CLIENT } from 'graphql/queries/get-app-auth-client.ee';
|
||||||
|
|
||||||
|
type QueryResponse = {
|
||||||
|
getAppAuthClient: AppConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useAppAuthClient(id: string) {
|
||||||
|
const [
|
||||||
|
getAppAuthClient,
|
||||||
|
{
|
||||||
|
data,
|
||||||
|
loading
|
||||||
|
}
|
||||||
|
] = useLazyQuery<QueryResponse>(GET_APP_AUTH_CLIENT);
|
||||||
|
const appAuthClient = data?.getAppAuthClient;
|
||||||
|
|
||||||
|
React.useEffect(function fetchUponId() {
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
getAppAuthClient({ variables: { id } });
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
appAuthClient,
|
||||||
|
loading,
|
||||||
|
};
|
||||||
|
}
|
33
packages/web/src/hooks/useAppAuthClients.ee.ts
Normal file
33
packages/web/src/hooks/useAppAuthClients.ee.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { useLazyQuery } from '@apollo/client';
|
||||||
|
import { AppAuthClient } from '@automatisch/types';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { GET_APP_AUTH_CLIENTS } from 'graphql/queries/get-app-auth-clients.ee';
|
||||||
|
|
||||||
|
type QueryResponse = {
|
||||||
|
getAppAuthClients: AppAuthClient[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useAppAuthClient(appKey: string) {
|
||||||
|
const [
|
||||||
|
getAppAuthClients,
|
||||||
|
{
|
||||||
|
data,
|
||||||
|
loading
|
||||||
|
}
|
||||||
|
] = useLazyQuery<QueryResponse>(GET_APP_AUTH_CLIENTS, {
|
||||||
|
context: { autoSnackbar: false },
|
||||||
|
});
|
||||||
|
const appAuthClients = data?.getAppAuthClients;
|
||||||
|
|
||||||
|
React.useEffect(function fetchUponAppKey() {
|
||||||
|
if (!appKey) return;
|
||||||
|
|
||||||
|
getAppAuthClients({ variables: { appKey, active: true } });
|
||||||
|
}, [appKey]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
appAuthClients,
|
||||||
|
loading,
|
||||||
|
};
|
||||||
|
}
|
27
packages/web/src/hooks/useAppConfig.ee.ts
Normal file
27
packages/web/src/hooks/useAppConfig.ee.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { useQuery } from '@apollo/client';
|
||||||
|
import { AppConfig } from '@automatisch/types';
|
||||||
|
|
||||||
|
import { GET_APP_CONFIG } from 'graphql/queries/get-app-config.ee';
|
||||||
|
|
||||||
|
type QueryResponse = {
|
||||||
|
getAppConfig: AppConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useAppConfig(key: string) {
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
loading
|
||||||
|
} = useQuery<QueryResponse>(
|
||||||
|
GET_APP_CONFIG,
|
||||||
|
{
|
||||||
|
variables: { key },
|
||||||
|
context: { autoSnackbar: false }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const appConfig = data?.getAppConfig;
|
||||||
|
|
||||||
|
return {
|
||||||
|
appConfig,
|
||||||
|
loading,
|
||||||
|
};
|
||||||
|
}
|
100
packages/web/src/hooks/useAuthenticateApp.ee.ts
Normal file
100
packages/web/src/hooks/useAuthenticateApp.ee.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { IApp } from '@automatisch/types';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { processStep } from 'helpers/authenticationSteps';
|
||||||
|
import computeAuthStepVariables from 'helpers/computeAuthStepVariables';
|
||||||
|
import useApp from './useApp';
|
||||||
|
|
||||||
|
type UseAuthenticateAppParams = {
|
||||||
|
appKey: string;
|
||||||
|
appAuthClientId?: string;
|
||||||
|
useShared?: boolean;
|
||||||
|
connectionId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthenticatePayload = {
|
||||||
|
fields?: Record<string, string>;
|
||||||
|
appAuthClientId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSteps(auth: IApp['auth'], hasConnection: boolean, useShared: boolean) {
|
||||||
|
if (hasConnection) {
|
||||||
|
if (useShared) {
|
||||||
|
return auth?.sharedReconnectionSteps;
|
||||||
|
}
|
||||||
|
|
||||||
|
return auth?.reconnectionSteps;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useShared) {
|
||||||
|
return auth?.sharedAuthenticationSteps;
|
||||||
|
}
|
||||||
|
|
||||||
|
return auth?.authenticationSteps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useAuthenticateApp(payload: UseAuthenticateAppParams) {
|
||||||
|
const {
|
||||||
|
appKey,
|
||||||
|
appAuthClientId,
|
||||||
|
connectionId,
|
||||||
|
useShared = false,
|
||||||
|
} = payload;
|
||||||
|
const { app } = useApp(appKey);
|
||||||
|
const [
|
||||||
|
authenticationInProgress,
|
||||||
|
setAuthenticationInProgress
|
||||||
|
] = React.useState(false);
|
||||||
|
const steps = getSteps(app?.auth, !!connectionId, useShared);
|
||||||
|
|
||||||
|
const authenticate = React.useMemo(() => {
|
||||||
|
if (!steps?.length) return;
|
||||||
|
|
||||||
|
return async function authenticate(payload: AuthenticatePayload = {}) {
|
||||||
|
const {
|
||||||
|
fields,
|
||||||
|
} = payload;
|
||||||
|
setAuthenticationInProgress(true);
|
||||||
|
|
||||||
|
const response: Record<string, any> = {
|
||||||
|
key: appKey,
|
||||||
|
appAuthClientId: appAuthClientId || payload.appAuthClientId,
|
||||||
|
connection: {
|
||||||
|
id: connectionId,
|
||||||
|
},
|
||||||
|
fields
|
||||||
|
};
|
||||||
|
|
||||||
|
let stepIndex = 0;
|
||||||
|
while (stepIndex < steps?.length) {
|
||||||
|
const step = steps[stepIndex];
|
||||||
|
const variables = computeAuthStepVariables(step.arguments, response);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stepResponse = await processStep(step, variables);
|
||||||
|
|
||||||
|
response[step.name] = stepResponse;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
throw err;
|
||||||
|
|
||||||
|
setAuthenticationInProgress(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
stepIndex++;
|
||||||
|
|
||||||
|
if (stepIndex === steps.length) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAuthenticationInProgress(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [steps, appKey, appAuthClientId, connectionId]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
authenticate,
|
||||||
|
inProgress: authenticationInProgress,
|
||||||
|
};
|
||||||
|
}
|
@@ -19,6 +19,7 @@
|
|||||||
"app.connectionCount": "{count} connections",
|
"app.connectionCount": "{count} connections",
|
||||||
"app.flowCount": "{count} flows",
|
"app.flowCount": "{count} flows",
|
||||||
"app.addConnection": "Add connection",
|
"app.addConnection": "Add connection",
|
||||||
|
"app.addCustomConnection": "Add custom connection",
|
||||||
"app.reconnectConnection": "Reconnect connection",
|
"app.reconnectConnection": "Reconnect connection",
|
||||||
"app.createFlow": "Create flow",
|
"app.createFlow": "Create flow",
|
||||||
"app.settings": "Settings",
|
"app.settings": "Settings",
|
||||||
@@ -69,6 +70,7 @@
|
|||||||
"filterConditions.orContinueIf": "OR continue if…",
|
"filterConditions.orContinueIf": "OR continue if…",
|
||||||
"chooseConnectionSubstep.continue": "Continue",
|
"chooseConnectionSubstep.continue": "Continue",
|
||||||
"chooseConnectionSubstep.addNewConnection": "Add new connection",
|
"chooseConnectionSubstep.addNewConnection": "Add new connection",
|
||||||
|
"chooseConnectionSubstep.addNewSharedConnection": "Add new shared connection",
|
||||||
"chooseConnectionSubstep.chooseConnection": "Choose connection",
|
"chooseConnectionSubstep.chooseConnection": "Choose connection",
|
||||||
"flow.createdAt": "created {datetime}",
|
"flow.createdAt": "created {datetime}",
|
||||||
"flow.updatedAt": "updated {datetime}",
|
"flow.updatedAt": "updated {datetime}",
|
||||||
@@ -209,5 +211,6 @@
|
|||||||
"roleList.description": "Description",
|
"roleList.description": "Description",
|
||||||
"permissionSettings.cancel": "Cancel",
|
"permissionSettings.cancel": "Cancel",
|
||||||
"permissionSettings.apply": "Apply",
|
"permissionSettings.apply": "Apply",
|
||||||
"permissionSettings.title": "Conditions"
|
"permissionSettings.title": "Conditions",
|
||||||
|
"appAuthClientsDialog.title": "Choose your authentication client"
|
||||||
}
|
}
|
||||||
|
@@ -19,9 +19,11 @@ import Tab from '@mui/material/Tab';
|
|||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
|
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
import useAppConfig from 'hooks/useAppConfig.ee';
|
||||||
import { GET_APP } from 'graphql/queries/get-app';
|
import { GET_APP } from 'graphql/queries/get-app';
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
|
|
||||||
|
import SplitButton from 'components/SplitButton';
|
||||||
import ConditionalIconButton from 'components/ConditionalIconButton';
|
import ConditionalIconButton from 'components/ConditionalIconButton';
|
||||||
import AppConnections from 'components/AppConnections';
|
import AppConnections from 'components/AppConnections';
|
||||||
import AppFlows from 'components/AppFlows';
|
import AppFlows from 'components/AppFlows';
|
||||||
@@ -35,6 +37,13 @@ type ApplicationParams = {
|
|||||||
connectionId?: string;
|
connectionId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ConnectionOption = {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
'data-test': string;
|
||||||
|
to: string;
|
||||||
|
};
|
||||||
|
|
||||||
const ReconnectConnection = (props: any): React.ReactElement => {
|
const ReconnectConnection = (props: any): React.ReactElement => {
|
||||||
const { application, onClose } = props;
|
const { application, onClose } = props;
|
||||||
const { connectionId } = useParams() as ApplicationParams;
|
const { connectionId } = useParams() as ApplicationParams;
|
||||||
@@ -61,11 +70,36 @@ export default function Application(): React.ReactElement | null {
|
|||||||
const { appKey } = useParams() as ApplicationParams;
|
const { appKey } = useParams() as ApplicationParams;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { data, loading } = useQuery(GET_APP, { variables: { key: appKey } });
|
const { data, loading } = useQuery(GET_APP, { variables: { key: appKey } });
|
||||||
|
const { appConfig } = useAppConfig(appKey);
|
||||||
|
|
||||||
const connectionId = searchParams.get('connectionId') || undefined;
|
const connectionId = searchParams.get('connectionId') || undefined;
|
||||||
const goToApplicationPage = () => navigate('connections');
|
const goToApplicationPage = () => navigate('connections');
|
||||||
const app = data?.getApp || {};
|
const app = data?.getApp || {};
|
||||||
|
|
||||||
|
const connectionOptions = React.useMemo(() => {
|
||||||
|
const shouldHaveCustomConnection =
|
||||||
|
appConfig?.canConnect && appConfig?.canCustomConnect;
|
||||||
|
const options: ConnectionOption[] = [
|
||||||
|
{
|
||||||
|
label: formatMessage('app.addConnection'),
|
||||||
|
key: 'addConnection',
|
||||||
|
'data-test': 'add-connection-button',
|
||||||
|
to: URLS.APP_ADD_CONNECTION(appKey, appConfig?.canConnect),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (shouldHaveCustomConnection) {
|
||||||
|
options.push({
|
||||||
|
label: formatMessage('app.addCustomConnection'),
|
||||||
|
key: 'addCustomConnection',
|
||||||
|
'data-test': 'add-custom-connection-button',
|
||||||
|
to: URLS.APP_ADD_CONNECTION(appKey),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}, [appKey, appConfig]);
|
||||||
|
|
||||||
if (loading) return null;
|
if (loading) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -111,19 +145,14 @@ export default function Application(): React.ReactElement | null {
|
|||||||
<Route
|
<Route
|
||||||
path={`${URLS.CONNECTIONS}/*`}
|
path={`${URLS.CONNECTIONS}/*`}
|
||||||
element={
|
element={
|
||||||
<ConditionalIconButton
|
<SplitButton
|
||||||
type="submit"
|
disabled={
|
||||||
variant="contained"
|
appConfig &&
|
||||||
color="primary"
|
!appConfig?.canConnect &&
|
||||||
size="large"
|
!appConfig?.canCustomConnect
|
||||||
component={Link}
|
}
|
||||||
to={URLS.APP_ADD_CONNECTION(appKey)}
|
options={connectionOptions}
|
||||||
fullWidth
|
/>
|
||||||
icon={<AddIcon />}
|
|
||||||
data-test="add-connection-button"
|
|
||||||
>
|
|
||||||
{formatMessage('app.addConnection')}
|
|
||||||
</ConditionalIconButton>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
Reference in New Issue
Block a user