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..5cd2e226
--- /dev/null
+++ b/packages/backend/src/apps/dropbox/auth/refresh-token.ts
@@ -0,0 +1,38 @@
+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}`
+ }
+ }
+ );
+
+ 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..984d0b3a
--- /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.headers.Authorization && $.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..334b70f9
--- /dev/null
+++ b/packages/backend/src/apps/dropbox/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: '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,
+});