diff --git a/CONTRIBUTOR_LICENSE_AGREEMENT.md b/CONTRIBUTOR_LICENSE_AGREEMENT.md new file mode 100644 index 00000000..423eb41c --- /dev/null +++ b/CONTRIBUTOR_LICENSE_AGREEMENT.md @@ -0,0 +1,5 @@ +# Automatisch Contributor License Agreement + +I give Automatisch permission to license my contributions on any terms they like. I am giving them this license in order to make it possible for them to accept my contributions into their project. + +**_As far as the law allows, my contributions come as is, without any warranty or condition, and I will not be liable to anyone for any damages related to this software or this license, under any kind of legal claim._** diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..daa2e5d7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,3 @@ +LICENSE.agpl (AGPL-3.0) applies to all files in this +repository, except for files that contain ".ee." in their name +which are covered by LICENSE.enterprise. diff --git a/LICENSE.md b/LICENSE.agpl similarity index 100% rename from LICENSE.md rename to LICENSE.agpl diff --git a/LICENSE.enterprise b/LICENSE.enterprise new file mode 100644 index 00000000..4a7c57cc --- /dev/null +++ b/LICENSE.enterprise @@ -0,0 +1,35 @@ +The Automatisch Enterprise license (the “Enterprise License”) +Copyright (c) 2023 Ömer Faruk Aydın, Ali Barın. + +With regard to the Automatisch Software: + +This software and associated documentation files (the "Software") may only be +used in production, if you (and any entity that you represent) have a valid +Automatisch Enterprise license for the correct number of user seats. Subject +to the foregoing sentence, you are free to modify this Software and publish +patches to the Software. You agree that Automatisch and/or its licensors +(as applicable) retain all right, title and interest in and to all such +modifications and/or patches, and all such modifications and/or patches may +only be used, copied, modified, displayed, distributed, or otherwise exploited +with a valid Automatisch Enterprise license for the correct number of user seats. +Notwithstanding the foregoing, you may copy and modify the Software for +development and testing purposes, without requiring a subscription. You agree +that Automatisch and/or its licensors (as applicable) retain all right, title +and interest in and to all such modifications. You are not granted any other +rights beyond what is expressly stated herein. Subject to the foregoing, it is +forbidden to copy, merge, publish, distribute, sublicense, and/or sell the Software. + +The full text of this Enterprise License shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +For all third party components incorporated into the Automatisch Software, those +components are licensed under the original license provided by the owner of the +applicable component. diff --git a/README.md b/README.md index ef4a4ae1..66ee1218 100644 --- a/README.md +++ b/README.md @@ -44,10 +44,18 @@ For other installation types, you can check the [installation](https://automatis ## Support -If you have any questions or problems, please visit our GitHub discussions page, and we'll try to help you as soon as possible. +If you have any questions or problems, please visit our GitHub issues page, and we'll try to help you as soon as possible. -[https://github.com/automatisch/automatisch/discussions](https://github.com/automatisch/automatisch/discussions) +[https://github.com/automatisch/automatisch/issues](https://github.com/automatisch/automatisch/issues) ## License -Automatisch is an open-source software with the [AGPL 3.0 license](https://github.com/automatisch/automatisch/blob/main/LICENSE.md). +Automatisch Community Edition (Automatisch CE) is an open-source software with the [AGPL-3.0 license](LICENSE.agpl). + +Automatisch Enterprise Edition (Automatisch EE) is a commercial offering with the [Enterprise license](LICENSE.enterprise). + +The Automatisch repository contains both AGPL-licensed and Enterprise-licensed files. We maintain a single repository to make development easier. + +All files that contain ".ee." in their name fall under the [Enterprise license](LICENSE.enterprise). All other files fall under the [AGPL-3.0 license](LICENSE.agpl). + +See the [LICENSE](LICENSE) file for more information. diff --git a/docker/Dockerfile b/docker/Dockerfile index e9bddf3b..f9cbef0f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -2,13 +2,13 @@ FROM node:16-alpine WORKDIR /automatisch -RUN apk --no-cache add --virtual build-dependencies python3 build-base +RUN \ + apk --no-cache add --virtual build-dependencies python3 build-base && \ + yarn global add @automatisch/cli@0.5.0 --network-timeout 1000000 && \ + rm -rf /usr/local/share/.cache/ && \ + apk del build-dependencies COPY ./entrypoint.sh /entrypoint.sh -RUN yarn global add @automatisch/cli@0.4.0 --network-timeout 1000000 - -RUN apk del build-dependencies python3 build-base - EXPOSE 3000 ENTRYPOINT ["sh", "/entrypoint.sh"] diff --git a/docker/Dockerfile.compose b/docker/Dockerfile.compose index 79f65cea..3bc8802c 100644 --- a/docker/Dockerfile.compose +++ b/docker/Dockerfile.compose @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1 -FROM automatischio/automatisch:0.4.0 +FROM automatischio/automatisch:0.5.0 WORKDIR /automatisch RUN apk add --no-cache openssl dos2unix diff --git a/package.json b/package.json index a81364f9..c3a0b2e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@automatisch/root", - "license": "AGPL-3.0", + "license": "See LICENSE file", "private": true, "scripts": { "start": "lerna run --stream --parallel --scope=@*/{web,backend} dev", diff --git a/packages/backend/package.json b/packages/backend/package.json index 5e7e99d6..889199e5 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,7 +1,7 @@ { "name": "@automatisch/backend", "version": "0.5.0", - "license": "AGPL-3.0", + "license": "See LICENSE file", "description": "The open source Zapier alternative. Build workflow automation without spending time and money.", "scripts": { "dev": "ts-node-dev --exit-child src/server.ts", @@ -45,11 +45,13 @@ "graphql-shield": "^7.5.0", "graphql-tools": "^8.2.0", "graphql-type-json": "^0.3.2", + "handlebars": "^4.7.7", "http-errors": "~1.6.3", "jsonwebtoken": "^9.0.0", "knex": "^2.4.0", "lodash.get": "^4.4.2", "luxon": "2.5.2", + "memory-cache": "^0.2.0", "morgan": "^1.10.0", "multer": "1.4.5-lts.1", "nodemailer": "6.7.0", @@ -103,6 +105,7 @@ "@types/http-errors": "^1.8.1", "@types/jsonwebtoken": "^8.5.8", "@types/lodash.get": "^4.4.6", + "@types/memory-cache": "^0.2.2", "@types/morgan": "^1.9.3", "@types/multer": "1.4.7", "@types/node": "^16.10.2", diff --git a/packages/backend/src/apps/slack/auth/generate-auth-url.ts b/packages/backend/src/apps/slack/auth/generate-auth-url.ts index 76282180..72e6ec03 100644 --- a/packages/backend/src/apps/slack/auth/generate-auth-url.ts +++ b/packages/backend/src/apps/slack/auth/generate-auth-url.ts @@ -29,6 +29,7 @@ const userScopes = [ 'groups:history', 'groups:read', 'groups:write', + 'im:read', 'im:write', 'mpim:write', 'reactions:read', diff --git a/packages/backend/src/apps/slack/dynamic-data/list-channels/index.ts b/packages/backend/src/apps/slack/dynamic-data/list-channels/index.ts index 15b8c367..7553406c 100644 --- a/packages/backend/src/apps/slack/dynamic-data/list-channels/index.ts +++ b/packages/backend/src/apps/slack/dynamic-data/list-channels/index.ts @@ -1,5 +1,24 @@ import { IGlobalVariable, IJSONObject } from '@automatisch/types'; +type TChannel = { + id: string; + name: string; +} + +type TConversationListResponseData = { + channels: TChannel[], + response_metadata?: { + next_cursor: string + }; + needed?: string; + error?: string; + ok: boolean; +} + +type TResponse = { + data: TConversationListResponseData; +} + export default { name: 'List channels', key: 'listChannels', @@ -13,24 +32,33 @@ export default { error: null, }; - const response = await $.http.get('/conversations.list', { - params: { - types: 'public_channel,private_channel', - limit: 1000, - exclude_archived: true, + let nextCursor; + do { + const response: TResponse = await $.http.get('/conversations.list', { + params: { + types: 'public_channel,private_channel,im', + cursor: nextCursor, + limit: 1000, + } + }); + + nextCursor = response.data.response_metadata?.next_cursor; + + if (response.data.error === 'missing_scope') { + throw new Error(`Missing "${response.data.needed}" scope while authorizing. Please, reconnect your connection!`); } - }); - if (response.data.ok === false) { - throw new Error(response.data); - } + if (response.data.ok === false) { + throw new Error(JSON.stringify(response.data, null, 2)); + } - channels.data = response.data.channels.map((channel: IJSONObject) => { - return { - value: channel.id, - name: channel.name, - }; - }); + for (const channel of response.data.channels) { + channels.data.push({ + value: channel.id as string, + name: channel.name as string, + }); + } + } while (nextCursor); return channels; }, diff --git a/packages/backend/src/config/app.ts b/packages/backend/src/config/app.ts index 4f30aab6..9571af84 100644 --- a/packages/backend/src/config/app.ts +++ b/packages/backend/src/config/app.ts @@ -32,6 +32,13 @@ type AppConfig = { bullMQDashboardPassword: string; telemetryEnabled: boolean; requestBodySizeLimit: string; + smtpHost: string; + smtpPort: number; + smtpSecure: boolean; + smtpUser: string; + smtpPassword: string; + fromEmail: string; + licenseKey: string; }; const host = process.env.HOST || 'localhost'; @@ -40,7 +47,7 @@ const port = process.env.PORT || '3000'; const serveWebAppSeparately = process.env.SERVE_WEB_APP_SEPARATELY === 'true' ? true : false; -let apiUrl = (new URL(`${protocol}://${host}:${port}`)).toString(); +let apiUrl = new URL(`${protocol}://${host}:${port}`).toString(); apiUrl = apiUrl.substring(0, apiUrl.length - 1); // use apiUrl by default, which has less priority over the following cases @@ -48,14 +55,14 @@ let webAppUrl = apiUrl; if (process.env.WEB_APP_URL) { // use env. var. if provided - webAppUrl = (new URL(process.env.WEB_APP_URL)).toString(); + webAppUrl = new URL(process.env.WEB_APP_URL).toString(); webAppUrl = webAppUrl.substring(0, webAppUrl.length - 1); } else if (serveWebAppSeparately) { // no env. var. and serving separately, sign of development - webAppUrl = 'http://localhost:3001' + webAppUrl = 'http://localhost:3001'; } -let webhookUrl = (new URL(process.env.WEBHOOK_URL || apiUrl)).toString(); +let webhookUrl = new URL(process.env.WEBHOOK_URL || apiUrl).toString(); webhookUrl = webhookUrl.substring(0, webhookUrl.length - 1); const appEnv = process.env.APP_ENV || 'development'; @@ -91,6 +98,13 @@ const appConfig: AppConfig = { webhookUrl, telemetryEnabled: process.env.TELEMETRY_ENABLED === 'false' ? false : true, requestBodySizeLimit: '1mb', + smtpHost: process.env.SMTP_HOST, + smtpPort: parseInt(process.env.SMTP_PORT || '587'), + smtpSecure: process.env.SMTP_SECURE === 'true', + smtpUser: process.env.SMTP_USER, + smtpPassword: process.env.SMTP_PASSWORD, + fromEmail: process.env.FROM_EMAIL, + licenseKey: process.env.LICENSE_KEY, }; if (!appConfig.encryptionKey) { diff --git a/packages/backend/src/db/migrations/20230218110748_add_role_to_users.ts b/packages/backend/src/db/migrations/20230218110748_add_role_to_users.ts new file mode 100644 index 00000000..66f75456 --- /dev/null +++ b/packages/backend/src/db/migrations/20230218110748_add_role_to_users.ts @@ -0,0 +1,15 @@ +import { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + return knex.schema.table('users', async (table) => { + table.string('role'); + + await knex('users').update({ role: 'admin' }); + }); +} + +export async function down(knex: Knex): Promise { + return knex.schema.table('users', (table) => { + table.dropColumn('role'); + }); +} diff --git a/packages/backend/src/db/migrations/20230218131824_alter_role_to_not_nullable_for_users.ts b/packages/backend/src/db/migrations/20230218131824_alter_role_to_not_nullable_for_users.ts new file mode 100644 index 00000000..505d18a4 --- /dev/null +++ b/packages/backend/src/db/migrations/20230218131824_alter_role_to_not_nullable_for_users.ts @@ -0,0 +1,13 @@ +import { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + return knex.schema.alterTable('users', (table) => { + table.string('role').notNullable().alter(); + }); +} + +export async function down(knex: Knex): Promise { + return knex.schema.alterTable('users', (table) => { + table.string('role').nullable().alter(); + }); +} diff --git a/packages/backend/src/db/migrations/20230218150517_add_reset_password_token_to_users.ts b/packages/backend/src/db/migrations/20230218150517_add_reset_password_token_to_users.ts new file mode 100644 index 00000000..7d6cc518 --- /dev/null +++ b/packages/backend/src/db/migrations/20230218150517_add_reset_password_token_to_users.ts @@ -0,0 +1,13 @@ +import { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + return knex.schema.table('users', (table) => { + table.string('reset_password_token'); + }); +} + +export async function down(knex: Knex): Promise { + return knex.schema.table('users', (table) => { + table.dropColumn('reset_password_token'); + }); +} diff --git a/packages/backend/src/db/migrations/20230218150758_add_reset_password_token_sent_at_to_users.ts b/packages/backend/src/db/migrations/20230218150758_add_reset_password_token_sent_at_to_users.ts new file mode 100644 index 00000000..0371d30f --- /dev/null +++ b/packages/backend/src/db/migrations/20230218150758_add_reset_password_token_sent_at_to_users.ts @@ -0,0 +1,13 @@ +import { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + return knex.schema.table('users', (table) => { + table.timestamp('reset_password_token_sent_at'); + }); +} + +export async function down(knex: Knex): Promise { + return knex.schema.table('users', (table) => { + table.dropColumn('reset_password_token_sent_at'); + }); +} diff --git a/packages/backend/src/graphql/mutation-resolvers.ts b/packages/backend/src/graphql/mutation-resolvers.ts index 97cec0a4..5d8b8615 100644 --- a/packages/backend/src/graphql/mutation-resolvers.ts +++ b/packages/backend/src/graphql/mutation-resolvers.ts @@ -12,7 +12,10 @@ import deleteFlow from './mutations/delete-flow'; import createStep from './mutations/create-step'; import updateStep from './mutations/update-step'; import deleteStep from './mutations/delete-step'; +import createUser from './mutations/create-user.ee'; import updateUser from './mutations/update-user'; +import forgotPassword from './mutations/forgot-password.ee'; +import resetPassword from './mutations/reset-password.ee'; import login from './mutations/login'; const mutationResolvers = { @@ -30,7 +33,10 @@ const mutationResolvers = { createStep, updateStep, deleteStep, + createUser, updateUser, + forgotPassword, + resetPassword, login, }; diff --git a/packages/backend/src/graphql/mutations/create-user.ee.ts b/packages/backend/src/graphql/mutations/create-user.ee.ts new file mode 100644 index 00000000..804daf00 --- /dev/null +++ b/packages/backend/src/graphql/mutations/create-user.ee.ts @@ -0,0 +1,28 @@ +import User from '../../models/user'; + +type Params = { + input: { + email: string; + password: string; + }; +}; + +const createUser = async (_parent: unknown, params: Params) => { + const { email, password } = params.input; + + const existingUser = await User.query().findOne({ email }); + + if (existingUser) { + throw new Error('User already exists!'); + } + + const user = await User.query().insert({ + email, + password, + role: 'user', + }); + + return user; +}; + +export default createUser; diff --git a/packages/backend/src/graphql/mutations/forgot-password.ee.ts b/packages/backend/src/graphql/mutations/forgot-password.ee.ts new file mode 100644 index 00000000..0d0ef97f --- /dev/null +++ b/packages/backend/src/graphql/mutations/forgot-password.ee.ts @@ -0,0 +1,46 @@ +import User from '../../models/user'; +import emailQueue from '../../queues/email'; +import { + REMOVE_AFTER_30_DAYS_OR_150_JOBS, + REMOVE_AFTER_7_DAYS_OR_50_JOBS, +} from '../../helpers/remove-job-configuration'; + +type Params = { + input: { + email: string; + }; +}; + +const forgotPassword = async (_parent: unknown, params: Params) => { + const { email } = params.input; + + const user = await User.query().findOne({ email }); + + if (!user) { + throw new Error('Email address not found!'); + } + + await user.generateResetPasswordToken(); + + const jobName = `Reset Password Email - ${user.id}`; + + const jobPayload = { + email: user.email, + subject: 'Reset Password', + template: 'reset-password-instructions', + params: { + token: user.resetPasswordToken, + }, + }; + + const jobOptions = { + removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS, + removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS, + }; + + await emailQueue.add(jobName, jobPayload, jobOptions); + + return; +}; + +export default forgotPassword; diff --git a/packages/backend/src/graphql/mutations/reset-password.ee.ts b/packages/backend/src/graphql/mutations/reset-password.ee.ts new file mode 100644 index 00000000..c8a350bc --- /dev/null +++ b/packages/backend/src/graphql/mutations/reset-password.ee.ts @@ -0,0 +1,30 @@ +import User from '../../models/user'; + +type Params = { + input: { + token: string; + password: string; + }; +}; + +const resetPassword = async (_parent: unknown, params: Params) => { + const { token, password } = params.input; + + if (!token) { + throw new Error('Reset password token is required!'); + } + + const user = await User.query().findOne({ reset_password_token: token }); + + if (!user || !user.isResetPasswordTokenValid()) { + throw new Error( + 'Reset password link is not valid or expired. Try generating a new link.' + ); + } + + await user.resetPassword(password); + + return; +}; + +export default resetPassword; diff --git a/packages/backend/src/graphql/queries/get-license.ee.ts b/packages/backend/src/graphql/queries/get-license.ee.ts new file mode 100644 index 00000000..94832808 --- /dev/null +++ b/packages/backend/src/graphql/queries/get-license.ee.ts @@ -0,0 +1,11 @@ +import checkLicense from '../../helpers/check-license.ee'; + +const getLicense = async () => { + const license = await checkLicense(); + + return { + type: license ? 'ee' : 'ce', + }; +}; + +export default getLicense; diff --git a/packages/backend/src/graphql/query-resolvers.ts b/packages/backend/src/graphql/query-resolvers.ts index 9e4f6489..35b6280a 100644 --- a/packages/backend/src/graphql/query-resolvers.ts +++ b/packages/backend/src/graphql/query-resolvers.ts @@ -10,6 +10,7 @@ import getExecutions from './queries/get-executions'; import getExecutionSteps from './queries/get-execution-steps'; import getDynamicData from './queries/get-dynamic-data'; import getCurrentUser from './queries/get-current-user'; +import getLicense from './queries/get-license.ee'; import healthcheck from './queries/healthcheck'; const queryResolvers = { @@ -25,6 +26,7 @@ const queryResolvers = { getExecutionSteps, getDynamicData, getCurrentUser, + getLicense, healthcheck, }; diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index a33d6f06..e60add0a 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -29,6 +29,7 @@ type Query { parameters: JSONObject ): JSONObject getCurrentUser: User + getLicense: GetLicense healthcheck: AppHealth } @@ -47,7 +48,10 @@ type Mutation { createStep(input: CreateStepInput): Step updateStep(input: UpdateStepInput): Step deleteStep(input: DeleteStepInput): Step + createUser(input: CreateUserInput): User updateUser(input: UpdateUserInput): User + forgotPassword(input: ForgotPasswordInput): Boolean + resetPassword(input: ResetPasswordInput): Boolean login(input: LoginInput): Auth } @@ -299,11 +303,25 @@ input DeleteStepInput { id: String! } +input CreateUserInput { + email: String! + password: String! +} + input UpdateUserInput { email: String password: String } +input ForgotPasswordInput { + email: String! +} + +input ResetPasswordInput { + token: String! + password: String! +} + input LoginInput { email: String! password: String! @@ -453,6 +471,10 @@ type AppHealth { version: String } +type GetLicense { + type: String +} + schema { query: Query mutation: Mutation diff --git a/packages/backend/src/helpers/authentication.ts b/packages/backend/src/helpers/authentication.ts index 037f6baa..c1368e0a 100644 --- a/packages/backend/src/helpers/authentication.ts +++ b/packages/backend/src/helpers/authentication.ts @@ -29,6 +29,8 @@ const authentication = shield( Mutation: { '*': isAuthenticated, login: allow, + createUser: allow, + forgotPassword: allow, }, }, { diff --git a/packages/backend/src/helpers/check-license.ee.ts b/packages/backend/src/helpers/check-license.ee.ts new file mode 100644 index 00000000..1ea04e59 --- /dev/null +++ b/packages/backend/src/helpers/check-license.ee.ts @@ -0,0 +1,31 @@ +import axios from 'axios'; +import appConfig from '../config/app'; +import memoryCache from 'memory-cache'; + +const CACHE_DURATION = 1000 * 60 * 60 * 24; // 24 hours in milliseconds + +const checkLicense = async () => { + const licenseKey = appConfig.licenseKey; + + if (!licenseKey) { + return false; + } + + const url = 'https://license.automatisch.io/api/v1/licenses/verify'; + const cachedResponse = memoryCache.get(url); + + if (cachedResponse) { + return cachedResponse; + } else { + try { + const { data } = await axios.post(url, { licenseKey }); + memoryCache.put(url, data.verified, CACHE_DURATION); + + return data.verified; + } catch (error) { + return false; + } + } +}; + +export default checkLicense; diff --git a/packages/backend/src/helpers/compile-email.ee.ts b/packages/backend/src/helpers/compile-email.ee.ts new file mode 100644 index 00000000..297b48e8 --- /dev/null +++ b/packages/backend/src/helpers/compile-email.ee.ts @@ -0,0 +1,12 @@ +import * as path from 'path'; +import * as fs from 'fs'; +import * as handlebars from 'handlebars'; + +const compileEmail = (emailPath: string, replacements: object = {}): string => { + const filePath = path.join(__dirname, `../views/emails/${emailPath}.ee.hbs`); + const source = fs.readFileSync(filePath, 'utf-8').toString(); + const template = handlebars.compile(source); + return template(replacements); +}; + +export default compileEmail; diff --git a/packages/backend/src/helpers/delay-as-milliseconds.ts b/packages/backend/src/helpers/delay-as-milliseconds.ts index 3b791a95..8f60dd03 100644 --- a/packages/backend/src/helpers/delay-as-milliseconds.ts +++ b/packages/backend/src/helpers/delay-as-milliseconds.ts @@ -4,18 +4,18 @@ import delayForAsMilliseconds, { } from './delay-for-as-milliseconds'; import delayUntilAsMilliseconds from './delay-until-as-milliseconds'; -const delayAsMilliseconds = (step: Step) => { +const delayAsMilliseconds = (eventKey: Step["key"], computedParameters: Step["parameters"]) => { let delayDuration = 0; - if (step.key === 'delayFor') { - const { delayForUnit, delayForValue } = step.parameters; + if (eventKey === 'delayFor') { + const { delayForUnit, delayForValue } = computedParameters; delayDuration = delayForAsMilliseconds( delayForUnit as TDelayForUnit, Number(delayForValue) ); - } else if (step.key === 'delayUntil') { - const { delayUntil } = step.parameters; + } else if (eventKey === 'delayUntil') { + const { delayUntil } = computedParameters; delayDuration = delayUntilAsMilliseconds(delayUntil as string); } diff --git a/packages/backend/src/helpers/mailer.ee.ts b/packages/backend/src/helpers/mailer.ee.ts new file mode 100644 index 00000000..ff6fff51 --- /dev/null +++ b/packages/backend/src/helpers/mailer.ee.ts @@ -0,0 +1,14 @@ +import nodemailer from 'nodemailer'; +import appConfig from '../config/app'; + +const mailer = nodemailer.createTransport({ + host: appConfig.smtpHost, + port: appConfig.smtpPort, + secure: appConfig.smtpSecure, + auth: { + user: appConfig.smtpUser, + pass: appConfig.smtpPassword, + }, +}); + +export default mailer; diff --git a/packages/backend/src/models/user.ts b/packages/backend/src/models/user.ts index 99908d41..7dc2935b 100644 --- a/packages/backend/src/models/user.ts +++ b/packages/backend/src/models/user.ts @@ -5,11 +5,15 @@ import Flow from './flow'; import Step from './step'; import Execution from './execution'; import bcrypt from 'bcrypt'; +import crypto from 'crypto'; class User extends Base { id!: string; email!: string; password!: string; + role: string; + resetPasswordToken: string; + resetPasswordTokenSentAt: string; connections?: Connection[]; flows?: Flow[]; steps?: Step[]; @@ -25,6 +29,7 @@ class User extends Base { id: { type: 'string', format: 'uuid' }, email: { type: 'string', format: 'email', minLength: 1, maxLength: 255 }, password: { type: 'string', minLength: 1, maxLength: 255 }, + role: { type: 'string', enum: ['admin', 'user'] }, }, }; @@ -75,6 +80,33 @@ class User extends Base { return bcrypt.compare(password, this.password); } + async generateResetPasswordToken() { + const resetPasswordToken = crypto.randomBytes(64).toString('hex'); + const resetPasswordTokenSentAt = new Date().toISOString(); + + await this.$query().patch({ resetPasswordToken, resetPasswordTokenSentAt }); + } + + async resetPassword(password: string) { + return await this.$query().patch({ + resetPasswordToken: null, + resetPasswordTokenSentAt: null, + password, + }); + } + + async isResetPasswordTokenValid() { + if (!this.resetPasswordTokenSentAt) { + return false; + } + + const sentAt = new Date(this.resetPasswordTokenSentAt); + const now = new Date(); + const fourHoursInMilliseconds = 1000 * 60 * 60 * 4; + + return now.getTime() - sentAt.getTime() < fourHoursInMilliseconds; + } + async generateHash() { this.password = await bcrypt.hash(this.password, 10); } diff --git a/packages/backend/src/queues/email.ts b/packages/backend/src/queues/email.ts new file mode 100644 index 00000000..e8ae8b45 --- /dev/null +++ b/packages/backend/src/queues/email.ts @@ -0,0 +1,25 @@ +import process from 'process'; +import { Queue } from 'bullmq'; +import redisConfig from '../config/redis'; +import logger from '../helpers/logger'; + +const CONNECTION_REFUSED = 'ECONNREFUSED'; + +const redisConnection = { + connection: redisConfig, +}; + +const emailQueue = new Queue('email', redisConnection); + +process.on('SIGTERM', async () => { + await emailQueue.close(); +}); + +emailQueue.on('error', (err) => { + if ((err as any).code === CONNECTION_REFUSED) { + logger.error('Make sure you have installed Redis and it is running.', err); + process.exit(); + } +}); + +export default emailQueue; diff --git a/packages/backend/src/services/action.ts b/packages/backend/src/services/action.ts index 962b2ece..39fd3987 100644 --- a/packages/backend/src/services/action.ts +++ b/packages/backend/src/services/action.ts @@ -65,5 +65,5 @@ export const processAction = async (options: ProcessActionOptions) => { errorDetails: $.actionOutput.error ? $.actionOutput.error : null, }); - return { flowId, stepId, executionId, executionStep }; + return { flowId, stepId, executionId, executionStep, computedParameters }; }; diff --git a/packages/backend/src/views/emails/reset-password-instructions.ee.hbs b/packages/backend/src/views/emails/reset-password-instructions.ee.hbs new file mode 100644 index 00000000..1fb678f4 --- /dev/null +++ b/packages/backend/src/views/emails/reset-password-instructions.ee.hbs @@ -0,0 +1,16 @@ + + + + Title + + + Hello {{ email }} + + Someone has requested a link to change your password, and you can do this through the link below. + + Change my password + + If you didn't request this, please ignore this email. + Your password won't change until you access the link above and create a new one. + + diff --git a/packages/backend/src/worker.ts b/packages/backend/src/worker.ts index 6a6358ee..8a5cb447 100644 --- a/packages/backend/src/worker.ts +++ b/packages/backend/src/worker.ts @@ -3,6 +3,7 @@ import './helpers/check-worker-readiness'; import './workers/flow'; import './workers/trigger'; import './workers/action'; +import './workers/email'; import telemetry from './helpers/telemetry'; telemetry.setServiceType('worker'); diff --git a/packages/backend/src/workers/action.ts b/packages/backend/src/workers/action.ts index cf16d42a..40c2e6c8 100644 --- a/packages/backend/src/workers/action.ts +++ b/packages/backend/src/workers/action.ts @@ -21,7 +21,7 @@ const DEFAULT_DELAY_DURATION = 0; export const worker = new Worker( 'action', async (job) => { - const { stepId, flowId, executionId } = await processAction( + const { stepId, flowId, executionId, computedParameters } = await processAction( job.data as JobData ); @@ -45,7 +45,7 @@ export const worker = new Worker( }; if (step.appKey === 'delay') { - jobOptions.delay = delayAsMilliseconds(step); + jobOptions.delay = delayAsMilliseconds(step.key, computedParameters); } await actionQueue.add(jobName, jobPayload, jobOptions); diff --git a/packages/backend/src/workers/email.ts b/packages/backend/src/workers/email.ts new file mode 100644 index 00000000..d8ff037c --- /dev/null +++ b/packages/backend/src/workers/email.ts @@ -0,0 +1,37 @@ +import { Worker } from 'bullmq'; +import redisConfig from '../config/redis'; +import logger from '../helpers/logger'; +import mailer from '../helpers/mailer.ee'; +import compileEmail from '../helpers/compile-email.ee'; +import appConfig from '../config/app'; + +export const worker = new Worker( + 'email', + async (job) => { + const { email, subject, templateName, params } = job.data; + + await mailer.sendMail({ + to: email, + from: appConfig.fromEmail, + subject: subject, + html: compileEmail(templateName, params), + }); + }, + { connection: redisConfig } +); + +worker.on('completed', (job) => { + logger.info( + `JOB ID: ${job.id} - ${job.data.subject} email sent to ${job.data.email}!` + ); +}); + +worker.on('failed', (job, err) => { + logger.info( + `JOB ID: ${job.id} - ${job.data.subject} email to ${job.data.email} has failed to send with ${err.message}` + ); +}); + +process.on('SIGTERM', async () => { + await worker.close(); +}); diff --git a/packages/cli/package.json b/packages/cli/package.json index daceb43f..3523f729 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@automatisch/cli", "version": "0.5.0", - "license": "AGPL-3.0", + "license": "See LICENSE file", "description": "The open source Zapier alternative. Build workflow automation without spending time and money.", "contributors": [ { diff --git a/packages/docs/package.json b/packages/docs/package.json index a43f6d87..8735f922 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -1,7 +1,7 @@ { "name": "@automatisch/docs", "version": "0.5.0", - "license": "AGPL-3.0", + "license": "See LICENSE file", "description": "The open source Zapier alternative. Build workflow automation without spending time and money.", "private": true, "scripts": { diff --git a/packages/docs/pages/build-integrations/auth.md b/packages/docs/pages/build-integrations/auth.md index 26dfebe5..226d0c6d 100644 --- a/packages/docs/pages/build-integrations/auth.md +++ b/packages/docs/pages/build-integrations/auth.md @@ -93,6 +93,10 @@ We have defined two fields for the auth. You have to add a screen name field in case there is no API endpoint where you can get the username or any other information about the user that you can use as a screen name. Some of the APIs have an endpoint for this purpose like `/me` or `/users/me`, but in our example, the cat API doesn't have such an endpoint. ::: +:::danger +If the third-party service you use provides both an API key and OAuth for the authentication, we expect you to use OAuth instead of an API key. Please consider that when you create a pull request for a new integration. Otherwise, we might ask you to have changes to use OAuth. To see apps with OAuth implementation, you can check [examples](/build-integrations/examples#_3-legged-oauth). +::: + ## Verify credentials So until now, we integrated auth folder with the app definition and defined the auth fields. Now we need to verify the credentials that the user entered. We will do that by defining the `verifyCredentials` method. diff --git a/packages/docs/pages/contributing/development-setup.md b/packages/docs/pages/contributing/development-setup.md index 13d2382c..90715975 100644 --- a/packages/docs/pages/contributing/development-setup.md +++ b/packages/docs/pages/contributing/development-setup.md @@ -68,7 +68,13 @@ cp .env-example .env ``` Start the frontend server in another terminal tab. -Open [http://localhost:3001](http://localhost:3001) with your browser to see the result. Then, use the `user@automatisch.io` email address and `sample` password to login. + +```bash +cd packages/web +yarn dev +``` + +It will automatically open [http://localhost:3001](http://localhost:3001) in your browser. Then, use the `user@automatisch.io` email address and `sample` password to login. ## Docs server diff --git a/packages/docs/pages/guide/request-integration.md b/packages/docs/pages/guide/request-integration.md index 748f78f0..263cb832 100644 --- a/packages/docs/pages/guide/request-integration.md +++ b/packages/docs/pages/guide/request-integration.md @@ -1,6 +1,6 @@ # Request Integration -You can request a new integration by using [Github discussions](https://github.com/automatisch/automatisch/discussions/categories/integration-request). +You can request a new integration by using [Github issues](https://github.com/automatisch/automatisch/issues). :::info @@ -10,6 +10,6 @@ While we are working hard to add as many integrations as possible, it might take :::tip -If there is already an integration request for the service you'd like, it's still crucial to upvote that discussion for us to analyze the potential audience around the integration. +If there is already an integration request for the service you'd like, it's still crucial to upvote or comment on that issue for us to analyze the potential audience around the integration. ::: diff --git a/packages/docs/pages/other/license.md b/packages/docs/pages/other/license.md index 2e206f52..acb385ef 100644 --- a/packages/docs/pages/other/license.md +++ b/packages/docs/pages/other/license.md @@ -1,3 +1,11 @@ # License -Automatisch is an open-source software with the [AGPL 3.0 license](https://github.com/automatisch/automatisch/blob/main/LICENSE.md). +Automatisch Community Edition (Automatisch CE) is an open-source software with the [AGPL-3.0 license](https://github.com/automatisch/automatisch/blob/main/LICENSE.agpl). + +Automatisch Enterprise Edition (Automatisch EE) is a commercial offering with the [Enterprise license](https://github.com/automatisch/automatisch/blob/main/LICENSE.enterprise). + +The Automatisch repository contains both AGPL-licensed and Enterprise-licensed files. We maintain a single repository to make development easier. + +All files that contain ".ee." in their name fall under the [Enterprise license](https://github.com/automatisch/automatisch/blob/main/LICENSE.enterprise). All other files fall under the [AGPL-3.0 license](https://github.com/automatisch/automatisch/blob/main/LICENSE.agpl). + +See the [LICENSE](https://github.com/automatisch/automatisch/blob/main/LICENSE) file for more information. diff --git a/packages/e2e-tests/package.json b/packages/e2e-tests/package.json index fbaefee9..87c72dd3 100644 --- a/packages/e2e-tests/package.json +++ b/packages/e2e-tests/package.json @@ -1,7 +1,7 @@ { "name": "@automatisch/e2e-tests", "version": "0.5.0", - "license": "AGPL-3.0", + "license": "See LICENSE file", "private": true, "description": "The open source Zapier alternative. Build workflow automation without spending time and money.", "scripts": { diff --git a/packages/types/package.json b/packages/types/package.json index 7be7f2a4..b2bce08e 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,7 +1,7 @@ { "name": "@automatisch/types", "version": "0.5.0", - "license": "AGPL-3.0", + "license": "See LICENSE file", "description": "Type definitions for automatisch", "homepage": "https://github.com/automatisch/automatisch", "types": "./index.d.ts", diff --git a/packages/web/package.json b/packages/web/package.json index c8b4546f..ed9fed5e 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,7 +1,7 @@ { "name": "@automatisch/web", "version": "0.5.0", - "license": "AGPL-3.0", + "license": "See LICENSE file", "description": "The open source Zapier alternative. Build workflow automation without spending time and money.", "dependencies": { "@apollo/client": "^3.6.9", diff --git a/yarn.lock b/yarn.lock index 10a2c171..f2276b23 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3956,6 +3956,11 @@ resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-2.3.1.tgz#e34763178b46232e4c5f079f1706e18692415519" integrity sha512-nAPUltOT28fal2eDZz8yyzNhBjHw1NEymFBP7Q9iCShqpflWPybxHbD7pw/46jQmT+HXOy1QN5hNTms8MOTlOQ== +"@types/memory-cache@^0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@types/memory-cache/-/memory-cache-0.2.2.tgz#f8fb6d8aa0eb006ed44fc659bf8bfdc1a5cc57fa" + integrity sha512-xNnm6EkmYYhTnLiOHC2bdKgcYY5qjjrq5vl9KXD2nh0em0koZoFS500EL4Q4V/eW+A3P7NC7P7GIYzNOSQp7jQ== + "@types/mime@^1": version "1.3.2" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" @@ -11874,6 +11879,11 @@ memfs@^3.1.2, memfs@^3.2.2: dependencies: fs-monkey "1.0.3" +memory-cache@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/memory-cache/-/memory-cache-0.2.0.tgz#7890b01d52c00c8ebc9d533e1f8eb17e3034871a" + integrity sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA== + meow@^8.0.0: version "8.1.2" resolved "https://registry.yarnpkg.com/meow/-/meow-8.1.2.tgz#bcbe45bda0ee1729d350c03cffc8395a36c4e897"