Compare commits

..

1 Commits

Author SHA1 Message Date
Rıdvan Akca
465dc8ba38 feat(notion): add update database item action 2024-01-15 18:20:13 +03:00
52 changed files with 1363 additions and 1366 deletions

View File

@@ -8,7 +8,7 @@
"version": "latest"
},
"ghcr.io/devcontainers/features/node:1": {
"version": 20
"version": 16
},
"ghcr.io/devcontainers/features/common-utils:1": {
"username": "vscode",

View File

@@ -6,7 +6,8 @@
"start": "lerna run --stream --parallel --scope=@*/{web,backend} dev",
"start:web": "lerna run --stream --scope=@*/web dev",
"start:backend": "lerna run --stream --scope=@*/backend dev",
"lint": "lerna run --no-bail --stream --parallel --scope=@*/{web,backend} lint",
"lint": "lerna run --no-bail --stream --parallel --scope=@*/{web,backend,cli} lint",
"build:watch": "lerna run --no-bail --stream --parallel --scope=@*/{web,backend,cli} build:watch",
"build:docs": "cd ./packages/docs && yarn install && yarn build"
},
"workspaces": {

View File

@@ -1 +0,0 @@
<svg height="255" preserveAspectRatio="xMidYMid" viewBox="0 0 256 255" width="256" xmlns="http://www.w3.org/2000/svg"><path d="m128.636514 155.746615v-155.23361889h-3.522242v.06873152l-124.60824865 64.03287157v60.8642488h.00597665v3.234366h-.00597665v60.868233l124.60824865 64.747082h3.842989v-98.581914z" fill="#ff8e4a"/><path d="m129.941416 254.328529 125.568498-64.747082v-124.9668478l-125.887253-64.10160309h-2.243237v253.81055289h2.243237" fill="#f48746"/><path d="m109.097837 87.2551595h36.19561v59.2077195h-36.19561z" fill="#ff8e4a"/><path d="m66.1735097 188.397074h14.8639378c9.4102412 0 12.6087471-2.238257 15.6189883-9.988981l8.2796572-21.353587h45.159596l8.280653 21.353587c3.011238 7.750724 6.396016 9.988981 15.805261 9.988981h14.677665v-19.114335h-3.011237c-3.19751 0-4.704622-.689307-5.831222-3.790194l-39.516638-99.3658524h-25.779299l-39.703907 99.3658524c-1.1285915 3.100887-2.632716 3.790194-5.833214 3.790194h-3.0102413zm44.4075333-49.939922 11.478163-30.655253c2.445448-6.714771 5.269417-18.2556889 5.269417-18.2556889h.375533s2.822972 11.5409179 5.269416 18.2556889l11.478163 30.655253z" fill="#fff"/><path d="m231.204856 150.082739v-51.8086223c.235082 4.5233303 2.970397 16.8432063 24.305058 27.8512063v11.653479zm0-53.1623343v1.353712c-.029883-.5926848-.01793-1.0479066 0-1.353712zm.041837-.4392841s-.022911.1534008-.041837.4392841v-.4392841z" fill="#d4763c"/><path d="m231.155051 94.3016342c-.013946.9931207.05877 1.8945993.049805 2.0460078-.01793.2480312-2.220327 16.094132 24.305058 29.777681v-60.863253c-23.325883 12.0349884-24.449494 25.7414475-24.354863 29.0395642" fill="#ff8e4a"/></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,44 +0,0 @@
import verifyCredentials from './verify-credentials.js';
import isStillVerified from './is-still-verified.js';
export default {
fields: [
{
key: 'screenName',
label: 'Screen Name',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description:
'Screen name of your connection to be used on Automatisch UI.',
clickToCopy: false,
},
{
key: 'instanceUrl',
label: 'Instance URL',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: 'Your subdomain as https://{yoursubdomain}.airbrake.io',
clickToCopy: false,
},
{
key: 'authToken',
label: 'Auth Token',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: 'Airbrake Auth Token of your account.',
clickToCopy: false,
},
],
verifyCredentials,
isStillVerified,
};

View File

@@ -1,8 +0,0 @@
import verifyCredentials from './verify-credentials.js';
const isStillVerified = async ($) => {
await verifyCredentials($);
return true;
};
export default isStillVerified;

View File

@@ -1,14 +0,0 @@
const verifyCredentials = async ($) => {
await $.http.get(`/api/v4/projects?key=${$.auth.data.authToken}`, {
additionalProperties: {
skipAddingAuthToken: true,
},
});
await $.auth.set({
screenName: $.auth.data.screenName,
authToken: $.auth.data.authToken,
});
};
export default verifyCredentials;

View File

@@ -1,10 +0,0 @@
const addAuthToken = ($, requestConfig) => {
if (requestConfig.additionalProperties?.skipAddingAuthToken)
return requestConfig;
requestConfig.url = requestConfig.url + `?key=${$.auth.data.authToken}`;
return requestConfig;
};
export default addAuthToken;

View File

@@ -1,11 +0,0 @@
const setBaseUrl = ($, requestConfig) => {
const subdomain = $.auth.data.instanceUrl;
if (subdomain) {
requestConfig.baseURL = `https://${subdomain}.airbrake.io`;
}
return requestConfig;
};
export default setBaseUrl;

View File

@@ -1,17 +0,0 @@
import defineApp from '../../helpers/define-app.js';
import setBaseUrl from './common/set-base-url.js';
import auth from './auth/index.js';
import addAuthToken from './common/add-auth-token.js';
export default defineApp({
name: 'Airbrake',
key: 'airbrake',
iconUrl: '{BASE_URL}/apps/airbrake/assets/favicon.svg',
authDocUrl: 'https://automatisch.io/docs/apps/airbrake/connection',
supportsConnections: true,
baseUrl: 'https://www.airbrake.io',
apiBaseUrl: '',
primaryColor: 'f58c54',
beforeRequest: [setBaseUrl, addAuthToken],
auth,
});

View File

@@ -1,6 +1,5 @@
import defineAction from '../../../../helpers/define-action.js';
import base64ToString from './transformers/base64-to-string.js';
import capitalize from './transformers/capitalize.js';
import extractEmailAddress from './transformers/extract-email-address.js';
import extractNumber from './transformers/extract-number.js';
@@ -9,12 +8,10 @@ import lowercase from './transformers/lowercase.js';
import markdownToHtml from './transformers/markdown-to-html.js';
import pluralize from './transformers/pluralize.js';
import replace from './transformers/replace.js';
import stringToBase64 from './transformers/string-to-base64.js';
import trimWhitespace from './transformers/trim-whitespace.js';
import useDefaultValue from './transformers/use-default-value.js';
const transformers = {
base64ToString,
capitalize,
extractEmailAddress,
extractNumber,
@@ -23,7 +20,6 @@ const transformers = {
markdownToHtml,
pluralize,
replace,
stringToBase64,
trimWhitespace,
useDefaultValue,
};
@@ -41,7 +37,6 @@ export default defineAction({
required: true,
variables: true,
options: [
{ label: 'Base64 to String', value: 'base64ToString' },
{ label: 'Capitalize', value: 'capitalize' },
{ label: 'Convert HTML to Markdown', value: 'htmlToMarkdown' },
{ label: 'Convert Markdown to HTML', value: 'markdownToHtml' },
@@ -50,7 +45,6 @@ export default defineAction({
{ label: 'Lowercase', value: 'lowercase' },
{ label: 'Pluralize', value: 'pluralize' },
{ label: 'Replace', value: 'replace' },
{ label: 'String to Base64', value: 'stringToBase64' },
{ label: 'Trim Whitespace', value: 'trimWhitespace' },
{ label: 'Use Default Value', value: 'useDefaultValue' },
],

View File

@@ -1,8 +0,0 @@
const base64ToString = ($) => {
const input = $.step.parameters.input;
const decodedString = Buffer.from(input, 'base64').toString('utf8');
return decodedString;
};
export default base64ToString;

View File

@@ -1,8 +0,0 @@
const stringtoBase64 = ($) => {
const input = $.step.parameters.input;
const base64String = Buffer.from(input).toString('base64');
return base64String;
};
export default stringtoBase64;

View File

@@ -1,4 +1,3 @@
import base64ToString from './text/base64-to-string.js';
import capitalize from './text/capitalize.js';
import extractEmailAddress from './text/extract-email-address.js';
import extractNumber from './text/extract-number.js';
@@ -7,7 +6,6 @@ import lowercase from './text/lowercase.js';
import markdownToHtml from './text/markdown-to-html.js';
import pluralize from './text/pluralize.js';
import replace from './text/replace.js';
import stringToBase64 from './text/string-to-base64.js';
import trimWhitespace from './text/trim-whitespace.js';
import useDefaultValue from './text/use-default-value.js';
import performMathOperation from './numbers/perform-math-operation.js';
@@ -17,7 +15,6 @@ import formatPhoneNumber from './numbers/format-phone-number.js';
import formatDateTime from './date-time/format-date-time.js';
const options = {
base64ToString,
capitalize,
extractEmailAddress,
extractNumber,
@@ -26,7 +23,6 @@ const options = {
markdownToHtml,
pluralize,
replace,
stringToBase64,
trimWhitespace,
useDefaultValue,
performMathOperation,

View File

@@ -1,12 +0,0 @@
const base64ToString = [
{
label: 'Input',
key: 'input',
type: 'string',
required: true,
description: 'Text that will be converted from Base64 to string.',
variables: true,
},
];
export default base64ToString;

View File

@@ -1,12 +0,0 @@
const stringToBase64 = [
{
label: 'Input',
key: 'input',
type: 'string',
required: true,
description: 'Text that will be converted to Base64.',
variables: true,
},
];
export default stringToBase64;

View File

@@ -1,5 +1,11 @@
import createDatabaseItem from './create-database-item/index.js';
import createPage from './create-page/index.js';
import findDatabaseItem from './find-database-item/index.js';
import updateDatabaseItem from './update-database-item/index.js';
export default [createDatabaseItem, createPage, findDatabaseItem];
export default [
createDatabaseItem,
createPage,
findDatabaseItem,
updateDatabaseItem,
];

View File

@@ -0,0 +1,157 @@
import defineAction from '../../../../helpers/define-action.js';
export default defineAction({
name: 'Update database item',
key: 'updateDatabaseItem',
description: 'Updates a database item.',
arguments: [
{
label: 'Database',
key: 'databaseId',
type: 'dropdown',
required: true,
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listDatabases',
},
],
},
},
{
label: 'Item',
key: 'itemId',
type: 'dropdown',
required: true,
variables: true,
dependsOn: ['parameters.databaseId'],
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listDatabaseItems',
},
{
name: 'parameters.databaseId',
value: '{parameters.databaseId}',
},
],
},
},
{
label: 'Name',
key: 'name',
type: 'string',
required: false,
description:
'This field has a 2000 character limit. Any characters beyond 2000 will not be included.',
variables: true,
},
{
label: 'Tags',
key: 'tags',
type: 'dynamic',
required: false,
description: '',
fields: [
{
label: 'Tag',
key: 'tag',
type: 'dropdown',
required: true,
variables: true,
dependsOn: ['parameters.databaseId'],
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listTags',
},
{
name: 'parameters.databaseId',
value: '{parameters.databaseId}',
},
],
},
},
],
},
{
label: 'Content',
key: 'content',
type: 'string',
required: false,
description:
'You can choose to add extra text to the database item, with a limit of up to 2000 characters if desired.',
variables: true,
},
],
async run($) {
const itemId = $.step.parameters.itemId;
const name = $.step.parameters.name;
const truncatedName = name.slice(0, 2000);
const content = $.step.parameters.content;
const truncatedContent = content.slice(0, 2000);
const tags = $.step.parameters.tags;
const formattedTags = tags
.filter((tag) => tag.tag !== '')
.map((tag) => tag.tag);
const body = {
properties: {},
};
if (truncatedName) {
body.properties.Name = {
title: [
{
text: {
content: truncatedName,
},
},
],
};
}
if (formattedTags?.length) {
body.properties.Tags = {
multi_select: formattedTags.map((tag) => ({ name: tag })),
};
}
if (truncatedContent) {
const response = await $.http.get(`/v1/blocks/${itemId}/children`);
const firstBlockId = response.data.results[0].id;
const body = {
paragraph: {
rich_text: [
{
type: 'text',
text: {
content: truncatedContent,
},
},
],
},
};
await $.http.patch(`/v1/blocks/${firstBlockId}`, body);
}
const { data } = await $.http.patch(`/v1/pages/${itemId}`, body);
$.setActionItem({
raw: data,
});
},
});

View File

@@ -1,4 +1,6 @@
import listDatabaseItems from './list-database-items/index.js';
import listDatabases from './list-databases/index.js';
import listParentPages from './list-parent-pages/index.js';
import listTags from './list-tags/index.js';
export default [listDatabases, listParentPages];
export default [listDatabaseItems, listDatabases, listParentPages, listTags];

View File

@@ -0,0 +1,38 @@
export default {
name: 'List database items',
key: 'listDatabaseItems',
async run($) {
const databases = {
data: [],
error: null,
};
const payload = {
start_cursor: undefined,
};
const databaseId = $.step.parameters.databaseId;
if (!databaseId) {
return databases;
}
do {
const response = await $.http.post(
`/v1/databases/${databaseId}/query`,
payload
);
payload.start_cursor = response.data.next_cursor;
for (const database of response.data.results) {
databases.data.push({
value: database.id,
name:
database.properties.Name?.title?.[0]?.plain_text || 'Untitled Page',
});
}
} while (payload.start_cursor);
return databases;
},
};

View File

@@ -22,7 +22,7 @@ export default {
for (const database of response.data.results) {
databases.data.push({
value: database.id,
name: database.title[0].plain_text,
name: database.title?.[0]?.plain_text || 'Untitled Database',
});
}
} while (payload.start_cursor);

View File

@@ -0,0 +1,38 @@
export default {
name: 'List tags',
key: 'listTags',
async run($) {
const tags = {
data: [],
error: null,
};
const databaseId = $.step.parameters.databaseId;
let allTags;
if (!databaseId) {
return tags;
}
const response = await $.http.get(`/v1/databases/${databaseId}`);
const tagsExist =
response.data.properties.Tags.multi_select.options.length !== 0;
if (tagsExist) {
allTags = response.data.properties.Tags.multi_select.options.map(
(tag) => tag.name
);
} else {
return tags;
}
for (const tag of allTags) {
tags.data.push({
value: tag,
name: tag,
});
}
return tags;
},
};

View File

@@ -18,9 +18,7 @@ const port = process.env.PORT || '3000';
const serveWebAppSeparately =
process.env.SERVE_WEB_APP_SEPARATELY === 'true' ? true : false;
let apiUrl = new URL(
process.env.API_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
@@ -90,10 +88,6 @@ const appConfig = {
licenseKey: process.env.LICENSE_KEY,
sentryDsn: process.env.SENTRY_DSN,
CI: process.env.CI === 'true',
disableNotificationsPage: process.env.DISABLE_NOTIFICATIONS_PAGE === 'true',
disableFavicon: process.env.DISABLE_FAVICON === 'true',
additionalDrawerLink: process.env.ADDITIONAL_DRAWER_LINK,
additionalDrawerLinkText: process.env.ADDITIONAL_DRAWER_LINK_TEXT,
};
if (!appConfig.encryptionKey) {

View File

@@ -1,10 +1,7 @@
import appConfig from '../../config/app.js';
import User from '../../models/user.js';
import Role from '../../models/role.js';
const registerUser = async (_parent, params) => {
if (!appConfig.isCloud) return;
const { fullName, email, password } = params.input;
const existingUser = await User.query().findOne({

View File

@@ -1,17 +1,9 @@
import appConfig from '../../config/app.js';
import { hasValidLicense } from '../../helpers/license.ee.js';
import Config from '../../models/config.js';
const getConfig = async (_parent, params) => {
if (!(await hasValidLicense())) return {};
const defaultConfig = {
disableNotificationsPage: appConfig.disableNotificationsPage,
disableFavicon: appConfig.disableFavicon,
additionalDrawerLink: appConfig.additionalDrawerLink,
additionalDrawerLinkText: appConfig.additionalDrawerLinkText,
};
const configQuery = Config.query();
if (Array.isArray(params.keys)) {
@@ -26,7 +18,7 @@ const getConfig = async (_parent, params) => {
computedConfig[key] = value?.data;
return computedConfig;
}, defaultConfig);
}, {});
};
export default getConfig;

View File

@@ -2,7 +2,6 @@ import { vi, describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../app';
import { createConfig } from '../../../test/factories/config';
import appConfig from '../../config/app';
import * as license from '../../helpers/license.ee';
describe('graphQL getConfig query', () => {
@@ -57,10 +56,6 @@ describe('graphQL getConfig query', () => {
[configOne.key]: configOne.value.data,
[configTwo.key]: configTwo.value.data,
[configThree.key]: configThree.value.data,
disableNotificationsPage: false,
disableFavicon: false,
additionalDrawerLink: undefined,
additionalDrawerLinkText: undefined,
},
},
};
@@ -87,48 +82,6 @@ describe('graphQL getConfig query', () => {
getConfig: {
[configOne.key]: configOne.value.data,
[configTwo.key]: configTwo.value.data,
disableNotificationsPage: false,
disableFavicon: false,
additionalDrawerLink: undefined,
additionalDrawerLinkText: undefined,
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
});
describe('and with different defaults', () => {
beforeEach(async () => {
vi.spyOn(appConfig, 'disableNotificationsPage', 'get').mockReturnValue(
true
);
vi.spyOn(appConfig, 'disableFavicon', 'get').mockReturnValue(true);
vi.spyOn(appConfig, 'additionalDrawerLink', 'get').mockReturnValue(
'https://automatisch.io'
);
vi.spyOn(appConfig, 'additionalDrawerLinkText', 'get').mockReturnValue(
'Automatisch'
);
});
it('should return custom config', async () => {
const response = await request(app)
.post('/graphql')
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: {
getConfig: {
[configOne.key]: configOne.value.data,
[configTwo.key]: configTwo.value.data,
[configThree.key]: configThree.value.data,
disableNotificationsPage: true,
disableFavicon: true,
additionalDrawerLink: 'https://automatisch.io',
additionalDrawerLinkText: 'Automatisch',
},
},
};

View File

@@ -6,6 +6,31 @@ import { createRole } from '../../../test/factories/role';
import { createUser } from '../../../test/factories/user';
describe('graphQL getCurrentUser query', () => {
describe('with unauthenticated user', () => {
it('should throw not authorized error', async () => {
const invalidUserToken = 'invalid-token';
const query = `
query {
getCurrentUser {
id
email
}
}
`;
const response = await request(app)
.post('/graphql')
.set('Authorization', invalidUserToken)
.send({ query })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not Authorised!');
});
});
describe('with authenticated user', () => {
let role, currentUser, token, requestObject;
beforeEach(async () => {
@@ -77,3 +102,4 @@ describe('graphQL getCurrentUser query', () => {
);
});
});
});

View File

@@ -40,7 +40,23 @@ describe('graphQL getExecutions query', () => {
}
`;
describe('and without correct permissions', () => {
const invalidToken = 'invalid-token';
describe('with unauthenticated user', () => {
it('should throw not authorized error', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', invalidToken)
.send({ query })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not Authorised!');
});
});
describe('with authenticated user', () => {
describe('and without permissions', () => {
it('should throw not authorized error', async () => {
const userWithoutPermissions = await createUser();
const token = createAuthTokenByUserId(userWithoutPermissions.id);
@@ -470,3 +486,4 @@ describe('graphQL getExecutions query', () => {
});
});
});
});

View File

@@ -40,6 +40,23 @@ describe('graphQL getFlow query', () => {
`;
};
describe('with unauthenticated user', () => {
it('should throw not authorized error', async () => {
const invalidToken = 'invalid-token';
const flow = await createFlow();
const response = await request(app)
.post('/graphql')
.set('Authorization', invalidToken)
.send({ query: query(flow.id) })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not Authorised!');
});
});
describe('with authenticated user', () => {
describe('and without permissions', () => {
it('should throw not authorized error', async () => {
const userWithoutPermissions = await createUser();
@@ -128,7 +145,9 @@ describe('graphQL getFlow query', () => {
{
appKey: actionStep.appKey,
connection: {
createdAt: actionConnection.createdAt.getTime().toString(),
createdAt: actionConnection.createdAt
.getTime()
.toString(),
id: actionConnection.id,
verified: actionConnection.verified,
},
@@ -215,7 +234,9 @@ describe('graphQL getFlow query', () => {
{
appKey: actionStep.appKey,
connection: {
createdAt: actionConnection.createdAt.getTime().toString(),
createdAt: actionConnection.createdAt
.getTime()
.toString(),
id: actionConnection.id,
verified: actionConnection.verified,
},
@@ -238,3 +259,4 @@ describe('graphQL getFlow query', () => {
});
});
});
});

View File

@@ -17,6 +17,7 @@ describe('graphQL getRole query', () => {
userWithoutPermissions,
tokenWithPermissions,
tokenWithoutPermissions,
invalidToken,
permissionOne,
permissionTwo;
@@ -73,8 +74,24 @@ describe('graphQL getRole query', () => {
tokenWithoutPermissions = createAuthTokenByUserId(
userWithoutPermissions.id
);
invalidToken = 'invalid-token';
});
describe('with unauthenticated user', () => {
it('should throw not authorized error', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', invalidToken)
.send({ query: queryWithValidRole })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not Authorised!');
});
});
describe('with authenticated user', () => {
describe('and with valid license', () => {
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
@@ -162,3 +179,4 @@ describe('graphQL getRole query', () => {
});
});
});
});

View File

@@ -15,7 +15,8 @@ describe('graphQL getRoles query', () => {
userWithPermissions,
userWithoutPermissions,
tokenWithPermissions,
tokenWithoutPermissions;
tokenWithoutPermissions,
invalidToken;
beforeEach(async () => {
currentUserRole = await createRole({ name: 'Current user role' });
@@ -52,8 +53,24 @@ describe('graphQL getRoles query', () => {
tokenWithoutPermissions = createAuthTokenByUserId(
userWithoutPermissions.id
);
invalidToken = 'invalid-token';
});
describe('with unauthenticated user', () => {
it('should throw not authorized error', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', invalidToken)
.send({ query })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not Authorised!');
});
});
describe('with authenticated user', () => {
describe('and with valid license', () => {
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
@@ -132,3 +149,4 @@ describe('graphQL getRoles query', () => {
});
});
});
});

View File

@@ -16,6 +16,22 @@ describe('graphQL getTrialStatus query', () => {
}
`;
const invalidToken = 'invalid-token';
describe('with unauthenticated user', () => {
it('should throw not authorized error', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', invalidToken)
.send({ query })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not Authorised!');
});
});
describe('with authenticated user', () => {
let user, userToken;
beforeEach(async () => {
@@ -98,3 +114,4 @@ describe('graphQL getTrialStatus query', () => {
});
});
});
});

View File

@@ -8,6 +8,31 @@ import { createPermission } from '../../../test/factories/permission';
import { createUser } from '../../../test/factories/user';
describe('graphQL getUser query', () => {
describe('with unauthenticated user', () => {
it('should throw not authorized error', async () => {
const invalidUserId = '123123123';
const query = `
query {
getUser(id: "${invalidUserId}") {
id
email
}
}
`;
const response = await request(app)
.post('/graphql')
.set('Authorization', 'invalid-token')
.send({ query })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not Authorised!');
});
});
describe('with authenticated user', () => {
describe('and without permissions', () => {
it('should throw not authorized error', async () => {
const userWithoutPermissions = await createUser();
@@ -59,7 +84,9 @@ describe('graphQL getUser query', () => {
});
token = createAuthTokenByUserId(currentUser.id);
requestObject = request(app).post('/graphql').set('Authorization', token);
requestObject = request(app)
.post('/graphql')
.set('Authorization', token);
});
it('should return user data for a valid user id', async () => {
@@ -144,3 +171,4 @@ describe('graphQL getUser query', () => {
});
});
});
});

View File

@@ -30,6 +30,20 @@ describe('graphQL getUsers query', () => {
}
`;
describe('with unauthenticated user', () => {
it('should throw not authorized error', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', 'invalid-token')
.send({ query })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not Authorised!');
});
});
describe('with authenticated user', () => {
describe('and without permissions', () => {
it('should throw not authorized error', async () => {
const userWithoutPermissions = await createUser();
@@ -72,7 +86,9 @@ describe('graphQL getUsers query', () => {
});
token = createAuthTokenByUserId(currentUser.id);
requestObject = request(app).post('/graphql').set('Authorization', token);
requestObject = request(app)
.post('/graphql')
.set('Authorization', token);
});
it('should return users data', async () => {
@@ -146,3 +162,4 @@ describe('graphQL getUsers query', () => {
});
});
});
});

View File

@@ -3,7 +3,7 @@ import jwt from 'jsonwebtoken';
import appConfig from '../config/app.js';
import User from '../models/user.js';
export const isAuthenticated = async (_parent, _args, req) => {
const isAuthenticated = rule()(async (_parent, _args, req) => {
const token = req.headers['authorization'];
if (token == null) return false;
@@ -26,13 +26,12 @@ export const isAuthenticated = async (_parent, _args, req) => {
} catch (error) {
return false;
}
};
});
const isAuthenticatedRule = rule()(isAuthenticated);
export const authenticationRules = {
const authentication = shield(
{
Query: {
'*': isAuthenticatedRule,
'*': isAuthenticated,
getAutomatischInfo: allow,
getConfig: allow,
getNotifications: allow,
@@ -40,18 +39,16 @@ export const authenticationRules = {
listSamlAuthProviders: allow,
},
Mutation: {
'*': isAuthenticatedRule,
'*': isAuthenticated,
forgotPassword: allow,
login: allow,
registerUser: allow,
resetPassword: allow,
},
};
const authenticationOptions = {
},
{
allowExternalErrors: true,
};
const authentication = shield(authenticationRules, authenticationOptions);
}
);
export default authentication;

View File

@@ -1,78 +0,0 @@
import { describe, it, expect, vi } from 'vitest';
import { allow } from 'graphql-shield';
import jwt from 'jsonwebtoken';
import User from '../models/user.js';
import { isAuthenticated, authenticationRules } from './authentication.js';
vi.mock('jsonwebtoken');
vi.mock('../models/user.js');
describe('isAuthenticated', () => {
it('should return false if no token is provided', async () => {
const req = { headers: {} };
expect(await isAuthenticated(null, null, req)).toBe(false);
});
it('should return false if token is invalid', async () => {
jwt.verify.mockImplementation(() => {
throw new Error('invalid token');
});
const req = { headers: { authorization: 'invalidToken' } };
expect(await isAuthenticated(null, null, req)).toBe(false);
});
it('should return true if token is valid', async () => {
jwt.verify.mockReturnValue({ userId: '123' });
User.query.mockReturnValue({
findById: vi.fn().mockReturnValue({
leftJoinRelated: vi.fn().mockReturnThis(),
withGraphFetched: vi
.fn()
.mockResolvedValue({ id: '123', role: {}, permissions: {} }),
}),
});
const req = { headers: { authorization: 'validToken' } };
expect(await isAuthenticated(null, null, req)).toBe(true);
});
});
describe('authentication rules', () => {
const getQueryAndMutationNames = (rules) => {
const queries = Object.keys(rules.Query || {});
const mutations = Object.keys(rules.Mutation || {});
return { queries, mutations };
};
const { queries, mutations } = getQueryAndMutationNames(authenticationRules);
describe('for queries', () => {
queries.forEach((query) => {
it(`should apply correct rule for query: ${query}`, () => {
const ruleApplied = authenticationRules.Query[query];
if (query === '*') {
expect(ruleApplied.func).toBe(isAuthenticated);
} else {
expect(ruleApplied).toEqual(allow);
}
});
});
});
describe('for mutations', () => {
mutations.forEach((mutation) => {
it(`should apply correct rule for mutation: ${mutation}`, () => {
const ruleApplied = authenticationRules.Mutation[mutation];
if (mutation === '*') {
expect(ruleApplied.func).toBe(isAuthenticated);
} else {
expect(ruleApplied).toBe(allow);
}
});
});
});
});

View File

@@ -1,9 +1,6 @@
import path from 'path';
import fs from 'fs';
import handlebars from 'handlebars';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
import * as path from 'path';
import * as fs from 'fs';
import * as handlebars from 'handlebars';
const compileEmail = (emailPath, replacements = {}) => {
const filePath = path.join(__dirname, `../views/emails/${emailPath}.ee.hbs`);

View File

@@ -15,7 +15,7 @@ const webUIHandler = async (app) => {
app.use(express.static(webBuildPath));
app.get('*', (_req, res) => {
res.set('Content-Security-Policy', 'frame-ancestors \'none\';');
res.set('Content-Security-Policy', 'frame-ancestors: none;');
res.set('X-Frame-Options', 'DENY');
res.sendFile(indexHtml);

View File

@@ -32,12 +32,6 @@ export default defineConfig({
],
sidebar: {
'/apps/': [
{
text: 'Airbrake',
collapsible: true,
collapsed: true,
items: [{ text: 'Connection', link: '/apps/airbrake/connection' }],
},
{
text: 'Carbone',
collapsible: true,
@@ -311,7 +305,7 @@ export default defineConfig({
collapsed: true,
items: [
{ text: 'Actions', link: '/apps/removebg/actions' },
{ text: 'Connection', link: '/apps/removebg/connection' },
{ text: 'Connection', link: '/apps/removebg/connection' }
],
},
{

View File

@@ -15,7 +15,7 @@ Please be careful with the `ENCRYPTION_KEY` and `WEBHOOK_SECRET_KEY` environment
:::
| Variable Name | Type | Default Value | Description |
| ---------------------------- | ------- | ------------------ | ----------------------------------------------------------------------------------- |
| --------------------------- | ------- | ------------------ | ---------------------------------------------------------------------------------------------------- |
| `HOST` | string | `localhost` | HTTP Host |
| `PROTOCOL` | string | `http` | HTTP Protocol |
| `PORT` | string | `3000` | HTTP Port |
@@ -42,5 +42,3 @@ Please be careful with the `ENCRYPTION_KEY` and `WEBHOOK_SECRET_KEY` environment
| `ENABLE_BULLMQ_DASHBOARD` | boolean | `false` | Enable BullMQ Dashboard |
| `BULLMQ_DASHBOARD_USERNAME` | string | | Username to login BullMQ Dashboard |
| `BULLMQ_DASHBOARD_PASSWORD` | string | | Password to login BullMQ Dashboard |
| `DISABLE_NOTIFICATIONS_PAGE` | boolean | `false` | Enable/Disable notifications page |
| `DISABLE_FAVICON` | boolean | `false` | Enable/Disable favicon |

View File

@@ -1,13 +0,0 @@
# Airbrake
:::info
This page explains the steps you need to follow to set up the Airbrake
connection in Automatisch. If any of the steps are outdated, please let us know!
:::
1. Login to your Airbrake account: [https://www.airbrake.io/](https://www.airbrake.io/).
2. Go to your profile & notifications page.
3. Copy `Auth Token` from the page to the `Auth Token` field on Automatisch.
4. Fill the instance URL field with your subdomain. (https://{yoursubdomain}.airbrake.io)
5. Write any screen name to be displayed in Automatisch.
6. Now, you can start using the Airbrake connection with Automatisch.

View File

@@ -7,6 +7,8 @@ items:
desc: Creates a page inside a parent page.
- name: Find database item
desc: Searches for an item in a database by property.
- name: Update database item
desc: Updates a database item.
---
<script setup>

View File

@@ -1 +0,0 @@
<svg height="255" preserveAspectRatio="xMidYMid" viewBox="0 0 256 255" width="256" xmlns="http://www.w3.org/2000/svg"><path d="m128.636514 155.746615v-155.23361889h-3.522242v.06873152l-124.60824865 64.03287157v60.8642488h.00597665v3.234366h-.00597665v60.868233l124.60824865 64.747082h3.842989v-98.581914z" fill="#ff8e4a"/><path d="m129.941416 254.328529 125.568498-64.747082v-124.9668478l-125.887253-64.10160309h-2.243237v253.81055289h2.243237" fill="#f48746"/><path d="m109.097837 87.2551595h36.19561v59.2077195h-36.19561z" fill="#ff8e4a"/><path d="m66.1735097 188.397074h14.8639378c9.4102412 0 12.6087471-2.238257 15.6189883-9.988981l8.2796572-21.353587h45.159596l8.280653 21.353587c3.011238 7.750724 6.396016 9.988981 15.805261 9.988981h14.677665v-19.114335h-3.011237c-3.19751 0-4.704622-.689307-5.831222-3.790194l-39.516638-99.3658524h-25.779299l-39.703907 99.3658524c-1.1285915 3.100887-2.632716 3.790194-5.833214 3.790194h-3.0102413zm44.4075333-49.939922 11.478163-30.655253c2.445448-6.714771 5.269417-18.2556889 5.269417-18.2556889h.375533s2.822972 11.5409179 5.269416 18.2556889l11.478163 30.655253z" fill="#fff"/><path d="m231.204856 150.082739v-51.8086223c.235082 4.5233303 2.970397 16.8432063 24.305058 27.8512063v11.653479zm0-53.1623343v1.353712c-.029883-.5926848-.01793-1.0479066 0-1.353712zm.041837-.4392841s-.022911.1534008-.041837.4392841v-.4392841z" fill="#d4763c"/><path d="m231.155051 94.3016342c-.013946.9931207.05877 1.8945993.049805 2.0460078-.01793.2480312-2.220327 16.094132 24.305058 29.777681v-60.863253c-23.325883 12.0349884-24.449494 25.7414475-24.354863 29.0395642" fill="#ff8e4a"/></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -2,6 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#0059F7" />
<meta

View File

@@ -2,6 +2,13 @@
"short_name": "automatisch",
"name": "automatisch",
"description": "Build workflow automation without spending time and money. No code is required.",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",

View File

@@ -15,7 +15,6 @@ import { SvgIconComponent } from '@mui/icons-material';
import AppBar from 'components/AppBar';
import Drawer from 'components/Drawer';
import * as URLS from 'config/urls';
import useFormatMessage from 'hooks/useFormatMessage';
import useCurrentUserAbility from 'hooks/useCurrentUserAbility';
type SettingsLayoutProps = {
@@ -87,11 +86,19 @@ function createDrawerLinks({
return items;
}
const drawerBottomLinks = [
{
Icon: ArrowBackIosNewIcon,
primary: 'adminSettingsDrawer.goBack',
to: '/',
dataTest: 'go-back-drawer-link',
},
];
export default function SettingsLayout({
children,
}: SettingsLayoutProps): React.ReactElement {
const theme = useTheme();
const formatMessage = useFormatMessage();
const currentUserAbility = useCurrentUserAbility();
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'));
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
@@ -109,15 +116,6 @@ export default function SettingsLayout({
canUpdateApp: currentUserAbility.can('update', 'App'),
});
const drawerBottomLinks = [
{
Icon: ArrowBackIosNewIcon,
primary: formatMessage('adminSettingsDrawer.goBack'),
to: '/',
dataTest: 'go-back-drawer-link',
},
];
return (
<>
<AppBar

View File

@@ -19,7 +19,6 @@ type DrawerLink = {
Icon: React.ElementType;
primary: string;
to: string;
target?: '_blank';
badgeContent?: React.ReactNode;
dataTest?: string;
};
@@ -70,7 +69,7 @@ export default function Drawer(props: DrawerProps): React.ReactElement {
<List sx={{ py: 0, mt: 3 }}>
{bottomLinks.map(
({ Icon, badgeContent, primary, to, dataTest, target }, index) => (
({ Icon, badgeContent, primary, to, dataTest }, index) => (
<ListItemLink
key={`${to}-${index}`}
icon={
@@ -78,10 +77,9 @@ export default function Drawer(props: DrawerProps): React.ReactElement {
<Icon htmlColor={theme.palette.primary.main} />
</Badge>
}
primary={primary}
primary={formatMessage(primary)}
to={to}
onClick={closeOnClick}
target={target}
data-test={dataTest}
/>
)

View File

@@ -7,14 +7,12 @@ import AppsIcon from '@mui/icons-material/Apps';
import SwapCallsIcon from '@mui/icons-material/SwapCalls';
import HistoryIcon from '@mui/icons-material/History';
import NotificationsIcon from '@mui/icons-material/Notifications';
import ArrowBackIosNew from '@mui/icons-material/ArrowBackIosNew';
import * as URLS from 'config/urls';
import useFormatMessage from 'hooks/useFormatMessage';
import useVersion from 'hooks/useVersion';
import AppBar from 'components/AppBar';
import Drawer from 'components/Drawer';
import useConfig from 'hooks/useConfig';
import useAutomatischInfo from 'hooks/useAutomatischInfo';
type PublicLayoutProps = {
children: React.ReactNode;
@@ -42,95 +40,48 @@ const drawerLinks = [
];
type GenerateDrawerBottomLinksOptions = {
disableNotificationsPage: boolean;
isMation: boolean;
loading: boolean;
notificationBadgeContent: number;
additionalDrawerLink?: string;
additionalDrawerLinkText?: string;
additionalDrawerLinkIcon?: string;
formatMessage: ReturnType<typeof useFormatMessage>;
};
const generateDrawerBottomLinks = async ({
disableNotificationsPage,
const generateDrawerBottomLinks = ({
isMation,
loading,
notificationBadgeContent = 0,
additionalDrawerLink,
additionalDrawerLinkText,
formatMessage,
}: GenerateDrawerBottomLinksOptions) => {
const notificationsPageLinkObject = {
if (loading || isMation) {
return [];
}
return [
{
Icon: NotificationsIcon,
primary: formatMessage('settingsDrawer.notifications'),
primary: 'settingsDrawer.notifications',
to: URLS.UPDATES,
badgeContent: notificationBadgeContent,
};
const hasAdditionalDrawerLink =
additionalDrawerLink && additionalDrawerLinkText;
const additionalDrawerLinkObject = {
Icon: ArrowBackIosNew,
primary: additionalDrawerLinkText || '',
to: additionalDrawerLink || '',
target: '_blank' as const,
};
const links = [];
if (!disableNotificationsPage) {
links.push(notificationsPageLinkObject);
}
if (hasAdditionalDrawerLink) {
links.push(additionalDrawerLinkObject);
}
return links;
};
type Link = {
Icon: React.ElementType;
primary: string;
target?: '_blank';
to: string;
badgeContent?: React.ReactNode;
},
];
};
export default function PublicLayout({
children,
}: PublicLayoutProps): React.ReactElement {
const version = useVersion();
const { config, loading } = useConfig([
'disableNotificationsPage',
'additionalDrawerLink',
'additionalDrawerLinkText',
]);
const { isMation, loading } = useAutomatischInfo();
const theme = useTheme();
const formatMessage = useFormatMessage();
const [bottomLinks, setBottomLinks] = React.useState<Link[]>([]);
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'));
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
const openDrawer = () => setDrawerOpen(true);
const closeDrawer = () => setDrawerOpen(false);
React.useEffect(() => {
async function perform() {
const newBottomLinks = await generateDrawerBottomLinks({
const drawerBottomLinks = generateDrawerBottomLinks({
notificationBadgeContent: version.newVersionCount,
disableNotificationsPage: config?.disableNotificationsPage as boolean,
additionalDrawerLink: config?.additionalDrawerLink as string,
additionalDrawerLinkText: config?.additionalDrawerLinkText as string,
formatMessage,
loading,
isMation,
});
setBottomLinks(newBottomLinks);
}
if (loading) return;
perform();
}, [config, loading, version.newVersionCount]);
return (
<>
<AppBar
@@ -142,7 +93,7 @@ export default function PublicLayout({
<Box sx={{ display: 'flex' }}>
<Drawer
links={drawerLinks}
bottomLinks={bottomLinks}
bottomLinks={drawerBottomLinks}
open={isDrawerOpen}
onOpen={openDrawer}
onClose={closeDrawer}

View File

@@ -9,7 +9,6 @@ type ListItemLinkProps = {
icon: React.ReactNode;
primary: string;
to: string;
target?: '_blank';
onClick?: (event: React.SyntheticEvent) => void;
'data-test'?: string;
};
@@ -17,29 +16,14 @@ type ListItemLinkProps = {
export default function ListItemLink(
props: ListItemLinkProps
): React.ReactElement {
const { icon, primary, to, onClick, 'data-test': dataTest, target } = props;
const { icon, primary, to, onClick, 'data-test': dataTest } = props;
const selected = useMatch({ path: to, end: true });
const CustomLink = React.useMemo(
() =>
React.forwardRef<HTMLAnchorElement, Omit<LinkProps, 'to'>>(
function InLineLink(linkProps, ref) {
try {
// challenge the link to check if it's absolute URL
new URL(to); // should throw an error if it's not an absolute URL
return (
<a
{...linkProps}
ref={ref}
href={to}
target={target}
rel="noopener noreferrer"
/>
);
} catch {
return <Link ref={ref} {...linkProps} to={to} />;
}
return <Link ref={ref} to={to} {...linkProps} />;
}
),
[to]
@@ -53,7 +37,6 @@ export default function ListItemLink(
selected={!!selected}
onClick={onClick}
data-test={dataTest}
target={target}
>
<ListItemIcon sx={{ minWidth: 52 }}>{icon}</ListItemIcon>
<ListItemText

View File

@@ -15,27 +15,6 @@ const MetadataProvider = ({
document.title = (config?.title as string) || 'Automatisch';
}, [config?.title]);
React.useEffect(() => {
const existingFaviconElement = document.querySelector(
"link[rel~='icon']"
) as HTMLLinkElement | null;
if (config?.disableFavicon === true) {
existingFaviconElement?.remove();
}
if (config?.disableFavicon === false) {
if (existingFaviconElement) {
existingFaviconElement.href = '/browser-tab.ico';
} else {
const newFaviconElement = document.createElement('link');
newFaviconElement.rel = 'icon';
document.head.appendChild(newFaviconElement);
newFaviconElement.href = '/browser-tab.ico';
}
}
}, [config?.disableFavicon]);
return <>{children}</>;
};

View File

@@ -9,7 +9,6 @@ import PaymentIcon from '@mui/icons-material/Payment';
import * as URLS from 'config/urls';
import useAutomatischInfo from 'hooks/useAutomatischInfo';
import useFormatMessage from 'hooks/useFormatMessage';
import AppBar from 'components/AppBar';
import Drawer from 'components/Drawer';
@@ -23,8 +22,8 @@ function createDrawerLinks({ isCloud }: { isCloud: boolean }) {
Icon: AccountCircleIcon,
primary: 'settingsDrawer.myProfile',
to: URLS.SETTINGS_PROFILE,
},
];
}
]
if (isCloud) {
items.push({
@@ -37,12 +36,19 @@ function createDrawerLinks({ isCloud }: { isCloud: boolean }) {
return items;
}
const drawerBottomLinks = [
{
Icon: ArrowBackIosNewIcon,
primary: 'settingsDrawer.goBack',
to: '/',
},
];
export default function SettingsLayout({
children,
}: SettingsLayoutProps): React.ReactElement {
const { isCloud } = useAutomatischInfo();
const theme = useTheme();
const formatMessage = useFormatMessage();
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'));
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
@@ -50,14 +56,6 @@ export default function SettingsLayout({
const closeDrawer = () => setDrawerOpen(false);
const drawerLinks = createDrawerLinks({ isCloud });
const drawerBottomLinks = [
{
Icon: ArrowBackIosNewIcon,
primary: formatMessage('settingsDrawer.goBack'),
to: '/',
},
];
return (
<>
<AppBar

View File

@@ -15728,9 +15728,9 @@ vite-node@1.1.3:
vite "^5.0.0"
vite@^3.1.6:
version "3.2.8"
resolved "https://registry.yarnpkg.com/vite/-/vite-3.2.8.tgz#0697e13addf99ed44b838b8462a3a922fdd9d37b"
integrity sha512-EtQU16PLIJpAZol2cTLttNP1mX6L0SyI0pgQB1VOoWeQnMSvtiwovV3D6NcjN8CZQWWyESD2v5NGnpz5RvgOZA==
version "3.2.7"
resolved "https://registry.npmjs.org/vite/-/vite-3.2.7.tgz"
integrity sha512-29pdXjk49xAP0QBr0xXqu2s5jiQIXNvE/xwd0vUizYT2Hzqe4BksNNoWllFVXJf4eLZ+UlVQmXfB4lWrc+t18g==
dependencies:
esbuild "^0.15.9"
postcss "^8.4.18"
@@ -15740,9 +15740,9 @@ vite@^3.1.6:
fsevents "~2.3.2"
vite@^5.0.0:
version "5.0.12"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.0.12.tgz#8a2ffd4da36c132aec4adafe05d7adde38333c47"
integrity sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==
version "5.0.11"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.0.11.tgz#31562e41e004cb68e1d51f5d2c641ab313b289e4"
integrity sha512-XBMnDjZcNAw/G1gEiskiM1v6yzM4GE5aMGvhWTlHAYYhxb7S3/V1s3m2LDHa8Vh6yIWYYB0iJwsEaS523c4oYA==
dependencies:
esbuild "^0.19.3"
postcss "^8.4.32"