diff --git a/packages/backend/src/apps/typeform/assets/favicon.svg b/packages/backend/src/apps/typeform/assets/favicon.svg new file mode 100644 index 00000000..f0fabb1c --- /dev/null +++ b/packages/backend/src/apps/typeform/assets/favicon.svg @@ -0,0 +1,4 @@ + + Typeform + + diff --git a/packages/backend/src/apps/typeform/auth/generate-auth-url.ts b/packages/backend/src/apps/typeform/auth/generate-auth-url.ts new file mode 100644 index 00000000..14ca84b8 --- /dev/null +++ b/packages/backend/src/apps/typeform/auth/generate-auth-url.ts @@ -0,0 +1,21 @@ +import { IField, IGlobalVariable } from '@automatisch/types'; +import { URLSearchParams } from 'url'; +import authScope from '../common/auth-scope'; + +export default async function generateAuthUrl($: IGlobalVariable) { + const oauthRedirectUrl = $.app.auth.fields.find( + (field: IField) => field.key == 'oAuthRedirectUrl' + ).value; + + const searchParams = new URLSearchParams({ + client_id: $.auth.data.clientId as string, + redirect_uri: oauthRedirectUrl as string, + scope: authScope.join(' '), + }); + + const url = `${$.app.apiBaseUrl}/oauth/authorize?${searchParams.toString()}`; + + await $.auth.set({ + url, + }); +} diff --git a/packages/backend/src/apps/typeform/auth/index.ts b/packages/backend/src/apps/typeform/auth/index.ts new file mode 100644 index 00000000..4bc4dc66 --- /dev/null +++ b/packages/backend/src/apps/typeform/auth/index.ts @@ -0,0 +1,48 @@ +import generateAuthUrl from './generate-auth-url'; +import verifyCredentials from './verify-credentials'; +import isStillVerified from './is-still-verified'; +import refreshToken from './refresh-token'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string' as const, + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/typeform/connections/add', + placeholder: null, + description: + 'When asked to input an OAuth callback or redirect URL in Typeform OAuth, enter the URL above.', + clickToCopy: true, + }, + { + key: 'clientId', + label: 'Client ID', + type: 'string' as const, + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'clientSecret', + label: 'Client Secret', + type: 'string' as const, + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + ], + + generateAuthUrl, + verifyCredentials, + isStillVerified, + refreshToken, +}; diff --git a/packages/backend/src/apps/typeform/auth/is-still-verified.ts b/packages/backend/src/apps/typeform/auth/is-still-verified.ts new file mode 100644 index 00000000..4fb0c3e3 --- /dev/null +++ b/packages/backend/src/apps/typeform/auth/is-still-verified.ts @@ -0,0 +1,9 @@ +import { IGlobalVariable } from '@automatisch/types'; + +const isStillVerified = async ($: IGlobalVariable) => { + await $.http.get('/me'); + + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/typeform/auth/refresh-token.ts b/packages/backend/src/apps/typeform/auth/refresh-token.ts new file mode 100644 index 00000000..7da4e118 --- /dev/null +++ b/packages/backend/src/apps/typeform/auth/refresh-token.ts @@ -0,0 +1,24 @@ +import { IGlobalVariable } from '@automatisch/types'; +import { URLSearchParams } from 'url'; +import authScope from '../common/auth-scope'; + +const refreshToken = async ($: IGlobalVariable) => { + const params = new URLSearchParams({ + grant_type: 'refresh_token', + client_id: $.auth.data.clientId as string, + client_secret: $.auth.data.clientSecret as string, + refresh_token: $.auth.data.refreshToken as string, + scope: authScope.join(' '), + }); + + const { data } = await $.http.post('/oauth/token', params.toString()); + + await $.auth.set({ + accessToken: data.access_token, + expiresIn: data.expires_in, + tokenType: data.token_type, + refreshToken: data.refresh_token, + }); +}; + +export default refreshToken; diff --git a/packages/backend/src/apps/typeform/auth/verify-credentials.ts b/packages/backend/src/apps/typeform/auth/verify-credentials.ts new file mode 100644 index 00000000..2d1e55ea --- /dev/null +++ b/packages/backend/src/apps/typeform/auth/verify-credentials.ts @@ -0,0 +1,46 @@ +import { IField, IGlobalVariable } from '@automatisch/types'; +import { URLSearchParams } from 'url'; + +const verifyCredentials = async ($: IGlobalVariable) => { + const oauthRedirectUrl = $.app.auth.fields.find( + (field: IField) => field.key == 'oAuthRedirectUrl' + ).value; + + const params = new URLSearchParams({ + grant_type: 'authorization_code', + code: $.auth.data.code as string, + client_id: $.auth.data.clientId as string, + client_secret: $.auth.data.clientSecret as string, + redirect_uri: oauthRedirectUrl as string, + }); + + const { data: verifiedCredentials } = await $.http.post( + '/oauth/token', + params.toString() + ); + + const { + access_token: accessToken, + expires_in: expiresIn, + token_type: tokenType, + refresh_token: refreshToken, + } = verifiedCredentials; + + const { data: user } = await $.http.get('/me', { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + + await $.auth.set({ + accessToken, + expiresIn, + tokenType, + userId: user.user_id, + screenName: user.alias, + email: user.email, + refreshToken, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/typeform/common/add-auth-header.ts b/packages/backend/src/apps/typeform/common/add-auth-header.ts new file mode 100644 index 00000000..d650c915 --- /dev/null +++ b/packages/backend/src/apps/typeform/common/add-auth-header.ts @@ -0,0 +1,12 @@ +import { TBeforeRequest } from '@automatisch/types'; + +const addAuthHeader: TBeforeRequest = ($, requestConfig) => { + if ($.auth.data?.accessToken) { + const authorizationHeader = `Bearer ${$.auth.data.accessToken}`; + requestConfig.headers.Authorization = authorizationHeader; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/typeform/common/auth-scope.ts b/packages/backend/src/apps/typeform/common/auth-scope.ts new file mode 100644 index 00000000..7b3154c8 --- /dev/null +++ b/packages/backend/src/apps/typeform/common/auth-scope.ts @@ -0,0 +1,12 @@ +const authScope: string[] = [ + 'forms:read', + 'forms:write', + 'webhooks:read', + 'webhooks:write', + 'responses:read', + 'accounts:read', + 'workspaces:read', + 'offline', +]; + +export default authScope; diff --git a/packages/backend/src/apps/typeform/index.ts b/packages/backend/src/apps/typeform/index.ts new file mode 100644 index 00000000..34e5bac7 --- /dev/null +++ b/packages/backend/src/apps/typeform/index.ts @@ -0,0 +1,16 @@ +import defineApp from '../../helpers/define-app'; +import addAuthHeader from './common/add-auth-header'; +import auth from './auth'; + +export default defineApp({ + name: 'Typeform', + key: 'typeform', + iconUrl: '{BASE_URL}/apps/typeform/assets/favicon.svg', + authDocUrl: 'https://automatisch.io/docs/apps/typeform/connection', + supportsConnections: true, + baseUrl: 'https://typeform.com', + apiBaseUrl: 'https://api.typeform.com', + primaryColor: '000000', + beforeRequest: [addAuthHeader], + auth, +});