diff --git a/packages/backend/src/apps/dropbox/actions/create-folder/index.ts b/packages/backend/src/apps/dropbox/actions/create-folder/index.ts new file mode 100644 index 00000000..181977cc --- /dev/null +++ b/packages/backend/src/apps/dropbox/actions/create-folder/index.ts @@ -0,0 +1,36 @@ +import path from 'node:path'; +import defineAction from '../../../../helpers/define-action'; + +export default defineAction({ + name: 'Create folder', + key: 'createFolder', + description: 'Create a new folder with the given parent folder and folder name', + arguments: [ + { + label: 'Folder', + key: 'parentFolder', + type: 'string' as const, + required: true, + description: 'Enter the parent folder path, like /TextFiles/ or /Documents/Taxes/', + variables: true, + }, + { + label: 'Folder Name', + key: 'folderName', + type: 'string' as const, + required: true, + description: 'Enter the name for the new folder', + variables: true, + }, + ], + + async run($) { + const parentFolder = $.step.parameters.parentFolder as string; + const folderName = $.step.parameters.folderName as string; + const folderPath = path.join(parentFolder, folderName); + + const response = await $.http.post('/2/files/create_folder_v2', { path: folderPath }); + + $.setActionItem({ raw: response.data }); + }, +}); diff --git a/packages/backend/src/apps/dropbox/actions/index.ts b/packages/backend/src/apps/dropbox/actions/index.ts new file mode 100644 index 00000000..14f401da --- /dev/null +++ b/packages/backend/src/apps/dropbox/actions/index.ts @@ -0,0 +1,4 @@ +import createFolder from "./create-folder"; +import renameFile from "./rename-file"; + +export default [createFolder, renameFile]; diff --git a/packages/backend/src/apps/dropbox/actions/rename-file/index.ts b/packages/backend/src/apps/dropbox/actions/rename-file/index.ts new file mode 100644 index 00000000..76bacffc --- /dev/null +++ b/packages/backend/src/apps/dropbox/actions/rename-file/index.ts @@ -0,0 +1,45 @@ +import path from 'node:path'; +import defineAction from '../../../../helpers/define-action'; + +export default defineAction({ + name: 'Rename file', + key: 'renameFile', + description: 'Rename a file with the given file path and new name', + arguments: [ + { + label: 'File Path', + key: 'filePath', + type: 'string' as const, + required: true, + description: + 'Write the full path to the file such as /Folder1/File.pdf', + variables: true, + }, + { + label: 'New Name', + key: 'newName', + type: 'string' as const, + required: true, + description: "Enter the new name for the file (without the extension, e.g., '.pdf')", + variables: true, + }, + ], + + async run($) { + const filePath = $.step.parameters.filePath as string; + const newName = $.step.parameters.newName as string; + const fileObject = path.parse(filePath); + const newPath = path.format({ + dir: fileObject.dir, + ext: fileObject.ext, + name: newName, + }); + + const response = await $.http.post('/2/files/move_v2', { + from_path: filePath, + to_path: newPath, + }); + + $.setActionItem({ raw: response.data.metadata }); + }, +}); diff --git a/packages/backend/src/apps/dropbox/assets/favicon.svg b/packages/backend/src/apps/dropbox/assets/favicon.svg new file mode 100644 index 00000000..59f38626 --- /dev/null +++ b/packages/backend/src/apps/dropbox/assets/favicon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/backend/src/apps/dropbox/auth/generate-auth-url.ts b/packages/backend/src/apps/dropbox/auth/generate-auth-url.ts new file mode 100644 index 00000000..d663c25c --- /dev/null +++ b/packages/backend/src/apps/dropbox/auth/generate-auth-url.ts @@ -0,0 +1,22 @@ +import { URLSearchParams } from 'url'; +import { IField, IGlobalVariable } from '@automatisch/types'; +import scopes from '../common/scopes'; + +export default async function generateAuthUrl($: IGlobalVariable) { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field: IField) => field.key == 'oAuthRedirectUrl' + ); + const callbackUrl = oauthRedirectUrlField.value as string; + + const searchParams = new URLSearchParams({ + client_id: $.auth.data.clientId as string, + redirect_uri: callbackUrl, + response_type: 'code', + scope: scopes.join(' '), + token_access_type: 'offline', + }); + + const url = `${$.app.baseUrl}/oauth2/authorize?${searchParams.toString()}`; + + await $.auth.set({ url }); +} diff --git a/packages/backend/src/apps/dropbox/auth/index.ts b/packages/backend/src/apps/dropbox/auth/index.ts new file mode 100644 index 00000000..4331302b --- /dev/null +++ b/packages/backend/src/apps/dropbox/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/dropbox/connections/add', + placeholder: null, + description: + 'When asked to input an OAuth callback or redirect URL in Dropbox OAuth, enter the URL above.', + clickToCopy: true, + }, + { + key: 'clientId', + label: 'App Key', + type: 'string' as const, + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'clientSecret', + label: 'App 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/dropbox/auth/is-still-verified.ts b/packages/backend/src/apps/dropbox/auth/is-still-verified.ts new file mode 100644 index 00000000..bf70f874 --- /dev/null +++ b/packages/backend/src/apps/dropbox/auth/is-still-verified.ts @@ -0,0 +1,9 @@ +import { IGlobalVariable } from '@automatisch/types'; +import getCurrentAccount from '../common/get-current-account'; + +const isStillVerified = async ($: IGlobalVariable) => { + const account = await getCurrentAccount($); + return !!account; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/dropbox/auth/refresh-token.ts b/packages/backend/src/apps/dropbox/auth/refresh-token.ts new file mode 100644 index 00000000..67c4454f --- /dev/null +++ b/packages/backend/src/apps/dropbox/auth/refresh-token.ts @@ -0,0 +1,41 @@ +import { Buffer } from 'node:buffer'; +import { IGlobalVariable } from '@automatisch/types'; + +const refreshToken = async ($: IGlobalVariable) => { + const params = { + grant_type: 'refresh_token', + refresh_token: $.auth.data.refreshToken as string, + }; + + const basicAuthToken = Buffer + .from(`${$.auth.data.clientId}:${$.auth.data.clientSecret}`) + .toString('base64'); + + const { data } = await $.http.post( + 'oauth2/token', + null, + { + params, + headers: { + Authorization: `Basic ${basicAuthToken}` + }, + additionalProperties: { + skipAddingAuthHeader: true + } + } + ); + + const { + access_token: accessToken, + expires_in: expiresIn, + token_type: tokenType, + } = data; + + await $.auth.set({ + accessToken, + expiresIn, + tokenType, + }); +}; + +export default refreshToken; diff --git a/packages/backend/src/apps/dropbox/auth/verify-credentials.ts b/packages/backend/src/apps/dropbox/auth/verify-credentials.ts new file mode 100644 index 00000000..f56b25d2 --- /dev/null +++ b/packages/backend/src/apps/dropbox/auth/verify-credentials.ts @@ -0,0 +1,102 @@ +import { IGlobalVariable, IField } from '@automatisch/types'; +import getCurrentAccount from '../common/get-current-account'; + +type TAccount = { + account_id: string, + name: { + given_name: string, + surname: string, + familiar_name: string, + display_name: string, + abbreviated_name: string, + }, + email: string, + email_verified: boolean, + disabled: boolean, + country: string, + locale: string, + referral_link: string, + is_paired: boolean, + account_type: { + ".tag": string, + }, + root_info: { + ".tag": string, + root_namespace_id: string, + home_namespace_id: string, + }, +} + +const verifyCredentials = async ($: IGlobalVariable) => { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field: IField) => field.key == 'oAuthRedirectUrl' + ); + const redirectUrl = oauthRedirectUrlField.value as string; + const params = { + client_id: $.auth.data.clientId as string, + redirect_uri: redirectUrl, + client_secret: $.auth.data.clientSecret as string, + code: $.auth.data.code as string, + grant_type: 'authorization_code', + } + const { data: verifiedCredentials } = await $.http.post( + '/oauth2/token', + null, + { params } + ); + + const { + access_token: accessToken, + refresh_token: refreshToken, + expires_in: expiresIn, + scope: scope, + token_type: tokenType, + account_id: accountId, + team_id: teamId, + id_token: idToken, + uid, + } = verifiedCredentials; + + await $.auth.set({ + accessToken, + refreshToken, + expiresIn, + scope, + tokenType, + accountId, + teamId, + idToken, + uid + }); + + const account = await getCurrentAccount($) as TAccount; + + await $.auth.set({ + accountId: account.account_id, + name: { + givenName: account.name.given_name, + surname: account.name.surname, + familiarName: account.name.familiar_name, + displayName: account.name.display_name, + abbreviatedName: account.name.abbreviated_name, + }, + email: account.email, + emailVerified: account.email_verified, + disabled: account.disabled, + country: account.country, + locale: account.locale, + referralLink: account.referral_link, + isPaired: account.is_paired, + accountType: { + ".tag": account.account_type['.tag'], + }, + rootInfo: { + ".tag": account.root_info['.tag'], + rootNamespaceId: account.root_info.root_namespace_id, + homeNamespaceId: account.root_info.home_namespace_id, + }, + screenName: `${account.name.display_name} - ${account.email}`, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/dropbox/common/add-auth-header.ts b/packages/backend/src/apps/dropbox/common/add-auth-header.ts new file mode 100644 index 00000000..ac7b7231 --- /dev/null +++ b/packages/backend/src/apps/dropbox/common/add-auth-header.ts @@ -0,0 +1,13 @@ +import { TBeforeRequest } from '@automatisch/types'; + +const addAuthHeader: TBeforeRequest = ($, requestConfig) => { + requestConfig.headers['Content-Type'] = 'application/json'; + + if (!requestConfig.additionalProperties?.skipAddingAuthHeader && $.auth.data?.accessToken) { + requestConfig.headers.Authorization = `Bearer ${$.auth.data.accessToken}`; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/dropbox/common/get-current-account.ts b/packages/backend/src/apps/dropbox/common/get-current-account.ts new file mode 100644 index 00000000..6764e781 --- /dev/null +++ b/packages/backend/src/apps/dropbox/common/get-current-account.ts @@ -0,0 +1,8 @@ +import { IGlobalVariable, IJSONObject } from '@automatisch/types'; + +const getCurrentAccount = async ($: IGlobalVariable): Promise => { + const response = await $.http.post('/2/users/get_current_account', null); + return response.data; +}; + +export default getCurrentAccount; diff --git a/packages/backend/src/apps/dropbox/common/scopes.ts b/packages/backend/src/apps/dropbox/common/scopes.ts new file mode 100644 index 00000000..b257d7cb --- /dev/null +++ b/packages/backend/src/apps/dropbox/common/scopes.ts @@ -0,0 +1,8 @@ +const scopes = [ + 'account_info.read', + 'files.metadata.read', + 'files.content.write', + 'files.content.read', +]; + +export default scopes; diff --git a/packages/backend/src/apps/dropbox/index.d.ts b/packages/backend/src/apps/dropbox/index.d.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/backend/src/apps/dropbox/index.ts b/packages/backend/src/apps/dropbox/index.ts new file mode 100644 index 00000000..d7bc877b --- /dev/null +++ b/packages/backend/src/apps/dropbox/index.ts @@ -0,0 +1,18 @@ +import defineApp from '../../helpers/define-app'; +import addAuthHeader from './common/add-auth-header'; +import auth from './auth'; +import actions from './actions'; + +export default defineApp({ + name: 'Dropbox', + key: 'dropbox', + iconUrl: '{BASE_URL}/apps/dropbox/assets/favicon.svg', + authDocUrl: 'https://automatisch.io/docs/apps/dropbox/connection', + supportsConnections: true, + baseUrl: 'https://dropbox.com', + apiBaseUrl: 'https://api.dropboxapi.com', + primaryColor: '0061ff', + beforeRequest: [addAuthHeader], + auth, + actions, +}); diff --git a/packages/backend/src/apps/spotify/auth/refresh-token.ts b/packages/backend/src/apps/spotify/auth/refresh-token.ts index dc20820a..b748f658 100644 --- a/packages/backend/src/apps/spotify/auth/refresh-token.ts +++ b/packages/backend/src/apps/spotify/auth/refresh-token.ts @@ -1,3 +1,4 @@ +import { Buffer } from 'node:buffer'; import { IGlobalVariable } from '@automatisch/types'; const refreshToken = async ($: IGlobalVariable) => { diff --git a/packages/docs/pages/.vitepress/config.js b/packages/docs/pages/.vitepress/config.js index 1118ac41..85915902 100644 --- a/packages/docs/pages/.vitepress/config.js +++ b/packages/docs/pages/.vitepress/config.js @@ -59,6 +59,15 @@ export default defineConfig({ { text: 'Connection', link: '/apps/discord/connection' }, ], }, + { + text: 'Dropbox', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/dropbox/actions' }, + { text: 'Connection', link: '/apps/dropbox/connection' }, + ], + }, { text: 'Flickr', collapsible: true, diff --git a/packages/docs/pages/apps/dropbox/actions.md b/packages/docs/pages/apps/dropbox/actions.md new file mode 100644 index 00000000..4d5d4b3e --- /dev/null +++ b/packages/docs/pages/apps/dropbox/actions.md @@ -0,0 +1,14 @@ +--- +favicon: /favicons/dropbox.svg +items: + - name: Create a folder + desc: Creates a new folder with the given parent folder and folder name. + - name: Rename a file + desc: Rename a file with the given file path and new name. +--- + + + + diff --git a/packages/docs/pages/apps/dropbox/connection.md b/packages/docs/pages/apps/dropbox/connection.md new file mode 100644 index 00000000..88696142 --- /dev/null +++ b/packages/docs/pages/apps/dropbox/connection.md @@ -0,0 +1,20 @@ +# Dropbox + +:::info +This page explains the steps you need to follow to set up the Dropbox +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to the [link](https://www.dropbox.com/developers/apps) to create a **new application** on Dropbox. +1. Choose the "Scoped access" option in the "Choose an API" section. +1. Choose the "Full Dropbox" option in the "Choose the type of access you need" section. +1. Name your application. +1. Click on the **Create app** button. +1. Copy **OAuth Redirect URL** from Automatisch to **Redirect URIs** field and add it. +1. Click on the **Scoped App** link in the "Permission type" section. +1. Check the checkbox for the "files.content.write" scope and click on the **Submit** button. +1. Go back to the "Settings" tab. +1. Copy **App key** to **App key** field on Automatisch. +1. Copy **App secret** to **App secret** field on Automatisch. +1. Click **Submit** button on Automatisch. +1. Congrats! Start using your new Dropbox connection within the flows. diff --git a/packages/docs/pages/guide/available-apps.md b/packages/docs/pages/guide/available-apps.md index 47f92456..1909c0f4 100644 --- a/packages/docs/pages/guide/available-apps.md +++ b/packages/docs/pages/guide/available-apps.md @@ -9,9 +9,10 @@ Following integrations are currently supported by Automatisch. - [DeepL](/apps/deepl/actions) - [Delay](/apps/delay/actions) - [Discord](/apps/discord/actions) +- [Dropbox](/apps/dropbox/actions) - [Flickr](/apps/flickr/triggers) -- [Google Drive](/apps/google-drive/triggers) - [Github](/apps/github/triggers) +- [Google Drive](/apps/google-drive/triggers) - [Google Forms](/apps/google-forms/triggers) - [HTTP Request](/apps/http-request/actions) - [Ntfy](/apps/ntfy/actions) diff --git a/packages/docs/pages/public/favicons/dropbox.svg b/packages/docs/pages/public/favicons/dropbox.svg new file mode 100644 index 00000000..59f38626 --- /dev/null +++ b/packages/docs/pages/public/favicons/dropbox.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file