Merge branch 'main' into feature/signalwire-integration
This commit is contained in:
5
CONTRIBUTOR_LICENSE_AGREEMENT.md
Normal file
5
CONTRIBUTOR_LICENSE_AGREEMENT.md
Normal file
@@ -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._**
|
3
LICENSE
Normal file
3
LICENSE
Normal file
@@ -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.
|
35
LICENSE.enterprise
Normal file
35
LICENSE.enterprise
Normal file
@@ -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.
|
14
README.md
14
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.
|
||||
|
@@ -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"]
|
||||
|
@@ -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
|
||||
|
@@ -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",
|
||||
|
@@ -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",
|
||||
|
@@ -29,6 +29,7 @@ const userScopes = [
|
||||
'groups:history',
|
||||
'groups:read',
|
||||
'groups:write',
|
||||
'im:read',
|
||||
'im:write',
|
||||
'mpim:write',
|
||||
'reactions:read',
|
||||
|
@@ -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', {
|
||||
let nextCursor;
|
||||
do {
|
||||
const response: TResponse = await $.http.get('/conversations.list', {
|
||||
params: {
|
||||
types: 'public_channel,private_channel',
|
||||
types: 'public_channel,private_channel,im',
|
||||
cursor: nextCursor,
|
||||
limit: 1000,
|
||||
exclude_archived: true,
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
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;
|
||||
},
|
||||
|
@@ -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) {
|
||||
|
@@ -0,0 +1,15 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
return knex.schema.table('users', async (table) => {
|
||||
table.string('role');
|
||||
|
||||
await knex('users').update({ role: 'admin' });
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
return knex.schema.table('users', (table) => {
|
||||
table.dropColumn('role');
|
||||
});
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
return knex.schema.alterTable('users', (table) => {
|
||||
table.string('role').notNullable().alter();
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
return knex.schema.alterTable('users', (table) => {
|
||||
table.string('role').nullable().alter();
|
||||
});
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
return knex.schema.table('users', (table) => {
|
||||
table.string('reset_password_token');
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
return knex.schema.table('users', (table) => {
|
||||
table.dropColumn('reset_password_token');
|
||||
});
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
return knex.schema.table('users', (table) => {
|
||||
table.timestamp('reset_password_token_sent_at');
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
return knex.schema.table('users', (table) => {
|
||||
table.dropColumn('reset_password_token_sent_at');
|
||||
});
|
||||
}
|
@@ -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,
|
||||
};
|
||||
|
||||
|
28
packages/backend/src/graphql/mutations/create-user.ee.ts
Normal file
28
packages/backend/src/graphql/mutations/create-user.ee.ts
Normal file
@@ -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;
|
46
packages/backend/src/graphql/mutations/forgot-password.ee.ts
Normal file
46
packages/backend/src/graphql/mutations/forgot-password.ee.ts
Normal file
@@ -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;
|
30
packages/backend/src/graphql/mutations/reset-password.ee.ts
Normal file
30
packages/backend/src/graphql/mutations/reset-password.ee.ts
Normal file
@@ -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;
|
11
packages/backend/src/graphql/queries/get-license.ee.ts
Normal file
11
packages/backend/src/graphql/queries/get-license.ee.ts
Normal file
@@ -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;
|
@@ -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,
|
||||
};
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -29,6 +29,8 @@ const authentication = shield(
|
||||
Mutation: {
|
||||
'*': isAuthenticated,
|
||||
login: allow,
|
||||
createUser: allow,
|
||||
forgotPassword: allow,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
31
packages/backend/src/helpers/check-license.ee.ts
Normal file
31
packages/backend/src/helpers/check-license.ee.ts
Normal file
@@ -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;
|
12
packages/backend/src/helpers/compile-email.ee.ts
Normal file
12
packages/backend/src/helpers/compile-email.ee.ts
Normal file
@@ -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;
|
@@ -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);
|
||||
}
|
||||
|
||||
|
14
packages/backend/src/helpers/mailer.ee.ts
Normal file
14
packages/backend/src/helpers/mailer.ee.ts
Normal file
@@ -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;
|
@@ -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);
|
||||
}
|
||||
|
25
packages/backend/src/queues/email.ts
Normal file
25
packages/backend/src/queues/email.ts
Normal file
@@ -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;
|
@@ -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 };
|
||||
};
|
||||
|
@@ -0,0 +1,16 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<body>
|
||||
Hello {{ email }}
|
||||
|
||||
Someone has requested a link to change your password, and you can do this through the link below.
|
||||
|
||||
<a href="/reset-password">Change my password</a>
|
||||
|
||||
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.
|
||||
</body>
|
||||
</html>
|
@@ -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');
|
||||
|
@@ -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);
|
||||
|
37
packages/backend/src/workers/email.ts
Normal file
37
packages/backend/src/workers/email.ts
Normal file
@@ -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();
|
||||
});
|
@@ -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": [
|
||||
{
|
||||
|
@@ -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": {
|
||||
|
@@ -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.
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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.
|
||||
|
||||
:::
|
||||
|
@@ -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.
|
||||
|
@@ -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": {
|
||||
|
@@ -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",
|
||||
|
@@ -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",
|
||||
|
10
yarn.lock
10
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"
|
||||
|
Reference in New Issue
Block a user