Merge branch 'main' into feature/signalwire-integration

This commit is contained in:
Ömer Faruk Aydın
2023-02-26 13:23:27 +01:00
committed by GitHub
45 changed files with 541 additions and 47 deletions

View 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
View 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
View 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.

View File

@@ -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.

View File

@@ -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"]

View File

@@ -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

View File

@@ -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",

View File

@@ -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",

View File

@@ -29,6 +29,7 @@ const userScopes = [
'groups:history',
'groups:read',
'groups:write',
'im:read',
'im:write',
'mpim:write',
'reactions:read',

View File

@@ -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;
},

View File

@@ -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) {

View File

@@ -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');
});
}

View File

@@ -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();
});
}

View File

@@ -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');
});
}

View File

@@ -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');
});
}

View File

@@ -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,
};

View 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;

View 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;

View 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;

View 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;

View File

@@ -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,
};

View File

@@ -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

View File

@@ -29,6 +29,8 @@ const authentication = shield(
Mutation: {
'*': isAuthenticated,
login: allow,
createUser: allow,
forgotPassword: allow,
},
},
{

View 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;

View 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;

View File

@@ -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);
}

View 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;

View File

@@ -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);
}

View 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;

View File

@@ -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 };
};

View File

@@ -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>

View File

@@ -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');

View File

@@ -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);

View 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();
});

View File

@@ -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": [
{

View File

@@ -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": {

View File

@@ -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.

View File

@@ -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

View File

@@ -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.
:::

View File

@@ -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.

View File

@@ -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": {

View File

@@ -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",

View File

@@ -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",

View File

@@ -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"