Merge pull request #2099 from automatisch/aut-1262

feat: use REST API endpoints to create and reconnect connections
This commit is contained in:
Ömer Faruk Aydın
2024-09-30 13:33:52 +03:00
committed by GitHub
25 changed files with 172 additions and 378 deletions

View File

@@ -169,7 +169,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
dynamicDataKey: 'listRepos', dynamicDataKey: 'listRepos',
parameters: {}, parameters: {},
}) })
.expect(200); .expect(422);
expect(response.body.errors).toEqual(errors); expect(response.body.errors).toEqual(errors);
}); });

View File

@@ -1,14 +1,3 @@
// Converted mutations const mutationResolvers = {};
import verifyConnection from './mutations/verify-connection.js';
import generateAuthUrl from './mutations/generate-auth-url.js';
import resetConnection from './mutations/reset-connection.js';
import updateConnection from './mutations/update-connection.js';
const mutationResolvers = {
generateAuthUrl,
resetConnection,
updateConnection,
verifyConnection,
};
export default mutationResolvers; export default mutationResolvers;

View File

@@ -1,30 +0,0 @@
import globalVariable from '../../helpers/global-variable.js';
import App from '../../models/app.js';
const generateAuthUrl = async (_parent, params, context) => {
context.currentUser.can('create', 'Connection');
const connection = await context.currentUser
.$relatedQuery('connections')
.findOne({
id: params.input.id,
})
.throwIfNotFound();
if (!connection.formattedData) {
return null;
}
const authInstance = (
await import(`../../apps/${connection.key}/auth/index.js`)
).default;
const app = await App.findOneByKey(connection.key);
const $ = await globalVariable({ connection, app });
await authInstance.generateAuthUrl($);
return connection.formattedData;
};
export default generateAuthUrl;

View File

@@ -1,22 +0,0 @@
const resetConnection = async (_parent, params, context) => {
context.currentUser.can('create', 'Connection');
let connection = await context.currentUser
.$relatedQuery('connections')
.findOne({
id: params.input.id,
})
.throwIfNotFound();
if (!connection.formattedData) {
return null;
}
connection = await connection.$query().patchAndFetch({
formattedData: { screenName: connection.formattedData.screenName },
});
return connection;
};
export default resetConnection;

View File

@@ -1,33 +0,0 @@
import AppAuthClient from '../../models/app-auth-client.js';
const updateConnection = async (_parent, params, context) => {
context.currentUser.can('create', 'Connection');
let connection = await context.currentUser
.$relatedQuery('connections')
.findOne({
id: params.input.id,
})
.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({
formattedData: {
...connection.formattedData,
...formattedData,
},
});
return connection;
};
export default updateConnection;

View File

@@ -1,29 +0,0 @@
import App from '../../models/app.js';
import globalVariable from '../../helpers/global-variable.js';
const verifyConnection = async (_parent, params, context) => {
context.currentUser.can('create', 'Connection');
let connection = await context.currentUser
.$relatedQuery('connections')
.findOne({
id: params.input.id,
})
.throwIfNotFound();
const app = await App.findOneByKey(connection.key);
const $ = await globalVariable({ connection, app });
await app.auth.verifyCredentials($);
connection = await connection.$query().patchAndFetch({
verified: true,
draft: false,
});
return {
...connection,
app,
};
};
export default verifyConnection;

View File

@@ -1,22 +1,10 @@
type Query { type Query {
placeholderQuery(name: String): Boolean placeholderQuery(name: String): Boolean
} }
type Mutation {
generateAuthUrl(input: GenerateAuthUrlInput): AuthLink
resetConnection(input: ResetConnectionInput): Connection
updateConnection(input: UpdateConnectionInput): Connection
verifyConnection(input: VerifyConnectionInput): Connection
}
""" type Mutation {
Exposes a URL that specifies the behaviour of this scalar. placeholderQuery(name: String): Boolean
""" }
directive @specifiedBy(
"""
The URL that specifies the behaviour of this scalar.
"""
url: String!
) on SCALAR
type Trigger { type Trigger {
name: String name: String
@@ -130,10 +118,6 @@ type AuthenticationStepProperty {
value: String value: String
} }
type AuthLink {
url: String
}
type Connection { type Connection {
id: String id: String
key: String key: String
@@ -200,24 +184,6 @@ type SamlAuthProvidersRoleMapping {
remoteRoleName: String remoteRoleName: String
} }
input GenerateAuthUrlInput {
id: String!
}
input UpdateConnectionInput {
id: String!
formattedData: JSONObject
appAuthClientId: String
}
input ResetConnectionInput {
id: String!
}
input VerifyConnectionInput {
id: String!
}
input UserRoleInput { input UserRoleInput {
id: String id: String
} }

View File

@@ -27,12 +27,7 @@ const authenticationStepsWithoutAuthUrl = [
{ {
type: 'mutation', type: 'mutation',
name: 'verifyConnection', name: 'verifyConnection',
arguments: [ arguments: [],
{
name: 'id',
value: '{createConnection.id}',
},
],
}, },
]; ];
@@ -54,12 +49,7 @@ const authenticationStepsWithAuthUrl = [
{ {
type: 'mutation', type: 'mutation',
name: 'generateAuthUrl', name: 'generateAuthUrl',
arguments: [ arguments: [],
{
name: 'id',
value: '{createConnection.id}',
},
],
}, },
{ {
type: 'openWithPopup', type: 'openWithPopup',
@@ -75,10 +65,6 @@ const authenticationStepsWithAuthUrl = [
type: 'mutation', type: 'mutation',
name: 'updateConnection', name: 'updateConnection',
arguments: [ arguments: [
{
name: 'id',
value: '{createConnection.id}',
},
{ {
name: 'formattedData', name: 'formattedData',
value: '{openAuthPopup.all}', value: '{openAuthPopup.all}',
@@ -88,12 +74,7 @@ const authenticationStepsWithAuthUrl = [
{ {
type: 'mutation', type: 'mutation',
name: 'verifyConnection', name: 'verifyConnection',
arguments: [ arguments: [],
{
name: 'id',
value: '{createConnection.id}',
},
],
}, },
]; ];
@@ -115,12 +96,7 @@ const sharedAuthenticationStepsWithAuthUrl = [
{ {
type: 'mutation', type: 'mutation',
name: 'generateAuthUrl', name: 'generateAuthUrl',
arguments: [ arguments: [],
{
name: 'id',
value: '{createConnection.id}',
},
],
}, },
{ {
type: 'openWithPopup', type: 'openWithPopup',
@@ -136,10 +112,6 @@ const sharedAuthenticationStepsWithAuthUrl = [
type: 'mutation', type: 'mutation',
name: 'updateConnection', name: 'updateConnection',
arguments: [ arguments: [
{
name: 'id',
value: '{createConnection.id}',
},
{ {
name: 'formattedData', name: 'formattedData',
value: '{openAuthPopup.all}', value: '{openAuthPopup.all}',
@@ -149,12 +121,7 @@ const sharedAuthenticationStepsWithAuthUrl = [
{ {
type: 'mutation', type: 'mutation',
name: 'verifyConnection', name: 'verifyConnection',
arguments: [ arguments: [],
{
name: 'id',
value: '{createConnection.id}',
},
],
}, },
]; ];

View File

@@ -1,54 +1,23 @@
import cloneDeep from 'lodash/cloneDeep.js'; import cloneDeep from 'lodash/cloneDeep.js';
const connectionIdArgument = {
name: 'id',
value: '{connection.id}',
};
const resetConnectionStep = { const resetConnectionStep = {
type: 'mutation', type: 'mutation',
name: 'resetConnection', name: 'resetConnection',
arguments: [connectionIdArgument], arguments: [],
}; };
function replaceCreateConnection(string) {
return string.replace('{createConnection.id}', '{connection.id}');
}
function removeAppKeyArgument(args) { function removeAppKeyArgument(args) {
return args.filter((argument) => argument.name !== 'key'); return args.filter((argument) => argument.name !== 'key');
} }
function addConnectionId(step) {
step.arguments = step.arguments.map((argument) => {
if (typeof argument.value === 'string') {
argument.value = replaceCreateConnection(argument.value);
}
if (argument.properties) {
argument.properties = argument.properties.map((property) => {
return {
name: property.name,
value: replaceCreateConnection(property.value),
};
});
}
return argument;
});
return step;
}
function replaceCreateConnectionsWithUpdate(steps) { function replaceCreateConnectionsWithUpdate(steps) {
const updatedSteps = cloneDeep(steps); const updatedSteps = cloneDeep(steps);
return updatedSteps.map((step) => { return updatedSteps.map((step) => {
const updatedStep = addConnectionId(step); const updatedStep = { ...step };
if (step.name === 'createConnection') { if (step.name === 'createConnection') {
updatedStep.name = 'updateConnection'; updatedStep.name = 'updateConnection';
updatedStep.arguments = removeAppKeyArgument(updatedStep.arguments); updatedStep.arguments = removeAppKeyArgument(updatedStep.arguments);
updatedStep.arguments.unshift(connectionIdArgument);
return updatedStep; return updatedStep;
} }

View File

@@ -50,7 +50,7 @@ const errorHandler = (error, request, response, next) => {
}, },
}; };
response.status(200).json(httpErrorPayload); response.status(422).json(httpErrorPayload);
} }
if (error instanceof NotAuthorizedError) { if (error instanceof NotAuthorizedError) {

View File

@@ -26,7 +26,8 @@ function AddAppConnection(props) {
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const [error, setError] = React.useState(null); const [errorMessage, setErrorMessage] = React.useState(null);
const [errorDetails, setErrorDetails] = React.useState(null);
const [inProgress, setInProgress] = React.useState(false); const [inProgress, setInProgress] = React.useState(false);
const hasConnection = Boolean(connectionId); const hasConnection = Boolean(connectionId);
const useShared = searchParams.get('shared') === 'true'; const useShared = searchParams.get('shared') === 'true';
@@ -76,7 +77,8 @@ function AddAppConnection(props) {
async (data) => { async (data) => {
if (!authenticate) return; if (!authenticate) return;
setInProgress(true); setInProgress(true);
setError(null); setErrorMessage(null);
setErrorDetails(null);
try { try {
const response = await authenticate({ const response = await authenticate({
fields: data, fields: data,
@@ -85,21 +87,19 @@ function AddAppConnection(props) {
await queryClient.invalidateQueries({ await queryClient.invalidateQueries({
queryKey: ['apps', key, 'connections'], queryKey: ['apps', key, 'connections'],
}); });
onClose(response); onClose(response);
} catch (err) { } catch (err) {
const error = err; const error = err;
console.log(error); console.log(error);
if (error.message) { setErrorMessage(error.message);
setError(error); setErrorDetails(error?.response?.data?.errors);
} else {
setError(error.graphQLErrors?.[0]);
}
} finally { } finally {
setInProgress(false); setInProgress(false);
} }
}, },
[authenticate], [authenticate, key, onClose, queryClient],
); );
if (useShared) if (useShared)
@@ -134,16 +134,16 @@ function AddAppConnection(props) {
</Alert> </Alert>
)} )}
{error && ( {(errorMessage || errorDetails) && (
<Alert <Alert
data-test="add-connection-error" data-test="add-connection-error"
severity="error" severity="error"
sx={{ mt: 1, fontWeight: 500, wordBreak: 'break-all' }} sx={{ mt: 1, fontWeight: 500, wordBreak: 'break-all' }}
> >
{error.message} {!errorDetails && errorMessage}
{error.details && ( {errorDetails && (
<pre style={{ whiteSpace: 'pre-wrap' }}> <pre style={{ whiteSpace: 'pre-wrap' }}>
{JSON.stringify(error.details, null, 2)} {JSON.stringify(errorDetails, null, 2)}
</pre> </pre>
)} )}
</Alert> </Alert>

View File

@@ -6,36 +6,7 @@ const cache = new InMemoryCache({
}, },
Mutation: { Mutation: {
mutationType: true, mutationType: true,
fields: { fields: {},
verifyConnection: {
merge(existing, verifiedConnection, { readField, cache }) {
const appKey = readField('key', verifiedConnection);
const appCacheId = cache.identify({
__typename: 'App',
key: appKey,
});
cache.modify({
id: appCacheId,
fields: {
connections: (existingConnections) => {
const existingConnectionIndex = existingConnections.findIndex(
(connection) => {
return connection.__ref === verifiedConnection.__ref;
}
);
const connectionExists = existingConnectionIndex !== -1;
// newly created and verified connection
if (!connectionExists) {
return [verifiedConnection, ...existingConnections];
}
return existingConnections;
},
},
});
return verifiedConnection;
},
},
},
}, },
}, },
}); });

View File

@@ -1,8 +0,0 @@
import { gql } from '@apollo/client';
export const GENERATE_AUTH_URL = gql`
mutation generateAuthUrl($input: GenerateAuthUrlInput) {
generateAuthUrl(input: $input) {
url
}
}
`;

View File

@@ -1,12 +0,0 @@
import { UPDATE_CONNECTION } from './update-connection';
import { VERIFY_CONNECTION } from './verify-connection';
import { RESET_CONNECTION } from './reset-connection';
import { GENERATE_AUTH_URL } from './generate-auth-url';
const mutations = {
updateConnection: UPDATE_CONNECTION,
verifyConnection: VERIFY_CONNECTION,
resetConnection: RESET_CONNECTION,
generateAuthUrl: GENERATE_AUTH_URL,
};
export default mutations;

View File

@@ -1,8 +0,0 @@
import { gql } from '@apollo/client';
export const RESET_CONNECTION = gql`
mutation ResetConnection($input: ResetConnectionInput) {
resetConnection(input: $input) {
id
}
}
`;

View File

@@ -1,13 +0,0 @@
import { gql } from '@apollo/client';
export const UPDATE_CONNECTION = gql`
mutation UpdateConnection($input: UpdateConnectionInput) {
updateConnection(input: $input) {
id
key
verified
formattedData {
screenName
}
}
}
`;

View File

@@ -1,17 +0,0 @@
import { gql } from '@apollo/client';
export const VERIFY_CONNECTION = gql`
mutation VerifyConnection($input: VerifyConnectionInput) {
verifyConnection(input: $input) {
id
key
verified
formattedData {
screenName
}
createdAt
app {
key
}
}
}
`;

View File

@@ -1,20 +1,3 @@
import apolloClient from 'graphql/client';
import MUTATIONS from 'graphql/mutations';
export const processMutation = async (mutationName, variables) => {
const mutation = MUTATIONS[mutationName];
const mutationResponse = await apolloClient.mutate({
mutation,
variables: { input: variables },
context: {
autoSnackbar: false,
},
});
const responseData = mutationResponse.data[mutationName];
return responseData;
};
const parseUrlSearchParams = (event) => { const parseUrlSearchParams = (event) => {
const searchParams = new URLSearchParams(event.data.payload.search); const searchParams = new URLSearchParams(event.data.payload.search);
const hashParams = new URLSearchParams(event.data.payload.hash.substring(1)); const hashParams = new URLSearchParams(event.data.payload.hash.substring(1));

View File

@@ -1,27 +1,35 @@
import template from 'lodash/template'; import template from 'lodash/template';
const interpolate = /{([\s\S]+?)}/g; const interpolate = /{([\s\S]+?)}/g;
const computeAuthStepVariables = (variableSchema, aggregatedData) => { const computeAuthStepVariables = (variableSchema, aggregatedData) => {
const variables = {}; const variables = {};
for (const variable of variableSchema) { for (const variable of variableSchema) {
if (variable.properties) { if (variable.properties) {
variables[variable.name] = computeAuthStepVariables( variables[variable.name] = computeAuthStepVariables(
variable.properties, variable.properties,
aggregatedData aggregatedData,
); );
continue; continue;
} }
if (variable.value) { if (variable.value) {
if (variable.value.endsWith('.all}')) { if (variable.value.endsWith('.all}')) {
const key = variable.value.replace('{', '').replace('.all}', ''); const key = variable.value.replace('{', '').replace('.all}', '');
variables[variable.name] = aggregatedData[key]; variables[variable.name] = aggregatedData[key];
continue; continue;
} }
const computedVariable = template(variable.value, { interpolate })( const computedVariable = template(variable.value, { interpolate })(
aggregatedData aggregatedData,
); );
variables[variable.name] = computedVariable; variables[variable.name] = computedVariable;
} }
} }
return variables; return variables;
}; };
export default computeAuthStepVariables; export default computeAuthStepVariables;

View File

@@ -1,14 +1,18 @@
import * as React from 'react'; import * as React from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { import {
processMutation,
processOpenWithPopup, processOpenWithPopup,
processPopupMessage, processPopupMessage,
} from 'helpers/authenticationSteps'; } from 'helpers/authenticationSteps';
import computeAuthStepVariables from 'helpers/computeAuthStepVariables'; import computeAuthStepVariables from 'helpers/computeAuthStepVariables';
import useFormatMessage from './useFormatMessage';
import useAppAuth from './useAppAuth'; import useAppAuth from './useAppAuth';
import useCreateConnection from './useCreateConnection'; import useCreateConnection from './useCreateConnection';
import useFormatMessage from './useFormatMessage'; import useCreateConnectionAuthUrl from './useCreateConnectionAuthUrl';
import useUpdateConnection from './useUpdateConnection';
import useResetConnection from './useResetConnection';
import useVerifyConnection from './useVerifyConnection';
function getSteps(auth, hasConnection, useShared) { function getSteps(auth, hasConnection, useShared) {
if (hasConnection) { if (hasConnection) {
@@ -28,11 +32,16 @@ function getSteps(auth, hasConnection, useShared) {
export default function useAuthenticateApp(payload) { export default function useAuthenticateApp(payload) {
const { appKey, appAuthClientId, connectionId, useShared = false } = payload; const { appKey, appAuthClientId, connectionId, useShared = false } = payload;
const { data: auth } = useAppAuth(appKey); const { data: auth } = useAppAuth(appKey);
const queryClient = useQueryClient();
const { mutateAsync: createConnection } = useCreateConnection(appKey); const { mutateAsync: createConnection } = useCreateConnection(appKey);
const { mutateAsync: createConnectionAuthUrl } = useCreateConnectionAuthUrl();
const { mutateAsync: updateConnection } = useUpdateConnection();
const { mutateAsync: resetConnection } = useResetConnection();
const [authenticationInProgress, setAuthenticationInProgress] = const [authenticationInProgress, setAuthenticationInProgress] =
React.useState(false); React.useState(false);
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const steps = getSteps(auth?.data, !!connectionId, useShared); const steps = getSteps(auth?.data, !!connectionId, useShared);
const { mutateAsync: verifyConnection } = useVerifyConnection();
const authenticate = React.useMemo(() => { const authenticate = React.useMemo(() => {
if (!steps?.length) return; if (!steps?.length) return;
@@ -44,9 +53,7 @@ export default function useAuthenticateApp(payload) {
const response = { const response = {
key: appKey, key: appKey,
appAuthClientId: appAuthClientId || payload.appAuthClientId, appAuthClientId: appAuthClientId || payload.appAuthClientId,
connection: { connectionId,
id: connectionId,
},
fields, fields,
}; };
let stepIndex = 0; let stepIndex = 0;
@@ -69,10 +76,29 @@ export default function useAuthenticateApp(payload) {
if (step.type === 'mutation') { if (step.type === 'mutation') {
if (step.name === 'createConnection') { if (step.name === 'createConnection') {
const stepResponse = await createConnection(variables); const stepResponse = await createConnection(variables);
response[step.name] = stepResponse.data;
response.connectionId = stepResponse.data.id;
} else if (step.name === 'generateAuthUrl') {
const stepResponse = await createConnectionAuthUrl(
response.connectionId,
);
response[step.name] = stepResponse.data;
} else if (step.name === 'updateConnection') {
const stepResponse = await updateConnection({
...variables,
connectionId: response.connectionId,
});
response[step.name] = stepResponse.data;
} else if (step.name === 'resetConnection') {
const stepResponse = await resetConnection(response.connectionId);
response[step.name] = stepResponse.data;
} else if (step.name === 'verifyConnection') {
const stepResponse = await verifyConnection(
response.connectionId,
);
response[step.name] = stepResponse?.data; response[step.name] = stepResponse?.data;
} else {
const stepResponse = await processMutation(step.name, variables);
response[step.name] = stepResponse;
} }
} else if (step.type === 'openWithPopup') { } else if (step.type === 'openWithPopup') {
const stepResponse = await processPopupMessage(popup); const stepResponse = await processPopupMessage(popup);
@@ -81,17 +107,38 @@ export default function useAuthenticateApp(payload) {
} catch (err) { } catch (err) {
console.log(err); console.log(err);
setAuthenticationInProgress(false); setAuthenticationInProgress(false);
queryClient.invalidateQueries({
queryKey: ['apps', appKey, 'connections'],
});
throw err; throw err;
} }
stepIndex++;
if (stepIndex === steps.length) { stepIndex++;
return response;
}
setAuthenticationInProgress(false);
} }
await queryClient.invalidateQueries({
queryKey: ['apps', appKey, 'connections'],
});
setAuthenticationInProgress(false);
return response;
}; };
}, [steps, appKey, appAuthClientId, connectionId, formatMessage]); }, [
steps,
appKey,
appAuthClientId,
connectionId,
queryClient,
formatMessage,
createConnection,
createConnectionAuthUrl,
updateConnection,
resetConnection,
verifyConnection,
]);
return { return {
authenticate, authenticate,

View File

@@ -1,10 +1,8 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import api from 'helpers/api'; import api from 'helpers/api';
export default function useCreateConnection(appKey) { export default function useCreateConnection(appKey) {
const queryClient = useQueryClient();
const query = useMutation({ const query = useMutation({
mutationFn: async ({ appAuthClientId, formattedData }) => { mutationFn: async ({ appAuthClientId, formattedData }) => {
const { data } = await api.post(`/v1/apps/${appKey}/connections`, { const { data } = await api.post(`/v1/apps/${appKey}/connections`, {
@@ -14,12 +12,6 @@ export default function useCreateConnection(appKey) {
return data; return data;
}, },
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ['apps', appKey, 'connections'],
});
},
}); });
return query; return query;

View File

@@ -0,0 +1,17 @@
import { useMutation } from '@tanstack/react-query';
import api from 'helpers/api';
export default function useCreateConnectionAuthUrl() {
const query = useMutation({
mutationFn: async (connectionId) => {
const { data } = await api.post(
`/v1/connections/${connectionId}/auth-url`,
);
return data;
},
});
return query;
}

View File

@@ -0,0 +1,15 @@
import { useMutation } from '@tanstack/react-query';
import api from 'helpers/api';
export default function useResetConnection() {
const query = useMutation({
mutationFn: async (connectionId) => {
const { data } = await api.post(`/v1/connections/${connectionId}/reset`);
return data;
},
});
return query;
}

View File

@@ -0,0 +1,18 @@
import { useMutation } from '@tanstack/react-query';
import api from 'helpers/api';
export default function useUpdateConnection() {
const query = useMutation({
mutationFn: async ({ connectionId, formattedData, appAuthClientId }) => {
const { data } = await api.patch(`/v1/connections/${connectionId}`, {
formattedData,
appAuthClientId,
});
return data;
},
});
return query;
}

View File

@@ -0,0 +1,24 @@
import { useMutation } from '@tanstack/react-query';
import api from 'helpers/api';
export default function useVerifyConnection() {
const query = useMutation({
mutationFn: async (connectionId) => {
try {
const { data } = await api.post(
`/v1/connections/${connectionId}/verify`,
);
return data;
} catch (err) {
if (err.response.status === 500) {
throw new Error('Failed while verifying connection!');
}
throw err;
}
},
});
return query;
}