refactor: Use functional design for the authentication of apps

This commit is contained in:
Faruk AYDIN
2022-10-03 21:14:54 +03:00
parent 0abab06124
commit 4d5be952ce
17 changed files with 523 additions and 22 deletions

View File

@@ -6,6 +6,7 @@
"authDocUrl": "https://automatisch.io/docs/connections/slack",
"primaryColor": "2DAAE1",
"supportsConnections": true,
"baseUrl": "https://slack.com/api",
"fields": [
{
"key": "accessToken",

View File

@@ -0,0 +1,100 @@
import verifyCredentials from './verify-credentials';
import isStillVerified from './is-still-verified';
export default {
fields: [
{
key: 'accessToken',
label: 'Access Token',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: 'Access token of slack that Automatisch will connect to.',
clickToCopy: false,
},
],
authenticationSteps: [
{
step: 1,
type: 'mutation',
name: 'createConnection',
arguments: [
{
name: 'key',
value: '{key}',
},
{
name: 'formattedData',
value: null,
properties: [
{
name: 'accessToken',
value: '{fields.accessToken}',
},
],
},
],
},
{
step: 2,
type: 'mutation',
name: 'verifyConnection',
arguments: [
{
name: 'id',
value: '{createConnection.id}',
},
],
},
],
reconnectionSteps: [
{
step: 1,
type: 'mutation',
name: 'resetConnection',
arguments: [
{
name: 'id',
value: '{connection.id}',
},
],
},
{
step: 2,
type: 'mutation',
name: 'updateConnection',
arguments: [
{
name: 'id',
value: '{connection.id}',
},
{
name: 'formattedData',
value: null,
properties: [
{
name: 'accessToken',
value: '{fields.accessToken}',
},
],
},
],
},
{
step: 3,
type: 'mutation',
name: 'verifyConnection',
arguments: [
{
name: 'id',
value: '{connection.id}',
},
],
},
],
verifyCredentials,
isStillVerified,
};

View File

@@ -0,0 +1,12 @@
import verifyCredentials from './verify-credentials';
const isStillVerified = async ($: any) => {
try {
await verifyCredentials($);
return true;
} catch (error) {
return false;
}
};
export default isStillVerified;

View File

@@ -0,0 +1,33 @@
import qs from 'qs';
const verifyCredentials = async ($: any) => {
const headers = {
'Content-Type': 'application/x-www-form-urlencoded',
};
const stringifiedBody = qs.stringify({
token: $.auth.accessToken,
});
const response = await $.http.post('/auth.test', stringifiedBody, {
headers,
});
if (response.data.ok === false) {
throw new Error(
`Error occured while verifying credentials: ${response.data.error}.(More info: https://api.slack.com/methods/auth.test#errors)`
);
}
const { bot_id: botId, user: screenName } = response.data;
$.auth.set({
botId,
screenName,
token: $.auth.accessToken,
});
return response.data;
};
export default verifyCredentials;

View File

@@ -0,0 +1,8 @@
export default {
name: 'Slack',
key: 'slack',
iconUrl: './assets/favicon.svg',
authDocUrl: 'https://automatisch.io/docs/connections/slack',
supportsConnections: true,
baseUrl: 'https://slack.com/api',
};

View File

@@ -6,6 +6,7 @@
"authDocUrl": "https://automatisch.io/docs/connections/twitter",
"primaryColor": "2DAAE1",
"supportsConnections": true,
"baseUrl": "https://api.twitter.com",
"fields": [
{
"key": "oAuthRedirectUrl",

View File

@@ -0,0 +1,43 @@
import { IJSONObject, IField } from '@automatisch/types';
import oauthClient from '../common/oauth-client';
import { URLSearchParams } from 'url';
export default async function createAuthData($: any) {
try {
const oauthRedirectUrlField = $.app.fields.find(
(field: IField) => field.key == 'oAuthRedirectUrl'
);
const callbackUrl = oauthRedirectUrlField.value;
const requestData = {
url: `${$.app.baseUrl}/oauth/request_token`,
method: 'POST',
data: { oauth_callback: callbackUrl },
};
const authHeader = oauthClient($).toHeader(
oauthClient($).authorize(requestData)
);
const response = await $.http.post(`/oauth/request_token`, null, {
headers: { ...authHeader },
});
const responseData = Object.fromEntries(new URLSearchParams(response.data));
await $.auth.set({
url: `${$.app.baseUrl}/oauth/authorize?oauth_token=${responseData.oauth_token}`,
accessToken: responseData.oauth_token,
accessSecret: responseData.oauth_token_secret,
});
} catch (error) {
const errorMessages = error.response.data.errors
.map((error: IJSONObject) => error.message)
.join(' ');
throw new Error(
`Error occured while verifying credentials: ${errorMessages}`
);
}
}

View File

@@ -0,0 +1,219 @@
import createAuthData from './create-auth-data';
import verifyCredentials from './verify-credentials';
import isStillVerified from './is-still-verified';
export default {
fields: [
{
key: 'oAuthRedirectUrl',
label: 'OAuth Redirect URL',
type: 'string',
required: true,
readOnly: true,
value: '{WEB_APP_URL}/app/twitter/connections/add',
placeholder: null,
description:
'When asked to input an OAuth callback or redirect URL in Twitter OAuth, enter the URL above.',
clickToCopy: true,
},
{
key: 'consumerKey',
label: 'API Key',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: null,
clickToCopy: false,
},
{
key: 'consumerSecret',
label: 'API Secret',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: null,
clickToCopy: false,
},
],
authenticationSteps: [
{
step: 1,
type: 'mutation',
name: 'createConnection',
arguments: [
{
name: 'key',
value: '{key}',
},
{
name: 'formattedData',
value: null,
properties: [
{
name: 'consumerKey',
value: '{fields.consumerKey}',
},
{
name: 'consumerSecret',
value: '{fields.consumerSecret}',
},
],
},
],
},
{
step: 2,
type: 'mutation',
name: 'createAuthData',
arguments: [
{
name: 'id',
value: '{createConnection.id}',
},
],
},
{
step: 3,
type: 'openWithPopup',
name: 'openAuthPopup',
arguments: [
{
name: 'url',
value: '{createAuthData.url}',
},
],
},
{
step: 4,
type: 'mutation',
name: 'updateConnection',
arguments: [
{
name: 'id',
value: '{createConnection.id}',
},
{
name: 'formattedData',
value: null,
properties: [
{
name: 'oauthVerifier',
value: '{openAuthPopup.oauth_verifier}',
},
],
},
],
},
{
step: 5,
type: 'mutation',
name: 'verifyConnection',
arguments: [
{
name: 'id',
value: '{createConnection.id}',
},
],
},
],
reconnectionSteps: [
{
step: 1,
type: 'mutation',
name: 'resetConnection',
arguments: [
{
name: 'id',
value: '{connection.id}',
},
],
},
{
step: 2,
type: 'mutation',
name: 'updateConnection',
arguments: [
{
name: 'id',
value: '{connection.id}',
},
{
name: 'formattedData',
value: null,
properties: [
{
name: 'consumerKey',
value: '{fields.consumerKey}',
},
{
name: 'consumerSecret',
value: '{fields.consumerSecret}',
},
],
},
],
},
{
step: 3,
type: 'mutation',
name: 'createAuthData',
arguments: [
{
name: 'id',
value: '{connection.id}',
},
],
},
{
step: 4,
type: 'openWithPopup',
name: 'openAuthPopup',
arguments: [
{
name: 'url',
value: '{createAuthData.url}',
},
],
},
{
step: 5,
type: 'mutation',
name: 'updateConnection',
arguments: [
{
name: 'id',
value: '{connection.id}',
},
{
name: 'formattedData',
value: null,
properties: [
{
name: 'oauthVerifier',
value: '{openAuthPopup.oauth_verifier}',
},
],
},
],
},
{
step: 6,
type: 'mutation',
name: 'verifyConnection',
arguments: [
{
name: 'id',
value: '{connection.id}',
},
],
},
],
createAuthData,
verifyCredentials,
isStillVerified,
};

View File

@@ -0,0 +1,12 @@
import getCurrentUser from '../common/get-current-user';
const isStillVerified = async ($: any) => {
try {
await getCurrentUser($);
return true;
} catch (error) {
return false;
}
};
export default isStillVerified;

View File

@@ -0,0 +1,21 @@
const verifyCredentials = async ($: any) => {
try {
const response = await $.http.post(
`/oauth/access_token?oauth_verifier=${$.auth.oauthVerifier}&oauth_token=${$.auth.accessToken}`,
null
);
const responseData = Object.fromEntries(new URLSearchParams(response.data));
await $.auth.set({
accessToken: responseData.oauth_token,
accessSecret: responseData.oauth_token_secret,
userId: responseData.user_id,
screenName: responseData.screen_name,
});
} catch (error) {
throw new Error(error.response.data);
}
};
export default verifyCredentials;

View File

@@ -0,0 +1,22 @@
import crypto from 'crypto';
import OAuth from 'oauth-1.0a';
const oauthClient = ($: any) => {
const consumerData = {
key: $.auth.consumerKey as string,
secret: $.auth.consumerSecret as string,
};
return new OAuth({
consumer: consumerData,
signature_method: 'HMAC-SHA1',
hash_function(base_string, key) {
return crypto
.createHmac('sha1', key)
.update(base_string)
.digest('base64');
},
});
};
export default oauthClient;

View File

@@ -0,0 +1,8 @@
export default {
name: 'Twitter',
key: 'twitter',
iconUrl: './assets/favicon.svg',
authDocUrl: 'https://automatisch.io/docs/connections/twitter',
supportsConnections: true,
baseUrl: 'https://api.twitter.com',
};

View File

@@ -1,5 +1,7 @@
import Context from '../../types/express/context';
import axios from 'axios';
import prepareGlobalVariableForConnection from '../../helpers/global-variable/connection';
import App from '../../models/app';
type Params = {
input: {
@@ -19,29 +21,24 @@ const createAuthData = async (
})
.throwIfNotFound();
const appClass = (await import(`../../apps/${connection.key}`)).default;
if (!connection.formattedData) {
return null;
}
const appInstance = new appClass(connection);
const authLink = await appInstance.authenticationClient.createAuthData();
const authInstance = (await import(`../../apps/${connection.key}2/auth`))
.default;
const app = App.findOneByKey(connection.key);
const $ = prepareGlobalVariableForConnection(connection, app);
await authInstance.createAuthData($);
try {
await axios.get(authLink.url);
await axios.get(connection.formattedData.url as string);
} catch (error) {
throw new Error('Error occured while creating authorization URL!');
}
await connection.$query().patch({
formattedData: {
...connection.formattedData,
...authLink,
},
});
return authLink;
return connection.formattedData;
};
export default createAuthData;

View File

@@ -1,5 +1,6 @@
import Context from '../../types/express/context';
import App from '../../models/app';
import prepareGlobalVariableForConnection from '../../helpers/global-variable/connection';
type Params = {
input: {
@@ -19,18 +20,14 @@ const verifyConnection = async (
})
.throwIfNotFound();
const appClass = (await import(`../../apps/${connection.key}`)).default;
const app = App.findOneByKey(connection.key);
const authInstance = (await import(`../../apps/${connection.key}2/auth`))
.default;
const appInstance = new appClass(connection);
const verifiedCredentials =
await appInstance.authenticationClient.verifyCredentials();
const $ = prepareGlobalVariableForConnection(connection, app);
await authInstance.verifyCredentials($);
connection = await connection.$query().patchAndFetch({
formattedData: {
...connection.formattedData,
...verifiedCredentials,
},
verified: true,
draft: false,
});

View File

@@ -0,0 +1,26 @@
import HttpClient from '../http-client';
import Connection from '../../models/connection';
import { IJSONObject, IApp } from '@automatisch/types';
const prepareGlobalVariableForConnection = (
connection: Connection,
appData: IApp
) => {
return {
auth: {
set: async (args: IJSONObject) => {
await connection.$query().patchAndFetch({
formattedData: {
...connection.formattedData,
...args,
},
});
},
...connection.formattedData,
},
app: appData,
http: new HttpClient({ baseURL: appData.baseUrl }),
};
};
export default prepareGlobalVariableForConnection;

View File

@@ -12,7 +12,7 @@ import Telemetry from '../helpers/telemetry';
class Connection extends Base {
id!: string;
key!: string;
data = '';
data: string;
formattedData?: IJSONObject;
userId!: string;
verified = false;

View File

@@ -154,6 +154,7 @@ export interface IApp {
authDocUrl: string;
primaryColor: string;
supportsConnections: boolean;
baseUrl: string;
fields: IField[];
authenticationSteps: IAuthenticationStep[];
reconnectionSteps: IAuthenticationStep[];