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',
parameters: {},
})
.expect(200);
.expect(422);
expect(response.body.errors).toEqual(errors);
});

View File

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

View File

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

View File

@@ -1,54 +1,23 @@
import cloneDeep from 'lodash/cloneDeep.js';
const connectionIdArgument = {
name: 'id',
value: '{connection.id}',
};
const resetConnectionStep = {
type: 'mutation',
name: 'resetConnection',
arguments: [connectionIdArgument],
arguments: [],
};
function replaceCreateConnection(string) {
return string.replace('{createConnection.id}', '{connection.id}');
}
function removeAppKeyArgument(args) {
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) {
const updatedSteps = cloneDeep(steps);
return updatedSteps.map((step) => {
const updatedStep = addConnectionId(step);
const updatedStep = { ...step };
if (step.name === 'createConnection') {
updatedStep.name = 'updateConnection';
updatedStep.arguments = removeAppKeyArgument(updatedStep.arguments);
updatedStep.arguments.unshift(connectionIdArgument);
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) {

View File

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

View File

@@ -6,36 +6,7 @@ const cache = new InMemoryCache({
},
Mutation: {
mutationType: true,
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;
},
},
},
fields: {},
},
},
});

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 searchParams = new URLSearchParams(event.data.payload.search);
const hashParams = new URLSearchParams(event.data.payload.hash.substring(1));

View File

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

View File

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