Compare commits
70 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
00e18cf42d | ||
![]() |
b3d2a1167c | ||
![]() |
43c34fcb7b | ||
![]() |
c98f24338f | ||
![]() |
d0748bb7e2 | ||
![]() |
81e8e4963c | ||
![]() |
282b2374a2 | ||
![]() |
b7e38f9d1c | ||
![]() |
024c7476c7 | ||
![]() |
30a7ffe93d | ||
![]() |
e2d803ebf7 | ||
![]() |
be7e67c940 | ||
![]() |
ead4b13ba5 | ||
![]() |
e02c42ee18 | ||
![]() |
d39886fdf8 | ||
![]() |
11a425f1de | ||
![]() |
f0e194e584 | ||
![]() |
d4b9331cf2 | ||
![]() |
37e1acc5f1 | ||
![]() |
ffaf6a577d | ||
![]() |
afdaf6ba39 | ||
![]() |
4c49367910 | ||
![]() |
a506c4411d | ||
![]() |
1859c9854e | ||
![]() |
6ff29b9ae6 | ||
![]() |
3578f6b849 | ||
![]() |
0347864fde | ||
![]() |
5f9786a2c7 | ||
![]() |
75aeff1898 | ||
![]() |
0afcdce6d3 | ||
![]() |
a591d0ea87 | ||
![]() |
0e111a3532 | ||
![]() |
b599466ffa | ||
![]() |
69727e78df | ||
![]() |
02ae67b147 | ||
![]() |
a769f78801 | ||
![]() |
d583e42428 | ||
![]() |
da732becb6 | ||
![]() |
b89a4d58d9 | ||
![]() |
09854147d1 | ||
![]() |
3648c2bfe3 | ||
![]() |
3f3ee032f6 | ||
![]() |
68e5d54331 | ||
![]() |
824c434b0b | ||
![]() |
9f0e0ca656 | ||
![]() |
95f89ba03e | ||
![]() |
697f72ecf4 | ||
![]() |
4f03f2ab51 | ||
![]() |
c81531cb7a | ||
![]() |
7b6e4aa153 | ||
![]() |
f21039d19d | ||
![]() |
8c936a91be | ||
![]() |
24451892ff | ||
![]() |
6bba2c82fe | ||
![]() |
3320dc6bc4 | ||
![]() |
9d42fd9293 | ||
![]() |
e6b806616f | ||
![]() |
6ec5872391 | ||
![]() |
a26cf932a1 | ||
![]() |
38a3e3ab9f | ||
![]() |
32b17c1418 | ||
![]() |
44aa6a1579 | ||
![]() |
2369aacd2a | ||
![]() |
7dafc6364b | ||
![]() |
3d25fa0aeb | ||
![]() |
0297b0f296 | ||
![]() |
4c7d09c3d8 | ||
![]() |
48a74826e8 | ||
![]() |
ef34068ac4 | ||
![]() |
3987a8db77 |
@@ -8,7 +8,7 @@
|
||||
"version": "latest"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"version": 16
|
||||
"version": 18
|
||||
},
|
||||
"ghcr.io/devcontainers/features/common-utils:1": {
|
||||
"username": "vscode",
|
||||
|
@@ -33,7 +33,6 @@
|
||||
"axios": "1.6.0",
|
||||
"bcrypt": "^5.0.1",
|
||||
"bullmq": "^3.0.0",
|
||||
"copyfiles": "^2.4.1",
|
||||
"cors": "^2.8.5",
|
||||
"crypto-js": "^4.1.1",
|
||||
"debug": "~2.6.9",
|
||||
@@ -45,7 +44,6 @@
|
||||
"graphql-middleware": "^6.1.15",
|
||||
"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",
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
@@ -68,7 +66,6 @@
|
||||
"pluralize": "^8.0.0",
|
||||
"raw-body": "^2.5.2",
|
||||
"showdown": "^2.1.0",
|
||||
"stripe": "^11.13.0",
|
||||
"winston": "^3.7.1",
|
||||
"xmlrpc": "^1.3.2"
|
||||
},
|
||||
|
@@ -0,0 +1,217 @@
|
||||
import defineAction from '../../../../helpers/define-action.js';
|
||||
|
||||
export default defineAction({
|
||||
name: 'Create contact',
|
||||
key: 'createContact',
|
||||
description: 'Creates a new contact.',
|
||||
arguments: [
|
||||
{
|
||||
label: 'Contact Owner',
|
||||
key: 'contactOwnerId',
|
||||
type: 'dropdown',
|
||||
required: false,
|
||||
description: '',
|
||||
variables: true,
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listContactOwners',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'First Name',
|
||||
key: 'firstName',
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: '',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'Last Name',
|
||||
key: 'lastName',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: '',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'Title',
|
||||
key: 'title',
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: '',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'Email',
|
||||
key: 'email',
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: '',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'Company ID',
|
||||
key: 'companyId',
|
||||
type: 'dropdown',
|
||||
required: false,
|
||||
description: '',
|
||||
variables: true,
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listCompanies',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Mobile',
|
||||
key: 'mobile',
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: '',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'Email Opt Out',
|
||||
key: 'emailOptOut',
|
||||
type: 'dropdown',
|
||||
required: false,
|
||||
description: '',
|
||||
variables: true,
|
||||
options: [
|
||||
{ label: 'True', value: true },
|
||||
{ label: 'False', value: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Tags',
|
||||
key: 'tags',
|
||||
type: 'dynamic',
|
||||
required: false,
|
||||
description: '',
|
||||
fields: [
|
||||
{
|
||||
label: 'Tag',
|
||||
key: 'tag',
|
||||
type: 'string',
|
||||
required: false,
|
||||
variables: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Description',
|
||||
key: 'description',
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: '',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'Mailing Street',
|
||||
key: 'mailingStreet',
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: '',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'Mailing City',
|
||||
key: 'mailingCity',
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: '',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'Mailing State',
|
||||
key: 'mailingState',
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: '',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'Mailing Country',
|
||||
key: 'mailingCountry',
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: '',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'Mailing Zip',
|
||||
key: 'mailingZip',
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: '',
|
||||
variables: true,
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
const {
|
||||
contactOwnerId,
|
||||
firstName,
|
||||
lastName,
|
||||
title,
|
||||
email,
|
||||
companyId,
|
||||
mobile,
|
||||
emailOptOut,
|
||||
tags,
|
||||
description,
|
||||
mailingStreet,
|
||||
mailingCity,
|
||||
mailingState,
|
||||
mailingCountry,
|
||||
mailingZip,
|
||||
} = $.step.parameters;
|
||||
|
||||
const allTags = tags.map((tag) => ({
|
||||
name: tag.tag,
|
||||
}));
|
||||
|
||||
const body = {
|
||||
data: [
|
||||
{
|
||||
Owner: {
|
||||
id: contactOwnerId,
|
||||
},
|
||||
Account_Name: {
|
||||
id: companyId,
|
||||
},
|
||||
First_Name: firstName,
|
||||
Last_Name: lastName,
|
||||
Title: title,
|
||||
Email: email,
|
||||
Mobile: mobile,
|
||||
Email_Opt_Out: emailOptOut,
|
||||
Tag: allTags,
|
||||
Description: description,
|
||||
Mailing_Street: mailingStreet,
|
||||
Mailing_City: mailingCity,
|
||||
Mailing_State: mailingState,
|
||||
Mailing_Country: mailingCountry,
|
||||
Mailing_Zip: mailingZip,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { data } = await $.http.post(`/bigin/v2/Contacts`, body);
|
||||
|
||||
$.setActionItem({
|
||||
raw: data[0],
|
||||
});
|
||||
},
|
||||
});
|
@@ -0,0 +1,293 @@
|
||||
import defineAction from '../../../../helpers/define-action.js';
|
||||
|
||||
export default defineAction({
|
||||
name: 'Create event',
|
||||
key: 'createEvent',
|
||||
description: 'Creates a new event.',
|
||||
arguments: [
|
||||
{
|
||||
label: 'Host',
|
||||
key: 'hostId',
|
||||
type: 'dropdown',
|
||||
required: false,
|
||||
description: '',
|
||||
variables: true,
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listContactOwners',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Title',
|
||||
key: 'title',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: '',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'From',
|
||||
key: 'from',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'The date format is ISO8601 (yyyy-mm-ddTHH:mm:ssZ).',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'To',
|
||||
key: 'to',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'The date format is ISO8601 (yyyy-mm-ddTHH:mm:ssZ).',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'All Day',
|
||||
key: 'allDay',
|
||||
type: 'dropdown',
|
||||
required: false,
|
||||
description: '',
|
||||
variables: true,
|
||||
options: [
|
||||
{ label: 'True', value: true },
|
||||
{ label: 'False', value: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Frequency (Recurring Activity)',
|
||||
key: 'frequency',
|
||||
type: 'dropdown',
|
||||
required: false,
|
||||
description:
|
||||
'Specifies the frequency of event recurrence. The options include DAILY, WEEKLY, MONTHLY, or YEARLY.',
|
||||
variables: true,
|
||||
options: [
|
||||
{ label: 'Daily Events', value: 'DAILY' },
|
||||
{ label: 'Weekly Events', value: 'WEEKLY' },
|
||||
{ label: 'Monthly Events', value: 'MONTHLY' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Interval (Recurring Activity)',
|
||||
key: 'interval',
|
||||
type: 'string',
|
||||
required: false,
|
||||
description:
|
||||
'Specifies the time difference between individual events. The INTERVAL can be anywhere from 1 to 99. For instance, with a WEEKLY event set at an INTERVAL of 2, there will be a two-week gap between each occurrence.',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'By Month Day (Recurring Activity)',
|
||||
key: 'byMonthDay',
|
||||
type: 'string',
|
||||
required: false,
|
||||
description:
|
||||
'This specifies the date within the month when the event recurs. The BYMONTHDAY value can be any number from 1 to 31. This rule applies exclusively to events that repeat monthly or yearly.',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'By Week (Recurring Activity)',
|
||||
key: 'byWeek',
|
||||
type: 'dropdown',
|
||||
required: false,
|
||||
description:
|
||||
'Only relevant for events that occur on a monthly or yearly basis.',
|
||||
variables: true,
|
||||
options: [
|
||||
{ label: 'First week of the month', value: '1' },
|
||||
{ label: 'Second week of the month', value: '2' },
|
||||
{ label: 'Third week of the month', value: '3' },
|
||||
{ label: 'Fourth week of the month', value: '4' },
|
||||
{ label: 'Last week of the month', value: '-1' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'By Day (Recurring Activity)',
|
||||
key: 'byDay',
|
||||
type: 'string',
|
||||
required: false,
|
||||
description:
|
||||
'This signifies the weekday when the event recurs. The options include SU, MO, TU, WE, TH, FR, or SA. This rule applies to events that repeat daily, weekly, monthly, and yearly (should not be combined with INTERVAL).',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'Count (Recurring Activity)',
|
||||
key: 'count',
|
||||
type: 'string',
|
||||
required: false,
|
||||
description:
|
||||
'Specifies the number of events you wish to generate. The count value range from 1 to 99.',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'Until (Recurring Activity)',
|
||||
key: 'until',
|
||||
type: 'string',
|
||||
required: false,
|
||||
description:
|
||||
'Specifies the concluding date for the event recurrence. Please input the date in the YYYY-MM-DD format.',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'Reminder',
|
||||
key: 'reminder',
|
||||
type: 'dropdown',
|
||||
required: false,
|
||||
description:
|
||||
'Provide the reminder list to notify or prompt participants prior to the event.',
|
||||
variables: true,
|
||||
options: [
|
||||
{ label: '5 min', value: '5 minutes' },
|
||||
{ label: '10 min', value: '10 minutes' },
|
||||
{ label: '15 min', value: '15 minutes' },
|
||||
{ label: '1 hrs', value: '1 hours' },
|
||||
{ label: '2 hrs', value: '2 hours' },
|
||||
{ label: '1 days', value: '1 days' },
|
||||
{ label: '2 days', value: '2 days' },
|
||||
{ label: '1 weeks', value: '1 weeks' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Location',
|
||||
key: 'location',
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: '',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'Related Module',
|
||||
key: 'relatedModule',
|
||||
type: 'dropdown',
|
||||
required: false,
|
||||
description: '',
|
||||
variables: true,
|
||||
options: [
|
||||
{ label: 'Companies', value: 'Accounts' },
|
||||
{ label: 'Contacts', value: 'Contacts' },
|
||||
{ label: 'Deals', value: 'Deals' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Participants',
|
||||
key: 'participants',
|
||||
type: 'dynamic',
|
||||
required: false,
|
||||
description: 'Email Address of participants.',
|
||||
fields: [
|
||||
{
|
||||
label: 'Participant',
|
||||
key: 'participant',
|
||||
type: 'string',
|
||||
required: false,
|
||||
variables: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Description',
|
||||
key: 'description',
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: '',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'Tags',
|
||||
key: 'tags',
|
||||
type: 'dynamic',
|
||||
required: false,
|
||||
description: '',
|
||||
fields: [
|
||||
{
|
||||
label: 'Tag',
|
||||
key: 'tag',
|
||||
type: 'string',
|
||||
required: false,
|
||||
variables: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
const {
|
||||
hostId,
|
||||
title,
|
||||
from,
|
||||
to,
|
||||
allDay,
|
||||
frequency,
|
||||
interval,
|
||||
byMonthDay,
|
||||
byDay,
|
||||
byWeek,
|
||||
count,
|
||||
until,
|
||||
reminder,
|
||||
location,
|
||||
relatedModule,
|
||||
participants,
|
||||
description,
|
||||
tags,
|
||||
} = $.step.parameters;
|
||||
|
||||
const allTags = tags.map((tag) => ({
|
||||
name: tag.tag,
|
||||
}));
|
||||
|
||||
const allParticipants = participants.map((participant) => ({
|
||||
type: 'email',
|
||||
participant: participant.participant,
|
||||
}));
|
||||
|
||||
const [unit, period] = reminder.split(' ');
|
||||
|
||||
let rrule = `FREQ=${frequency};INTERVAL=${interval};`;
|
||||
if (count) rrule += `COUNT=${count};`;
|
||||
if (byMonthDay) rrule += `BYMONTHDAY=${byMonthDay};`;
|
||||
if (byDay) rrule += `BYDAY=${byDay};`;
|
||||
if (byWeek) rrule += `BYSETPOS=${byWeek};`;
|
||||
if (until) rrule += `UNTIL=${until};`;
|
||||
|
||||
const body = {
|
||||
data: [
|
||||
{
|
||||
Owner: {
|
||||
id: hostId,
|
||||
},
|
||||
Event_Title: title,
|
||||
Start_DateTime: from,
|
||||
End_DateTime: to,
|
||||
All_day: allDay,
|
||||
$se_module: relatedModule,
|
||||
Recurring_Activity: {
|
||||
RRULE: rrule,
|
||||
},
|
||||
Remind_At: [
|
||||
{
|
||||
unit: Number(unit),
|
||||
period,
|
||||
},
|
||||
],
|
||||
Venue: location,
|
||||
Participants: allParticipants,
|
||||
Description: description,
|
||||
Tag: allTags,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { data } = await $.http.post(`/bigin/v2/Events`, body);
|
||||
|
||||
$.setActionItem({
|
||||
raw: data,
|
||||
});
|
||||
},
|
||||
});
|
@@ -0,0 +1,4 @@
|
||||
import createContact from './create-contact/index.js';
|
||||
import createEvent from './create-event/index.js';
|
||||
|
||||
export default [createContact, createEvent];
|
@@ -0,0 +1,32 @@
|
||||
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" id="b" data-name="Layer 2" width="327.714" height="120" viewBox="0 0 327.714 120">
|
||||
<defs>
|
||||
<style>
|
||||
.d {
|
||||
fill: #039649;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="c" data-name="Layer 1">
|
||||
<g>
|
||||
<path class="d" d="m52.39,120c-1.801,0-3.6-.6-5.101-1.5-2.1-1.5-3.6-4.2-3.6-6.9v-24.6L1.09,12.901C-.41,9.9-.41,6.601,1.391,4.2,2.891,1.5,5.89,0,8.89,0h78c2.999,0,5.698,1.5,7.199,4.2,1.801,2.702,1.801,6.3.301,9.002l-5.101,9h10.201c2.999,0,5.698,1.5,7.199,4.2,1.801,2.7,1.801,6.3.301,9l-29.701,51.3v18.599c0,3.899-2.399,6.9-5.999,8.099l-16.2,6.3c-.6.301-1.799.301-2.7.301M8.89,8.4q-.301.301-.301.602l42.301,73.798c1.199,2.1,1.199,3.299,1.199,4.501v23.998s.301.301.6,0l16.2-6.3h.6v-17.999c0-1.199,0-3.299,1.201-4.8l29.4-50.999c0-.301,0-.6-.301-.6l-53.699-.301,14.399,24.901,10.201-17.7c1.199-2.1,3.6-2.7,5.7-1.5,2.1,1.199,2.7,3.6,1.498,5.7l-11.998,20.699c-2.702,4.2-8.701,3.901-11.102.301l-17.4-30.9c-1.199-1.799-1.199-4.2,0-6.3,1.201-2.1,3.301-3.6,5.7-3.6h36.6l7.499-12.899s0-.301-.299-.602H8.89Z"/>
|
||||
<g>
|
||||
<path d="m181.725,48.737c0,3.089-.54,5.684-1.618,7.788-1.081,2.104-2.548,3.79-4.407,5.062-1.858,1.27-4.016,2.179-6.476,2.726-2.459.547-5.069.819-7.828.819h-24.591V6.521h23.034c2.676,0,5.198.24,7.561.718,2.364.479,4.427,1.311,6.189,2.5,1.762,1.189,3.149,2.787,4.16,4.795,1.011,2.008,1.517,4.53,1.517,7.562,0,3.17-.786,5.813-2.357,7.93-1.57,2.119-3.913,3.628-7.028,4.53,4.017.819,6.995,2.371,8.935,4.652s2.91,5.458,2.91,9.529Zm-33.813-17.624h10.533c1.612,0,3.033-.136,4.263-.41,1.23-.272,2.268-.738,3.115-1.393.846-.656,1.489-1.55,1.927-2.684.436-1.134.655-2.562.655-4.284,0-1.612-.267-2.923-.799-3.934-.532-1.01-1.25-1.809-2.152-2.398-.902-.587-1.967-.99-3.197-1.209s-2.542-.328-3.934-.328h-10.411v16.64Zm0,26.067h12.09c1.64,0,3.115-.169,4.427-.512,1.311-.342,2.424-.894,3.341-1.66.915-.764,1.612-1.748,2.091-2.951.478-1.202.716-2.663.716-4.385,0-1.802-.294-3.285-.881-4.447-.588-1.161-1.394-2.083-2.419-2.766s-2.227-1.154-3.606-1.414c-1.381-.26-2.863-.39-4.447-.39h-11.312v18.525Z"/>
|
||||
<path d="m202.257,9.555c0,.847-.151,1.633-.451,2.356-.3.724-.716,1.34-1.25,1.844-.532.507-1.167.902-1.905,1.189s-1.53.431-2.377.431c-.819,0-1.598-.144-2.336-.431s-1.388-.69-1.947-1.209c-.56-.519-.998-1.133-1.311-1.844-.315-.711-.471-1.489-.471-2.336s.156-1.626.471-2.336c.314-.711.744-1.325,1.291-1.845.546-.519,1.187-.922,1.925-1.209s1.53-.431,2.377-.431c.821,0,1.598.144,2.336.431s1.373.69,1.907,1.209c.532.52.955,1.134,1.27,1.845.314.71.471,1.489.471,2.336Zm-.778,12.705v42.871h-10.246V22.261h10.246Z"/>
|
||||
<path d="m254.186,61.935c0,3.361-.587,6.236-1.762,8.627-1.174,2.391-2.78,4.352-4.815,5.882-2.036,1.529-4.407,2.65-7.111,3.361-2.706.71-5.589,1.065-8.648,1.065-2.023,0-4.037-.157-6.045-.471-2.009-.314-3.908-.861-5.697-1.64-1.79-.778-3.395-1.837-4.816-3.175-1.421-1.34-2.555-3.021-3.402-5.042l8.074-3.525c.519,1.202,1.202,2.193,2.049,2.971.847.779,1.797,1.408,2.848,1.885,1.051.479,2.172.814,3.361,1.005s2.398.287,3.628.287c1.885,0,3.572-.232,5.062-.696,1.489-.466,2.752-1.169,3.79-2.111,1.039-.943,1.838-2.132,2.399-3.566.559-1.434.839-3.107.839-5.02v-6.393c-.71,1.229-1.592,2.336-2.643,3.319-1.053.983-2.207,1.824-3.464,2.52s-2.582,1.237-3.976,1.62c-1.393.383-2.814.574-4.263.574-3.033,0-5.737-.56-8.114-1.681-2.377-1.119-4.379-2.636-6.005-4.55-1.625-1.912-2.862-4.139-3.709-6.68-.847-2.542-1.27-5.233-1.27-8.074,0-2.814.451-5.491,1.353-8.033.901-2.542,2.192-4.775,3.873-6.702,1.68-1.927,3.709-3.464,6.086-4.611,2.376-1.147,5.054-1.721,8.033-1.721,2.923,0,5.621.594,8.094,1.782,2.472,1.189,4.473,3.082,6.004,5.677v-6.557h10.246v39.674Zm-33.198-19.017c0,1.666.252,3.265.758,4.795s1.237,2.876,2.193,4.037c.957,1.162,2.137,2.084,3.545,2.767s3.013,1.025,4.816,1.025c2.021,0,3.784-.355,5.287-1.066,1.502-.711,2.746-1.673,3.729-2.89.985-1.215,1.722-2.643,2.213-4.283.492-1.64.738-3.374.738-5.206,0-1.802-.239-3.49-.716-5.062-.479-1.57-1.203-2.93-2.173-4.077s-2.179-2.056-3.626-2.726c-1.449-.67-3.157-1.005-5.123-1.005-2.049,0-3.812.363-5.287,1.086-1.476.724-2.684,1.709-3.628,2.951-.943,1.243-1.633,2.699-2.069,4.365-.438,1.666-.656,3.429-.656,5.287Z"/>
|
||||
<path d="m277.096,9.555c0,.847-.151,1.633-.451,2.356-.3.724-.716,1.34-1.25,1.844-.532.507-1.167.902-1.905,1.189s-1.53.431-2.377.431c-.819,0-1.598-.144-2.336-.431s-1.388-.69-1.947-1.209c-.56-.519-.998-1.133-1.311-1.844-.315-.711-.471-1.489-.471-2.336s.156-1.626.471-2.336c.314-.711.744-1.325,1.291-1.845.546-.519,1.187-.922,1.925-1.209s1.53-.431,2.377-.431c.821,0,1.598.144,2.336.431s1.373.69,1.907,1.209c.532.52.955,1.134,1.27,1.845.314.71.471,1.489.471,2.336Zm-.778,12.705v42.871h-10.246V22.261h10.246Z"/>
|
||||
<path d="m308.451,29.801c-1.585,0-2.999.24-4.243.718-1.243.479-2.288,1.162-3.135,2.049-.847.889-1.496,1.961-1.947,3.217-.451,1.258-.676,2.651-.676,4.181v25.165h-10.246V22.261h10.246v6.803c.683-1.338,1.537-2.492,2.562-3.462,1.025-.97,2.165-1.769,3.422-2.399,1.257-.627,2.59-1.091,3.997-1.393,1.406-.3,2.82-.451,4.241-.451,2.623,0,4.884.431,6.783,1.291s3.464,2.049,4.694,3.565c1.229,1.517,2.131,3.307,2.704,5.37s.861,4.311.861,6.742v26.805h-10.328v-25.822c0-3.005-.711-5.341-2.132-7.008-1.421-1.666-3.688-2.5-6.803-2.5Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="m152.871,102.12c0,1.138-.168,2.221-.507,3.25-.337,1.028-.828,1.935-1.471,2.72s-1.436,1.41-2.379,1.874-2.021.696-3.234.696c-.579,0-1.155-.061-1.723-.183-.569-.121-1.107-.306-1.613-.553-.505-.247-.974-.561-1.407-.941s-.801-.822-1.107-1.328v2.72h-2.625v-24.446h2.625v10.689c.273-.464.632-.882,1.076-1.257.442-.374.93-.696,1.463-.964.531-.27,1.082-.477,1.652-.626.569-.146,1.122-.22,1.66-.22,1.244,0,2.34.227,3.289.679.95.454,1.743,1.068,2.38,1.843s1.117,1.685,1.44,2.728c.321,1.044.482,2.151.482,3.321Zm-13.536.126c0,.917.123,1.756.371,2.515.249.759.614,1.415,1.1,1.968.485.553,1.079.984,1.787,1.289.706.306,1.517.459,2.435.459.991,0,1.813-.178,2.467-.53.653-.354,1.178-.828,1.573-1.424.395-.595.674-1.283.838-2.063.163-.78.245-1.603.245-2.467,0-.801-.104-1.576-.308-2.325-.206-.748-.52-1.415-.941-1.999-.422-.586-.958-1.055-1.605-1.407-.648-.354-1.415-.53-2.3-.53-.981,0-1.827.174-2.538.521-.711.349-1.3.818-1.764,1.409-.464.59-.806,1.28-1.028,2.071s-.332,1.629-.332,2.514Z"/>
|
||||
<path d="m169.949,93.834l-9.141,22.297h-2.656l3.131-7.464-6.388-14.833h2.814l4.965,11.86,4.586-11.86h2.689Z"/>
|
||||
<path d="m194.11,108.034v2.34h-16.414v-1.139l13.189-19.228h-12.05v-2.246h15.465v1.139l-12.84,19.134h12.65Z"/>
|
||||
<path d="m213.229,102.215c0,1.202-.206,2.319-.617,3.352-.411,1.034-.986,1.927-1.723,2.681-.739.753-1.611,1.344-2.618,1.77-1.007.428-2.106.641-3.296.641-1.256,0-2.388-.222-3.4-.665s-1.874-1.053-2.585-1.834-1.257-1.697-1.637-2.752c-.38-1.053-.57-2.192-.57-3.416,0-1.191.201-2.3.601-3.329.4-1.028.964-1.92,1.692-2.68.727-.759,1.594-1.354,2.601-1.787s2.116-.648,3.329-.648c1.254,0,2.391.22,3.408.663s1.881,1.055,2.593,1.835,1.26,1.697,1.644,2.751c.385,1.055.578,2.192.578,3.416Zm-13.726.031c0,.886.117,1.708.349,2.467s.579,1.418,1.043,1.977c.464.558,1.038.995,1.723,1.311.685.317,1.481.476,2.388.476.938,0,1.753-.175,2.443-.522.691-.349,1.263-.816,1.717-1.407.452-.591.79-1.275,1.012-2.056.22-.78.332-1.602.332-2.466,0-.844-.117-1.646-.349-2.404-.232-.759-.579-1.429-1.043-2.008s-1.038-1.038-1.723-1.376c-.685-.337-1.481-.505-2.388-.505-.971,0-1.802.174-2.498.521-.696.349-1.265.82-1.708,1.416-.443.595-.77,1.288-.981,2.078-.21.792-.316,1.624-.316,2.498Z"/>
|
||||
<path d="m224.423,95.827c-.696,0-1.323.105-1.881.316s-1.034.512-1.423.902c-.391.39-.691.864-.902,1.423s-.316,1.186-.316,1.881v10.026h-2.625v-24.446h2.625v10.531c.253-.516.564-.956.933-1.32s.777-.663,1.226-.902c.447-.237.933-.411,1.454-.521.522-.111,1.057-.166,1.605-.166,1.033,0,1.937.171,2.712.513.775.343,1.423.818,1.945,1.424.522.605.915,1.326,1.178,2.157.263.833.395,1.745.395,2.735v9.994h-2.625v-10.184c0-1.423-.36-2.506-1.083-3.25-.722-.742-1.795-1.114-3.217-1.114Z"/>
|
||||
<path d="m251.685,102.215c0,1.202-.206,2.319-.617,3.352-.411,1.034-.986,1.927-1.723,2.681-.739.753-1.611,1.344-2.618,1.77-1.007.428-2.106.641-3.296.641-1.256,0-2.388-.222-3.4-.665s-1.874-1.053-2.585-1.834-1.257-1.697-1.637-2.752c-.38-1.053-.57-2.192-.57-3.416,0-1.191.201-2.3.601-3.329.4-1.028.964-1.92,1.692-2.68.727-.759,1.594-1.354,2.601-1.787s2.116-.648,3.329-.648c1.254,0,2.391.22,3.408.663s1.881,1.055,2.593,1.835,1.26,1.697,1.644,2.751c.385,1.055.578,2.192.578,3.416Zm-13.726.031c0,.886.117,1.708.349,2.467s.579,1.418,1.043,1.977c.464.558,1.038.995,1.723,1.311.685.317,1.481.476,2.388.476.938,0,1.753-.175,2.443-.522.691-.349,1.263-.816,1.717-1.407.452-.591.79-1.275,1.012-2.056.22-.78.332-1.602.332-2.466,0-.844-.117-1.646-.349-2.404-.232-.759-.579-1.429-1.043-2.008s-1.038-1.038-1.723-1.376c-.685-.337-1.481-.505-2.388-.505-.971,0-1.802.174-2.498.521-.696.349-1.265.82-1.708,1.416-.443.595-.77,1.288-.981,2.078-.21.792-.316,1.624-.316,2.498Z"/>
|
||||
<path d="m281.097,103.923c-.38.959-.884,1.85-1.511,2.672-.627.823-1.346,1.534-2.157,2.135-.812.601-1.703,1.073-2.673,1.415-.969.342-1.976.514-3.02.514-1.76,0-3.312-.295-4.656-.886-1.345-.59-2.472-1.407-3.385-2.45-.912-1.044-1.603-2.279-2.072-3.709-.469-1.428-.704-2.98-.704-4.657,0-1.033.114-2.037.341-3.013.227-.974.553-1.889.98-2.743.428-.854.95-1.637,1.565-2.348.617-.711,1.318-1.32,2.104-1.827.785-.505,1.652-.901,2.601-1.186.949-.284,1.961-.426,3.036-.426,1.012,0,2.003.148,2.973.442.971.295,1.866.718,2.689,1.266.822.548,1.55,1.209,2.182,1.984s1.127,1.642,1.486,2.602l-2.246.98c-.295-.768-.673-1.468-1.13-2.095-.459-.627-.989-1.162-1.59-1.604-.601-.443-1.265-.785-1.992-1.029-.728-.242-1.508-.363-2.341-.363-1.359,0-2.532.268-3.518.806s-1.797,1.254-2.435,2.151c-.638.895-1.107,1.919-1.407,3.067-.301,1.149-.451,2.335-.451,3.558,0,1.244.166,2.424.498,3.543.333,1.117.833,2.098,1.503,2.94.669.844,1.507,1.513,2.514,2.008,1.007.496,2.18.744,3.518.744.801,0,1.576-.143,2.325-.428.749-.284,1.44-.671,2.072-1.162.632-.49,1.191-1.064,1.675-1.723.485-.658.864-1.357,1.139-2.095l2.088.917Z"/>
|
||||
<path d="m287.991,100.539v9.835h-2.751v-22.613h7.431c1.044,0,2.042.111,2.997.332.954.222,1.795.586,2.522,1.092.728.505,1.307,1.168,1.74,1.984.431.818.648,1.822.648,3.013,0,.885-.137,1.673-.411,2.364-.275.691-.66,1.289-1.155,1.795-.496.507-1.086.925-1.771,1.257-.685.333-1.44.583-2.261.752l6.926,10.026h-3.068l-6.736-9.835h-4.112Zm0-2.183h4.871c.727,0,1.393-.075,1.999-.228s1.131-.402,1.574-.744c.442-.342.785-.785,1.028-1.328s.363-1.21.363-2.001c0-.801-.126-1.466-.378-1.992-.254-.527-.601-.943-1.044-1.25-.443-.305-.967-.521-1.573-.648s-1.262-.189-1.968-.189h-4.871v8.38Z"/>
|
||||
<path d="m308.231,91.335v19.039h-2.529v-22.613h3.826l7.242,17.932,7.148-17.932h3.795v22.613h-2.752v-19.039l-7.653,19.039h-1.17l-7.907-19.039Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,25 @@
|
||||
import { URLSearchParams } from 'url';
|
||||
import authScope from '../common/auth-scope.js';
|
||||
|
||||
export default async function generateAuthUrl($) {
|
||||
const oauthRedirectUrlField = $.app.auth.fields.find(
|
||||
(field) => field.key == 'oAuthRedirectUrl'
|
||||
);
|
||||
const redirectUri = oauthRedirectUrlField.value;
|
||||
const searchParams = new URLSearchParams({
|
||||
scope: authScope.join(','),
|
||||
client_id: $.auth.data.clientId,
|
||||
response_type: 'code',
|
||||
access_type: 'offline',
|
||||
redirect_uri: redirectUri,
|
||||
});
|
||||
|
||||
const domain =
|
||||
$.auth.data.region !== 'cn' ? 'account.zoho.com' : 'accounts.zoho.com.cn';
|
||||
|
||||
const url = `https://${domain}/oauth/v2/auth?${searchParams.toString()}`;
|
||||
|
||||
await $.auth.set({
|
||||
url,
|
||||
});
|
||||
}
|
66
packages/backend/src/apps/bigin-by-zoho-crm/auth/index.js
Normal file
66
packages/backend/src/apps/bigin-by-zoho-crm/auth/index.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import generateAuthUrl from './generate-auth-url.js';
|
||||
import verifyCredentials from './verify-credentials.js';
|
||||
import refreshToken from './refresh-token.js';
|
||||
import isStillVerified from './is-still-verified.js';
|
||||
|
||||
export default {
|
||||
fields: [
|
||||
{
|
||||
key: 'oAuthRedirectUrl',
|
||||
label: 'OAuth Redirect URL',
|
||||
type: 'string',
|
||||
required: true,
|
||||
readOnly: true,
|
||||
value: '{WEB_APP_URL}/app/bigin-by-zoho-crm/connections/add',
|
||||
placeholder: null,
|
||||
description:
|
||||
'When asked to input a redirect URL in Bigin By Zoho CRM, enter the URL above.',
|
||||
clickToCopy: true,
|
||||
},
|
||||
{
|
||||
key: 'region',
|
||||
label: 'Region',
|
||||
type: 'dropdown',
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: '',
|
||||
options: [
|
||||
{ label: 'United States', value: 'us' },
|
||||
{ label: 'European Union', value: 'eu' },
|
||||
{ label: 'Australia', value: 'au' },
|
||||
{ label: 'India', value: 'in' },
|
||||
{ label: 'China', value: 'cn' },
|
||||
],
|
||||
clickToCopy: false,
|
||||
},
|
||||
{
|
||||
key: 'clientId',
|
||||
label: 'Client ID',
|
||||
type: 'string',
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: null,
|
||||
clickToCopy: false,
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret',
|
||||
type: 'string',
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: null,
|
||||
clickToCopy: false,
|
||||
},
|
||||
],
|
||||
|
||||
generateAuthUrl,
|
||||
verifyCredentials,
|
||||
isStillVerified,
|
||||
refreshToken,
|
||||
};
|
@@ -0,0 +1,8 @@
|
||||
import getCurrentOrganization from '../common/get-current-organization.js';
|
||||
|
||||
const isStillVerified = async ($) => {
|
||||
const org = await getCurrentOrganization($);
|
||||
return !!org.id;
|
||||
};
|
||||
|
||||
export default isStillVerified;
|
@@ -0,0 +1,34 @@
|
||||
import { URLSearchParams } from 'node:url';
|
||||
|
||||
import authScope from '../common/auth-scope.js';
|
||||
import { regionUrlMap } from '../common/region-url-map.js';
|
||||
|
||||
const refreshToken = async ($) => {
|
||||
const location = $.auth.data.location;
|
||||
const params = new URLSearchParams({
|
||||
client_id: $.auth.data.clientId,
|
||||
client_secret: $.auth.data.clientSecret,
|
||||
refresh_token: $.auth.data.refreshToken,
|
||||
grant_type: 'refresh_token',
|
||||
});
|
||||
|
||||
const { data } = await $.http.post(
|
||||
`${regionUrlMap[location]}/oauth/v2/token`,
|
||||
params.toString(),
|
||||
{
|
||||
additionalProperties: {
|
||||
skipAddingBaseUrl: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
await $.auth.set({
|
||||
accessToken: data.access_token,
|
||||
apiDomain: data.api_domain,
|
||||
scope: authScope.join(','),
|
||||
tokenType: data.token_type,
|
||||
expiresIn: data.expires_in,
|
||||
});
|
||||
};
|
||||
|
||||
export default refreshToken;
|
@@ -0,0 +1,46 @@
|
||||
import { URLSearchParams } from 'node:url';
|
||||
import { regionUrlMap } from '../common/region-url-map.js';
|
||||
import getCurrentOrganization from '../common/get-current-organization.js';
|
||||
|
||||
const verifyCredentials = async ($) => {
|
||||
const oauthRedirectUrlField = $.app.auth.fields.find(
|
||||
(field) => field.key == 'oAuthRedirectUrl'
|
||||
);
|
||||
const redirectUri = oauthRedirectUrlField.value;
|
||||
const location = $.auth.data.location;
|
||||
const params = new URLSearchParams({
|
||||
client_id: $.auth.data.clientId,
|
||||
client_secret: $.auth.data.clientSecret,
|
||||
code: $.auth.data.code,
|
||||
redirect_uri: redirectUri,
|
||||
grant_type: 'authorization_code',
|
||||
});
|
||||
|
||||
const { data } = await $.http.post(
|
||||
`${regionUrlMap[location]}/oauth/v2/token`,
|
||||
params.toString()
|
||||
);
|
||||
|
||||
await $.auth.set({
|
||||
accessToken: data.access_token,
|
||||
tokenType: data.token_type,
|
||||
apiDomain: data.api_domain,
|
||||
});
|
||||
|
||||
const organization = await getCurrentOrganization($);
|
||||
|
||||
const screenName = [organization.company_name, organization.primary_email]
|
||||
.filter(Boolean)
|
||||
.join(' @ ');
|
||||
|
||||
await $.auth.set({
|
||||
clientId: $.auth.data.clientId,
|
||||
clientSecret: $.auth.data.clientSecret,
|
||||
scope: $.auth.data.scope,
|
||||
expiresIn: data.expires_in,
|
||||
refreshToken: data.refresh_token,
|
||||
screenName,
|
||||
});
|
||||
};
|
||||
|
||||
export default verifyCredentials;
|
@@ -0,0 +1,9 @@
|
||||
const addAuthHeader = ($, requestConfig) => {
|
||||
if ($.auth.data?.accessToken) {
|
||||
requestConfig.headers.Authorization = `Zoho-oauthtoken ${$.auth.data.accessToken}`;
|
||||
}
|
||||
|
||||
return requestConfig;
|
||||
};
|
||||
|
||||
export default addAuthHeader;
|
@@ -0,0 +1,10 @@
|
||||
const authScope = [
|
||||
'ZohoBigin.notifications.ALL',
|
||||
'ZohoBigin.users.ALL',
|
||||
'ZohoBigin.modules.ALL',
|
||||
'ZohoBigin.org.READ',
|
||||
'ZohoBigin.settings.ALL',
|
||||
'ZohoBigin.modules.ALL',
|
||||
];
|
||||
|
||||
export default authScope;
|
@@ -0,0 +1,6 @@
|
||||
const getCurrentOrganization = async ($) => {
|
||||
const response = await $.http.get('/bigin/v2/org');
|
||||
return response.data.org[0];
|
||||
};
|
||||
|
||||
export default getCurrentOrganization;
|
@@ -0,0 +1,8 @@
|
||||
export const regionUrlMap = {
|
||||
us: 'https://accounts.zoho.com',
|
||||
au: 'https://accounts.zoho.com.au',
|
||||
eu: 'https://accounts.zoho.eu',
|
||||
in: 'https://accounts.zoho.in',
|
||||
cn: 'https://accounts.zoho.com.cn',
|
||||
jp: 'https://accounts.zoho.jp',
|
||||
};
|
@@ -0,0 +1,14 @@
|
||||
const setBaseUrl = ($, requestConfig) => {
|
||||
if (requestConfig.additionalProperties?.skipAddingBaseUrl)
|
||||
return requestConfig;
|
||||
|
||||
const apiDomain = $.auth.data.apiDomain;
|
||||
|
||||
if (apiDomain) {
|
||||
requestConfig.baseURL = apiDomain;
|
||||
}
|
||||
|
||||
return requestConfig;
|
||||
};
|
||||
|
||||
export default setBaseUrl;
|
@@ -0,0 +1,5 @@
|
||||
import listCompanies from './list-companies/index.js';
|
||||
import listOrganizations from './list-organizations/index.js';
|
||||
import listContactOwners from './list-contact-owners/index.js';
|
||||
|
||||
export default [listCompanies, listOrganizations, listContactOwners];
|
@@ -0,0 +1,39 @@
|
||||
export default {
|
||||
name: 'List companies',
|
||||
key: 'listCompanies',
|
||||
|
||||
async run($) {
|
||||
const companies = {
|
||||
data: [],
|
||||
};
|
||||
|
||||
const params = new URLSearchParams({
|
||||
page: 1,
|
||||
pageSize: 200,
|
||||
fields: 'Account_Name',
|
||||
});
|
||||
|
||||
let next = false;
|
||||
do {
|
||||
const { data } = await $.http.get('/bigin/v2/Accounts', { params });
|
||||
|
||||
if (data.info.more_records) {
|
||||
params.page = params.page + 1;
|
||||
next = true;
|
||||
} else {
|
||||
next = false;
|
||||
}
|
||||
|
||||
if (data.data) {
|
||||
for (const account of data.data) {
|
||||
companies.data.push({
|
||||
value: account.id,
|
||||
name: account.Account_Name,
|
||||
});
|
||||
}
|
||||
}
|
||||
} while (next);
|
||||
|
||||
return companies;
|
||||
},
|
||||
};
|
@@ -0,0 +1,39 @@
|
||||
export default {
|
||||
name: 'List contact owners',
|
||||
key: 'listContactOwners',
|
||||
|
||||
async run($) {
|
||||
const contactOwners = {
|
||||
data: [],
|
||||
};
|
||||
|
||||
const params = {
|
||||
type: 'AllUsers',
|
||||
page: 1,
|
||||
pageSize: 200,
|
||||
};
|
||||
|
||||
let next = false;
|
||||
do {
|
||||
const { data } = await $.http.get('/bigin/v2/users', params);
|
||||
|
||||
if (data.users.length === params.pageSize) {
|
||||
next = true;
|
||||
params.page = params.page + 1;
|
||||
} else {
|
||||
next = false;
|
||||
}
|
||||
|
||||
if (data.users) {
|
||||
for (const user of data.users) {
|
||||
contactOwners.data.push({
|
||||
value: user.id,
|
||||
name: user.full_name,
|
||||
});
|
||||
}
|
||||
}
|
||||
} while (next);
|
||||
|
||||
return contactOwners;
|
||||
},
|
||||
};
|
@@ -0,0 +1,23 @@
|
||||
export default {
|
||||
name: 'List organizations',
|
||||
key: 'listOrganizations',
|
||||
|
||||
async run($) {
|
||||
const organizations = {
|
||||
data: [],
|
||||
};
|
||||
|
||||
const { data } = await $.http.get('/bigin/v2/org');
|
||||
|
||||
if (data.org) {
|
||||
for (const org of data.org) {
|
||||
organizations.data.push({
|
||||
value: org.id,
|
||||
name: org.company_name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return organizations;
|
||||
},
|
||||
};
|
23
packages/backend/src/apps/bigin-by-zoho-crm/index.js
Normal file
23
packages/backend/src/apps/bigin-by-zoho-crm/index.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import defineApp from '../../helpers/define-app.js';
|
||||
import addAuthHeader from './common/add-auth-header.js';
|
||||
import auth from './auth/index.js';
|
||||
import setBaseUrl from './common/set-base-url.js';
|
||||
import triggers from './triggers/index.js';
|
||||
import dynamicData from './dynamic-data/index.js';
|
||||
import actions from './actions/index.js';
|
||||
|
||||
export default defineApp({
|
||||
name: 'Bigin By Zoho CRM',
|
||||
key: 'bigin-by-zoho-crm',
|
||||
baseUrl: 'https://www.bigin.com',
|
||||
apiBaseUrl: '',
|
||||
iconUrl: '{BASE_URL}/apps/bigin-by-zoho-crm/assets/favicon.svg',
|
||||
authDocUrl: 'https://automatisch.io/docs/apps/bigin-by-zoho-crm/connection',
|
||||
primaryColor: '039649',
|
||||
supportsConnections: true,
|
||||
beforeRequest: [setBaseUrl, addAuthHeader],
|
||||
auth,
|
||||
triggers,
|
||||
dynamicData,
|
||||
actions,
|
||||
});
|
@@ -0,0 +1,7 @@
|
||||
import newCalls from './new-calls/index.js';
|
||||
import newCompanies from './new-companies/index.js';
|
||||
import newContacts from './new-contacts/index.js';
|
||||
import newProducts from './new-products/index.js';
|
||||
import newTasks from './new-tasks/index.js';
|
||||
|
||||
export default [newCalls, newCompanies, newContacts, newProducts, newTasks];
|
@@ -0,0 +1,89 @@
|
||||
import Crypto from 'crypto';
|
||||
import defineTrigger from '../../../../helpers/define-trigger.js';
|
||||
|
||||
export default defineTrigger({
|
||||
name: 'New calls',
|
||||
key: 'newCalls',
|
||||
type: 'webhook',
|
||||
description: 'Triggers when a new call is added.',
|
||||
arguments: [
|
||||
{
|
||||
label: 'Organization',
|
||||
key: 'organizationId',
|
||||
type: 'dropdown',
|
||||
required: true,
|
||||
description: '',
|
||||
variables: false,
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listOrganizations',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
const dataItem = {
|
||||
raw: $.request.body,
|
||||
meta: {
|
||||
internalId: Crypto.randomUUID(),
|
||||
},
|
||||
};
|
||||
|
||||
$.pushTriggerItem(dataItem);
|
||||
},
|
||||
|
||||
async testRun($) {
|
||||
const organizationId = $.step.parameters.organizationId;
|
||||
|
||||
const sampleEventData = {
|
||||
ids: ['111111111111111111'],
|
||||
token: null,
|
||||
module: 'Calls',
|
||||
operation: 'insert',
|
||||
channel_id: organizationId,
|
||||
server_time: 1708426963120,
|
||||
query_params: {},
|
||||
resource_uri: `${$.auth.data.apiDomain}/bigin/v1/Calls`,
|
||||
affected_fields: [],
|
||||
};
|
||||
|
||||
const dataItem = {
|
||||
raw: sampleEventData,
|
||||
meta: {
|
||||
internalId: sampleEventData.channel_id,
|
||||
},
|
||||
};
|
||||
|
||||
$.pushTriggerItem(dataItem);
|
||||
},
|
||||
|
||||
async registerHook($) {
|
||||
const organizationId = $.step.parameters.organizationId;
|
||||
|
||||
const payload = {
|
||||
watch: [
|
||||
{
|
||||
channel_id: organizationId,
|
||||
notify_url: $.webhookUrl,
|
||||
events: ['Calls.create'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await $.http.post('/bigin/v2/actions/watch', payload);
|
||||
|
||||
await $.flow.setRemoteWebhookId(organizationId);
|
||||
},
|
||||
|
||||
async unregisterHook($) {
|
||||
await $.http.delete(
|
||||
`/bigin/v2/actions/watch?channel_ids=${$.flow.remoteWebhookId}`
|
||||
);
|
||||
},
|
||||
});
|
@@ -0,0 +1,89 @@
|
||||
import Crypto from 'crypto';
|
||||
import defineTrigger from '../../../../helpers/define-trigger.js';
|
||||
|
||||
export default defineTrigger({
|
||||
name: 'New companies',
|
||||
key: 'newCompanies',
|
||||
type: 'webhook',
|
||||
description: 'Triggers when a new company is created.',
|
||||
arguments: [
|
||||
{
|
||||
label: 'Organization',
|
||||
key: 'organizationId',
|
||||
type: 'dropdown',
|
||||
required: true,
|
||||
description: '',
|
||||
variables: false,
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listOrganizations',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
const dataItem = {
|
||||
raw: $.request.body,
|
||||
meta: {
|
||||
internalId: Crypto.randomUUID(),
|
||||
},
|
||||
};
|
||||
|
||||
$.pushTriggerItem(dataItem);
|
||||
},
|
||||
|
||||
async testRun($) {
|
||||
const organizationId = $.step.parameters.organizationId;
|
||||
|
||||
const sampleEventData = {
|
||||
ids: ['111111111111111111'],
|
||||
token: null,
|
||||
module: 'Accounts',
|
||||
operation: 'insert',
|
||||
channel_id: organizationId,
|
||||
server_time: 1708426963120,
|
||||
query_params: {},
|
||||
resource_uri: `${$.auth.data.apiDomain}/bigin/v1/Accounts`,
|
||||
affected_fields: [],
|
||||
};
|
||||
|
||||
const dataItem = {
|
||||
raw: sampleEventData,
|
||||
meta: {
|
||||
internalId: sampleEventData.channel_id,
|
||||
},
|
||||
};
|
||||
|
||||
$.pushTriggerItem(dataItem);
|
||||
},
|
||||
|
||||
async registerHook($) {
|
||||
const organizationId = $.step.parameters.organizationId;
|
||||
|
||||
const payload = {
|
||||
watch: [
|
||||
{
|
||||
channel_id: organizationId,
|
||||
notify_url: $.webhookUrl,
|
||||
events: ['Accounts.create'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await $.http.post('/bigin/v2/actions/watch', payload);
|
||||
|
||||
await $.flow.setRemoteWebhookId(organizationId);
|
||||
},
|
||||
|
||||
async unregisterHook($) {
|
||||
await $.http.delete(
|
||||
`/bigin/v2/actions/watch?channel_ids=${$.flow.remoteWebhookId}`
|
||||
);
|
||||
},
|
||||
});
|
@@ -0,0 +1,89 @@
|
||||
import Crypto from 'crypto';
|
||||
import defineTrigger from '../../../../helpers/define-trigger.js';
|
||||
|
||||
export default defineTrigger({
|
||||
name: 'New contacts',
|
||||
key: 'newContacts',
|
||||
type: 'webhook',
|
||||
description: 'Triggers when a new contact is created.',
|
||||
arguments: [
|
||||
{
|
||||
label: 'Organization',
|
||||
key: 'organizationId',
|
||||
type: 'dropdown',
|
||||
required: true,
|
||||
description: '',
|
||||
variables: false,
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listOrganizations',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
const dataItem = {
|
||||
raw: $.request.body,
|
||||
meta: {
|
||||
internalId: Crypto.randomUUID(),
|
||||
},
|
||||
};
|
||||
|
||||
$.pushTriggerItem(dataItem);
|
||||
},
|
||||
|
||||
async testRun($) {
|
||||
const organizationId = $.step.parameters.organizationId;
|
||||
|
||||
const sampleEventData = {
|
||||
ids: ['111111111111111111'],
|
||||
token: null,
|
||||
module: 'Contacts',
|
||||
operation: 'insert',
|
||||
channel_id: organizationId,
|
||||
server_time: 1708426963120,
|
||||
query_params: {},
|
||||
resource_uri: `${$.auth.data.apiDomain}/bigin/v1/Contacts`,
|
||||
affected_fields: [],
|
||||
};
|
||||
|
||||
const dataItem = {
|
||||
raw: sampleEventData,
|
||||
meta: {
|
||||
internalId: sampleEventData.channel_id,
|
||||
},
|
||||
};
|
||||
|
||||
$.pushTriggerItem(dataItem);
|
||||
},
|
||||
|
||||
async registerHook($) {
|
||||
const organizationId = $.step.parameters.organizationId;
|
||||
|
||||
const payload = {
|
||||
watch: [
|
||||
{
|
||||
channel_id: organizationId,
|
||||
notify_url: $.webhookUrl,
|
||||
events: ['Contacts.create'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await $.http.post('/bigin/v2/actions/watch', payload);
|
||||
|
||||
await $.flow.setRemoteWebhookId(organizationId);
|
||||
},
|
||||
|
||||
async unregisterHook($) {
|
||||
await $.http.delete(
|
||||
`/bigin/v2/actions/watch?channel_ids=${$.flow.remoteWebhookId}`
|
||||
);
|
||||
},
|
||||
});
|
@@ -0,0 +1,89 @@
|
||||
import Crypto from 'crypto';
|
||||
import defineTrigger from '../../../../helpers/define-trigger.js';
|
||||
|
||||
export default defineTrigger({
|
||||
name: 'New products',
|
||||
key: 'newProducts',
|
||||
type: 'webhook',
|
||||
description: 'Triggers when a new product is created.',
|
||||
arguments: [
|
||||
{
|
||||
label: 'Organization',
|
||||
key: 'organizationId',
|
||||
type: 'dropdown',
|
||||
required: true,
|
||||
description: '',
|
||||
variables: false,
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listOrganizations',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
const dataItem = {
|
||||
raw: $.request.body,
|
||||
meta: {
|
||||
internalId: Crypto.randomUUID(),
|
||||
},
|
||||
};
|
||||
|
||||
$.pushTriggerItem(dataItem);
|
||||
},
|
||||
|
||||
async testRun($) {
|
||||
const organizationId = $.step.parameters.organizationId;
|
||||
|
||||
const sampleEventData = {
|
||||
ids: ['111111111111111111'],
|
||||
token: null,
|
||||
module: 'Products',
|
||||
operation: 'insert',
|
||||
channel_id: organizationId,
|
||||
server_time: 1708426963120,
|
||||
query_params: {},
|
||||
resource_uri: `${$.auth.data.apiDomain}/bigin/v1/Products`,
|
||||
affected_fields: [],
|
||||
};
|
||||
|
||||
const dataItem = {
|
||||
raw: sampleEventData,
|
||||
meta: {
|
||||
internalId: sampleEventData.channel_id,
|
||||
},
|
||||
};
|
||||
|
||||
$.pushTriggerItem(dataItem);
|
||||
},
|
||||
|
||||
async registerHook($) {
|
||||
const organizationId = $.step.parameters.organizationId;
|
||||
|
||||
const payload = {
|
||||
watch: [
|
||||
{
|
||||
channel_id: organizationId,
|
||||
notify_url: $.webhookUrl,
|
||||
events: ['Products.create'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await $.http.post('/bigin/v2/actions/watch', payload);
|
||||
|
||||
await $.flow.setRemoteWebhookId(organizationId);
|
||||
},
|
||||
|
||||
async unregisterHook($) {
|
||||
await $.http.delete(
|
||||
`/bigin/v2/actions/watch?channel_ids=${$.flow.remoteWebhookId}`
|
||||
);
|
||||
},
|
||||
});
|
@@ -0,0 +1,89 @@
|
||||
import Crypto from 'crypto';
|
||||
import defineTrigger from '../../../../helpers/define-trigger.js';
|
||||
|
||||
export default defineTrigger({
|
||||
name: 'New tasks',
|
||||
key: 'newTasks',
|
||||
type: 'webhook',
|
||||
description: 'Triggers when a new task is created.',
|
||||
arguments: [
|
||||
{
|
||||
label: 'Organization',
|
||||
key: 'organizationId',
|
||||
type: 'dropdown',
|
||||
required: true,
|
||||
description: '',
|
||||
variables: false,
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listOrganizations',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
const dataItem = {
|
||||
raw: $.request.body,
|
||||
meta: {
|
||||
internalId: Crypto.randomUUID(),
|
||||
},
|
||||
};
|
||||
|
||||
$.pushTriggerItem(dataItem);
|
||||
},
|
||||
|
||||
async testRun($) {
|
||||
const organizationId = $.step.parameters.organizationId;
|
||||
|
||||
const sampleEventData = {
|
||||
ids: ['111111111111111111'],
|
||||
token: null,
|
||||
module: 'Tasks',
|
||||
operation: 'insert',
|
||||
channel_id: organizationId,
|
||||
server_time: 1708426963120,
|
||||
query_params: {},
|
||||
resource_uri: `${$.auth.data.apiDomain}/bigin/v1/Tasks`,
|
||||
affected_fields: [],
|
||||
};
|
||||
|
||||
const dataItem = {
|
||||
raw: sampleEventData,
|
||||
meta: {
|
||||
internalId: sampleEventData.channel_id,
|
||||
},
|
||||
};
|
||||
|
||||
$.pushTriggerItem(dataItem);
|
||||
},
|
||||
|
||||
async registerHook($) {
|
||||
const organizationId = $.step.parameters.organizationId;
|
||||
|
||||
const payload = {
|
||||
watch: [
|
||||
{
|
||||
channel_id: organizationId,
|
||||
notify_url: $.webhookUrl,
|
||||
events: ['Tasks.create'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await $.http.post('/bigin/v2/actions/watch', payload);
|
||||
|
||||
await $.flow.setRemoteWebhookId(organizationId);
|
||||
},
|
||||
|
||||
async unregisterHook($) {
|
||||
await $.http.delete(
|
||||
`/bigin/v2/actions/watch?channel_ids=${$.flow.remoteWebhookId}`
|
||||
);
|
||||
},
|
||||
});
|
@@ -1,5 +1,6 @@
|
||||
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';
|
||||
@@ -8,10 +9,12 @@ 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,
|
||||
@@ -20,6 +23,7 @@ const transformers = {
|
||||
markdownToHtml,
|
||||
pluralize,
|
||||
replace,
|
||||
stringToBase64,
|
||||
trimWhitespace,
|
||||
useDefaultValue,
|
||||
};
|
||||
@@ -37,6 +41,7 @@ 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' },
|
||||
@@ -45,6 +50,7 @@ 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' },
|
||||
],
|
||||
|
@@ -0,0 +1,8 @@
|
||||
const base64ToString = ($) => {
|
||||
const input = $.step.parameters.input;
|
||||
const decodedString = Buffer.from(input, 'base64').toString('utf8');
|
||||
|
||||
return decodedString;
|
||||
};
|
||||
|
||||
export default base64ToString;
|
@@ -0,0 +1,8 @@
|
||||
const stringtoBase64 = ($) => {
|
||||
const input = $.step.parameters.input;
|
||||
const base64String = Buffer.from(input).toString('base64');
|
||||
|
||||
return base64String;
|
||||
};
|
||||
|
||||
export default stringtoBase64;
|
@@ -1,3 +1,4 @@
|
||||
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';
|
||||
@@ -6,6 +7,7 @@ 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';
|
||||
@@ -15,6 +17,7 @@ import formatPhoneNumber from './numbers/format-phone-number.js';
|
||||
import formatDateTime from './date-time/format-date-time.js';
|
||||
|
||||
const options = {
|
||||
base64ToString,
|
||||
capitalize,
|
||||
extractEmailAddress,
|
||||
extractNumber,
|
||||
@@ -23,6 +26,7 @@ const options = {
|
||||
markdownToHtml,
|
||||
pluralize,
|
||||
replace,
|
||||
stringToBase64,
|
||||
trimWhitespace,
|
||||
useDefaultValue,
|
||||
performMathOperation,
|
||||
|
@@ -0,0 +1,12 @@
|
||||
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;
|
@@ -0,0 +1,12 @@
|
||||
const stringToBase64 = [
|
||||
{
|
||||
label: 'Input',
|
||||
key: 'input',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Text that will be converted to Base64.',
|
||||
variables: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default stringToBase64;
|
@@ -18,7 +18,9 @@ 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(
|
||||
process.env.API_URL || `${protocol}://${host}:${port}`
|
||||
).toString();
|
||||
apiUrl = apiUrl.substring(0, apiUrl.length - 1);
|
||||
|
||||
// use apiUrl by default, which has less priority over the following cases
|
||||
@@ -88,6 +90,10 @@ 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) {
|
||||
|
@@ -0,0 +1,6 @@
|
||||
import appConfig from '../../../../config/app.js';
|
||||
import { renderObject } from '../../../../helpers/renderer.js';
|
||||
|
||||
export default async (request, response) => {
|
||||
renderObject(response, { version: appConfig.version });
|
||||
};
|
@@ -0,0 +1,26 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import request from 'supertest';
|
||||
import app from '../../../../app.js';
|
||||
|
||||
describe('GET /api/v1/automatisch/version', () => {
|
||||
it('should return Automatisch version', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/v1/automatisch/version')
|
||||
.expect(200);
|
||||
|
||||
const expectedPayload = {
|
||||
data: {
|
||||
version: '0.10.0',
|
||||
},
|
||||
meta: {
|
||||
count: 1,
|
||||
currentPage: null,
|
||||
isArray: false,
|
||||
totalPages: null,
|
||||
type: 'Object',
|
||||
},
|
||||
};
|
||||
|
||||
expect(response.body).toEqual(expectedPayload);
|
||||
});
|
||||
});
|
@@ -0,0 +1,5 @@
|
||||
import { renderObject } from '../../../../helpers/renderer.js';
|
||||
|
||||
export default async (request, response) => {
|
||||
renderObject(response, request.currentUser);
|
||||
};
|
@@ -0,0 +1,26 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import request from 'supertest';
|
||||
import app from '../../../../app.js';
|
||||
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
|
||||
import { createUser } from '../../../../../test/factories/user';
|
||||
import getCurrentUserMock from '../../../../../test/mocks/rest/api/v1/users/get-current-user';
|
||||
|
||||
describe('GET /api/v1/users/me', () => {
|
||||
let role, currentUser, token;
|
||||
|
||||
beforeEach(async () => {
|
||||
currentUser = await createUser();
|
||||
role = await currentUser.$relatedQuery('role');
|
||||
token = createAuthTokenByUserId(currentUser.id);
|
||||
});
|
||||
|
||||
it('should return current user info', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/v1/users/me')
|
||||
.set('Authorization', token)
|
||||
.expect(200);
|
||||
|
||||
const expectedPayload = getCurrentUserMock(currentUser, role);
|
||||
expect(response.body).toEqual(expectedPayload);
|
||||
});
|
||||
});
|
@@ -0,0 +1,12 @@
|
||||
import { renderObject } from '../../../../helpers/renderer.js';
|
||||
|
||||
export default async (request, response) => {
|
||||
const inTrial = await request.currentUser.inTrial();
|
||||
|
||||
const trialInfo = {
|
||||
inTrial,
|
||||
expireAt: request.currentUser.trialExpiryDate,
|
||||
};
|
||||
|
||||
renderObject(response, trialInfo);
|
||||
};
|
@@ -0,0 +1,38 @@
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import request from 'supertest';
|
||||
import app from '../../../../app.js';
|
||||
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js';
|
||||
import { createUser } from '../../../../../test/factories/user.js';
|
||||
import getUserTrialMock from '../../../../../test/mocks/rest/api/v1/users/get-user-trial.js';
|
||||
import appConfig from '../../../../config/app.js';
|
||||
import { DateTime } from 'luxon';
|
||||
import User from '../../../../models/user.js';
|
||||
|
||||
describe('GET /api/v1/users/:userId/trial', () => {
|
||||
let user, token;
|
||||
|
||||
beforeEach(async () => {
|
||||
const trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate();
|
||||
user = await createUser({ trialExpiryDate });
|
||||
token = createAuthTokenByUserId(user.id);
|
||||
|
||||
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true);
|
||||
});
|
||||
|
||||
describe('should return in trial, active subscription and expire at info', () => {
|
||||
beforeEach(async () => {
|
||||
vi.spyOn(User.prototype, 'inTrial').mockResolvedValue(false);
|
||||
vi.spyOn(User.prototype, 'hasActiveSubscription').mockResolvedValue(true);
|
||||
});
|
||||
|
||||
it('should return null', async () => {
|
||||
const response = await request(app)
|
||||
.get(`/api/v1/users/${user.id}/trial`)
|
||||
.set('Authorization', token)
|
||||
.expect(200);
|
||||
|
||||
const expectedResponsePayload = await getUserTrialMock(user);
|
||||
expect(response.body).toEqual(expectedResponsePayload);
|
||||
});
|
||||
});
|
||||
});
|
16
packages/backend/src/controllers/api/v1/users/get-user.js
Normal file
16
packages/backend/src/controllers/api/v1/users/get-user.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { renderObject } from '../../../../helpers/renderer.js';
|
||||
import User from '../../../../models/user.js';
|
||||
|
||||
export default async (request, response) => {
|
||||
const user = await User.query()
|
||||
.leftJoinRelated({
|
||||
role: true,
|
||||
})
|
||||
.withGraphFetched({
|
||||
role: true,
|
||||
})
|
||||
.findById(request.params.userId)
|
||||
.throwIfNotFound();
|
||||
|
||||
renderObject(response, user);
|
||||
};
|
@@ -0,0 +1,36 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import request from 'supertest';
|
||||
import app from '../../../../app.js';
|
||||
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
|
||||
import { createUser } from '../../../../../test/factories/user';
|
||||
import { createPermission } from '../../../../../test/factories/permission';
|
||||
import getUserMock from '../../../../../test/mocks/rest/api/v1/users/get-user';
|
||||
|
||||
describe('GET /api/v1/users/:userId', () => {
|
||||
let currentUser, currentUserRole, anotherUser, anotherUserRole, token;
|
||||
|
||||
beforeEach(async () => {
|
||||
currentUser = await createUser();
|
||||
anotherUser = await createUser();
|
||||
currentUserRole = await currentUser.$relatedQuery('role');
|
||||
anotherUserRole = await anotherUser.$relatedQuery('role');
|
||||
|
||||
await createPermission({
|
||||
roleId: currentUserRole.id,
|
||||
action: 'read',
|
||||
subject: 'User',
|
||||
});
|
||||
|
||||
token = createAuthTokenByUserId(currentUser.id);
|
||||
});
|
||||
|
||||
it('should return specified user info', async () => {
|
||||
const response = await request(app)
|
||||
.get(`/api/v1/users/${anotherUser.id}`)
|
||||
.set('Authorization', token)
|
||||
.expect(200);
|
||||
|
||||
const expectedPayload = getUserMock(anotherUser, anotherUserRole);
|
||||
expect(response.body).toEqual(expectedPayload);
|
||||
});
|
||||
});
|
18
packages/backend/src/controllers/api/v1/users/get-users.js
Normal file
18
packages/backend/src/controllers/api/v1/users/get-users.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { renderObject } from '../../../../helpers/renderer.js';
|
||||
import User from '../../../../models/user.js';
|
||||
import paginateRest from '../../../../helpers/pagination-rest.js';
|
||||
|
||||
export default async (request, response) => {
|
||||
const usersQuery = User.query()
|
||||
.leftJoinRelated({
|
||||
role: true,
|
||||
})
|
||||
.withGraphFetched({
|
||||
role: true,
|
||||
})
|
||||
.orderBy('full_name', 'asc');
|
||||
|
||||
const users = await paginateRest(usersQuery, request.query.page);
|
||||
|
||||
renderObject(response, users);
|
||||
};
|
@@ -0,0 +1,56 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import request from 'supertest';
|
||||
import app from '../../../../app';
|
||||
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
|
||||
import { createRole } from '../../../../../test/factories/role';
|
||||
import { createPermission } from '../../../../../test/factories/permission';
|
||||
import { createUser } from '../../../../../test/factories/user';
|
||||
import getUsersMock from '../../../../../test/mocks/rest/api/v1/users/get-users';
|
||||
|
||||
describe('GET /api/v1/users', () => {
|
||||
let currentUser, currentUserRole, anotherUser, anotherUserRole, token;
|
||||
|
||||
beforeEach(async () => {
|
||||
currentUserRole = await createRole({
|
||||
key: 'currentUser',
|
||||
name: 'Current user role',
|
||||
});
|
||||
|
||||
await createPermission({
|
||||
action: 'read',
|
||||
subject: 'User',
|
||||
roleId: currentUserRole.id,
|
||||
});
|
||||
|
||||
currentUser = await createUser({
|
||||
roleId: currentUserRole.id,
|
||||
fullName: 'Current User',
|
||||
});
|
||||
|
||||
anotherUserRole = await createRole({
|
||||
key: 'anotherUser',
|
||||
name: 'Another user role',
|
||||
});
|
||||
|
||||
anotherUser = await createUser({
|
||||
roleId: anotherUserRole.id,
|
||||
fullName: 'Another User',
|
||||
});
|
||||
|
||||
token = createAuthTokenByUserId(currentUser.id);
|
||||
});
|
||||
|
||||
it('should return users data', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/v1/users')
|
||||
.set('Authorization', token)
|
||||
.expect(200);
|
||||
|
||||
const expectedResponsePayload = await getUsersMock(
|
||||
[anotherUser, currentUser],
|
||||
[anotherUserRole, currentUserRole]
|
||||
);
|
||||
|
||||
expect(response.body).toEqual(expectedResponsePayload);
|
||||
});
|
||||
});
|
3
packages/backend/src/controllers/healthcheck/index.js
Normal file
3
packages/backend/src/controllers/healthcheck/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export default async (request, response) => {
|
||||
response.status(200).end();
|
||||
};
|
@@ -0,0 +1,9 @@
|
||||
import { describe, it } from 'vitest';
|
||||
import request from 'supertest';
|
||||
import app from '../../app.js';
|
||||
|
||||
describe('GET /healthcheck', () => {
|
||||
it('should return 200 response with version data', async () => {
|
||||
await request(app).get('/healthcheck').expect(200);
|
||||
});
|
||||
});
|
@@ -1,7 +1,10 @@
|
||||
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({
|
||||
|
@@ -1,9 +1,17 @@
|
||||
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)) {
|
||||
@@ -18,7 +26,7 @@ const getConfig = async (_parent, params) => {
|
||||
computedConfig[key] = value?.data;
|
||||
|
||||
return computedConfig;
|
||||
}, {});
|
||||
}, defaultConfig);
|
||||
};
|
||||
|
||||
export default getConfig;
|
||||
|
@@ -2,6 +2,7 @@ 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', () => {
|
||||
@@ -56,6 +57,10 @@ 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,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -82,6 +87,48 @@ 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',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@@ -20,7 +20,8 @@ export const isAuthenticated = async (_parent, _args, req) => {
|
||||
.withGraphFetched({
|
||||
role: true,
|
||||
permissions: true,
|
||||
});
|
||||
})
|
||||
.throwIfNotFound();
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
@@ -28,6 +29,14 @@ export const isAuthenticated = async (_parent, _args, req) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const authenticateUser = async (request, response, next) => {
|
||||
if (await isAuthenticated(null, null, request)) {
|
||||
next();
|
||||
} else {
|
||||
return response.status(401).end();
|
||||
}
|
||||
};
|
||||
|
||||
const isAuthenticatedRule = rule()(isAuthenticated);
|
||||
|
||||
export const authenticationRules = {
|
||||
|
@@ -1,11 +1,8 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { describe, it, expect } 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');
|
||||
import { createUser } from '../../test/factories/user.js';
|
||||
import createAuthTokenByUserId from '../helpers/create-auth-token-by-user-id.js';
|
||||
|
||||
describe('isAuthenticated', () => {
|
||||
it('should return false if no token is provided', async () => {
|
||||
@@ -14,29 +11,26 @@ describe('isAuthenticated', () => {
|
||||
});
|
||||
|
||||
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' });
|
||||
it('should return true if token is valid and there is a user', async () => {
|
||||
const user = await createUser();
|
||||
const token = createAuthTokenByUserId(user.id);
|
||||
|
||||
User.query.mockReturnValue({
|
||||
findById: vi.fn().mockReturnValue({
|
||||
leftJoinRelated: vi.fn().mockReturnThis(),
|
||||
withGraphFetched: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ id: '123', role: {}, permissions: {} }),
|
||||
}),
|
||||
});
|
||||
|
||||
const req = { headers: { authorization: 'validToken' } };
|
||||
const req = { headers: { authorization: token } };
|
||||
expect(await isAuthenticated(null, null, req)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if token is valid and but there is no user', async () => {
|
||||
const user = await createUser();
|
||||
const token = createAuthTokenByUserId(user.id);
|
||||
await user.$query().delete();
|
||||
|
||||
const req = { headers: { authorization: token } };
|
||||
expect(await isAuthenticated(null, null, req)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('authentication rules', () => {
|
||||
|
22
packages/backend/src/helpers/authorization.js
Normal file
22
packages/backend/src/helpers/authorization.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const authorizationList = {
|
||||
'/api/v1/users/:userId': {
|
||||
action: 'read',
|
||||
subject: 'User',
|
||||
},
|
||||
'/api/v1/users/': {
|
||||
action: 'read',
|
||||
subject: 'User',
|
||||
},
|
||||
};
|
||||
|
||||
export const authorizeUser = async (request, response, next) => {
|
||||
const currentRoute = request.baseUrl + request.route.path;
|
||||
const currentRouteRule = authorizationList[currentRoute];
|
||||
|
||||
try {
|
||||
request.currentUser.can(currentRouteRule.action, currentRouteRule.subject);
|
||||
next();
|
||||
} catch (error) {
|
||||
return response.status(403).end();
|
||||
}
|
||||
};
|
11
packages/backend/src/helpers/check-is-cloud.js
Normal file
11
packages/backend/src/helpers/check-is-cloud.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import appConfig from '../config/app.js';
|
||||
|
||||
export const checkIsCloud = async (request, response, next) => {
|
||||
if (appConfig.isCloud) {
|
||||
next();
|
||||
} else {
|
||||
return response.status(404).end();
|
||||
}
|
||||
};
|
||||
|
||||
export default checkIsCloud;
|
@@ -1,6 +1,9 @@
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as handlebars from 'handlebars';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import handlebars from 'handlebars';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const compileEmail = (emailPath, replacements = {}) => {
|
||||
const filePath = path.join(__dirname, `../views/emails/${emailPath}.ee.hbs`);
|
||||
|
25
packages/backend/src/helpers/pagination-rest.js
Normal file
25
packages/backend/src/helpers/pagination-rest.js
Normal file
@@ -0,0 +1,25 @@
|
||||
const paginateRest = async (query, page) => {
|
||||
const pageSize = 10;
|
||||
|
||||
page = parseInt(page, 10);
|
||||
|
||||
if (isNaN(page) || page < 1) {
|
||||
page = 1;
|
||||
}
|
||||
|
||||
const [records, count] = await Promise.all([
|
||||
query.limit(pageSize).offset((page - 1) * pageSize),
|
||||
query.resultSize(),
|
||||
]);
|
||||
|
||||
return {
|
||||
pageInfo: {
|
||||
currentPage: page,
|
||||
totalPages: Math.ceil(count / pageSize),
|
||||
},
|
||||
totalCount: count,
|
||||
records,
|
||||
};
|
||||
};
|
||||
|
||||
export default paginateRest;
|
42
packages/backend/src/helpers/renderer.js
Normal file
42
packages/backend/src/helpers/renderer.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import serializers from '../serializers/index.js';
|
||||
|
||||
const isPaginated = (object) =>
|
||||
object?.pageInfo &&
|
||||
object?.totalCount !== undefined &&
|
||||
Array.isArray(object?.records);
|
||||
|
||||
const isArray = (object) =>
|
||||
Array.isArray(object) || Array.isArray(object?.records);
|
||||
|
||||
const totalCount = (object) =>
|
||||
isPaginated(object) ? object.totalCount : isArray(object) ? object.length : 1;
|
||||
|
||||
const renderObject = (response, object) => {
|
||||
let data = isPaginated(object) ? object.records : object;
|
||||
const type = isPaginated(object)
|
||||
? object.records[0].constructor.name
|
||||
: object.constructor.name;
|
||||
|
||||
const serializer = serializers[type];
|
||||
|
||||
if (serializer) {
|
||||
data = Array.isArray(data)
|
||||
? data.map((item) => serializer(item))
|
||||
: serializer(data);
|
||||
}
|
||||
|
||||
const computedPayload = {
|
||||
data,
|
||||
meta: {
|
||||
type,
|
||||
count: totalCount(object),
|
||||
isArray: isArray(object),
|
||||
currentPage: isPaginated(object) ? object.pageInfo.currentPage : null,
|
||||
totalPages: isPaginated(object) ? object.pageInfo.totalPages : null,
|
||||
},
|
||||
};
|
||||
|
||||
return response.json(computedPayload);
|
||||
};
|
||||
|
||||
export { renderObject };
|
@@ -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);
|
||||
|
8
packages/backend/src/routes/api/v1/automatisch.js
Normal file
8
packages/backend/src/routes/api/v1/automatisch.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Router } from 'express';
|
||||
import versionAction from '../../../controllers/api/v1/automatisch/version.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/version', versionAction);
|
||||
|
||||
export default router;
|
22
packages/backend/src/routes/api/v1/users.js
Normal file
22
packages/backend/src/routes/api/v1/users.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Router } from 'express';
|
||||
import { authenticateUser } from '../../../helpers/authentication.js';
|
||||
import { authorizeUser } from '../../../helpers/authorization.js';
|
||||
import checkIsCloud from '../../../helpers/check-is-cloud.js';
|
||||
import getCurrentUserAction from '../../../controllers/api/v1/users/get-current-user.js';
|
||||
import getUserAction from '../../../controllers/api/v1/users/get-user.js';
|
||||
import getUsersAction from '../../../controllers/api/v1/users/get-users.js';
|
||||
import getUserTrialAction from '../../../controllers/api/v1/users/get-user-trial.ee.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/', authenticateUser, authorizeUser, getUsersAction);
|
||||
router.get('/me', authenticateUser, getCurrentUserAction);
|
||||
router.get('/:userId', authenticateUser, authorizeUser, getUserAction);
|
||||
router.get(
|
||||
'/:userId/trial',
|
||||
authenticateUser,
|
||||
checkIsCloud,
|
||||
getUserTrialAction
|
||||
);
|
||||
|
||||
export default router;
|
8
packages/backend/src/routes/healthcheck.js
Normal file
8
packages/backend/src/routes/healthcheck.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Router } from 'express';
|
||||
import indexAction from '../controllers/healthcheck/index.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/', indexAction);
|
||||
|
||||
export default router;
|
@@ -2,11 +2,17 @@ import { Router } from 'express';
|
||||
import graphQLInstance from '../helpers/graphql-instance.js';
|
||||
import webhooksRouter from './webhooks.js';
|
||||
import paddleRouter from './paddle.ee.js';
|
||||
import healthcheckRouter from './healthcheck.js';
|
||||
import automatischRouter from './api/v1/automatisch.js';
|
||||
import usersRouter from './api/v1/users.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use('/graphql', graphQLInstance);
|
||||
router.use('/webhooks', webhooksRouter);
|
||||
router.use('/paddle', paddleRouter);
|
||||
router.use('/healthcheck', healthcheckRouter);
|
||||
router.use('/api/v1/automatisch', automatischRouter);
|
||||
router.use('/api/v1/users', usersRouter);
|
||||
|
||||
export default router;
|
||||
|
11
packages/backend/src/serializers/index.js
Normal file
11
packages/backend/src/serializers/index.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import userSerializer from './user.js';
|
||||
import roleSerializer from './role.js';
|
||||
import permissionSerializer from './permission.js';
|
||||
|
||||
const serializers = {
|
||||
User: userSerializer,
|
||||
Role: roleSerializer,
|
||||
Permission: permissionSerializer,
|
||||
};
|
||||
|
||||
export default serializers;
|
13
packages/backend/src/serializers/permission.js
Normal file
13
packages/backend/src/serializers/permission.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const permissionSerializer = (permission) => {
|
||||
return {
|
||||
id: permission.id,
|
||||
roleId: permission.roleId,
|
||||
action: permission.action,
|
||||
subject: permission.subject,
|
||||
conditions: permission.conditions,
|
||||
createdAt: permission.createdAt,
|
||||
updatedAt: permission.updatedAt,
|
||||
};
|
||||
};
|
||||
|
||||
export default permissionSerializer;
|
25
packages/backend/src/serializers/permission.test.js
Normal file
25
packages/backend/src/serializers/permission.test.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { createPermission } from '../../test/factories/permission';
|
||||
import permissionSerializer from './permission';
|
||||
|
||||
describe('permissionSerializer', () => {
|
||||
let permission;
|
||||
|
||||
beforeEach(async () => {
|
||||
permission = await createPermission();
|
||||
});
|
||||
|
||||
it('should return permission data', async () => {
|
||||
const expectedPayload = {
|
||||
id: permission.id,
|
||||
roleId: permission.roleId,
|
||||
action: permission.action,
|
||||
subject: permission.subject,
|
||||
conditions: permission.conditions,
|
||||
createdAt: permission.createdAt,
|
||||
updatedAt: permission.updatedAt,
|
||||
};
|
||||
|
||||
expect(permissionSerializer(permission)).toEqual(expectedPayload);
|
||||
});
|
||||
});
|
13
packages/backend/src/serializers/role.js
Normal file
13
packages/backend/src/serializers/role.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const roleSerializer = (role) => {
|
||||
return {
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
key: role.key,
|
||||
description: role.description,
|
||||
createdAt: role.createdAt,
|
||||
updatedAt: role.updatedAt,
|
||||
isAdmin: role.isAdmin,
|
||||
};
|
||||
};
|
||||
|
||||
export default roleSerializer;
|
25
packages/backend/src/serializers/role.test.js
Normal file
25
packages/backend/src/serializers/role.test.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { createRole } from '../../test/factories/role';
|
||||
import roleSerializer from './role';
|
||||
|
||||
describe('roleSerializer', () => {
|
||||
let role;
|
||||
|
||||
beforeEach(async () => {
|
||||
role = await createRole();
|
||||
});
|
||||
|
||||
it('should return role data', async () => {
|
||||
const expectedPayload = {
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
key: role.key,
|
||||
description: role.description,
|
||||
createdAt: role.createdAt,
|
||||
updatedAt: role.updatedAt,
|
||||
isAdmin: role.isAdmin,
|
||||
};
|
||||
|
||||
expect(roleSerializer(role)).toEqual(expectedPayload);
|
||||
});
|
||||
});
|
32
packages/backend/src/serializers/user.js
Normal file
32
packages/backend/src/serializers/user.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import roleSerializer from './role.js';
|
||||
import permissionSerializer from './permission.js';
|
||||
import appConfig from '../config/app.js';
|
||||
|
||||
const userSerializer = (user) => {
|
||||
let userData = {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
createdAt: user.createdAt,
|
||||
updatedAt: user.updatedAt,
|
||||
fullName: user.fullName,
|
||||
roleId: user.roleId,
|
||||
};
|
||||
|
||||
if (user.role) {
|
||||
userData.role = roleSerializer(user.role);
|
||||
}
|
||||
|
||||
if (user.permissions) {
|
||||
userData.permissions = user.permissions.map((permission) =>
|
||||
permissionSerializer(permission)
|
||||
);
|
||||
}
|
||||
|
||||
if (appConfig.isCloud && user.trialExpiryDate) {
|
||||
userData.trialExpiryDate = user.trialExpiryDate;
|
||||
}
|
||||
|
||||
return userData;
|
||||
};
|
||||
|
||||
export default userSerializer;
|
76
packages/backend/src/serializers/user.test.js
Normal file
76
packages/backend/src/serializers/user.test.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import { DateTime } from 'luxon';
|
||||
import appConfig from '../config/app';
|
||||
import { createUser } from '../../test/factories/user';
|
||||
import { createPermission } from '../../test/factories/permission';
|
||||
import userSerializer from './user';
|
||||
|
||||
describe('userSerializer', () => {
|
||||
let user, role, permissionOne, permissionTwo;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await createUser();
|
||||
role = await user.$relatedQuery('role');
|
||||
|
||||
permissionOne = await createPermission({
|
||||
roleId: role.id,
|
||||
action: 'read',
|
||||
subject: 'User',
|
||||
});
|
||||
|
||||
permissionTwo = await createPermission({
|
||||
roleId: role.id,
|
||||
action: 'read',
|
||||
subject: 'Role',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return user data', async () => {
|
||||
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false);
|
||||
|
||||
const expectedPayload = {
|
||||
createdAt: user.createdAt,
|
||||
email: user.email,
|
||||
fullName: user.fullName,
|
||||
id: user.id,
|
||||
roleId: user.roleId,
|
||||
updatedAt: user.updatedAt,
|
||||
};
|
||||
|
||||
expect(userSerializer(user)).toEqual(expectedPayload);
|
||||
});
|
||||
|
||||
it('should return user data with the role', async () => {
|
||||
user.role = role;
|
||||
|
||||
const expectedPayload = {
|
||||
role,
|
||||
};
|
||||
|
||||
expect(userSerializer(user)).toMatchObject(expectedPayload);
|
||||
});
|
||||
|
||||
it('should return user data with the permissions', async () => {
|
||||
user.permissions = [permissionOne, permissionTwo];
|
||||
|
||||
const expectedPayload = {
|
||||
permissions: [permissionOne, permissionTwo],
|
||||
};
|
||||
|
||||
expect(userSerializer(user)).toMatchObject(expectedPayload);
|
||||
});
|
||||
|
||||
it('should return user data with trial expiry date', async () => {
|
||||
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true);
|
||||
|
||||
await user.$query().patch({
|
||||
trialExpiryDate: DateTime.now().plus({ days: 30 }).toISODate(),
|
||||
});
|
||||
|
||||
const expectedPayload = {
|
||||
trialExpiryDate: user.trialExpiryDate,
|
||||
};
|
||||
|
||||
expect(userSerializer(user)).toMatchObject(expectedPayload);
|
||||
});
|
||||
});
|
@@ -1,4 +1,5 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import Config from '../../src/models/config';
|
||||
|
||||
export const createConfig = async (params = {}) => {
|
||||
const configData = {
|
||||
@@ -6,10 +7,7 @@ export const createConfig = async (params = {}) => {
|
||||
value: params?.value || { data: 'sampleConfig' },
|
||||
};
|
||||
|
||||
const [config] = await global.knex
|
||||
.table('config')
|
||||
.insert(configData)
|
||||
.returning('*');
|
||||
const config = await Config.query().insert(configData).returning('*');
|
||||
|
||||
return config;
|
||||
};
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import appConfig from '../../src/config/app';
|
||||
import { AES } from 'crypto-js';
|
||||
import Connection from '../../src/models/connection';
|
||||
|
||||
export const createConnection = async (params = {}) => {
|
||||
params.key = params?.key || 'deepl';
|
||||
@@ -16,10 +17,7 @@ export const createConnection = async (params = {}) => {
|
||||
appConfig.encryptionKey
|
||||
).toString();
|
||||
|
||||
const [connection] = await global.knex
|
||||
.table('connections')
|
||||
.insert(params)
|
||||
.returning('*');
|
||||
const connection = await Connection.query().insert(params).returning('*');
|
||||
|
||||
return connection;
|
||||
};
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import ExecutionStep from '../../src/models/execution-step';
|
||||
import { createExecution } from './execution';
|
||||
import { createStep } from './step';
|
||||
|
||||
@@ -8,8 +9,7 @@ export const createExecutionStep = async (params = {}) => {
|
||||
params.dataIn = params?.dataIn || { dataIn: 'dataIn' };
|
||||
params.dataOut = params?.dataOut || { dataOut: 'dataOut' };
|
||||
|
||||
const [executionStep] = await global.knex
|
||||
.table('executionSteps')
|
||||
const executionStep = await ExecutionStep.query()
|
||||
.insert(params)
|
||||
.returning('*');
|
||||
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import Execution from '../../src/models/execution';
|
||||
import { createFlow } from './flow';
|
||||
|
||||
export const createExecution = async (params = {}) => {
|
||||
@@ -6,10 +7,7 @@ export const createExecution = async (params = {}) => {
|
||||
params.createdAt = params?.createdAt || new Date().toISOString();
|
||||
params.updatedAt = params?.updatedAt || new Date().toISOString();
|
||||
|
||||
const [execution] = await global.knex
|
||||
.table('executions')
|
||||
.insert(params)
|
||||
.returning('*');
|
||||
const execution = await Execution.query().insert(params).returning('*');
|
||||
|
||||
return execution;
|
||||
};
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import Flow from '../../src/models/flow';
|
||||
import { createUser } from './user';
|
||||
|
||||
export const createFlow = async (params = {}) => {
|
||||
@@ -6,7 +7,7 @@ export const createFlow = async (params = {}) => {
|
||||
params.createdAt = params?.createdAt || new Date().toISOString();
|
||||
params.updatedAt = params?.updatedAt || new Date().toISOString();
|
||||
|
||||
const [flow] = await global.knex.table('flows').insert(params).returning('*');
|
||||
const flow = await Flow.query().insert(params).returning('*');
|
||||
|
||||
return flow;
|
||||
};
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import Permission from '../../src/models/permission';
|
||||
import { createRole } from './role';
|
||||
|
||||
export const createPermission = async (params = {}) => {
|
||||
@@ -6,10 +7,7 @@ export const createPermission = async (params = {}) => {
|
||||
params.subject = params?.subject || 'User';
|
||||
params.conditions = params?.conditions || ['isCreator'];
|
||||
|
||||
const [permission] = await global.knex
|
||||
.table('permissions')
|
||||
.insert(params)
|
||||
.returning('*');
|
||||
const permission = await Permission.query().insert(params).returning('*');
|
||||
|
||||
return permission;
|
||||
};
|
||||
|
@@ -1,8 +1,10 @@
|
||||
import Role from '../../src/models/role';
|
||||
|
||||
export const createRole = async (params = {}) => {
|
||||
params.name = params?.name || 'Viewer';
|
||||
params.key = params?.key || 'viewer';
|
||||
|
||||
const [role] = await global.knex.table('roles').insert(params).returning('*');
|
||||
const role = await Role.query().insert(params).returning('*');
|
||||
|
||||
return role;
|
||||
};
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import Step from '../../src/models/step';
|
||||
import { createFlow } from './flow';
|
||||
|
||||
export const createStep = async (params = {}) => {
|
||||
@@ -16,7 +17,7 @@ export const createStep = async (params = {}) => {
|
||||
params.appKey =
|
||||
params?.appKey || (params.type === 'action' ? 'deepl' : 'webhook');
|
||||
|
||||
const [step] = await global.knex.table('steps').insert(params).returning('*');
|
||||
const step = await Step.query().insert(params).returning('*');
|
||||
|
||||
return step;
|
||||
};
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { createRole } from './role';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import User from '../../src/models/user';
|
||||
|
||||
export const createUser = async (params = {}) => {
|
||||
params.roleId = params?.roleId || (await createRole()).id;
|
||||
@@ -7,7 +8,7 @@ export const createUser = async (params = {}) => {
|
||||
params.email = params?.email || faker.internet.email();
|
||||
params.password = params?.password || faker.internet.password();
|
||||
|
||||
const [user] = await global.knex.table('users').insert(params).returning('*');
|
||||
const user = await User.query().insert(params).returning('*');
|
||||
|
||||
return user;
|
||||
};
|
||||
|
@@ -0,0 +1,32 @@
|
||||
const getCurrentUserMock = (currentUser, role) => {
|
||||
return {
|
||||
data: {
|
||||
createdAt: currentUser.createdAt.toISOString(),
|
||||
email: currentUser.email,
|
||||
fullName: currentUser.fullName,
|
||||
id: currentUser.id,
|
||||
permissions: [],
|
||||
role: {
|
||||
createdAt: role.createdAt.toISOString(),
|
||||
description: null,
|
||||
id: role.id,
|
||||
isAdmin: role.isAdmin,
|
||||
key: role.key,
|
||||
name: role.name,
|
||||
updatedAt: role.updatedAt.toISOString(),
|
||||
},
|
||||
roleId: role.id,
|
||||
trialExpiryDate: currentUser.trialExpiryDate.toISOString(),
|
||||
updatedAt: currentUser.updatedAt.toISOString(),
|
||||
},
|
||||
meta: {
|
||||
count: 1,
|
||||
currentPage: null,
|
||||
isArray: false,
|
||||
totalPages: null,
|
||||
type: 'User',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default getCurrentUserMock;
|
@@ -0,0 +1,17 @@
|
||||
const getUserTrialMock = async (currentUser) => {
|
||||
return {
|
||||
data: {
|
||||
inTrial: await currentUser.inTrial(),
|
||||
expireAt: currentUser.trialExpiryDate.toISOString(),
|
||||
},
|
||||
meta: {
|
||||
count: 1,
|
||||
currentPage: null,
|
||||
isArray: false,
|
||||
totalPages: null,
|
||||
type: 'Object',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default getUserTrialMock;
|
31
packages/backend/test/mocks/rest/api/v1/users/get-user.js
Normal file
31
packages/backend/test/mocks/rest/api/v1/users/get-user.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const getUserMock = (currentUser, role) => {
|
||||
return {
|
||||
data: {
|
||||
createdAt: currentUser.createdAt.toISOString(),
|
||||
email: currentUser.email,
|
||||
fullName: currentUser.fullName,
|
||||
id: currentUser.id,
|
||||
role: {
|
||||
createdAt: role.createdAt.toISOString(),
|
||||
description: null,
|
||||
id: role.id,
|
||||
isAdmin: role.isAdmin,
|
||||
key: role.key,
|
||||
name: role.name,
|
||||
updatedAt: role.updatedAt.toISOString(),
|
||||
},
|
||||
roleId: role.id,
|
||||
trialExpiryDate: currentUser.trialExpiryDate.toISOString(),
|
||||
updatedAt: currentUser.updatedAt.toISOString(),
|
||||
},
|
||||
meta: {
|
||||
count: 1,
|
||||
currentPage: null,
|
||||
isArray: false,
|
||||
totalPages: null,
|
||||
type: 'User',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default getUserMock;
|
38
packages/backend/test/mocks/rest/api/v1/users/get-users.js
Normal file
38
packages/backend/test/mocks/rest/api/v1/users/get-users.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const getUsersMock = async (users, roles) => {
|
||||
const data = users.map((user) => {
|
||||
const role = roles.find((r) => r.id === user.roleId);
|
||||
return {
|
||||
createdAt: user.createdAt.toISOString(),
|
||||
email: user.email,
|
||||
fullName: user.fullName,
|
||||
id: user.id,
|
||||
role: role
|
||||
? {
|
||||
createdAt: role.createdAt.toISOString(),
|
||||
description: role.description,
|
||||
id: role.id,
|
||||
isAdmin: role.isAdmin,
|
||||
key: role.key,
|
||||
name: role.name,
|
||||
updatedAt: role.updatedAt.toISOString(),
|
||||
}
|
||||
: null, // Fallback to null if role not found
|
||||
roleId: user.roleId,
|
||||
trialExpiryDate: user.trialExpiryDate.toISOString(),
|
||||
updatedAt: user.updatedAt.toISOString(),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
data: data,
|
||||
meta: {
|
||||
count: data.length,
|
||||
currentPage: 1,
|
||||
isArray: true,
|
||||
totalPages: 1,
|
||||
type: 'User',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default getUsersMock;
|
@@ -32,6 +32,16 @@ export default defineConfig({
|
||||
],
|
||||
sidebar: {
|
||||
'/apps/': [
|
||||
{
|
||||
text: 'Bigin By Zoho CRM',
|
||||
collapsible: true,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Triggers', link: '/apps/bigin-by-zoho-crm/triggers' },
|
||||
{ text: 'Actions', link: '/apps/bigin-by-zoho-crm/actions' },
|
||||
{ text: 'Connection', link: '/apps/bigin-by-zoho-crm/connection' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Carbone',
|
||||
collapsible: true,
|
||||
@@ -305,7 +315,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' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@@ -14,31 +14,33 @@ The default values for some environment variables might be different in our deve
|
||||
Please be careful with the `ENCRYPTION_KEY` and `WEBHOOK_SECRET_KEY` environment variables. They are used to encrypt your credentials from third-party services and verify webhook requests. If you change them, your existing connections and flows will not continue to work.
|
||||
:::
|
||||
|
||||
| Variable Name | Type | Default Value | Description |
|
||||
| --------------------------- | ------- | ------------------ | ---------------------------------------------------------------------------------------------------- |
|
||||
| `HOST` | string | `localhost` | HTTP Host |
|
||||
| `PROTOCOL` | string | `http` | HTTP Protocol |
|
||||
| `PORT` | string | `3000` | HTTP Port |
|
||||
| `APP_ENV` | string | `production` | Automatisch Environment |
|
||||
| `WEB_APP_URL` | string | | Can be used to override connection URLs and CORS URL |
|
||||
| `WEBHOOK_URL` | string | | Can be used to override webhook URL |
|
||||
| `LOG_LEVEL` | string | `info` | Can be used to configure log level such as `error`, `warn`, `info`, `http`, `debug` |
|
||||
| `POSTGRES_DATABASE` | string | `automatisch` | Database Name |
|
||||
| `POSTGRES_SCHEMA` | string | `public` | Database Schema |
|
||||
| `POSTGRES_PORT` | number | `5432` | Database Port |
|
||||
| `POSTGRES_ENABLE_SSL` | boolean | `false` | Enable/Disable SSL for the database |
|
||||
| `POSTGRES_HOST` | string | `postgres` | Database Host |
|
||||
| `POSTGRES_USERNAME` | string | `automatisch_user` | Database User |
|
||||
| `POSTGRES_PASSWORD` | string | | Password of Database User |
|
||||
| `ENCRYPTION_KEY` | string | | Encryption Key to store credentials |
|
||||
| `WEBHOOK_SECRET_KEY` | string | | Webhook Secret Key to verify webhook requests |
|
||||
| `APP_SECRET_KEY` | string | | Secret Key to authenticate the user |
|
||||
| `REDIS_HOST` | string | `redis` | Redis Host |
|
||||
| `REDIS_PORT` | number | `6379` | Redis Port |
|
||||
| `REDIS_USERNAME` | string | | Redis Username |
|
||||
| `REDIS_PASSWORD` | string | | Redis Password |
|
||||
| `REDIS_TLS` | boolean | `false` | Redis TLS |
|
||||
| `TELEMETRY_ENABLED` | boolean | `true` | Enable/Disable Telemetry |
|
||||
| `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 |
|
||||
| Variable Name | Type | Default Value | Description |
|
||||
| ---------------------------- | ------- | ------------------ | ----------------------------------------------------------------------------------- |
|
||||
| `HOST` | string | `localhost` | HTTP Host |
|
||||
| `PROTOCOL` | string | `http` | HTTP Protocol |
|
||||
| `PORT` | string | `3000` | HTTP Port |
|
||||
| `APP_ENV` | string | `production` | Automatisch Environment |
|
||||
| `WEB_APP_URL` | string | | Can be used to override connection URLs and CORS URL |
|
||||
| `WEBHOOK_URL` | string | | Can be used to override webhook URL |
|
||||
| `LOG_LEVEL` | string | `info` | Can be used to configure log level such as `error`, `warn`, `info`, `http`, `debug` |
|
||||
| `POSTGRES_DATABASE` | string | `automatisch` | Database Name |
|
||||
| `POSTGRES_SCHEMA` | string | `public` | Database Schema |
|
||||
| `POSTGRES_PORT` | number | `5432` | Database Port |
|
||||
| `POSTGRES_ENABLE_SSL` | boolean | `false` | Enable/Disable SSL for the database |
|
||||
| `POSTGRES_HOST` | string | `postgres` | Database Host |
|
||||
| `POSTGRES_USERNAME` | string | `automatisch_user` | Database User |
|
||||
| `POSTGRES_PASSWORD` | string | | Password of Database User |
|
||||
| `ENCRYPTION_KEY` | string | | Encryption Key to store credentials |
|
||||
| `WEBHOOK_SECRET_KEY` | string | | Webhook Secret Key to verify webhook requests |
|
||||
| `APP_SECRET_KEY` | string | | Secret Key to authenticate the user |
|
||||
| `REDIS_HOST` | string | `redis` | Redis Host |
|
||||
| `REDIS_PORT` | number | `6379` | Redis Port |
|
||||
| `REDIS_USERNAME` | string | | Redis Username |
|
||||
| `REDIS_PASSWORD` | string | | Redis Password |
|
||||
| `REDIS_TLS` | boolean | `false` | Redis TLS |
|
||||
| `TELEMETRY_ENABLED` | boolean | `true` | Enable/Disable Telemetry |
|
||||
| `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 |
|
||||
|
14
packages/docs/pages/apps/bigin-by-zoho-crm/actions.md
Normal file
14
packages/docs/pages/apps/bigin-by-zoho-crm/actions.md
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
favicon: /favicons/bigin-by-zoho-crm.svg
|
||||
items:
|
||||
- name: Create contact
|
||||
desc: Creates a new contact.
|
||||
- name: Create event
|
||||
desc: Creates a new event.
|
||||
---
|
||||
|
||||
<script setup>
|
||||
import CustomListing from '../../components/CustomListing.vue'
|
||||
</script>
|
||||
|
||||
<CustomListing />
|
18
packages/docs/pages/apps/bigin-by-zoho-crm/connection.md
Normal file
18
packages/docs/pages/apps/bigin-by-zoho-crm/connection.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Bigin By Zoho CRM
|
||||
|
||||
:::info
|
||||
This page explains the steps you need to follow to set up the Bigin By Zoho CRM
|
||||
connection in Automatisch. If any of the steps are outdated, please let us know!
|
||||
:::
|
||||
|
||||
1. Go to the [Zoho API Console](https://api-console.zoho.com) to register an application.
|
||||
2. Login to your account.
|
||||
3. Click on the **GET STARTED** button.
|
||||
4. Choose the **Server-based Applications** client type.
|
||||
5. Fill the **Create New Client** form.
|
||||
6. Copy **OAuth Redirect URL** from Automatisch to **Authorized redirect URIs** field, and click on the **Create** button.
|
||||
7. Copy the **Client ID** value to the `Client ID` field on Automatisch.
|
||||
8. Copy the **Client Secret** value to the `Client Secret` field on Automatisch.
|
||||
9. Select the region appropriate for your account.
|
||||
10. Click **Submit** button on Automatisch.
|
||||
11. Congrats! Start using your new Bigin By Zoho CRM connection within the flows.
|
20
packages/docs/pages/apps/bigin-by-zoho-crm/triggers.md
Normal file
20
packages/docs/pages/apps/bigin-by-zoho-crm/triggers.md
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
favicon: /favicons/bigin-by-zoho-crm.svg
|
||||
items:
|
||||
- name: New calls
|
||||
desc: Triggers when a new call is added.
|
||||
- name: New companies
|
||||
desc: Triggers when a new company is created.
|
||||
- name: New contacts
|
||||
desc: Triggers when a new contact is created.
|
||||
- name: New products
|
||||
desc: Triggers when a new product is created.
|
||||
- name: New tasks
|
||||
desc: Triggers when a new task is created.
|
||||
---
|
||||
|
||||
<script setup>
|
||||
import CustomListing from '../../components/CustomListing.vue'
|
||||
</script>
|
||||
|
||||
<CustomListing />
|
@@ -2,6 +2,7 @@
|
||||
|
||||
The following integrations are currently supported by Automatisch.
|
||||
|
||||
- [Bigin By Zoho CRM](/apps/bigin-by-zoho-crm/triggers)
|
||||
- [Carbone](/apps/carbone/actions)
|
||||
- [DeepL](/apps/deepl/actions)
|
||||
- [Delay](/apps/delay/actions)
|
||||
|
32
packages/docs/pages/public/favicons/bigin-by-zoho-crm.svg
Normal file
32
packages/docs/pages/public/favicons/bigin-by-zoho-crm.svg
Normal file
@@ -0,0 +1,32 @@
|
||||
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" id="b" data-name="Layer 2" width="327.714" height="120" viewBox="0 0 327.714 120">
|
||||
<defs>
|
||||
<style>
|
||||
.d {
|
||||
fill: #039649;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="c" data-name="Layer 1">
|
||||
<g>
|
||||
<path class="d" d="m52.39,120c-1.801,0-3.6-.6-5.101-1.5-2.1-1.5-3.6-4.2-3.6-6.9v-24.6L1.09,12.901C-.41,9.9-.41,6.601,1.391,4.2,2.891,1.5,5.89,0,8.89,0h78c2.999,0,5.698,1.5,7.199,4.2,1.801,2.702,1.801,6.3.301,9.002l-5.101,9h10.201c2.999,0,5.698,1.5,7.199,4.2,1.801,2.7,1.801,6.3.301,9l-29.701,51.3v18.599c0,3.899-2.399,6.9-5.999,8.099l-16.2,6.3c-.6.301-1.799.301-2.7.301M8.89,8.4q-.301.301-.301.602l42.301,73.798c1.199,2.1,1.199,3.299,1.199,4.501v23.998s.301.301.6,0l16.2-6.3h.6v-17.999c0-1.199,0-3.299,1.201-4.8l29.4-50.999c0-.301,0-.6-.301-.6l-53.699-.301,14.399,24.901,10.201-17.7c1.199-2.1,3.6-2.7,5.7-1.5,2.1,1.199,2.7,3.6,1.498,5.7l-11.998,20.699c-2.702,4.2-8.701,3.901-11.102.301l-17.4-30.9c-1.199-1.799-1.199-4.2,0-6.3,1.201-2.1,3.301-3.6,5.7-3.6h36.6l7.499-12.899s0-.301-.299-.602H8.89Z"/>
|
||||
<g>
|
||||
<path d="m181.725,48.737c0,3.089-.54,5.684-1.618,7.788-1.081,2.104-2.548,3.79-4.407,5.062-1.858,1.27-4.016,2.179-6.476,2.726-2.459.547-5.069.819-7.828.819h-24.591V6.521h23.034c2.676,0,5.198.24,7.561.718,2.364.479,4.427,1.311,6.189,2.5,1.762,1.189,3.149,2.787,4.16,4.795,1.011,2.008,1.517,4.53,1.517,7.562,0,3.17-.786,5.813-2.357,7.93-1.57,2.119-3.913,3.628-7.028,4.53,4.017.819,6.995,2.371,8.935,4.652s2.91,5.458,2.91,9.529Zm-33.813-17.624h10.533c1.612,0,3.033-.136,4.263-.41,1.23-.272,2.268-.738,3.115-1.393.846-.656,1.489-1.55,1.927-2.684.436-1.134.655-2.562.655-4.284,0-1.612-.267-2.923-.799-3.934-.532-1.01-1.25-1.809-2.152-2.398-.902-.587-1.967-.99-3.197-1.209s-2.542-.328-3.934-.328h-10.411v16.64Zm0,26.067h12.09c1.64,0,3.115-.169,4.427-.512,1.311-.342,2.424-.894,3.341-1.66.915-.764,1.612-1.748,2.091-2.951.478-1.202.716-2.663.716-4.385,0-1.802-.294-3.285-.881-4.447-.588-1.161-1.394-2.083-2.419-2.766s-2.227-1.154-3.606-1.414c-1.381-.26-2.863-.39-4.447-.39h-11.312v18.525Z"/>
|
||||
<path d="m202.257,9.555c0,.847-.151,1.633-.451,2.356-.3.724-.716,1.34-1.25,1.844-.532.507-1.167.902-1.905,1.189s-1.53.431-2.377.431c-.819,0-1.598-.144-2.336-.431s-1.388-.69-1.947-1.209c-.56-.519-.998-1.133-1.311-1.844-.315-.711-.471-1.489-.471-2.336s.156-1.626.471-2.336c.314-.711.744-1.325,1.291-1.845.546-.519,1.187-.922,1.925-1.209s1.53-.431,2.377-.431c.821,0,1.598.144,2.336.431s1.373.69,1.907,1.209c.532.52.955,1.134,1.27,1.845.314.71.471,1.489.471,2.336Zm-.778,12.705v42.871h-10.246V22.261h10.246Z"/>
|
||||
<path d="m254.186,61.935c0,3.361-.587,6.236-1.762,8.627-1.174,2.391-2.78,4.352-4.815,5.882-2.036,1.529-4.407,2.65-7.111,3.361-2.706.71-5.589,1.065-8.648,1.065-2.023,0-4.037-.157-6.045-.471-2.009-.314-3.908-.861-5.697-1.64-1.79-.778-3.395-1.837-4.816-3.175-1.421-1.34-2.555-3.021-3.402-5.042l8.074-3.525c.519,1.202,1.202,2.193,2.049,2.971.847.779,1.797,1.408,2.848,1.885,1.051.479,2.172.814,3.361,1.005s2.398.287,3.628.287c1.885,0,3.572-.232,5.062-.696,1.489-.466,2.752-1.169,3.79-2.111,1.039-.943,1.838-2.132,2.399-3.566.559-1.434.839-3.107.839-5.02v-6.393c-.71,1.229-1.592,2.336-2.643,3.319-1.053.983-2.207,1.824-3.464,2.52s-2.582,1.237-3.976,1.62c-1.393.383-2.814.574-4.263.574-3.033,0-5.737-.56-8.114-1.681-2.377-1.119-4.379-2.636-6.005-4.55-1.625-1.912-2.862-4.139-3.709-6.68-.847-2.542-1.27-5.233-1.27-8.074,0-2.814.451-5.491,1.353-8.033.901-2.542,2.192-4.775,3.873-6.702,1.68-1.927,3.709-3.464,6.086-4.611,2.376-1.147,5.054-1.721,8.033-1.721,2.923,0,5.621.594,8.094,1.782,2.472,1.189,4.473,3.082,6.004,5.677v-6.557h10.246v39.674Zm-33.198-19.017c0,1.666.252,3.265.758,4.795s1.237,2.876,2.193,4.037c.957,1.162,2.137,2.084,3.545,2.767s3.013,1.025,4.816,1.025c2.021,0,3.784-.355,5.287-1.066,1.502-.711,2.746-1.673,3.729-2.89.985-1.215,1.722-2.643,2.213-4.283.492-1.64.738-3.374.738-5.206,0-1.802-.239-3.49-.716-5.062-.479-1.57-1.203-2.93-2.173-4.077s-2.179-2.056-3.626-2.726c-1.449-.67-3.157-1.005-5.123-1.005-2.049,0-3.812.363-5.287,1.086-1.476.724-2.684,1.709-3.628,2.951-.943,1.243-1.633,2.699-2.069,4.365-.438,1.666-.656,3.429-.656,5.287Z"/>
|
||||
<path d="m277.096,9.555c0,.847-.151,1.633-.451,2.356-.3.724-.716,1.34-1.25,1.844-.532.507-1.167.902-1.905,1.189s-1.53.431-2.377.431c-.819,0-1.598-.144-2.336-.431s-1.388-.69-1.947-1.209c-.56-.519-.998-1.133-1.311-1.844-.315-.711-.471-1.489-.471-2.336s.156-1.626.471-2.336c.314-.711.744-1.325,1.291-1.845.546-.519,1.187-.922,1.925-1.209s1.53-.431,2.377-.431c.821,0,1.598.144,2.336.431s1.373.69,1.907,1.209c.532.52.955,1.134,1.27,1.845.314.71.471,1.489.471,2.336Zm-.778,12.705v42.871h-10.246V22.261h10.246Z"/>
|
||||
<path d="m308.451,29.801c-1.585,0-2.999.24-4.243.718-1.243.479-2.288,1.162-3.135,2.049-.847.889-1.496,1.961-1.947,3.217-.451,1.258-.676,2.651-.676,4.181v25.165h-10.246V22.261h10.246v6.803c.683-1.338,1.537-2.492,2.562-3.462,1.025-.97,2.165-1.769,3.422-2.399,1.257-.627,2.59-1.091,3.997-1.393,1.406-.3,2.82-.451,4.241-.451,2.623,0,4.884.431,6.783,1.291s3.464,2.049,4.694,3.565c1.229,1.517,2.131,3.307,2.704,5.37s.861,4.311.861,6.742v26.805h-10.328v-25.822c0-3.005-.711-5.341-2.132-7.008-1.421-1.666-3.688-2.5-6.803-2.5Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="m152.871,102.12c0,1.138-.168,2.221-.507,3.25-.337,1.028-.828,1.935-1.471,2.72s-1.436,1.41-2.379,1.874-2.021.696-3.234.696c-.579,0-1.155-.061-1.723-.183-.569-.121-1.107-.306-1.613-.553-.505-.247-.974-.561-1.407-.941s-.801-.822-1.107-1.328v2.72h-2.625v-24.446h2.625v10.689c.273-.464.632-.882,1.076-1.257.442-.374.93-.696,1.463-.964.531-.27,1.082-.477,1.652-.626.569-.146,1.122-.22,1.66-.22,1.244,0,2.34.227,3.289.679.95.454,1.743,1.068,2.38,1.843s1.117,1.685,1.44,2.728c.321,1.044.482,2.151.482,3.321Zm-13.536.126c0,.917.123,1.756.371,2.515.249.759.614,1.415,1.1,1.968.485.553,1.079.984,1.787,1.289.706.306,1.517.459,2.435.459.991,0,1.813-.178,2.467-.53.653-.354,1.178-.828,1.573-1.424.395-.595.674-1.283.838-2.063.163-.78.245-1.603.245-2.467,0-.801-.104-1.576-.308-2.325-.206-.748-.52-1.415-.941-1.999-.422-.586-.958-1.055-1.605-1.407-.648-.354-1.415-.53-2.3-.53-.981,0-1.827.174-2.538.521-.711.349-1.3.818-1.764,1.409-.464.59-.806,1.28-1.028,2.071s-.332,1.629-.332,2.514Z"/>
|
||||
<path d="m169.949,93.834l-9.141,22.297h-2.656l3.131-7.464-6.388-14.833h2.814l4.965,11.86,4.586-11.86h2.689Z"/>
|
||||
<path d="m194.11,108.034v2.34h-16.414v-1.139l13.189-19.228h-12.05v-2.246h15.465v1.139l-12.84,19.134h12.65Z"/>
|
||||
<path d="m213.229,102.215c0,1.202-.206,2.319-.617,3.352-.411,1.034-.986,1.927-1.723,2.681-.739.753-1.611,1.344-2.618,1.77-1.007.428-2.106.641-3.296.641-1.256,0-2.388-.222-3.4-.665s-1.874-1.053-2.585-1.834-1.257-1.697-1.637-2.752c-.38-1.053-.57-2.192-.57-3.416,0-1.191.201-2.3.601-3.329.4-1.028.964-1.92,1.692-2.68.727-.759,1.594-1.354,2.601-1.787s2.116-.648,3.329-.648c1.254,0,2.391.22,3.408.663s1.881,1.055,2.593,1.835,1.26,1.697,1.644,2.751c.385,1.055.578,2.192.578,3.416Zm-13.726.031c0,.886.117,1.708.349,2.467s.579,1.418,1.043,1.977c.464.558,1.038.995,1.723,1.311.685.317,1.481.476,2.388.476.938,0,1.753-.175,2.443-.522.691-.349,1.263-.816,1.717-1.407.452-.591.79-1.275,1.012-2.056.22-.78.332-1.602.332-2.466,0-.844-.117-1.646-.349-2.404-.232-.759-.579-1.429-1.043-2.008s-1.038-1.038-1.723-1.376c-.685-.337-1.481-.505-2.388-.505-.971,0-1.802.174-2.498.521-.696.349-1.265.82-1.708,1.416-.443.595-.77,1.288-.981,2.078-.21.792-.316,1.624-.316,2.498Z"/>
|
||||
<path d="m224.423,95.827c-.696,0-1.323.105-1.881.316s-1.034.512-1.423.902c-.391.39-.691.864-.902,1.423s-.316,1.186-.316,1.881v10.026h-2.625v-24.446h2.625v10.531c.253-.516.564-.956.933-1.32s.777-.663,1.226-.902c.447-.237.933-.411,1.454-.521.522-.111,1.057-.166,1.605-.166,1.033,0,1.937.171,2.712.513.775.343,1.423.818,1.945,1.424.522.605.915,1.326,1.178,2.157.263.833.395,1.745.395,2.735v9.994h-2.625v-10.184c0-1.423-.36-2.506-1.083-3.25-.722-.742-1.795-1.114-3.217-1.114Z"/>
|
||||
<path d="m251.685,102.215c0,1.202-.206,2.319-.617,3.352-.411,1.034-.986,1.927-1.723,2.681-.739.753-1.611,1.344-2.618,1.77-1.007.428-2.106.641-3.296.641-1.256,0-2.388-.222-3.4-.665s-1.874-1.053-2.585-1.834-1.257-1.697-1.637-2.752c-.38-1.053-.57-2.192-.57-3.416,0-1.191.201-2.3.601-3.329.4-1.028.964-1.92,1.692-2.68.727-.759,1.594-1.354,2.601-1.787s2.116-.648,3.329-.648c1.254,0,2.391.22,3.408.663s1.881,1.055,2.593,1.835,1.26,1.697,1.644,2.751c.385,1.055.578,2.192.578,3.416Zm-13.726.031c0,.886.117,1.708.349,2.467s.579,1.418,1.043,1.977c.464.558,1.038.995,1.723,1.311.685.317,1.481.476,2.388.476.938,0,1.753-.175,2.443-.522.691-.349,1.263-.816,1.717-1.407.452-.591.79-1.275,1.012-2.056.22-.78.332-1.602.332-2.466,0-.844-.117-1.646-.349-2.404-.232-.759-.579-1.429-1.043-2.008s-1.038-1.038-1.723-1.376c-.685-.337-1.481-.505-2.388-.505-.971,0-1.802.174-2.498.521-.696.349-1.265.82-1.708,1.416-.443.595-.77,1.288-.981,2.078-.21.792-.316,1.624-.316,2.498Z"/>
|
||||
<path d="m281.097,103.923c-.38.959-.884,1.85-1.511,2.672-.627.823-1.346,1.534-2.157,2.135-.812.601-1.703,1.073-2.673,1.415-.969.342-1.976.514-3.02.514-1.76,0-3.312-.295-4.656-.886-1.345-.59-2.472-1.407-3.385-2.45-.912-1.044-1.603-2.279-2.072-3.709-.469-1.428-.704-2.98-.704-4.657,0-1.033.114-2.037.341-3.013.227-.974.553-1.889.98-2.743.428-.854.95-1.637,1.565-2.348.617-.711,1.318-1.32,2.104-1.827.785-.505,1.652-.901,2.601-1.186.949-.284,1.961-.426,3.036-.426,1.012,0,2.003.148,2.973.442.971.295,1.866.718,2.689,1.266.822.548,1.55,1.209,2.182,1.984s1.127,1.642,1.486,2.602l-2.246.98c-.295-.768-.673-1.468-1.13-2.095-.459-.627-.989-1.162-1.59-1.604-.601-.443-1.265-.785-1.992-1.029-.728-.242-1.508-.363-2.341-.363-1.359,0-2.532.268-3.518.806s-1.797,1.254-2.435,2.151c-.638.895-1.107,1.919-1.407,3.067-.301,1.149-.451,2.335-.451,3.558,0,1.244.166,2.424.498,3.543.333,1.117.833,2.098,1.503,2.94.669.844,1.507,1.513,2.514,2.008,1.007.496,2.18.744,3.518.744.801,0,1.576-.143,2.325-.428.749-.284,1.44-.671,2.072-1.162.632-.49,1.191-1.064,1.675-1.723.485-.658.864-1.357,1.139-2.095l2.088.917Z"/>
|
||||
<path d="m287.991,100.539v9.835h-2.751v-22.613h7.431c1.044,0,2.042.111,2.997.332.954.222,1.795.586,2.522,1.092.728.505,1.307,1.168,1.74,1.984.431.818.648,1.822.648,3.013,0,.885-.137,1.673-.411,2.364-.275.691-.66,1.289-1.155,1.795-.496.507-1.086.925-1.771,1.257-.685.333-1.44.583-2.261.752l6.926,10.026h-3.068l-6.736-9.835h-4.112Zm0-2.183h4.871c.727,0,1.393-.075,1.999-.228s1.131-.402,1.574-.744c.442-.342.785-.785,1.028-1.328s.363-1.21.363-2.001c0-.801-.126-1.466-.378-1.992-.254-.527-.601-.943-1.044-1.25-.443-.305-.967-.521-1.573-.648s-1.262-.189-1.968-.189h-4.871v8.38Z"/>
|
||||
<path d="m308.231,91.335v19.039h-2.529v-22.613h3.826l7.242,17.932,7.148-17.932h3.795v22.613h-2.752v-19.039l-7.653,19.039h-1.17l-7.907-19.039Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -2,7 +2,6 @@
|
||||
<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
|
||||
|
@@ -2,13 +2,6 @@
|
||||
"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",
|
||||
|
@@ -15,6 +15,7 @@ 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 = {
|
||||
@@ -86,19 +87,11 @@ 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);
|
||||
@@ -116,6 +109,15 @@ 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
|
||||
|
@@ -19,6 +19,7 @@ type DrawerLink = {
|
||||
Icon: React.ElementType;
|
||||
primary: string;
|
||||
to: string;
|
||||
target?: '_blank';
|
||||
badgeContent?: React.ReactNode;
|
||||
dataTest?: string;
|
||||
};
|
||||
@@ -69,7 +70,7 @@ export default function Drawer(props: DrawerProps): React.ReactElement {
|
||||
|
||||
<List sx={{ py: 0, mt: 3 }}>
|
||||
{bottomLinks.map(
|
||||
({ Icon, badgeContent, primary, to, dataTest }, index) => (
|
||||
({ Icon, badgeContent, primary, to, dataTest, target }, index) => (
|
||||
<ListItemLink
|
||||
key={`${to}-${index}`}
|
||||
icon={
|
||||
@@ -77,9 +78,10 @@ export default function Drawer(props: DrawerProps): React.ReactElement {
|
||||
<Icon htmlColor={theme.palette.primary.main} />
|
||||
</Badge>
|
||||
}
|
||||
primary={formatMessage(primary)}
|
||||
primary={primary}
|
||||
to={to}
|
||||
onClick={closeOnClick}
|
||||
target={target}
|
||||
data-test={dataTest}
|
||||
/>
|
||||
)
|
||||
|
@@ -3,16 +3,19 @@ import Box from '@mui/material/Box';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import Stack from '@mui/material/Stack';
|
||||
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 useAutomatischInfo from 'hooks/useAutomatischInfo';
|
||||
import useConfig from 'hooks/useConfig';
|
||||
|
||||
type PublicLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
@@ -40,47 +43,94 @@ const drawerLinks = [
|
||||
];
|
||||
|
||||
type GenerateDrawerBottomLinksOptions = {
|
||||
isMation: boolean;
|
||||
loading: boolean;
|
||||
disableNotificationsPage: boolean;
|
||||
notificationBadgeContent: number;
|
||||
additionalDrawerLink?: string;
|
||||
additionalDrawerLinkText?: string;
|
||||
additionalDrawerLinkIcon?: string;
|
||||
formatMessage: ReturnType<typeof useFormatMessage>;
|
||||
};
|
||||
|
||||
const generateDrawerBottomLinks = ({
|
||||
isMation,
|
||||
loading,
|
||||
const generateDrawerBottomLinks = async ({
|
||||
disableNotificationsPage,
|
||||
notificationBadgeContent = 0,
|
||||
additionalDrawerLink,
|
||||
additionalDrawerLinkText,
|
||||
formatMessage,
|
||||
}: GenerateDrawerBottomLinksOptions) => {
|
||||
if (loading || isMation) {
|
||||
return [];
|
||||
const notificationsPageLinkObject = {
|
||||
Icon: NotificationsIcon,
|
||||
primary: formatMessage('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);
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
Icon: NotificationsIcon,
|
||||
primary: 'settingsDrawer.notifications',
|
||||
to: URLS.UPDATES,
|
||||
badgeContent: notificationBadgeContent,
|
||||
},
|
||||
];
|
||||
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 { isMation, loading } = useAutomatischInfo();
|
||||
const { config, loading } = useConfig([
|
||||
'disableNotificationsPage',
|
||||
'additionalDrawerLink',
|
||||
'additionalDrawerLinkText',
|
||||
]);
|
||||
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);
|
||||
|
||||
const drawerBottomLinks = generateDrawerBottomLinks({
|
||||
notificationBadgeContent: version.newVersionCount,
|
||||
loading,
|
||||
isMation,
|
||||
});
|
||||
React.useEffect(() => {
|
||||
async function perform() {
|
||||
const newBottomLinks = await generateDrawerBottomLinks({
|
||||
notificationBadgeContent: version.newVersionCount,
|
||||
disableNotificationsPage: config?.disableNotificationsPage as boolean,
|
||||
additionalDrawerLink: config?.additionalDrawerLink as string,
|
||||
additionalDrawerLinkText: config?.additionalDrawerLinkText as string,
|
||||
formatMessage,
|
||||
});
|
||||
|
||||
setBottomLinks(newBottomLinks);
|
||||
}
|
||||
|
||||
if (loading) return;
|
||||
|
||||
perform();
|
||||
}, [config, loading, version.newVersionCount]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -90,20 +140,19 @@ export default function PublicLayout({
|
||||
onDrawerClose={closeDrawer}
|
||||
/>
|
||||
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<Box sx={{ display: 'flex', height: '100%' }}>
|
||||
<Drawer
|
||||
links={drawerLinks}
|
||||
bottomLinks={drawerBottomLinks}
|
||||
bottomLinks={bottomLinks}
|
||||
open={isDrawerOpen}
|
||||
onOpen={openDrawer}
|
||||
onClose={closeDrawer}
|
||||
/>
|
||||
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Stack flex={1}>
|
||||
<Toolbar />
|
||||
|
||||
{children}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
@@ -9,6 +9,7 @@ type ListItemLinkProps = {
|
||||
icon: React.ReactNode;
|
||||
primary: string;
|
||||
to: string;
|
||||
target?: '_blank';
|
||||
onClick?: (event: React.SyntheticEvent) => void;
|
||||
'data-test'?: string;
|
||||
};
|
||||
@@ -16,14 +17,29 @@ type ListItemLinkProps = {
|
||||
export default function ListItemLink(
|
||||
props: ListItemLinkProps
|
||||
): React.ReactElement {
|
||||
const { icon, primary, to, onClick, 'data-test': dataTest } = props;
|
||||
const { icon, primary, to, onClick, 'data-test': dataTest, target } = props;
|
||||
const selected = useMatch({ path: to, end: true });
|
||||
|
||||
const CustomLink = React.useMemo(
|
||||
() =>
|
||||
React.forwardRef<HTMLAnchorElement, Omit<LinkProps, 'to'>>(
|
||||
function InLineLink(linkProps, ref) {
|
||||
return <Link ref={ref} to={to} {...linkProps} />;
|
||||
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} />;
|
||||
}
|
||||
}
|
||||
),
|
||||
[to]
|
||||
@@ -37,6 +53,7 @@ export default function ListItemLink(
|
||||
selected={!!selected}
|
||||
onClick={onClick}
|
||||
data-test={dataTest}
|
||||
target={target}
|
||||
>
|
||||
<ListItemIcon sx={{ minWidth: 52 }}>{icon}</ListItemIcon>
|
||||
<ListItemText
|
||||
|
@@ -15,6 +15,27 @@ 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}</>;
|
||||
};
|
||||
|
||||
|
44
packages/web/src/components/NotFound/index.tsx
Normal file
44
packages/web/src/components/NotFound/index.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import Button from '@mui/material/Button';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
import * as URLS from 'config/urls';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import useAuthentication from 'hooks/useAuthentication';
|
||||
import Layout from 'components/Layout';
|
||||
import PublicLayout from 'components/PublicLayout';
|
||||
|
||||
export default function NoResultFound(): React.ReactElement {
|
||||
const formatMessage = useFormatMessage();
|
||||
const { isAuthenticated } = useAuthentication();
|
||||
|
||||
const pageContent = (
|
||||
<Stack
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
flex={1}
|
||||
spacing={1}
|
||||
p={2}
|
||||
mb={11}
|
||||
>
|
||||
<Typography variant="h1" color="primary" textAlign="center">
|
||||
404
|
||||
</Typography>
|
||||
<Typography variant="body1" textAlign="center">
|
||||
{formatMessage('notFoundPage.title')}
|
||||
</Typography>
|
||||
<Link to={isAuthenticated ? URLS.FLOWS : URLS.LOGIN}>
|
||||
<Button variant="contained" sx={{ mt: 3 }} component="div">
|
||||
{formatMessage('notFoundPage.button')}
|
||||
</Button>
|
||||
</Link>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
return isAuthenticated ? (
|
||||
<Layout>{pageContent}</Layout>
|
||||
) : (
|
||||
<PublicLayout>{pageContent}</PublicLayout>
|
||||
);
|
||||
}
|
@@ -9,6 +9,7 @@ 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';
|
||||
|
||||
@@ -22,8 +23,8 @@ function createDrawerLinks({ isCloud }: { isCloud: boolean }) {
|
||||
Icon: AccountCircleIcon,
|
||||
primary: 'settingsDrawer.myProfile',
|
||||
to: URLS.SETTINGS_PROFILE,
|
||||
}
|
||||
]
|
||||
},
|
||||
];
|
||||
|
||||
if (isCloud) {
|
||||
items.push({
|
||||
@@ -36,19 +37,12 @@ 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);
|
||||
|
||||
@@ -56,6 +50,14 @@ export default function SettingsLayout({
|
||||
const closeDrawer = () => setDrawerOpen(false);
|
||||
const drawerLinks = createDrawerLinks({ isCloud });
|
||||
|
||||
const drawerBottomLinks = [
|
||||
{
|
||||
Icon: ArrowBackIosNewIcon,
|
||||
primary: formatMessage('settingsDrawer.goBack'),
|
||||
to: '/',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppBar
|
||||
|
@@ -265,5 +265,7 @@
|
||||
"authClient.buttonSubmit": "Submit",
|
||||
"authClient.inputName": "Name",
|
||||
"authClient.inputActive": "Active",
|
||||
"updateAuthClient.title": "Update auth client"
|
||||
"updateAuthClient.title": "Update auth client",
|
||||
"notFoundPage.title": "We can't seem to find a page you're looking for.",
|
||||
"notFoundPage.button": "Back to home page"
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { Route, Routes, Navigate } from 'react-router-dom';
|
||||
import { Route, Routes as ReactRouterRoutes, Navigate } from 'react-router-dom';
|
||||
import Layout from 'components/Layout';
|
||||
import NoResultFound from 'components/NotFound';
|
||||
import PublicLayout from 'components/PublicLayout';
|
||||
import Applications from 'pages/Applications';
|
||||
import Application from 'pages/Application';
|
||||
@@ -17,127 +18,134 @@ import * as URLS from 'config/urls';
|
||||
import settingsRoutes from './settingsRoutes';
|
||||
import adminSettingsRoutes from './adminSettingsRoutes';
|
||||
import Notifications from 'pages/Notifications';
|
||||
import useConfig from 'hooks/useConfig';
|
||||
import useAuthentication from 'hooks/useAuthentication';
|
||||
|
||||
export default (
|
||||
<Routes>
|
||||
<Route
|
||||
path={URLS.EXECUTIONS}
|
||||
element={
|
||||
<Layout>
|
||||
<Executions />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
function Routes() {
|
||||
const { config } = useConfig();
|
||||
const { isAuthenticated } = useAuthentication();
|
||||
|
||||
<Route
|
||||
path={URLS.EXECUTION_PATTERN}
|
||||
element={
|
||||
<Layout>
|
||||
<Execution />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
return (
|
||||
<ReactRouterRoutes>
|
||||
<Route
|
||||
path={URLS.EXECUTIONS}
|
||||
element={
|
||||
<Layout>
|
||||
<Executions />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={URLS.FLOWS}
|
||||
element={
|
||||
<Layout>
|
||||
<Flows />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={URLS.EXECUTION_PATTERN}
|
||||
element={
|
||||
<Layout>
|
||||
<Execution />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={URLS.FLOW_PATTERN}
|
||||
element={
|
||||
<Layout>
|
||||
<Flow />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={URLS.FLOWS}
|
||||
element={
|
||||
<Layout>
|
||||
<Flows />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={`${URLS.APPS}/*`}
|
||||
element={
|
||||
<Layout>
|
||||
<Applications />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={URLS.FLOW_PATTERN}
|
||||
element={
|
||||
<Layout>
|
||||
<Flow />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={`${URLS.APP_PATTERN}/*`}
|
||||
element={
|
||||
<Layout>
|
||||
<Application />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={`${URLS.APPS}/*`}
|
||||
element={
|
||||
<Layout>
|
||||
<Applications />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route path={`${URLS.EDITOR}/*`} element={<EditorRoutes />} />
|
||||
<Route
|
||||
path={`${URLS.APP_PATTERN}/*`}
|
||||
element={
|
||||
<Layout>
|
||||
<Application />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={URLS.LOGIN}
|
||||
element={
|
||||
<PublicLayout>
|
||||
<Login />
|
||||
</PublicLayout>
|
||||
}
|
||||
/>
|
||||
<Route path={`${URLS.EDITOR}/*`} element={<EditorRoutes />} />
|
||||
|
||||
<Route
|
||||
path={URLS.LOGIN_CALLBACK}
|
||||
element={<LoginCallback />}
|
||||
/>
|
||||
<Route
|
||||
path={URLS.LOGIN}
|
||||
element={
|
||||
<PublicLayout>
|
||||
<Login />
|
||||
</PublicLayout>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={URLS.SIGNUP}
|
||||
element={
|
||||
<PublicLayout>
|
||||
<SignUp />
|
||||
</PublicLayout>
|
||||
}
|
||||
/>
|
||||
<Route path={URLS.LOGIN_CALLBACK} element={<LoginCallback />} />
|
||||
|
||||
<Route
|
||||
path={URLS.FORGOT_PASSWORD}
|
||||
element={
|
||||
<PublicLayout>
|
||||
<ForgotPassword />
|
||||
</PublicLayout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={URLS.SIGNUP}
|
||||
element={
|
||||
<PublicLayout>
|
||||
<SignUp />
|
||||
</PublicLayout>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={URLS.RESET_PASSWORD}
|
||||
element={
|
||||
<PublicLayout>
|
||||
<ResetPassword />
|
||||
</PublicLayout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={URLS.FORGOT_PASSWORD}
|
||||
element={
|
||||
<PublicLayout>
|
||||
<ForgotPassword />
|
||||
</PublicLayout>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={URLS.UPDATES}
|
||||
element={
|
||||
<Layout>
|
||||
<Notifications />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={URLS.RESET_PASSWORD}
|
||||
element={
|
||||
<PublicLayout>
|
||||
<ResetPassword />
|
||||
</PublicLayout>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route path="/" element={<Navigate to={URLS.FLOWS} replace />} />
|
||||
{!config?.disableNotificationsPage && (
|
||||
<Route
|
||||
path={URLS.UPDATES}
|
||||
element={
|
||||
<Layout>
|
||||
<Notifications />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Route path={URLS.SETTINGS}>{settingsRoutes}</Route>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<Navigate to={isAuthenticated ? URLS.FLOWS : URLS.LOGIN} replace />
|
||||
}
|
||||
/>
|
||||
|
||||
<Route path={URLS.ADMIN_SETTINGS}>{adminSettingsRoutes}</Route>
|
||||
<Route path={URLS.SETTINGS}>{settingsRoutes}</Route>
|
||||
|
||||
<Route
|
||||
element={
|
||||
<Layout>
|
||||
<div>404</div>
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
);
|
||||
<Route path={URLS.ADMIN_SETTINGS}>{adminSettingsRoutes}</Route>
|
||||
|
||||
<Route path="*" element={<NoResultFound />} />
|
||||
</ReactRouterRoutes>
|
||||
);
|
||||
}
|
||||
|
||||
export default <Routes />;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user