Compare commits

..

13 Commits

Author SHA1 Message Date
Rıdvan Akca
d3bc3a796b feat(auth): add loading state for user and role management 2023-07-31 17:56:31 +03:00
Ali BARIN
9e64af4793 feat(auth): add user and role management 2023-07-26 18:12:54 +00:00
Ali BARIN
b581f539e2 feat(sso): introduce authentication with SAML 2023-07-18 21:07:31 +00:00
Ali BARIN
aac1295c10 feat(authorization): add update connection checks 2023-07-18 21:06:26 +00:00
Ali BARIN
e8f2802ee0 feat(authorization): add read connection checks 2023-07-18 21:06:22 +00:00
Ali BARIN
75b3730a70 feat(authorization): add delete flow checks 2023-07-18 21:06:18 +00:00
Ali BARIN
af29dc9c3f feat(authorization): add create flow checks 2023-07-18 21:06:08 +00:00
Ali BARIN
181cb5f335 feat(authorization): add delete connection checks 2023-07-18 21:06:04 +00:00
Ali BARIN
94e560c262 feat(authorization): add create connection checks 2023-07-18 21:06:00 +00:00
Ali BARIN
f802061722 feat(authorization): add read execution checks 2023-07-18 21:05:53 +00:00
Ali BARIN
58a7f6eec6 feat(authorization): add update flow checks 2023-07-18 21:03:43 +00:00
Ali BARIN
5e11d3cc4d feat(authorization): add read flow checks 2023-07-18 21:03:29 +00:00
Ali BARIN
399fb8312a feat: introduce role based access control 2023-07-17 23:16:01 +03:00
118 changed files with 747 additions and 2264 deletions

View File

@@ -4,7 +4,7 @@ WORKDIR /automatisch
RUN \ RUN \
apk --no-cache add --virtual build-dependencies python3 build-base && \ apk --no-cache add --virtual build-dependencies python3 build-base && \
yarn global add @automatisch/cli@0.8.0 --network-timeout 1000000 && \ yarn global add @automatisch/cli@0.7.1 --network-timeout 1000000 && \
rm -rf /usr/local/share/.cache/ && \ rm -rf /usr/local/share/.cache/ && \
apk del build-dependencies apk del build-dependencies

View File

@@ -1,5 +1,5 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
FROM automatischio/automatisch:0.8.0 FROM automatischio/automatisch:0.7.1
WORKDIR /automatisch WORKDIR /automatisch
RUN apk add --no-cache openssl dos2unix RUN apk add --no-cache openssl dos2unix

View File

@@ -2,7 +2,7 @@
"packages": [ "packages": [
"packages/*" "packages/*"
], ],
"version": "0.8.0", "version": "0.7.1",
"npmClient": "yarn", "npmClient": "yarn",
"useWorkspaces": true, "useWorkspaces": true,
"command": { "command": {

View File

@@ -3,16 +3,36 @@ import logger from '../../src/helpers/logger';
import client from './client'; import client from './client';
import User from '../../src/models/user'; import User from '../../src/models/user';
import Role from '../../src/models/role'; import Role from '../../src/models/role';
import Permission from '../../src/models/permission';
import '../../src/config/orm'; import '../../src/config/orm';
async function fetchAdminRole() { async function seedPermissionsIfNeeded() {
const role = await Role const existingPermissions = await Permission.query().limit(1).first();
.query()
.where({ if (!existingPermissions) return;
key: 'admin'
}) const getPermission = (subject: string, actions: string[]) => actions.map(action => ({ subject, action }));
.limit(1)
.first(); await Permission.query().insert([
...getPermission('Connection', ['create', 'read', 'delete', 'update']),
...getPermission('Execution', ['read']),
...getPermission('Flow', ['create', 'delete', 'publish', 'read', 'update']),
...getPermission('Role', ['create', 'delete', 'read', 'update']),
...getPermission('User', ['create', 'delete', 'read', 'update']),
])
}
async function createOrFetchRole() {
const role = await Role.query().limit(1).first();
if (!role) {
const createdRole = await Role.query().insertAndFetch({
name: 'Admin',
key: 'admin',
});
return createdRole;
}
return role; return role;
} }
@@ -23,7 +43,9 @@ export async function createUser(
) { ) {
const UNIQUE_VIOLATION_CODE = '23505'; const UNIQUE_VIOLATION_CODE = '23505';
const role = await fetchAdminRole(); await seedPermissionsIfNeeded();
const role = await createOrFetchRole();
const userParams = { const userParams = {
email, email,
password, password,

View File

@@ -1,6 +1,6 @@
{ {
"name": "@automatisch/backend", "name": "@automatisch/backend",
"version": "0.8.0", "version": "0.7.1",
"license": "See LICENSE file", "license": "See LICENSE file",
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.", "description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
"scripts": { "scripts": {
@@ -22,7 +22,7 @@
"prebuild": "rm -rf ./dist" "prebuild": "rm -rf ./dist"
}, },
"dependencies": { "dependencies": {
"@automatisch/web": "^0.8.0", "@automatisch/web": "^0.7.1",
"@bull-board/express": "^3.10.1", "@bull-board/express": "^3.10.1",
"@casl/ability": "^6.5.0", "@casl/ability": "^6.5.0",
"@graphql-tools/graphql-file-loader": "^7.3.4", "@graphql-tools/graphql-file-loader": "^7.3.4",
@@ -53,8 +53,6 @@
"graphql-type-json": "^0.3.2", "graphql-type-json": "^0.3.2",
"handlebars": "^4.7.7", "handlebars": "^4.7.7",
"http-errors": "~1.6.3", "http-errors": "~1.6.3",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.1",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"knex": "^2.4.0", "knex": "^2.4.0",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
@@ -108,7 +106,7 @@
"url": "https://github.com/automatisch/automatisch/issues" "url": "https://github.com/automatisch/automatisch/issues"
}, },
"devDependencies": { "devDependencies": {
"@automatisch/types": "^0.8.0", "@automatisch/types": "^0.7.1",
"@types/bcrypt": "^5.0.0", "@types/bcrypt": "^5.0.0",
"@types/bull": "^3.15.8", "@types/bull": "^3.15.8",
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",

View File

@@ -6,7 +6,7 @@ import actions from './actions';
import dynamicData from './dynamic-data'; import dynamicData from './dynamic-data';
export default defineApp({ export default defineApp({
name: 'GitHub', name: 'Github',
key: 'github', key: 'github',
baseUrl: 'https://github.com', baseUrl: 'https://github.com',
apiBaseUrl: 'https://api.github.com', apiBaseUrl: 'https://api.github.com',

View File

@@ -9,11 +9,11 @@ export default {
// ref: // ref:
// - https://docs.gitlab.com/ee/api/projects.html#list-all-projects // - https://docs.gitlab.com/ee/api/projects.html#list-all-projects
// - https://docs.gitlab.com/ee/api/rest/index.html#keyset-based-pagination // - https://docs.gitlab.com/ee/api/rest/index.html#keyset-based-pagination
const firstPageRequest = $.http.get('/api/v4/projects', { const firstPageRequest = $.http.get('/api/v4/projects', {
params: { params: {
simple: true, simple: true,
pagination: 'keyset', pagination: 'keyset',
membership: true,
order_by: 'id', order_by: 'id',
sort: 'asc', sort: 'asc',
}, },

View File

@@ -6,7 +6,7 @@ import triggers from './triggers';
import dynamicData from './dynamic-data'; import dynamicData from './dynamic-data';
export default defineApp({ export default defineApp({
name: 'GitLab', name: 'Gitlab',
key: 'gitlab', key: 'gitlab',
baseUrl: 'https://gitlab.com', baseUrl: 'https://gitlab.com',
apiBaseUrl: 'https://gitlab.com', apiBaseUrl: 'https://gitlab.com',

View File

@@ -1,191 +0,0 @@
import { IJSONObject } from '@automatisch/types';
import defineAction from '../../../../helpers/define-action';
type THeaders = {
__id: string;
header: string;
}[];
type TSheetsResponse = {
sheets: {
properties: {
sheetId: string;
title: string;
};
}[];
};
type TBody = {
requests: IJSONObject[];
};
export default defineAction({
name: 'Create worksheet',
key: 'createWorksheet',
description:
'Create a blank worksheet with a title. Optionally, provide headers.',
arguments: [
{
label: 'Drive',
key: 'driveId',
type: 'dropdown' as const,
required: false,
description:
'The Google Drive where your spreadsheet resides. If nothing is selected, then your personal Google Drive will be used.',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listDrives',
},
],
},
},
{
label: 'Spreadsheet',
key: 'spreadsheetId',
type: 'dropdown' as const,
required: true,
dependsOn: ['parameters.driveId'],
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listSpreadsheets',
},
{
name: 'parameters.driveId',
value: '{parameters.driveId}',
},
],
},
},
{
label: 'Title',
key: 'title',
type: 'string' as const,
required: true,
description: '',
variables: true,
},
{
label: 'Headers',
key: 'headers',
type: 'dynamic' as const,
required: false,
fields: [
{
label: 'Header',
key: 'header',
type: 'string' as const,
required: true,
variables: true,
},
],
},
{
label: 'Overwrite',
key: 'overwrite',
type: 'dropdown' as const,
required: false,
value: false,
description:
'If a worksheet with the specified title exists, its content would be lost. Please, use with caution.',
variables: true,
options: [
{
label: 'Yes',
value: 'true',
},
{
label: 'No',
value: 'false',
},
],
},
],
async run($) {
const {
data: { sheets },
} = await $.http.get<TSheetsResponse>(
`/v4/spreadsheets/${$.step.parameters.spreadsheetId}`
);
const selectedSheet = sheets.find(
(sheet) => sheet.properties.title === $.step.parameters.title
);
const headers = $.step.parameters.headers as THeaders;
const values = headers.map((entry) => entry.header);
const body: TBody = {
requests: [
{
addSheet: {
properties: {
title: $.step.parameters.title,
},
},
},
],
};
if ($.step.parameters.overwrite === 'true' && selectedSheet) {
body.requests.unshift({
deleteSheet: {
sheetId: selectedSheet.properties.sheetId,
},
});
}
const { data } = await $.http.post(
`https://sheets.googleapis.com/v4/spreadsheets/${$.step.parameters.spreadsheetId}:batchUpdate`,
body
);
if (values.length) {
const body = {
requests: [
{
updateCells: {
rows: [
{
values: values.map((header) => ({
userEnteredValue: { stringValue: header },
})),
},
],
fields: '*',
start: {
sheetId:
data.replies[data.replies.length - 1].addSheet.properties
.sheetId,
rowIndex: 0,
columnIndex: 0,
},
},
},
],
};
const { data: response } = await $.http.post(
`https://sheets.googleapis.com/v4/spreadsheets/${$.step.parameters.spreadsheetId}:batchUpdate`,
body
);
$.setActionItem({
raw: response,
});
return;
}
$.setActionItem({
raw: data,
});
},
});

View File

@@ -1,5 +1,4 @@
import createSpreadsheet from './create-spreadsheet'; import createSpreadsheet from './create-spreadsheet';
import createSpreadsheetRow from './create-spreadsheet-row'; import createSpreadsheetRow from './create-spreadsheet-row';
import createWorksheet from './create-worksheet';
export default [createSpreadsheet, createSpreadsheetRow, createWorksheet]; export default [createSpreadsheet, createSpreadsheetRow];

View File

@@ -1,100 +0,0 @@
import { IJSONArray, IJSONObject } from '@automatisch/types';
import defineAction from '../../../../helpers/define-action';
type TBody = {
parent: IJSONObject;
properties: IJSONObject;
children: IJSONArray;
};
export default defineAction({
name: 'Create database item',
key: 'createDatabaseItem',
description: 'Creates an item in a database.',
arguments: [
{
label: 'Database',
key: 'databaseId',
type: 'dropdown' as const,
required: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listDatabases',
},
],
},
},
{
label: 'Name',
key: 'name',
type: 'string' as const,
required: false,
description:
'This field has a 2000 character limit. Any characters beyond 2000 will not be included.',
variables: true,
},
{
label: 'Content',
key: 'content',
type: 'string' as const,
required: false,
description:
'The text to add to the page body. The max length for this field is 2000 characters. Any characters beyond 2000 will not be included.',
variables: true,
},
],
async run($) {
const name = $.step.parameters.name as string;
const truncatedName = name.slice(0, 2000);
const content = $.step.parameters.content as string;
const truncatedContent = content.slice(0, 2000);
const body: TBody = {
parent: {
database_id: $.step.parameters.databaseId,
},
properties: {},
children: [],
};
if (name) {
body.properties.Name = {
title: [
{
text: {
content: truncatedName,
},
},
],
};
}
if (content) {
body.children = [
{
object: 'block',
paragraph: {
rich_text: [
{
text: {
content: truncatedContent,
},
},
],
},
},
];
}
const { data } = await $.http.post('/v1/pages', body);
$.setActionItem({
raw: data,
});
},
});

View File

@@ -1,104 +0,0 @@
import { IJSONArray, IJSONObject } from '@automatisch/types';
import defineAction from '../../../../helpers/define-action';
type TBody = {
parent: IJSONObject;
properties: IJSONObject;
children: IJSONArray;
};
export default defineAction({
name: 'Create page',
key: 'createPage',
description: 'Creates a page inside a parent page',
arguments: [
{
label: 'Parent page',
key: 'parentPageId',
type: 'dropdown' as const,
required: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listParentPages',
},
],
},
},
{
label: 'Title',
key: 'title',
type: 'string' as const,
required: false,
description:
'This field has a 2000 character limit. Any characters beyond 2000 will not be included.',
variables: true,
},
{
label: 'Content',
key: 'content',
type: 'string' as const,
required: false,
description:
'The text to add to the page body. The max length for this field is 2000 characters. Any characters beyond 2000 will not be included.',
variables: true,
},
],
async run($) {
const parentPageId = $.step.parameters.parentPageId as string;
const title = $.step.parameters.title as string;
const truncatedTitle = title.slice(0, 2000);
const content = $.step.parameters.content as string;
const truncatedContent = content.slice(0, 2000);
const body: TBody = {
parent: {
page_id: parentPageId,
},
properties: {},
children: [],
};
if (title) {
body.properties.title = {
type: 'title',
title: [
{
text: {
content: truncatedTitle,
},
},
],
};
}
if (content) {
body.children = [
{
object: 'block',
type: 'paragraph',
paragraph: {
rich_text: [
{
type: 'text',
text: {
content: truncatedContent,
},
},
],
},
},
];
}
const { data } = await $.http.post('/v1/pages', body);
$.setActionItem({
raw: data,
});
},
});

View File

@@ -1,70 +0,0 @@
import { IJSONArray, IJSONObject } from '@automatisch/types';
import defineAction from '../../../../helpers/define-action';
type TBody = {
filter: IJSONObject;
sorts: IJSONArray;
};
export default defineAction({
name: 'Find database item',
key: 'findDatabaseItem',
description: 'Searches for an item in a database by property.',
arguments: [
{
label: 'Database',
key: 'databaseId',
type: 'dropdown' as const,
required: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listDatabases',
},
],
},
},
{
label: 'Name',
key: 'name',
type: 'string' as const,
required: false,
description:
'This field has a 2000 character limit. Any characters beyond 2000 will not be included.',
variables: true,
},
],
async run($) {
const databaseId = $.step.parameters.databaseId as string;
const name = $.step.parameters.name as string;
const truncatedName = name.slice(0, 2000);
const body: TBody = {
filter: {
property: 'Name',
rich_text: {
equals: truncatedName,
},
},
sorts: [
{
timestamp: 'last_edited_time',
direction: 'descending',
},
],
};
const { data } = await $.http.post(
`/v1/databases/${databaseId}/query`,
body
);
$.setActionItem({
raw: data.results[0],
});
},
});

View File

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

View File

@@ -1,4 +1,3 @@
import listDatabases from './list-databases'; import listDatabases from './list-databases';
import listParentPages from './list-parent-pages';
export default [listDatabases, listParentPages]; export default [listDatabases];

View File

@@ -1,70 +0,0 @@
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
type Page = {
id: string;
properties: {
title: {
title: [
{
plain_text: string;
}
];
};
};
parent: {
workspace: boolean;
};
};
type ResponseData = {
results: Page[];
next_cursor?: string;
};
type Payload = {
filter: {
value: 'page';
property: 'object';
};
start_cursor?: string;
};
export default {
name: 'List parent pages',
key: 'listParentPages',
async run($: IGlobalVariable) {
const parentPages: {
data: IJSONObject[];
error: IJSONObject | null;
} = {
data: [],
error: null,
};
const payload: Payload = {
filter: {
value: 'page',
property: 'object',
},
};
do {
const response = await $.http.post<ResponseData>('/v1/search', payload);
payload.start_cursor = response.data.next_cursor;
const topLevelPages = response.data.results.filter(
(page) => page.parent.workspace
);
for (const pages of topLevelPages) {
parentPages.data.push({
value: pages.id as string,
name: pages.properties.title.title[0].plain_text as string,
});
}
} while (payload.start_cursor);
return parentPages;
},
};

View File

@@ -3,7 +3,6 @@ import addAuthHeader from './common/add-auth-header';
import addNotionVersionHeader from './common/add-notion-version-header'; import addNotionVersionHeader from './common/add-notion-version-header';
import auth from './auth'; import auth from './auth';
import triggers from './triggers'; import triggers from './triggers';
import actions from './actions';
import dynamicData from './dynamic-data'; import dynamicData from './dynamic-data';
export default defineApp({ export default defineApp({
@@ -15,9 +14,11 @@ export default defineApp({
authDocUrl: 'https://automatisch.io/docs/apps/notion/connection', authDocUrl: 'https://automatisch.io/docs/apps/notion/connection',
primaryColor: '000000', primaryColor: '000000',
supportsConnections: true, supportsConnections: true,
beforeRequest: [addAuthHeader, addNotionVersionHeader], beforeRequest: [
addAuthHeader,
addNotionVersionHeader,
],
auth, auth,
triggers, triggers,
actions,
dynamicData, dynamicData,
}); });

View File

@@ -16,27 +16,13 @@ export async function up(knex: Knex): Promise<void> {
.select('role') .select('role')
.groupBy('role'); .groupBy('role');
let shouldCreateAdminRole = true;
for (const { role } of uniqueUserRoles) { for (const { role } of uniqueUserRoles) {
// skip empty roles // skip empty roles
if (!role) continue; if (!role) continue;
const lowerCaseRole = lowerCase(role);
if (lowerCaseRole === 'admin') {
shouldCreateAdminRole = false;
}
await knex('roles').insert({ await knex('roles').insert({
name: capitalize(role), name: capitalize(role),
key: lowerCaseRole, key: lowerCase(role),
});
}
if (shouldCreateAdminRole) {
await knex('roles').insert({
name: 'Admin',
key: 'admin',
}); });
} }
} }

View File

@@ -1,46 +1,23 @@
import { Knex } from 'knex'; import { Knex } from 'knex';
const getPermissionForRole = (roleId: string, subject: string, actions: string[], conditions: string[] = []) => actions const getPermission = (subject: string, actions: string[]) => actions.map(action => ({ subject, action }));
.map(action => ({
role_id: roleId,
subject,
action,
conditions,
}));
export async function up(knex: Knex): Promise<void> { export async function up(knex: Knex): Promise<void> {
await knex.schema.createTable('permissions', (table) => { await knex.schema.createTable('permissions', (table) => {
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()')); table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
table.uuid('role_id').references('id').inTable('roles');
table.string('action').notNullable(); table.string('action').notNullable();
table.string('subject').notNullable(); table.string('subject').notNullable();
table.jsonb('conditions').notNullable().defaultTo([]);
table.timestamps(true, true); table.timestamps(true, true);
}); });
const roles = await knex('roles').select(['id', 'key']) as { id: string, key: string }[]; await knex('permissions').insert([
...getPermission('Connection', ['create', 'read', 'delete', 'update']),
for (const role of roles) { ...getPermission('Execution', ['read']),
// `admin` role should have no conditions unlike others by default ...getPermission('Flow', ['create', 'delete', 'publish', 'read', 'update']),
const isAdmin = role.key === 'admin'; ...getPermission('Role', ['create', 'delete', 'read', 'update']),
const roleConditions = isAdmin ? [] : ['isCreator']; ...getPermission('User', ['create', 'delete', 'read', 'update']),
]);
// default permissions
await knex('permissions').insert([
...getPermissionForRole(role.id, 'Connection', ['create', 'read', 'delete', 'update'], roleConditions),
...getPermissionForRole(role.id, 'Execution', ['read'], roleConditions),
...getPermissionForRole(role.id, 'Flow', ['create', 'delete', 'publish', 'read', 'update'], roleConditions),
]);
// admin specific permission
if (isAdmin) {
await knex('permissions').insert([
...getPermissionForRole(role.id, 'User', ['create', 'read', 'delete', 'update']),
...getPermissionForRole(role.id, 'Role', ['create', 'read', 'delete', 'update']),
]);
}
}
} }
export async function down(knex: Knex): Promise<void> { export async function down(knex: Knex): Promise<void> {

View File

@@ -0,0 +1,25 @@
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
await knex.schema.createTable('roles_permissions', (table) => {
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
table.uuid('role_id').references('id').inTable('roles');
table.uuid('permission_id').references('id').inTable('permissions');
});
const roles = await knex('roles').select('id');
const permissions = await knex('permissions').select('id');
for (const role of roles) {
for (const permission of permissions) {
await knex('roles_permissions').insert({
role_id: role.id,
permission_id: permission.id,
});
}
}
}
export async function down(knex: Knex): Promise<void> {
return knex.schema.dropTable('roles_permissions');
}

View File

@@ -13,7 +13,6 @@ export async function up(knex: Knex): Promise<void> {
table.text('email_attribute_name').notNullable(); table.text('email_attribute_name').notNullable();
table.text('role_attribute_name').notNullable(); table.text('role_attribute_name').notNullable();
table.uuid('default_role_id').references('id').inTable('roles'); table.uuid('default_role_id').references('id').inTable('roles');
table.boolean('active').defaultTo(false);
table.timestamps(true, true); table.timestamps(true, true);
}); });

View File

@@ -0,0 +1,13 @@
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
return knex.schema.alterTable('permissions', (table) => {
table.jsonb('conditions').notNullable().defaultTo([]);
});
}
export async function down(knex: Knex): Promise<void> {
return knex.schema.alterTable('permissions', (table) => {
table.dropColumn('conditions');
});
}

View File

@@ -1,33 +0,0 @@
import { Knex } from 'knex';
const getPermissionForRole = (
roleId: string,
subject: string,
actions: string[]
) =>
actions.map((action) => ({
role_id: roleId,
subject,
action,
conditions: [],
}));
export async function up(knex: Knex): Promise<void> {
const role = (await knex('roles')
.first(['id', 'key'])
.where({ key: 'admin' })
.limit(1)) as { id: string; key: string };
await knex('permissions').insert(
getPermissionForRole(role.id, 'SamlAuthProvider', [
'create',
'read',
'delete',
'update',
])
);
}
export async function down(knex: Knex): Promise<void> {
await knex('permissions').where({ subject: 'SamlAuthProvider' }).delete();
}

View File

@@ -25,8 +25,6 @@ import updateRole from './mutations/update-role.ee';
import updateStep from './mutations/update-step'; import updateStep from './mutations/update-step';
import updateUser from './mutations/update-user.ee'; import updateUser from './mutations/update-user.ee';
import verifyConnection from './mutations/verify-connection'; import verifyConnection from './mutations/verify-connection';
import createSamlAuthProvider from './mutations/create-saml-auth-provider.ee';
import updateSamlAuthProvider from './mutations/update-saml-auth-provider.ee';
const mutationResolvers = { const mutationResolvers = {
createConnection, createConnection,
@@ -56,8 +54,6 @@ const mutationResolvers = {
updateRole, updateRole,
updateStep, updateStep,
verifyConnection, verifyConnection,
createSamlAuthProvider,
updateSamlAuthProvider,
}; };
export default mutationResolvers; export default mutationResolvers;

View File

@@ -1,7 +1,6 @@
import kebabCase from 'lodash/kebabCase'; import kebabCase from 'lodash/kebabCase';
import Permission from '../../models/permission';
import Role from '../../models/role'; import Role from '../../models/role';
import Context from '../../types/express/context'; import Permission from '../../models/permission';
type Params = { type Params = {
input: { input: {
@@ -11,9 +10,8 @@ type Params = {
}; };
}; };
const createRole = async (_parent: unknown, params: Params, context: Context) => { // TODO: access
context.currentUser.can('create', 'Role'); const createRole = async (_parent: unknown, params: Params) => {
const { name, description, permissions } = params.input; const { name, description, permissions } = params.input;
const key = kebabCase(name); const key = kebabCase(name);

View File

@@ -1,54 +0,0 @@
import type { SamlConfig } from '@node-saml/passport-saml';
import SamlAuthProvider from '../../models/saml-auth-provider.ee';
import Context from '../../types/express/context';
type Params = {
input: {
name: string;
certificate: string;
signatureAlgorithm: SamlConfig['signatureAlgorithm'];
issuer: string;
entryPoint: string;
firstnameAttributeName: string;
surnameAttributeName: string;
emailAttributeName: string;
roleAttributeName: string;
defaultRoleId: string;
active: boolean;
};
};
const createSamlAuthProvider = async (
_parent: unknown,
params: Params,
context: Context
) => {
context.currentUser.can('create', 'SamlAuthProvider');
const samlAuthProviderPayload: Partial<SamlAuthProvider> = {
...params.input,
};
const existingSamlAuthProvider = await SamlAuthProvider.query()
.limit(1)
.first();
let samlAuthProvider: SamlAuthProvider;
if (!existingSamlAuthProvider) {
samlAuthProvider = await SamlAuthProvider.query().insert(
samlAuthProviderPayload
);
return samlAuthProvider;
}
samlAuthProvider = await SamlAuthProvider.query().patchAndFetchById(
existingSamlAuthProvider.id,
samlAuthProviderPayload
);
return samlAuthProvider;
};
export default createSamlAuthProvider;

View File

@@ -1,22 +1,17 @@
import User from '../../models/user'; import User from '../../models/user';
import Role from '../../models/role';
import Context from '../../types/express/context';
type Params = { type Params = {
input: { input: {
fullName: string; fullName: string;
email: string; email: string;
password: string; password: string;
role: { roleId: string;
id: string;
};
}; };
}; };
const createUser = async (_parent: unknown, params: Params, context: Context) => { // TODO: access
context.currentUser.can('create', 'User'); const createUser = async (_parent: unknown, params: Params) => {
const { fullName, email, password, roleId } = params.input;
const { fullName, email, password } = params.input;
const existingUser = await User.query().findOne({ email }); const existingUser = await User.query().findOne({ email });
@@ -24,23 +19,12 @@ const createUser = async (_parent: unknown, params: Params, context: Context) =>
throw new Error('User already exists!'); throw new Error('User already exists!');
} }
const userPayload: Partial<User> = { const user = await User.query().insert({
fullName, fullName,
email, email,
password, password,
}; roleId,
});
try {
context.currentUser.can('update', 'Role');
userPayload.roleId = params.input.role.id;
} catch {
// void
const role = await Role.query().findOne({ key: 'user' });
userPayload.roleId = role.id;
}
const user = await User.query().insert(userPayload);
return user; return user;
}; };

View File

@@ -2,7 +2,8 @@ import { Duration } from 'luxon';
import Context from '../../types/express/context'; import Context from '../../types/express/context';
import deleteUserQueue from '../../queues/delete-user.ee'; import deleteUserQueue from '../../queues/delete-user.ee';
const deleteCurrentUser = async (_parent: unknown, params: never, context: Context) => { // TODO: access
const deleteUser = async (_parent: unknown, params: never, context: Context) => {
const id = context.currentUser.id; const id = context.currentUser.id;
await context.currentUser.$query().delete(); await context.currentUser.$query().delete();
@@ -19,4 +20,4 @@ const deleteCurrentUser = async (_parent: unknown, params: never, context: Conte
return true; return true;
}; };
export default deleteCurrentUser; export default deleteUser;

View File

@@ -1,5 +1,4 @@
import Role from '../../models/role'; import Role from '../../models/role';
import SamlAuthProvider from '../../models/saml-auth-provider.ee';
import Context from '../../types/express/context'; import Context from '../../types/express/context';
type Params = { type Params = {
@@ -8,37 +7,22 @@ type Params = {
}; };
}; };
// TODO: access
const deleteRole = async ( const deleteRole = async (
_parent: unknown, _parent: unknown,
params: Params, params: Params,
context: Context context: Context
) => { ) => {
context.currentUser.can('delete', 'Role');
const role = await Role.query().findById(params.input.id).throwIfNotFound(); const role = await Role.query().findById(params.input.id).throwIfNotFound();
const count = await role.$relatedQuery('users').resultSize();
if (count > 0) {
throw new Error('All users must be migrated away from the role!');
}
if (role.isAdmin) { if (role.isAdmin) {
throw new Error('Admin role cannot be deleted!'); throw new Error('Admin role cannot be deleted!');
} }
const samlAuthProviderUsingDefaultRole = await SamlAuthProvider.query() /**
.where({ default_role_id: role.id }) * TODO: consider migrations for users that still have the role or
.limit(1) * do not let the role get deleted if there are still associated users
.first(); */
if (samlAuthProviderUsingDefaultRole) {
throw new Error(
'You need to change the default role in the SAML configuration before deleting this role.'
);
}
// delete permissions first
await role.$relatedQuery('permissions').delete();
await role.$query().delete(); await role.$query().delete();
return true; return true;

View File

@@ -9,13 +9,12 @@ type Params = {
}; };
}; };
// TODO: access
const deleteUser = async ( const deleteUser = async (
_parent: unknown, _parent: unknown,
params: Params, params: Params,
context: Context context: Context
) => { ) => {
context.currentUser.can('delete', 'User');
const id = params.input.id; const id = params.input.id;
await User.query().deleteById(id); await User.query().deleteById(id);

View File

@@ -13,7 +13,7 @@ const TOKEN_EXPIRES_IN = '14d';
const login = async (_parent: unknown, params: Params) => { const login = async (_parent: unknown, params: Params) => {
const user = await User.query().findOne({ const user = await User.query().findOne({
email: params.input.email.toLowerCase(), email: params.input.email,
}); });
if (user && (await user.login(params.input.password))) { if (user && (await user.login(params.input.password))) {

View File

@@ -1,7 +1,6 @@
import Context from '../../types/express/context'; import Context from '../../types/express/context';
import Role from '../../models/role'; import Role from '../../models/role';
import Permission from '../../models/permission'; import Permission from '../../models/permission';
import permissionCatalog from '../../helpers/permission-catalog.ee';
type Params = { type Params = {
input: { input: {
@@ -12,13 +11,12 @@ type Params = {
}; };
}; };
const updateRole = async ( // TODO: access
const updateUser = async (
_parent: unknown, _parent: unknown,
params: Params, params: Params,
context: Context context: Context
) => { ) => {
context.currentUser.can('update', 'Role');
const { const {
id, id,
name, name,
@@ -26,66 +24,21 @@ const updateRole = async (
permissions, permissions,
} = params.input; } = params.input;
const role = await Role const role = await Role.query().findById(id).throwIfNotFound();
.query()
.findById(id)
.throwIfNotFound();
try { // TODO: delete the unrelated items!
const updatedRole = await Role.transaction(async (trx) => { await role.$relatedQuery('permissions').unrelate();
await role.$relatedQuery('permissions', trx).delete();
if (permissions?.length) { // TODO: possibly assert that given permissions do actually exist in catalog
const sanitizedPermissions = permissions // TODO: possibly optimize it with patching the different permissions compared to current ones
.filter((permission) => { return await role.$query()
const { .patchAndFetch(
action, {
subject, name,
conditions, description,
} = permission; permissions,
const relevantAction = permissionCatalog.actions.find(actionCatalogItem => actionCatalogItem.key === action);
const validSubject = relevantAction.subjects.includes(subject);
const validConditions = conditions.every(condition => {
return !!permissionCatalog
.conditions
.find((conditionCatalogItem) => conditionCatalogItem.key === condition);
})
return validSubject && validConditions;
})
.map((permission) => ({
...permission,
roleId: role.id,
}));
await Permission.query().insert(sanitizedPermissions);
} }
);
await role
.$query(trx)
.patch(
{
name,
description,
}
);
return await Role
.query(trx)
.leftJoinRelated({
permissions: true
})
.withGraphFetched({
permissions: true
})
.findById(id);
});
return updatedRole;
} catch (err) {
throw new Error('The role could not be updated!');
}
}; };
export default updateRole; export default updateUser;

View File

@@ -1,45 +0,0 @@
import type { SamlConfig } from '@node-saml/passport-saml';
import SamlAuthProvider from '../../models/saml-auth-provider.ee';
import Context from '../../types/express/context';
type Params = {
input: {
name: string;
certificate: string;
signatureAlgorithm: SamlConfig['signatureAlgorithm'];
issuer: string;
entryPoint: string;
firstnameAttributeName: string;
surnameAttributeName: string;
emailAttributeName: string;
roleAttributeName: string;
defaultRoleId: string;
active: boolean;
};
};
const updateSamlAuthProvider = async (
_parent: unknown,
params: Params,
context: Context
) => {
context.currentUser.can('update', 'SamlAuthProvider');
const samlAuthProviderPayload: Partial<SamlAuthProvider> = {
...params.input,
};
const existingSamlAuthProvider = await SamlAuthProvider.query()
.limit(1)
.first()
.throwIfNotFound();
const samlAuthProvider = await SamlAuthProvider.query().patchAndFetchById(
existingSamlAuthProvider.id,
samlAuthProviderPayload
);
return samlAuthProvider;
};
export default updateSamlAuthProvider;

View File

@@ -12,30 +12,21 @@ type Params = {
}; };
}; };
// TODO: access
const updateUser = async ( const updateUser = async (
_parent: unknown, _parent: unknown,
params: Params, params: Params,
context: Context context: Context
) => { ) => {
context.currentUser.can('update', 'User'); const user = await User.query()
.patchAndFetchById(
const userPayload: Partial<User> = { params.input.id,
email: params.input.email, {
fullName: params.input.fullName, email: params.input.email,
}; fullName: params.input.fullName,
roleId: params.input.role.id,
try { }
context.currentUser.can('update', 'Role'); );
userPayload.roleId = params.input.role.id;
} catch {
// void
}
const user = await User.query().patchAndFetchById(
params.input.id,
userPayload
);
return user; return user;
}; };

View File

@@ -1,5 +1,4 @@
import App from '../../models/app'; import App from '../../models/app';
import Connection from '../../models/connection';
import Context from '../../types/express/context'; import Context from '../../types/express/context';
type Params = { type Params = {
@@ -7,17 +6,13 @@ type Params = {
}; };
const getApp = async (_parent: unknown, params: Params, context: Context) => { const getApp = async (_parent: unknown, params: Params, context: Context) => {
const conditions = context.currentUser.can('read', 'Connection'); context.currentUser.can('read', 'Connection');
const userConnections = context.currentUser.$relatedQuery('connections');
const allConnections = Connection.query();
const connectionBaseQuery = conditions.isCreator ? userConnections : allConnections;
const app = await App.findOneByKey(params.key); const app = await App.findOneByKey(params.key);
if (context.currentUser) { if (context.currentUser) {
const connections = await connectionBaseQuery const connections = await context.currentUser
.clone() .$relatedQuery('connections')
.select('connections.*') .select('connections.*')
.fullOuterJoinRelated('steps') .fullOuterJoinRelated('steps')
.where({ .where({

View File

@@ -1,8 +1,6 @@
import { IConnection } from '@automatisch/types'; import { IConnection } from '@automatisch/types';
import App from '../../models/app'; import App from '../../models/app';
import Context from '../../types/express/context'; import Context from '../../types/express/context';
import Flow from '../../models/flow';
import Connection from '../../models/connection';
type Params = { type Params = {
name: string; name: string;
@@ -13,27 +11,19 @@ const getConnectedApps = async (
params: Params, params: Params,
context: Context context: Context
) => { ) => {
const conditions = context.currentUser.can('read', 'Connection'); context.currentUser.can('read', 'Connection');
const userConnections = context.currentUser.$relatedQuery('connections');
const allConnections = Connection.query();
const connectionBaseQuery = conditions.isCreator ? userConnections : allConnections;
const userFlows = context.currentUser.$relatedQuery('flows');
const allFlows = Flow.query();
const flowBaseQuery = conditions.isCreator ? userFlows : allFlows;
let apps = await App.findAll(params.name); let apps = await App.findAll(params.name);
const connections = await connectionBaseQuery const connections = await context.currentUser
.clone() .$relatedQuery('connections')
.select('connections.key') .select('connections.key')
.where({ draft: false }) .where({ draft: false })
.count('connections.id as count') .count('connections.id as count')
.groupBy('connections.key'); .groupBy('connections.key');
const flows = await flowBaseQuery const flows = await context.currentUser
.clone() .$relatedQuery('flows')
.withGraphJoined('steps') .withGraphJoined('steps')
.orderBy('created_at', 'desc'); .orderBy('created_at', 'desc');

View File

@@ -1,7 +1,6 @@
import { IDynamicData, IJSONObject } from '@automatisch/types'; import { IDynamicData, IJSONObject } from '@automatisch/types';
import Context from '../../types/express/context'; import Context from '../../types/express/context';
import App from '../../models/app'; import App from '../../models/app';
import Step from '../../models/step';
import ExecutionStep from '../../models/execution-step'; import ExecutionStep from '../../models/execution-step';
import globalVariable from '../../helpers/global-variable'; import globalVariable from '../../helpers/global-variable';
import computeParameters from '../../helpers/compute-parameters'; import computeParameters from '../../helpers/compute-parameters';
@@ -17,13 +16,10 @@ const getDynamicData = async (
params: Params, params: Params,
context: Context context: Context
) => { ) => {
const conditions = context.currentUser.can('update', 'Flow'); context.currentUser.can('update', 'Flow');
const userSteps = context.currentUser.$relatedQuery('steps');
const allSteps = Step.query();
const stepBaseQuery = conditions.isCreator ? userSteps : allSteps;
const step = await stepBaseQuery const step = await context.currentUser
.clone() .$relatedQuery('steps')
.withGraphFetched({ .withGraphFetched({
connection: true, connection: true,
flow: true, flow: true,

View File

@@ -1,7 +1,6 @@
import { IDynamicFields, IJSONObject } from '@automatisch/types'; import { IDynamicFields, IJSONObject } from '@automatisch/types';
import Context from '../../types/express/context'; import Context from '../../types/express/context';
import App from '../../models/app'; import App from '../../models/app';
import Step from '../../models/step';
import globalVariable from '../../helpers/global-variable'; import globalVariable from '../../helpers/global-variable';
type Params = { type Params = {
@@ -15,13 +14,10 @@ const getDynamicFields = async (
params: Params, params: Params,
context: Context context: Context
) => { ) => {
const conditions = context.currentUser.can('update', 'Flow'); context.currentUser.can('update', 'Flow');
const userSteps = context.currentUser.$relatedQuery('steps');
const allSteps = Step.query();
const stepBaseQuery = conditions.isCreator ? userSteps : allSteps;
const step = await stepBaseQuery const step = await context.currentUser
.clone() .$relatedQuery('steps')
.withGraphFetched({ .withGraphFetched({
connection: true, connection: true,
flow: true, flow: true,

View File

@@ -1,6 +1,5 @@
import Context from '../../types/express/context'; import Context from '../../types/express/context';
import paginate from '../../helpers/pagination'; import paginate from '../../helpers/pagination';
import Execution from '../../models/execution';
type Params = { type Params = {
executionId: string; executionId: string;
@@ -13,13 +12,10 @@ const getExecutionSteps = async (
params: Params, params: Params,
context: Context context: Context
) => { ) => {
const conditions = context.currentUser.can('read', 'Execution'); context.currentUser.can('read', 'Execution');
const userExecutions = context.currentUser.$relatedQuery('executions');
const allExecutions = Execution.query();
const executionBaseQuery = conditions.isCreator ? userExecutions : allExecutions;
const execution = await executionBaseQuery const execution = await context.currentUser
.clone() .$relatedQuery('executions')
.withSoftDeleted() .withSoftDeleted()
.findById(params.executionId) .findById(params.executionId)
.throwIfNotFound(); .throwIfNotFound();

View File

@@ -1,5 +1,4 @@
import Context from '../../types/express/context'; import Context from '../../types/express/context';
import Execution from '../../models/execution';
type Params = { type Params = {
executionId: string; executionId: string;
@@ -10,13 +9,10 @@ const getExecution = async (
params: Params, params: Params,
context: Context context: Context
) => { ) => {
const conditions = context.currentUser.can('read', 'Execution'); context.currentUser.can('read', 'Execution');
const userExecutions = context.currentUser.$relatedQuery('executions');
const allExecutions = Execution.query();
const executionBaseQuery = conditions.isCreator ? userExecutions : allExecutions;
const execution = await executionBaseQuery const execution = await context.currentUser
.clone() .$relatedQuery('executions')
.withGraphFetched({ .withGraphFetched({
flow: { flow: {
steps: true, steps: true,

View File

@@ -1,6 +1,5 @@
import { raw } from 'objection'; import { raw } from 'objection';
import Context from '../../types/express/context'; import Context from '../../types/express/context';
import Execution from '../../models/execution';
import paginate from '../../helpers/pagination'; import paginate from '../../helpers/pagination';
type Params = { type Params = {
@@ -13,11 +12,7 @@ const getExecutions = async (
params: Params, params: Params,
context: Context context: Context
) => { ) => {
const conditions = context.currentUser.can('read', 'Execution'); context.currentUser.can('read', 'Execution');
const userExecutions = context.currentUser.$relatedQuery('executions');
const allExecutions = Execution.query();
const executionBaseQuery = conditions.isCreator ? userExecutions : allExecutions;
const selectStatusStatement = ` const selectStatusStatement = `
case case
@@ -28,8 +23,8 @@ const getExecutions = async (
as status as status
`; `;
const executions = executionBaseQuery const executions = context.currentUser
.clone() .$relatedQuery('executions')
.joinRelated('executionSteps as execution_steps') .joinRelated('executionSteps as execution_steps')
.select('executions.*', raw(selectStatusStatement)) .select('executions.*', raw(selectStatusStatement))
.withSoftDeleted() .withSoftDeleted()

View File

@@ -1,18 +1,14 @@
import Context from '../../types/express/context'; import Context from '../../types/express/context';
import Flow from '../../models/flow';
type Params = { type Params = {
id: string; id: string;
}; };
const getFlow = async (_parent: unknown, params: Params, context: Context) => { const getFlow = async (_parent: unknown, params: Params, context: Context) => {
const conditions = context.currentUser.can('read', 'Flow'); context.currentUser.can('read', 'Flow');
const userFlows = context.currentUser.$relatedQuery('flows');
const allFlows = Flow.query();
const baseQuery = conditions.isCreator ? userFlows : allFlows;
const flow = await baseQuery const flow = await context.currentUser
.clone() .$relatedQuery('flows')
.withGraphJoined('[steps.[connection]]') .withGraphJoined('[steps.[connection]]')
.orderBy('steps.position', 'asc') .orderBy('steps.position', 'asc')
.findOne({ 'flows.id': params.id }) .findOne({ 'flows.id': params.id })

View File

@@ -1,4 +1,3 @@
import Flow from '../../models/flow';
import Context from '../../types/express/context'; import Context from '../../types/express/context';
import paginate from '../../helpers/pagination'; import paginate from '../../helpers/pagination';
@@ -11,13 +10,10 @@ type Params = {
}; };
const getFlows = async (_parent: unknown, params: Params, context: Context) => { const getFlows = async (_parent: unknown, params: Params, context: Context) => {
const conditions = context.currentUser.can('read', 'Flow'); context.currentUser.can('read', 'Flow');
const userFlows = context.currentUser.$relatedQuery('flows');
const allFlows = Flow.query();
const baseQuery = conditions.isCreator ? userFlows : allFlows;
const flowsQuery = baseQuery const flowsQuery = context.currentUser
.clone() .$relatedQuery('flows')
.joinRelated({ .joinRelated({
steps: true, steps: true,
}) })

View File

@@ -1,7 +0,0 @@
import permissionCatalog from '../../helpers/permission-catalog.ee';
const getPermissionCatalog = async () => {
return permissionCatalog;
};
export default getPermissionCatalog;

View File

@@ -0,0 +1,76 @@
const getPermissions = async () => {
const Connection = {
label: 'Connection',
key: 'Connection',
};
const Flow = {
label: 'Flow',
key: 'Flow',
};
const Execution = {
label: 'Execution',
key: 'Execution',
};
const permissions = {
conditions: [
{
key: 'isCreator',
label: 'Is creator'
}
],
actions: [
{
label: 'Create',
action: 'create',
subjects: [
Connection.key,
Flow.key,
]
},
{
label: 'Read',
action: 'read',
subjects: [
Connection.key,
Execution.key,
Flow.key,
]
},
{
label: 'Update',
action: 'update',
subjects: [
Connection.key,
Flow.key,
]
},
{
label: 'Delete',
action: 'delete',
subjects: [
Connection.key,
Flow.key,
]
},
{
label: 'Publish',
action: 'publish',
subjects: [
Flow.key,
]
}
],
subjects: [
Connection,
Flow,
Execution
]
};
return permissions;
};
export default getPermissions;

View File

@@ -5,19 +5,9 @@ type Params = {
id: string id: string
}; };
// TODO: access
const getRole = async (_parent: unknown, params: Params, context: Context) => { const getRole = async (_parent: unknown, params: Params, context: Context) => {
context.currentUser.can('read', 'Role'); return await Role.query().findById(params.id).throwIfNotFound();
return await Role
.query()
.leftJoinRelated({
permissions: true
})
.withGraphFetched({
permissions: true
})
.findById(params.id)
.throwIfNotFound();
}; };
export default getRole; export default getRole;

View File

@@ -1,10 +1,9 @@
import Context from '../../types/express/context'; import Context from '../../types/express/context';
import Role from '../../models/role'; import Role from '../../models/role';
// TODO: access
const getRoles = async (_parent: unknown, params: unknown, context: Context) => { const getRoles = async (_parent: unknown, params: unknown, context: Context) => {
context.currentUser.can('read', 'Role'); return await Role.query();
return await Role.query().orderBy('name');
}; };
export default getRoles; export default getRoles;

View File

@@ -1,7 +1,7 @@
import SamlAuthProvider from '../../models/saml-auth-provider.ee'; import SamlAuthProvider from '../../models/saml-auth-provider.ee';
const getSamlAuthProviders = async () => { const getSamlAuthProviders = async () => {
const providers = await SamlAuthProvider.query().where({ active: true }); const providers = await SamlAuthProvider.query();
return providers; return providers;
}; };

View File

@@ -1,7 +1,6 @@
import { ref } from 'objection';
import ExecutionStep from '../../models/execution-step';
import Step from '../../models/step';
import Context from '../../types/express/context'; import Context from '../../types/express/context';
import ExecutionStep from '../../models/execution-step';
import { ref } from 'objection';
type Params = { type Params = {
stepId: string; stepId: string;
@@ -12,18 +11,15 @@ const getStepWithTestExecutions = async (
params: Params, params: Params,
context: Context context: Context
) => { ) => {
const conditions = context.currentUser.can('update', 'Flow'); context.currentUser.can('update', 'Flow');
const userSteps = context.currentUser.$relatedQuery('steps');
const allSteps = Step.query();
const stepBaseQuery = conditions.isCreator ? userSteps : allSteps;
const step = await stepBaseQuery const step = await context.currentUser
.clone() .$relatedQuery('steps')
.findOne({ 'steps.id': params.stepId }) .findOne({ 'steps.id': params.stepId })
.throwIfNotFound(); .throwIfNotFound();
const previousStepsWithCurrentStep = await stepBaseQuery const previousStepsWithCurrentStep = await context.currentUser
.clone() .$relatedQuery('steps')
.withGraphJoined('executionSteps') .withGraphJoined('executionSteps')
.where('flow_id', '=', step.flowId) .where('flow_id', '=', step.flowId)
.andWhere('position', '<', step.position) .andWhere('position', '<', step.position)

View File

@@ -5,9 +5,8 @@ type Params = {
id: string id: string
}; };
// TODO: access
const getUser = async (_parent: unknown, params: Params, context: Context) => { const getUser = async (_parent: unknown, params: Params, context: Context) => {
context.currentUser.can('read', 'User');
return await User return await User
.query() .query()
.leftJoinRelated({ .leftJoinRelated({

View File

@@ -7,9 +7,8 @@ type Params = {
offset: number; offset: number;
}; };
// TODO: access
const getUsers = async (_parent: unknown, params: Params, context: Context) => { const getUsers = async (_parent: unknown, params: Params, context: Context) => {
context.currentUser.can('read', 'User');
const usersQuery = User const usersQuery = User
.query() .query()
.leftJoinRelated({ .leftJoinRelated({

View File

@@ -1,6 +1,5 @@
import Context from '../../types/express/context'; import Context from '../../types/express/context';
import App from '../../models/app'; import App from '../../models/app';
import Connection from '../../models/connection';
import globalVariable from '../../helpers/global-variable'; import globalVariable from '../../helpers/global-variable';
type Params = { type Params = {
@@ -13,13 +12,10 @@ const testConnection = async (
params: Params, params: Params,
context: Context context: Context
) => { ) => {
const conditions = context.currentUser.can('update', 'Connection'); context.currentUser.can('update', 'Connection');
const userConnections = context.currentUser.$relatedQuery('connections');
const allConnections = Connection.query();
const connectionBaseQuery = conditions.isCreator ? userConnections : allConnections;
let connection = await connectionBaseQuery let connection = await context.currentUser
.clone() .$relatedQuery('connections')
.findOne({ .findOne({
id: params.id, id: params.id,
}) })

View File

@@ -16,7 +16,7 @@ import getUsers from './queries/get-users';
import getInvoices from './queries/get-invoices.ee'; import getInvoices from './queries/get-invoices.ee';
import getPaddleInfo from './queries/get-paddle-info.ee'; import getPaddleInfo from './queries/get-paddle-info.ee';
import getPaymentPlans from './queries/get-payment-plans.ee'; import getPaymentPlans from './queries/get-payment-plans.ee';
import getPermissionCatalog from './queries/get-permission-catalog.ee'; import getPermissions from './queries/get-permissions.ee';
import getRole from './queries/get-role.ee'; import getRole from './queries/get-role.ee';
import getRoles from './queries/get-roles.ee'; import getRoles from './queries/get-roles.ee';
import getSamlAuthProviders from './queries/get-saml-auth-providers.ee'; import getSamlAuthProviders from './queries/get-saml-auth-providers.ee';
@@ -43,7 +43,7 @@ const queryResolvers = {
getInvoices, getInvoices,
getPaddleInfo, getPaddleInfo,
getPaymentPlans, getPaymentPlans,
getPermissionCatalog, getPermissions,
getRole, getRole,
getRoles, getRoles,
getSamlAuthProviders, getSamlAuthProviders,

View File

@@ -42,11 +42,14 @@ type Query {
getTrialStatus: GetTrialStatus getTrialStatus: GetTrialStatus
getSubscriptionStatus: GetSubscriptionStatus getSubscriptionStatus: GetSubscriptionStatus
getSamlAuthProviders: [GetSamlAuthProviders] getSamlAuthProviders: [GetSamlAuthProviders]
getUsers(limit: Int!, offset: Int!): UserConnection getUsers(
limit: Int!
offset: Int!
): UserConnection
getUser(id: String!): User getUser(id: String!): User
getRoles: [Role] getRoles: [Role]
getRole(id: String!): Role getRole(id: String!): Role
getPermissionCatalog: PermissionCatalog getPermissions: Permissions
healthcheck: AppHealth healthcheck: AppHealth
} }
@@ -78,8 +81,6 @@ type Mutation {
updateStep(input: UpdateStepInput): Step updateStep(input: UpdateStepInput): Step
updateUser(input: UpdateUserInput): User updateUser(input: UpdateUserInput): User
verifyConnection(input: VerifyConnectionInput): Connection verifyConnection(input: VerifyConnectionInput): Connection
createSamlAuthProvider(input: CreateSamlAuthProviderInput): SamlAuthProvider
updateSamlAuthProvider(input: UpdateSamlAuthProviderInput): SamlAuthProvider
} }
""" """
@@ -291,20 +292,6 @@ type Execution {
flow: Flow flow: Flow
} }
type SamlAuthProvider {
id: String
name: String
certificate: String
signatureAlgorithm: String
issuer: String
entryPoint: String
firstnameAttributeName: String
surnameAttributeName: String
emailAttributeName: String
roleAttributeName: String
active: Boolean
}
type UserConnection { type UserConnection {
edges: [UserEdge] edges: [UserEdge]
pageInfo: PageInfo pageInfo: PageInfo
@@ -336,34 +323,6 @@ input VerifyConnectionInput {
id: String! id: String!
} }
input CreateSamlAuthProviderInput {
name: String!
certificate: String!
signatureAlgorithm: String!
issuer: String!
entryPoint: String!
firstnameAttributeName: String!
surnameAttributeName: String!
emailAttributeName: String!
roleAttributeName: String!
defaultRoleId: String!
active: Boolean!
}
input UpdateSamlAuthProviderInput {
name: String!
certificate: String!
signatureAlgorithm: String!
issuer: String!
entryPoint: String!
firstnameAttributeName: String!
surnameAttributeName: String!
emailAttributeName: String!
roleAttributeName: String!
defaultRoleId: String!
active: Boolean!
}
input DeleteConnectionInput { input DeleteConnectionInput {
id: String! id: String!
} }
@@ -681,13 +640,13 @@ type GetSamlAuthProviders {
} }
type Permission { type Permission {
id: String
action: String action: String
subject: String subject: String
conditions: [String] conditions: [String]
} }
type PermissionCatalog { # TODO: emphasize it's a catalog item
type Permissions {
actions: [Action] actions: [Action]
subjects: [Subject] subjects: [Subject]
conditions: [Condition] conditions: [Condition]
@@ -695,7 +654,7 @@ type PermissionCatalog {
type Action { type Action {
label: String label: String
key: String action: String
subjects: [String] subjects: [String]
} }

View File

@@ -1,24 +0,0 @@
import axios, { AxiosRequestConfig } from 'axios';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { HttpProxyAgent } from 'http-proxy-agent';
const config: AxiosRequestConfig = {};
const httpProxyUrl = process.env.http_proxy;
const httpsProxyUrl = process.env.https_proxy;
const supportsProxy = httpProxyUrl || httpsProxyUrl;
if (supportsProxy) {
if (httpProxyUrl) {
config.httpAgent = new HttpProxyAgent(process.env.http_proxy);
}
if (httpsProxyUrl) {
config.httpsAgent = new HttpsProxyAgent(process.env.https_proxy);
}
config.proxy = false;
}
const axiosWithProxyInstance = axios.create(config);
export default axiosWithProxyInstance;

View File

@@ -1,4 +1,3 @@
// TODO: replace with axios-with-proxy when needed
import axios from 'axios'; import axios from 'axios';
import appConfig from '../../config/app'; import appConfig from '../../config/app';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';

View File

@@ -1,4 +1,3 @@
// TODO: replace with axios-with-proxy
import axios from 'axios'; import axios from 'axios';
import appConfig from '../config/app'; import appConfig from '../config/app';
import memoryCache from 'memory-cache'; import memoryCache from 'memory-cache';

View File

@@ -3,7 +3,7 @@ import ExecutionStep from '../models/execution-step';
import get from 'lodash.get'; import get from 'lodash.get';
// INFO: don't remove space in allowed character group! // INFO: don't remove space in allowed character group!
const variableRegExp = /({{step\.[\da-zA-Z-]+(?:\.[\da-zA-Z-_ :]+)+}})/g; const variableRegExp = /({{step\.[\da-zA-Z-]+(?:\.[\da-zA-Z-_ ]+)+}})/g;
export default function computeParameters( export default function computeParameters(
parameters: Step['parameters'], parameters: Step['parameters'],
@@ -42,7 +42,7 @@ export default function computeParameters(
if (Array.isArray(value)) { if (Array.isArray(value)) {
return { return {
...result, ...result,
[key]: value.map((item) => computeParameters(item, executionSteps)), [key]: value.map(item => computeParameters(item, executionSteps)),
}; };
} }

View File

@@ -1,10 +1,8 @@
import { IHttpClientParams } from '@automatisch/types'; import axios, { AxiosRequestConfig } from 'axios';
import { AxiosRequestConfig } from 'axios';
import { URL } from 'node:url';
export { AxiosInstance as IHttpClient } from 'axios'; export { AxiosInstance as IHttpClient } from 'axios';
import { IHttpClientParams } from '@automatisch/types';
import { URL } from 'url';
import HttpError from '../../errors/http'; import HttpError from '../../errors/http';
import axios from '../axios-with-proxy';
const removeBaseUrlForAbsoluteUrls = ( const removeBaseUrlForAbsoluteUrls = (
requestConfig: AxiosRequestConfig requestConfig: AxiosRequestConfig

View File

@@ -1,72 +0,0 @@
const Connection = {
label: 'Connection',
key: 'Connection',
};
const Flow = {
label: 'Flow',
key: 'Flow',
};
const Execution = {
label: 'Execution',
key: 'Execution',
};
const permissionCatalog = {
conditions: [
{
key: 'isCreator',
label: 'Is creator'
}
],
actions: [
{
label: 'Create',
key: 'create',
subjects: [
Connection.key,
Flow.key,
]
},
{
label: 'Read',
key: 'read',
subjects: [
Connection.key,
Execution.key,
Flow.key,
]
},
{
label: 'Update',
key: 'update',
subjects: [
Connection.key,
Flow.key,
]
},
{
label: 'Delete',
key: 'delete',
subjects: [
Connection.key,
Flow.key,
]
},
{
label: 'Publish',
key: 'publish',
subjects: [
Flow.key,
]
}
],
subjects: [
Connection,
Flow,
Execution
]
};
export default permissionCatalog;

View File

@@ -1,20 +0,0 @@
import { PureAbility, fieldPatternMatcher, mongoQueryMatcher } from '@casl/ability';
import type User from '../models/user'
// Must be kept in sync with `packages/web/src/helpers/userAbility.ts`!
export default function userAbility(user: Partial<User>) {
const permissions = user?.permissions;
const role = user?.role;
// We're not using mongo, but our fields, conditions match
const options = {
conditionsMatcher: mongoQueryMatcher,
fieldMatcher: fieldPatternMatcher
};
if (!role || !permissions) {
return new PureAbility([], options);
}
return new PureAbility<[string, string], string[]>(permissions, options);
}

View File

@@ -2,7 +2,6 @@ import Base from './base';
class Permission extends Base { class Permission extends Base {
id: string; id: string;
roleId: string;
action: string; action: string;
subject: string; subject: string;
conditions: string[]; conditions: string[];
@@ -11,11 +10,10 @@ class Permission extends Base {
static jsonSchema = { static jsonSchema = {
type: 'object', type: 'object',
required: ['roleId', 'action', 'subject'], required: ['action', 'subject'],
properties: { properties: {
id: { type: 'string', format: 'uuid' }, id: { type: 'string', format: 'uuid' },
roleId: { type: 'string', format: 'uuid' },
action: { type: 'string', minLength: 1 }, action: { type: 'string', minLength: 1 },
subject: { type: 'string', minLength: 1 }, subject: { type: 'string', minLength: 1 },
conditions: { type: 'array', items: { type: 'string' } }, conditions: { type: 'array', items: { type: 'string' } },

View File

@@ -20,7 +20,7 @@ class Role extends Base {
id: { type: 'string', format: 'uuid' }, id: { type: 'string', format: 'uuid' },
name: { type: 'string', minLength: 1 }, name: { type: 'string', minLength: 1 },
key: { type: 'string', minLength: 1 }, key: { type: 'string', minLength: 1 },
description: { type: ['string', 'null'], maxLength: 255 }, description: { type: ['string', 'null'], minLength: 1, maxLength: 255 },
createdAt: { type: 'string' }, createdAt: { type: 'string' },
updatedAt: { type: 'string' }, updatedAt: { type: 'string' },
}, },
@@ -40,11 +40,15 @@ class Role extends Base {
}, },
}, },
permissions: { permissions: {
relation: Base.HasManyRelation, relation: Base.ManyToManyRelation,
modelClass: Permission, modelClass: Permission,
join: { join: {
from: 'roles.id', from: 'roles.id',
to: 'permissions.role_id', through: {
from: 'roles_permissions.role_id',
to: 'roles_permissions.permission_id',
},
to: 'permissions.id',
}, },
}, },
}); });

View File

@@ -8,7 +8,7 @@ class SamlAuthProvider extends Base {
id!: string; id!: string;
name: string; name: string;
certificate: string; certificate: string;
signatureAlgorithm: SamlConfig['signatureAlgorithm']; signatureAlgorithm: SamlConfig["signatureAlgorithm"];
issuer: string; issuer: string;
entryPoint: string; entryPoint: string;
firstnameAttributeName: string; firstnameAttributeName: string;
@@ -16,7 +16,6 @@ class SamlAuthProvider extends Base {
emailAttributeName: string; emailAttributeName: string;
roleAttributeName: string; roleAttributeName: string;
defaultRoleId: string; defaultRoleId: string;
active: boolean;
static tableName = 'saml_auth_providers'; static tableName = 'saml_auth_providers';
@@ -39,18 +38,14 @@ class SamlAuthProvider extends Base {
id: { type: 'string', format: 'uuid' }, id: { type: 'string', format: 'uuid' },
name: { type: 'string', minLength: 1 }, name: { type: 'string', minLength: 1 },
certificate: { type: 'string', minLength: 1 }, certificate: { type: 'string', minLength: 1 },
signatureAlgorithm: { signatureAlgorithm: { type: 'string', enum: ['sha1', 'sha256', 'sha512'] },
type: 'string',
enum: ['sha1', 'sha256', 'sha512'],
},
issuer: { type: 'string', minLength: 1 }, issuer: { type: 'string', minLength: 1 },
entryPoint: { type: 'string', minLength: 1 }, entryPoint: { type: 'string', minLength: 1 },
firstnameAttributeName: { type: 'string', minLength: 1 }, firstnameAttributeName: { type: 'string', minLength: 1 },
surnameAttributeName: { type: 'string', minLength: 1 }, surnameAttributeName: { type: 'string', minLength: 1 },
emailAttributeName: { type: 'string', minLength: 1 }, emailAttributeName: { type: 'string', minLength: 1 },
roleAttributeName: { type: 'string', minLength: 1 }, roleAttributeName: { type: 'string', minLength: 1 },
defaultRoleId: { type: 'string', format: 'uuid' }, defaultRoleId: { type: 'string', format: 'uuid' }
active: { type: 'boolean' },
}, },
}; };
@@ -77,7 +72,7 @@ class SamlAuthProvider extends Base {
entryPoint: this.entryPoint, entryPoint: this.entryPoint,
issuer: this.issuer, issuer: this.issuer,
signatureAlgorithm: this.signatureAlgorithm, signatureAlgorithm: this.signatureAlgorithm,
}; }
} }
} }

View File

@@ -1,22 +1,22 @@
import crypto from 'node:crypto';
import { QueryContext, ModelOptions } from 'objection';
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import crypto from 'node:crypto'; import { PureAbility, fieldPatternMatcher, mongoQueryMatcher } from '@casl/ability';
import { ModelOptions, QueryContext } from 'objection'; import type { Subject } from '@casl/ability';
import appConfig from '../config/app'; import appConfig from '../config/app';
import checkLicense from '../helpers/check-license.ee';
import userAbility from '../helpers/user-ability';
import Base from './base'; import Base from './base';
import Connection from './connection';
import Execution from './execution';
import Flow from './flow';
import Identity from './identity.ee';
import Permission from './permission';
import ExtendedQueryBuilder from './query-builder'; import ExtendedQueryBuilder from './query-builder';
import Role from './role'; import Connection from './connection';
import Flow from './flow';
import Step from './step'; import Step from './step';
import Subscription from './subscription.ee'; import Role from './role';
import Permission from './permission';
import Execution from './execution';
import Identity from './identity.ee';
import UsageData from './usage-data.ee'; import UsageData from './usage-data.ee';
import Subscription from './subscription.ee';
class User extends Base { class User extends Base {
id!: string; id!: string;
@@ -148,11 +148,15 @@ class User extends Base {
}, },
}, },
permissions: { permissions: {
relation: Base.HasManyRelation, relation: Base.ManyToManyRelation,
modelClass: Permission, modelClass: Permission,
join: { join: {
from: 'users.role_id', from: 'users.role_id',
to: 'permissions.role_id', through: {
from: 'roles_permissions.role_id',
to: 'roles_permissions.permission_id',
},
to: 'permissions.id',
}, },
}, },
identities: { identities: {
@@ -161,8 +165,8 @@ class User extends Base {
join: { join: {
from: 'identities.user_id', from: 'identities.user_id',
to: 'users.id', to: 'users.id',
}, }
}, }
}); });
login(password: string) { login(password: string) {
@@ -288,44 +292,27 @@ class User extends Base {
} }
} }
async $afterFind(): Promise<any> { get ability() {
const hasValidLicense = await checkLicense(); if (!this.permissions) {
throw new Error('User.permissions must be fetched!');
if (hasValidLicense) return this;
if (Array.isArray(this.permissions)) {
this.permissions = this.permissions.filter((permission) => {
const isRolePermission = permission.subject === 'Role';
const isSamlAuthProviderPermission =
permission.subject === 'SamlAuthProvider';
return !isRolePermission && !isSamlAuthProviderPermission;
});
} }
return this; // We're not using mongo, but our fields, conditions match
return new PureAbility(this.permissions, {
conditionsMatcher: mongoQueryMatcher,
fieldMatcher: fieldPatternMatcher
});
} }
get ability(): ReturnType<typeof userAbility> { can(action: string, subject: Subject) {
return userAbility(this);
}
can(action: string, subject: string) {
const can = this.ability.can(action, subject); const can = this.ability.can(action, subject);
if (!can) throw new Error('Not authorized!'); if (!can) throw new Error('Not authorized!');
const relevantRule = this.ability.relevantRuleFor(action, subject); return can;
const conditions = (relevantRule?.conditions as string[]) || [];
const conditionMap: Record<string, true> = Object.fromEntries(
conditions.map((condition) => [condition, true])
);
return conditionMap;
} }
cannot(action: string, subject: string) { cannot(action: string, subject: Subject) {
const cannot = this.ability.cannot(action, subject); const cannot = this.ability.cannot(action, subject);
if (cannot) throw new Error('Not authorized!'); if (cannot) throw new Error('Not authorized!');

View File

@@ -1,6 +1,6 @@
{ {
"name": "@automatisch/cli", "name": "@automatisch/cli",
"version": "0.8.0", "version": "0.7.1",
"license": "See LICENSE file", "license": "See LICENSE file",
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.", "description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
"contributors": [ "contributors": [
@@ -33,7 +33,7 @@
"version": "oclif readme && git add README.md" "version": "oclif readme && git add README.md"
}, },
"dependencies": { "dependencies": {
"@automatisch/backend": "^0.8.0", "@automatisch/backend": "^0.7.1",
"@oclif/core": "^1", "@oclif/core": "^1",
"@oclif/plugin-help": "^5", "@oclif/plugin-help": "^5",
"@oclif/plugin-plugins": "^2.0.1", "@oclif/plugin-plugins": "^2.0.1",

View File

@@ -15593,6 +15593,11 @@ winston@^3.6.0, winston@^3.7.1:
triple-beam "^1.3.0" triple-beam "^1.3.0"
winston-transport "^4.5.0" winston-transport "^4.5.0"
word-wrap@^1.2.3, word-wrap@~1.2.3:
version "1.2.3"
resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz"
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
workbox-background-sync@6.5.4: workbox-background-sync@6.5.4:
version "6.5.4" version "6.5.4"
resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-6.5.4.tgz#3141afba3cc8aa2ae14c24d0f6811374ba8ff6a9" resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-6.5.4.tgz#3141afba3cc8aa2ae14c24d0f6811374ba8ff6a9"

View File

@@ -1,6 +1,6 @@
{ {
"name": "@automatisch/docs", "name": "@automatisch/docs",
"version": "0.8.0", "version": "0.7.1",
"license": "See LICENSE file", "license": "See LICENSE file",
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.", "description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
"private": true, "private": true,

View File

@@ -157,7 +157,6 @@ export default defineConfig({
collapsed: true, collapsed: true,
items: [ items: [
{ text: 'Triggers', link: '/apps/notion/triggers' }, { text: 'Triggers', link: '/apps/notion/triggers' },
{ text: 'Actions', link: '/apps/notion/actions' },
{ text: 'Connection', link: '/apps/notion/connection' }, { text: 'Connection', link: '/apps/notion/connection' },
], ],
}, },

View File

@@ -5,8 +5,6 @@ items:
desc: Create a blank spreadsheet or duplicate an existing spreadsheet. Optionally, provide headers. desc: Create a blank spreadsheet or duplicate an existing spreadsheet. Optionally, provide headers.
- name: Create spreadsheet row - name: Create spreadsheet row
desc: Creates a new row in a specific spreadsheet. desc: Creates a new row in a specific spreadsheet.
- name: Create worksheet
desc: Create a blank worksheet with a title. Optionally, provide headers.
--- ---
<script setup> <script setup>

View File

@@ -1,16 +0,0 @@
---
favicon: /favicons/notion.svg
items:
- name: Create database item
desc: Creates an item in a database.
- name: Create page
desc: Creates a page inside a parent page.
- name: Find database item
desc: Searches for an item in a database by property.
---
<script setup>
import CustomListing from '../../components/CustomListing.vue'
</script>
<CustomListing />

View File

@@ -1,5 +1,9 @@
# Available Apps # Available Apps
:::warning
We just have a few available integrations at the moment and we also know that workflows you can build with them is limited. But we still made the project public and want to share them with you so you can get a sense of what Automatisch can do. Meanwhile, we're working on adding more integrations and improving the existing ones.
:::
Following integrations are currently supported by Automatisch. Following integrations are currently supported by Automatisch.
- [DeepL](/apps/deepl/actions) - [DeepL](/apps/deepl/actions)

View File

@@ -1,6 +1,6 @@
{ {
"name": "@automatisch/e2e-tests", "name": "@automatisch/e2e-tests",
"version": "0.8.0", "version": "0.7.1",
"license": "See LICENSE file", "license": "See LICENSE file",
"private": true, "private": true,
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.", "description": "The open source Zapier alternative. Build workflow automation without spending time and money.",

View File

@@ -96,7 +96,6 @@ export interface IUser {
flows: IFlow[]; flows: IFlow[];
steps: IStep[]; steps: IStep[];
role: IRole; role: IRole;
permissions: IPermission[];
} }
export interface IRole { export interface IRole {
@@ -105,20 +104,6 @@ export interface IRole {
name: string; name: string;
description: string; description: string;
isAdmin: boolean; isAdmin: boolean;
permissions: IPermission[];
}
export interface IPermission {
id: string;
action: string;
subject: string;
conditions: string[];
}
export interface IPermissionCatalog {
actions: { label: string; key: string; subjects: string[] }[];
subjects: { label: string; key: string; }[];
conditions: { label: string; key: string; }[];
} }
export interface IFieldDropdown { export interface IFieldDropdown {
@@ -432,11 +417,6 @@ declare module 'axios' {
interface AxiosRequestConfig { interface AxiosRequestConfig {
additionalProperties?: Record<string, unknown>; additionalProperties?: Record<string, unknown>;
} }
// ref: https://github.com/axios/axios/issues/5095
interface AxiosInstance {
create(config?: CreateAxiosDefaults): AxiosInstance;
}
} }
export interface IRequest extends Request { export interface IRequest extends Request {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@automatisch/types", "name": "@automatisch/types",
"version": "0.8.0", "version": "0.7.1",
"license": "See LICENSE file", "license": "See LICENSE file",
"description": "Type definitions for automatisch", "description": "Type definitions for automatisch",
"homepage": "https://github.com/automatisch/automatisch", "homepage": "https://github.com/automatisch/automatisch",

View File

@@ -1,13 +1,11 @@
{ {
"name": "@automatisch/web", "name": "@automatisch/web",
"version": "0.8.0", "version": "0.7.1",
"license": "See LICENSE file", "license": "See LICENSE file",
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.", "description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
"dependencies": { "dependencies": {
"@apollo/client": "^3.6.9", "@apollo/client": "^3.6.9",
"@automatisch/types": "^0.8.0", "@automatisch/types": "^0.7.1",
"@casl/ability": "^6.5.0",
"@casl/react": "^3.1.0",
"@emotion/react": "^11.4.1", "@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0", "@emotion/styled": "^11.3.0",
"@hookform/resolvers": "^2.8.8", "@hookform/resolvers": "^2.8.8",
@@ -33,7 +31,7 @@
"notistack": "^2.0.2", "notistack": "^2.0.2",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-hook-form": "^7.45.2", "react-hook-form": "^7.43.9",
"react-intl": "^5.20.12", "react-intl": "^5.20.12",
"react-json-tree": "^0.16.2", "react-json-tree": "^0.16.2",
"react-router-dom": "^6.0.2", "react-router-dom": "^6.0.2",

View File

@@ -8,74 +8,60 @@ import CreateRole from 'pages/CreateRole/index.ee';
import EditRole from 'pages/EditRole/index.ee'; import EditRole from 'pages/EditRole/index.ee';
import * as URLS from 'config/urls'; import * as URLS from 'config/urls';
import Can from 'components/Can';
// TODO: consider introducing redirections to `/` as fallback
export default ( export default (
<> <>
<Route <Route
path={URLS.USERS} path={URLS.USERS}
element={ element={
<Can I="read" a="User"> <AdminSettingsLayout>
<AdminSettingsLayout> <Users />
<Users /> </AdminSettingsLayout>
</AdminSettingsLayout>
</Can>
} }
/> />
<Route <Route
path={URLS.CREATE_USER} path={URLS.CREATE_USER}
element={ element={
<Can I="create" a="User"> <AdminSettingsLayout>
<AdminSettingsLayout> <CreateUser />
<CreateUser /> </AdminSettingsLayout>
</AdminSettingsLayout>
</Can>
} }
/> />
<Route <Route
path={URLS.USER_PATTERN} path={URLS.USER_PATTERN}
element={ element={
<Can I="update" a="User"> <AdminSettingsLayout>
<AdminSettingsLayout> <EditUser />
<EditUser /> </AdminSettingsLayout>
</AdminSettingsLayout>
</Can>
} }
/> />
<Route <Route
path={URLS.ROLES} path={URLS.ROLES}
element={ element={
<Can I="read" a="Role"> <AdminSettingsLayout>
<AdminSettingsLayout> <Roles />
<Roles /> </AdminSettingsLayout>
</AdminSettingsLayout>
</Can>
} }
/> />
<Route <Route
path={URLS.CREATE_ROLE} path={URLS.CREATE_ROLE}
element={ element={
<Can I="create" a="Role"> <AdminSettingsLayout>
<AdminSettingsLayout> <CreateRole />
<CreateRole /> </AdminSettingsLayout>
</AdminSettingsLayout>
</Can>
} }
/> />
<Route <Route
path={URLS.ROLE_PATTERN} path={URLS.ROLE_PATTERN}
element={ element={
<Can I="update" a="Role"> <AdminSettingsLayout>
<AdminSettingsLayout> <EditRole />
<EditRole /> </AdminSettingsLayout>
</AdminSettingsLayout>
</Can>
} }
/> />

View File

@@ -4,7 +4,6 @@ import MenuItem from '@mui/material/MenuItem';
import Menu, { MenuProps } from '@mui/material/Menu'; import Menu, { MenuProps } from '@mui/material/Menu';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import Can from 'components/Can';
import apolloClient from 'graphql/client'; import apolloClient from 'graphql/client';
import * as URLS from 'config/urls'; import * as URLS from 'config/urls';
import useAuthentication from 'hooks/useAuthentication'; import useAuthentication from 'hooks/useAuthentication';
@@ -55,14 +54,9 @@ function AccountDropdownMenu(
{formatMessage('accountDropdownMenu.settings')} {formatMessage('accountDropdownMenu.settings')}
</MenuItem> </MenuItem>
<Can I="read" a="User"> <MenuItem component={Link} to={URLS.ADMIN_SETTINGS_DASHBOARD}>
<MenuItem {formatMessage('accountDropdownMenu.adminSettings')}
component={Link} </MenuItem>
to={URLS.ADMIN_SETTINGS_DASHBOARD}
>
{formatMessage('accountDropdownMenu.adminSettings')}
</MenuItem>
</Can>
<MenuItem onClick={logout} data-test="logout-item"> <MenuItem onClick={logout} data-test="logout-item">
{formatMessage('accountDropdownMenu.logout')} {formatMessage('accountDropdownMenu.logout')}

View File

@@ -12,7 +12,7 @@ import useFormatMessage from 'hooks/useFormatMessage';
import computeAuthStepVariables from 'helpers/computeAuthStepVariables'; import computeAuthStepVariables from 'helpers/computeAuthStepVariables';
import { processStep } from 'helpers/authenticationSteps'; import { processStep } from 'helpers/authenticationSteps';
import InputCreator from 'components/InputCreator'; import InputCreator from 'components/InputCreator';
import { generateExternalLink } from '../../helpers/translationValues'; import { generateExternalLink } from '../../helpers/translation-values';
import { Form } from './style'; import { Form } from './style';
type AddAppConnectionProps = { type AddAppConnectionProps = {

View File

@@ -1,42 +1,34 @@
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'; import * as React from 'react';
import GroupIcon from '@mui/icons-material/Group';
import GroupsIcon from '@mui/icons-material/Groups';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Toolbar from '@mui/material/Toolbar'; import Toolbar from '@mui/material/Toolbar';
import { useTheme } from '@mui/material/styles'; import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery'; import useMediaQuery from '@mui/material/useMediaQuery';
import * as React from 'react'; import GroupIcon from '@mui/icons-material/Group';
import GroupsIcon from '@mui/icons-material/Groups';
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
import { SvgIconComponent } from '@mui/icons-material'; import * as URLS from 'config/urls';
import useAutomatischInfo from 'hooks/useAutomatischInfo';
import AppBar from 'components/AppBar'; import AppBar from 'components/AppBar';
import Drawer from 'components/Drawer'; import Drawer from 'components/Drawer';
import * as URLS from 'config/urls';
import useCurrentUserAbility from 'hooks/useCurrentUserAbility';
type SettingsLayoutProps = { type SettingsLayoutProps = {
children: React.ReactNode; children: React.ReactNode;
}; };
type DrawerLink = { function createDrawerLinks({ isCloud }: { isCloud: boolean }) {
Icon: SvgIconComponent,
primary: string,
to: string,
}
function createDrawerLinks({ canReadRole, canReadUser }: { canReadRole: boolean; canReadUser: boolean; }) {
const items = [ const items = [
canReadUser ? { {
Icon: GroupIcon, Icon: GroupIcon,
primary: 'adminSettingsDrawer.users', primary: 'adminSettingsDrawer.users',
to: URLS.USERS, to: URLS.USERS,
} : null, },
canReadRole ? { {
Icon: GroupsIcon, Icon: GroupsIcon,
primary: 'adminSettingsDrawer.roles', primary: 'adminSettingsDrawer.roles',
to: URLS.ROLES, to: URLS.ROLES,
} : null }
] ]
.filter(Boolean) as DrawerLink[];
return items; return items;
} }
@@ -52,17 +44,14 @@ const drawerBottomLinks = [
export default function SettingsLayout({ export default function SettingsLayout({
children, children,
}: SettingsLayoutProps): React.ReactElement { }: SettingsLayoutProps): React.ReactElement {
const { isCloud } = useAutomatischInfo();
const theme = useTheme(); const theme = useTheme();
const currentUserAbility = useCurrentUserAbility();
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg')); const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'));
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens); const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
const openDrawer = () => setDrawerOpen(true); const openDrawer = () => setDrawerOpen(true);
const closeDrawer = () => setDrawerOpen(false); const closeDrawer = () => setDrawerOpen(false);
const drawerLinks = createDrawerLinks({ const drawerLinks = createDrawerLinks({ isCloud });
canReadUser: currentUserAbility.can('read', 'User'),
canReadRole: currentUserAbility.can('read', 'Role'),
});
return ( return (
<> <>

View File

@@ -1,22 +0,0 @@
import { Can as OriginalCan } from '@casl/react';
import * as React from 'react';
import useCurrentUserAbility from 'hooks/useCurrentUserAbility';
type CanProps = {
I: string;
a: string;
passThrough?: boolean;
children: React.ReactNode | ((isAllowed: boolean) => React.ReactNode);
} | {
I: string;
an: string;
passThrough?: boolean;
children: React.ReactNode | ((isAllowed: boolean) => React.ReactNode);
};
export default function Can(props: CanProps) {
const currentUserAbility = useCurrentUserAbility();
return (<OriginalCan ability={currentUserAbility} {...props} />);
};

View File

@@ -20,7 +20,6 @@ export default function ConditionalIconButton(props: any): React.ReactElement {
size={buttonProps.size} size={buttonProps.size}
component={buttonProps.component} component={buttonProps.component}
to={buttonProps.to} to={buttonProps.to}
disabled={buttonProps.disabled}
> >
{icon} {icon}
</IconButton> </IconButton>

View File

@@ -2,7 +2,7 @@ import { styled } from '@mui/material/styles';
import MuiIconButton, { iconButtonClasses } from '@mui/material/IconButton'; import MuiIconButton, { iconButtonClasses } from '@mui/material/IconButton';
export const IconButton = styled(MuiIconButton)` export const IconButton = styled(MuiIconButton)`
&.${iconButtonClasses.colorPrimary}:not(.${iconButtonClasses.disabled}) { &.${iconButtonClasses.colorPrimary} {
background: ${({ theme }) => theme.palette.primary.main}; background: ${({ theme }) => theme.palette.primary.main};
color: ${({ theme }) => theme.palette.primary.contrastText}; color: ${({ theme }) => theme.palette.primary.contrastText};

View File

@@ -1,57 +0,0 @@
import * as React from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import Checkbox, { CheckboxProps } from '@mui/material/Checkbox';
type ControlledCheckboxProps = {
name: string;
} & CheckboxProps;
export default function ControlledCheckbox(props: ControlledCheckboxProps): React.ReactElement {
const { control } = useFormContext();
const {
required,
name,
defaultValue = false,
disabled = false,
onBlur,
onChange,
...checkboxProps
} = props;
return (
<Controller
rules={{ required }}
name={name}
defaultValue={defaultValue}
control={control}
render={({
field: {
ref,
onChange: controllerOnChange,
onBlur: controllerOnBlur,
value,
name,
...field
},
}) => {
return (
<Checkbox
{...checkboxProps}
{...field}
checked={!!value}
name={name}
disabled={disabled}
onChange={(...args) => {
controllerOnChange(...args);
onChange?.(...args);
}}
onBlur={(...args) => {
controllerOnBlur();
onBlur?.(...args);
}}
inputRef={ref}
/>
)}}
/>
);
}

View File

@@ -2,54 +2,35 @@ import * as React from 'react';
import { useMutation } from '@apollo/client'; import { useMutation } from '@apollo/client';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import DeleteIcon from '@mui/icons-material/Delete'; import DeleteIcon from '@mui/icons-material/Delete';
import { useSnackbar } from 'notistack';
import Can from 'components/Can';
import ConfirmationDialog from 'components/ConfirmationDialog'; import ConfirmationDialog from 'components/ConfirmationDialog';
import { DELETE_ROLE } from 'graphql/mutations/delete-role.ee'; import { DELETE_ROLE } from 'graphql/mutations/delete-role.ee';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
type DeleteRoleButtonProps = { type DeleteRoleButtonProps = {
disabled?: boolean;
roleId: string; roleId: string;
}; };
export default function DeleteRoleButton(props: DeleteRoleButtonProps) { export default function DeleteRoleButton(props: DeleteRoleButtonProps) {
const { disabled, roleId } = props; const { roleId } = props;
const [showConfirmation, setShowConfirmation] = React.useState(false); const [showConfirmation, setShowConfirmation] = React.useState(false);
const [deleteRole] = useMutation(DELETE_ROLE, { const [deleteRole] = useMutation(DELETE_ROLE, {
variables: { input: { id: roleId } }, variables: { input: { id: roleId } },
refetchQueries: ['GetRoles'], refetchQueries: ['GetRoles'],
}); });
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const { enqueueSnackbar } = useSnackbar();
const handleConfirm = React.useCallback(async () => { const handleConfirm = React.useCallback(async () => {
try { await deleteRole();
await deleteRole();
setShowConfirmation(false); setShowConfirmation(false);
enqueueSnackbar(formatMessage('deleteRoleButton.successfullyDeleted'), {
variant: 'success',
});
} catch (error) {
throw new Error('Failed while deleting!');
}
}, [deleteRole]); }, [deleteRole]);
return ( return (
<> <>
<Can I="delete" a="Role" passThrough> <IconButton onClick={() => setShowConfirmation(true)} size="small">
{(allowed) => ( <DeleteIcon />
<IconButton </IconButton>
disabled={!allowed || disabled}
onClick={() => setShowConfirmation(true)}
size="small"
>
<DeleteIcon />
</IconButton>
)}
</Can>
<ConfirmationDialog <ConfirmationDialog
open={showConfirmation} open={showConfirmation}

View File

@@ -2,7 +2,6 @@ import * as React from 'react';
import { useMutation } from '@apollo/client'; import { useMutation } from '@apollo/client';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import DeleteIcon from '@mui/icons-material/Delete'; import DeleteIcon from '@mui/icons-material/Delete';
import { useSnackbar } from 'notistack';
import ConfirmationDialog from 'components/ConfirmationDialog'; import ConfirmationDialog from 'components/ConfirmationDialog';
import { DELETE_USER } from 'graphql/mutations/delete-user.ee'; import { DELETE_USER } from 'graphql/mutations/delete-user.ee';
@@ -20,19 +19,11 @@ export default function DeleteUserButton(props: DeleteUserButtonProps) {
refetchQueries: ['GetUsers'], refetchQueries: ['GetUsers'],
}); });
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const { enqueueSnackbar } = useSnackbar();
const handleConfirm = React.useCallback(async () => { const handleConfirm = React.useCallback(async () => {
try { await deleteUser();
await deleteUser();
setShowConfirmation(false); setShowConfirmation(false);
enqueueSnackbar(formatMessage('deleteUserButton.successfullyDeleted'), {
variant: 'success',
});
} catch (error) {
throw new Error('Failed while deleting!');
}
}, [deleteUser]); }, [deleteUser]);
return ( return (

View File

@@ -6,7 +6,6 @@ import type { PopoverProps } from '@mui/material/Popover';
import MenuItem from '@mui/material/MenuItem'; import MenuItem from '@mui/material/MenuItem';
import { useSnackbar } from 'notistack'; import { useSnackbar } from 'notistack';
import Can from 'components/Can';
import { DELETE_FLOW } from 'graphql/mutations/delete-flow'; import { DELETE_FLOW } from 'graphql/mutations/delete-flow';
import { DUPLICATE_FLOW } from 'graphql/mutations/duplicate-flow'; import { DUPLICATE_FLOW } from 'graphql/mutations/duplicate-flow';
import * as URLS from 'config/urls'; import * as URLS from 'config/urls';
@@ -73,39 +72,13 @@ export default function ContextMenu(
hideBackdrop={false} hideBackdrop={false}
anchorEl={anchorEl} anchorEl={anchorEl}
> >
<Can I="read" a="Flow" passThrough> <MenuItem component={Link} to={URLS.FLOW(flowId)}>
{(allowed) => ( {formatMessage('flow.view')}
<MenuItem </MenuItem>
disabled={!allowed}
component={Link}
to={URLS.FLOW(flowId)}
>
{formatMessage('flow.view')}
</MenuItem>
)}
</Can>
<Can I="create" a="Flow" passThrough> <MenuItem onClick={onFlowDuplicate}>{formatMessage('flow.duplicate')}</MenuItem>
{(allowed) => (
<MenuItem
disabled={!allowed}
onClick={onFlowDuplicate}
>
{formatMessage('flow.duplicate')}
</MenuItem>
)}
</Can>
<Can I="delete" a="Flow" passThrough> <MenuItem onClick={onFlowDelete}>{formatMessage('flow.delete')}</MenuItem>
{(allowed) => (
<MenuItem
disabled={!allowed}
onClick={onFlowDelete}
>
{formatMessage('flow.delete')}
</MenuItem>
)}
</Can>
</Menu> </Menu>
); );
} }

View File

@@ -0,0 +1,47 @@
import {
IconButton,
Skeleton,
Stack,
TableCell,
TableRow,
} from '@mui/material';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
type ListLoaderProps = {
rowsNumber: number;
cellNumber: number;
};
const ListLoader = ({ rowsNumber, cellNumber }: ListLoaderProps) => {
return (
<>
{[...Array(rowsNumber)].map((row, index) => (
<TableRow
key={index}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
>
{[...Array(cellNumber)].map((cell, index) => (
<TableCell key={index} scope="row">
<Skeleton />
</TableCell>
))}
<TableCell>
<Stack direction="row" gap={1} justifyContent="right">
<IconButton size="small">
<EditIcon />
</IconButton>
<IconButton size="small">
<DeleteIcon />
</IconButton>
</Stack>
</TableCell>
</TableRow>
))}
</>
);
};
export default ListLoader;

View File

@@ -1,142 +0,0 @@
import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import Paper from '@mui/material/Paper';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Typography from '@mui/material/Typography';
import * as React from 'react';
import { useFormContext } from 'react-hook-form';
import { IPermissionCatalog } from '@automatisch/types';
import ControlledCheckbox from 'components/ControlledCheckbox';
import useFormatMessage from 'hooks/useFormatMessage';
type PermissionSettingsProps = {
onClose: () => void;
fieldPrefix: string;
subject: string;
actions: IPermissionCatalog['actions'];
conditions: IPermissionCatalog['conditions'];
}
export default function PermissionSettings(props: PermissionSettingsProps) {
const {
onClose,
fieldPrefix,
subject,
actions,
conditions,
} = props;
const formatMessage = useFormatMessage();
const { getValues, resetField } = useFormContext();
const cancel = () => {
for (const action of actions) {
for (const condition of conditions) {
const fieldName = `${fieldPrefix}.${action.key}.conditions.${condition.key}`;
resetField(fieldName);
}
}
onClose();
}
const apply = () => {
for (const action of actions) {
for (const condition of conditions) {
const fieldName = `${fieldPrefix}.${action.key}.conditions.${condition.key}`;
const value = getValues(fieldName);
resetField(fieldName, { defaultValue: value });
}
}
onClose();
}
return (
<Dialog open onClose={cancel}>
<DialogTitle>
{formatMessage('permissionSettings.title')}
</DialogTitle>
<DialogContent>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell component="th" />
{actions.map(action => (
<TableCell component="th" key={action.key}>
<Typography
variant="subtitle1"
align="center"
sx={{
color: 'text.secondary',
fontWeight: 700
}}
>
{action.label}
</Typography>
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{conditions.map((condition) => (
<TableRow
key={condition.key}
sx={{ '&:last-child td': { border: 0 } }}
>
<TableCell scope="row">
<Typography
variant="subtitle2"
>
{condition.label}
</Typography>
</TableCell>
{actions.map((action) => (
<TableCell
key={`${action.key}.${condition.key}`}
align="center"
>
<Typography
variant="subtitle2"
>
{action.subjects.includes(subject) && (
<ControlledCheckbox
name={`${fieldPrefix}.${action.key}.conditions.${condition.key}`}
disabled={getValues(`${fieldPrefix}.${action.key}.value`) !== true}
/>
)}
{!action.subjects.includes(subject) && '-'}
</Typography>
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</DialogContent>
<DialogActions>
<Button onClick={cancel}>{formatMessage('permissionSettings.cancel')}</Button>
<Button onClick={apply} color="error">
{formatMessage('permissionSettings.apply')}
</Button>
</DialogActions>
</Dialog>
)
}

View File

@@ -1,122 +0,0 @@
import SettingsIcon from '@mui/icons-material/Settings';
import IconButton from '@mui/material/IconButton';
import Paper from '@mui/material/Paper';
import Stack from '@mui/material/Stack';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Typography from '@mui/material/Typography';
import * as React from 'react';
import ControlledCheckbox from 'components/ControlledCheckbox';
import usePermissionCatalog from 'hooks/usePermissionCatalog.ee';
import PermissionSettings from './PermissionSettings.ee';
type PermissionCatalogFieldProps = {
name?: string;
disabled?: boolean;
};
const PermissionCatalogField = ({ name = 'permissions', disabled = false }: PermissionCatalogFieldProps) => {
const permissionCatalog = usePermissionCatalog();
const [dialogName, setDialogName] = React.useState<string>();
if (!permissionCatalog) return (<React.Fragment />);
return (
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell component="th" />
{permissionCatalog.actions.map(action => (
<TableCell component="th" key={action.key}>
<Typography
variant="subtitle1"
align="center"
sx={{
color: 'text.secondary',
fontWeight: 700
}}
>
{action.label}
</Typography>
</TableCell>
))}
<TableCell component="th" />
</TableRow>
</TableHead>
<TableBody>
{permissionCatalog.subjects.map((subject) => (
<TableRow
key={subject.key}
sx={{ '&:last-child td': { border: 0 } }}
>
<TableCell scope="row">
<Typography
variant="subtitle2"
>
{subject.label}
</Typography>
</TableCell>
{permissionCatalog.actions.map((action) => (
<TableCell
key={`${subject.key}.${action.key}`}
align="center"
>
<Typography
variant="subtitle2"
>
{action.subjects.includes(subject.key) && (
<ControlledCheckbox
disabled={disabled}
name={`${name}.${subject.key}.${action.key}.value`}
/>
)}
{!action.subjects.includes(subject.key) && '-'}
</Typography>
</TableCell>
))}
<TableCell>
<Stack
direction="row"
gap={1}
justifyContent="right"
>
<IconButton
color="info"
size="small"
onClick={() => setDialogName(subject.key)}
disabled={disabled}
>
<SettingsIcon />
</IconButton>
{dialogName === subject.key && (
<PermissionSettings
onClose={() => setDialogName('')}
fieldPrefix={`${name}.${subject.key}`}
subject={subject.key}
actions={permissionCatalog.actions}
conditions={permissionCatalog.conditions}
/>
)}
</Stack>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)
};
export default PermissionCatalogField;

View File

@@ -13,14 +13,15 @@ import Typography from '@mui/material/Typography';
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import DeleteRoleButton from 'components/DeleteRoleButton/index.ee'; import DeleteRoleButton from 'components/DeleteRoleButton/index.ee';
import ListLoader from 'components/ListLoader';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
import useRoles from 'hooks/useRoles.ee'; import useRoles from 'hooks/useRoles.ee';
import * as URLS from 'config/urls'; import * as URLS from 'config/urls';
// TODO: introduce loading bar // TODO: introduce interaction feedback upon deletion (successful + failure)
export default function RoleList(): React.ReactElement { export default function RoleList(): React.ReactElement {
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const { roles } = useRoles(); const { roles, loading } = useRoles();
return ( return (
<TableContainer component={Paper}> <TableContainer component={Paper}>
@@ -49,34 +50,40 @@ export default function RoleList(): React.ReactElement {
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{roles.map((role) => ( {loading ? (
<TableRow <ListLoader rowsNumber={3} cellNumber={2} />
key={role.id} ) : (
sx={{ '&:last-child td, &:last-child th': { border: 0 } }} roles.map((role) => (
> <TableRow
<TableCell scope="row"> key={role.id}
<Typography variant="subtitle2">{role.name}</Typography> sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
</TableCell> >
<TableCell scope="row">
<Typography variant="subtitle2">{role.name}</Typography>
</TableCell>
<TableCell scope="row"> <TableCell scope="row">
<Typography variant="subtitle2">{role.description}</Typography> <Typography variant="subtitle2">
</TableCell> {role.description}
</Typography>
</TableCell>
<TableCell> <TableCell>
<Stack direction="row" gap={1} justifyContent="right"> <Stack direction="row" gap={1} justifyContent="right">
<IconButton <IconButton
size="small" size="small"
component={Link} component={Link}
to={URLS.ROLE(role.id)} to={URLS.ROLE(role.id)}
> >
<EditIcon /> <EditIcon />
</IconButton> </IconButton>
<DeleteRoleButton disabled={role.isAdmin} roleId={role.id} /> <DeleteRoleButton roleId={role.id} />
</Stack> </Stack>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))
)}
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </TableContainer>

View File

@@ -4,7 +4,7 @@ import Button from '@mui/material/Button';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import Divider from '@mui/material/Divider'; import Divider from '@mui/material/Divider';
import * as URLS from 'config/urls'; import appConfig from 'config/app';
import useSamlAuthProviders from 'hooks/useSamlAuthProviders.ee'; import useSamlAuthProviders from 'hooks/useSamlAuthProviders.ee';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
@@ -24,12 +24,10 @@ function SsoProviders() {
<Button <Button
key={provider.id} key={provider.id}
component="a" component="a"
href={URLS.SSO_LOGIN(provider.issuer)} href={`${appConfig.apiUrl}/login/saml/${provider.issuer}`}
variant="outlined" variant="outlined"
> >
{formatMessage('ssoProviders.loginWithProvider', { {provider.name}
providerName: provider.name
})}
</Button> </Button>
))} ))}
</Stack> </Stack>

View File

@@ -3,7 +3,7 @@ import Alert from '@mui/material/Alert';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import * as URLS from 'config/urls'; import * as URLS from 'config/urls';
import { generateInternalLink } from 'helpers/translationValues'; import { generateInternalLink } from 'helpers/translation-values';
import useTrialStatus from 'hooks/useTrialStatus.ee'; import useTrialStatus from 'hooks/useTrialStatus.ee';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';

View File

@@ -13,11 +13,13 @@ import Typography from '@mui/material/Typography';
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import DeleteUserButton from 'components/DeleteUserButton/index.ee'; import DeleteUserButton from 'components/DeleteUserButton/index.ee';
import ListLoader from 'components/ListLoader';
import useUsers from 'hooks/useUsers'; import useUsers from 'hooks/useUsers';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
import * as URLS from 'config/urls'; import * as URLS from 'config/urls';
// TODO: introduce loading bar // TODO: introduce translation entries
// TODO: introduce interaction feedback upon deletion (successful + failure)
export default function UserList(): React.ReactElement { export default function UserList(): React.ReactElement {
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const { users, loading } = useUsers(); const { users, loading } = useUsers();
@@ -49,34 +51,38 @@ export default function UserList(): React.ReactElement {
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{users.map((user) => ( {loading ? (
<TableRow <ListLoader rowsNumber={3} cellNumber={2} />
key={user.id} ) : (
sx={{ '&:last-child td, &:last-child th': { border: 0 } }} users.map((user) => (
> <TableRow
<TableCell scope="row"> key={user.id}
<Typography variant="subtitle2">{user.fullName}</Typography> sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
</TableCell> >
<TableCell scope="row">
<Typography variant="subtitle2">{user.fullName}</Typography>
</TableCell>
<TableCell> <TableCell>
<Typography variant="subtitle2">{user.email}</Typography> <Typography variant="subtitle2">{user.email}</Typography>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Stack direction="row" gap={1} justifyContent="right"> <Stack direction="row" gap={1} justifyContent="right">
<IconButton <IconButton
size="small" size="small"
component={Link} component={Link}
to={URLS.USER(user.id)} to={URLS.USER(user.id)}
> >
<EditIcon /> <EditIcon />
</IconButton> </IconButton>
<DeleteUserButton userId={user.id} /> <DeleteUserButton userId={user.id} />
</Stack> </Stack>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))
)}
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </TableContainer>

View File

@@ -3,7 +3,7 @@ import { FormattedMessage } from 'react-intl';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import type { AlertProps } from '@mui/material/Alert'; import type { AlertProps } from '@mui/material/Alert';
import { generateExternalLink } from '../../helpers/translationValues'; import { generateExternalLink } from '../../helpers/translation-values';
import { WEBHOOK_DOCS } from '../../config/urls'; import { WEBHOOK_DOCS } from '../../config/urls';
import TextField from '../TextField'; import TextField from '../TextField';
import { Alert } from './style'; import { Alert } from './style';

View File

@@ -1,7 +1,7 @@
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
import MuiAlert, { alertClasses } from '@mui/material/Alert'; import MuiAlert, { alertClasses } from '@mui/material/Alert';
export const Alert = styled(MuiAlert)(() => ({ export const Alert = styled(MuiAlert)(({ theme }) => ({
[`&.${alertClasses.root}`]: { [`&.${alertClasses.root}`]: {
fontWeight: 300, fontWeight: 300,
width: '100%', width: '100%',

View File

@@ -17,9 +17,7 @@ const config: Config = {
supportEmailAddress: 'support@automatisch.io' supportEmailAddress: 'support@automatisch.io'
}; };
if (!config.apiUrl && !config.graphqlUrl) { if (!config.apiUrl) {
config.apiUrl = '/';
} else if (!config.apiUrl) {
config.apiUrl = (new URL(config.graphqlUrl)).origin; config.apiUrl = (new URL(config.graphqlUrl)).origin;
} }

View File

@@ -1,5 +1,3 @@
import appConfig from './app';
export const CONNECTIONS = '/connections'; export const CONNECTIONS = '/connections';
export const EXECUTIONS = '/executions'; export const EXECUTIONS = '/executions';
export const EXECUTION_PATTERN = '/executions/:executionId'; export const EXECUTION_PATTERN = '/executions/:executionId';
@@ -8,7 +6,6 @@ export const EXECUTION = (executionId: string) =>
export const LOGIN = '/login'; export const LOGIN = '/login';
export const LOGIN_CALLBACK = `${LOGIN}/callback`; export const LOGIN_CALLBACK = `${LOGIN}/callback`;
export const SSO_LOGIN = (issuer: string) => `${appConfig.apiUrl}/login/saml/${issuer}`;
export const SIGNUP = '/sign-up'; export const SIGNUP = '/sign-up';
export const FORGOT_PASSWORD = '/forgot-password'; export const FORGOT_PASSWORD = '/forgot-password';
export const RESET_PASSWORD = '/reset-password'; export const RESET_PASSWORD = '/reset-password';

View File

@@ -6,12 +6,6 @@ export const UPDATE_ROLE = gql`
id id
name name
description description
permissions {
id
action
subject
conditions
}
} }
} }
`; `;

View File

@@ -7,11 +7,9 @@ export const GET_CURRENT_USER = gql`
fullName fullName
email email
role { role {
id
isAdmin isAdmin
} }
permissions { permissions {
id
action action
subject subject
conditions conditions

Some files were not shown because too many files have changed in this diff Show More