Compare commits
13 Commits
update-sam
...
add-loadin
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d3bc3a796b | ||
![]() |
9e64af4793 | ||
![]() |
b581f539e2 | ||
![]() |
aac1295c10 | ||
![]() |
e8f2802ee0 | ||
![]() |
75b3730a70 | ||
![]() |
af29dc9c3f | ||
![]() |
181cb5f335 | ||
![]() |
94e560c262 | ||
![]() |
f802061722 | ||
![]() |
58a7f6eec6 | ||
![]() |
5e11d3cc4d | ||
![]() |
399fb8312a |
@@ -4,7 +4,7 @@ WORKDIR /automatisch
|
||||
|
||||
RUN \
|
||||
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/ && \
|
||||
apk del build-dependencies
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM automatischio/automatisch:0.8.0
|
||||
FROM automatischio/automatisch:0.7.1
|
||||
WORKDIR /automatisch
|
||||
|
||||
RUN apk add --no-cache openssl dos2unix
|
||||
|
@@ -2,7 +2,7 @@
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"version": "0.8.0",
|
||||
"version": "0.7.1",
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"command": {
|
||||
|
@@ -3,16 +3,36 @@ import logger from '../../src/helpers/logger';
|
||||
import client from './client';
|
||||
import User from '../../src/models/user';
|
||||
import Role from '../../src/models/role';
|
||||
import Permission from '../../src/models/permission';
|
||||
import '../../src/config/orm';
|
||||
|
||||
async function fetchAdminRole() {
|
||||
const role = await Role
|
||||
.query()
|
||||
.where({
|
||||
key: 'admin'
|
||||
})
|
||||
.limit(1)
|
||||
.first();
|
||||
async function seedPermissionsIfNeeded() {
|
||||
const existingPermissions = await Permission.query().limit(1).first();
|
||||
|
||||
if (!existingPermissions) return;
|
||||
|
||||
const getPermission = (subject: string, actions: string[]) => actions.map(action => ({ subject, action }));
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -23,7 +43,9 @@ export async function createUser(
|
||||
) {
|
||||
const UNIQUE_VIOLATION_CODE = '23505';
|
||||
|
||||
const role = await fetchAdminRole();
|
||||
await seedPermissionsIfNeeded();
|
||||
|
||||
const role = await createOrFetchRole();
|
||||
const userParams = {
|
||||
email,
|
||||
password,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@automatisch/backend",
|
||||
"version": "0.8.0",
|
||||
"version": "0.7.1",
|
||||
"license": "See LICENSE file",
|
||||
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
||||
"scripts": {
|
||||
@@ -22,7 +22,7 @@
|
||||
"prebuild": "rm -rf ./dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@automatisch/web": "^0.8.0",
|
||||
"@automatisch/web": "^0.7.1",
|
||||
"@bull-board/express": "^3.10.1",
|
||||
"@casl/ability": "^6.5.0",
|
||||
"@graphql-tools/graphql-file-loader": "^7.3.4",
|
||||
@@ -53,8 +53,6 @@
|
||||
"graphql-type-json": "^0.3.2",
|
||||
"handlebars": "^4.7.7",
|
||||
"http-errors": "~1.6.3",
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
"https-proxy-agent": "^7.0.1",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"knex": "^2.4.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
@@ -108,7 +106,7 @@
|
||||
"url": "https://github.com/automatisch/automatisch/issues"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@automatisch/types": "^0.8.0",
|
||||
"@automatisch/types": "^0.7.1",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/bull": "^3.15.8",
|
||||
"@types/cors": "^2.8.12",
|
||||
|
@@ -6,7 +6,7 @@ import actions from './actions';
|
||||
import dynamicData from './dynamic-data';
|
||||
|
||||
export default defineApp({
|
||||
name: 'GitHub',
|
||||
name: 'Github',
|
||||
key: 'github',
|
||||
baseUrl: 'https://github.com',
|
||||
apiBaseUrl: 'https://api.github.com',
|
||||
|
@@ -9,11 +9,11 @@ export default {
|
||||
// ref:
|
||||
// - https://docs.gitlab.com/ee/api/projects.html#list-all-projects
|
||||
// - https://docs.gitlab.com/ee/api/rest/index.html#keyset-based-pagination
|
||||
|
||||
const firstPageRequest = $.http.get('/api/v4/projects', {
|
||||
params: {
|
||||
simple: true,
|
||||
pagination: 'keyset',
|
||||
membership: true,
|
||||
order_by: 'id',
|
||||
sort: 'asc',
|
||||
},
|
||||
|
@@ -6,7 +6,7 @@ import triggers from './triggers';
|
||||
import dynamicData from './dynamic-data';
|
||||
|
||||
export default defineApp({
|
||||
name: 'GitLab',
|
||||
name: 'Gitlab',
|
||||
key: 'gitlab',
|
||||
baseUrl: 'https://gitlab.com',
|
||||
apiBaseUrl: 'https://gitlab.com',
|
||||
|
@@ -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,
|
||||
});
|
||||
},
|
||||
});
|
@@ -1,5 +1,4 @@
|
||||
import createSpreadsheet from './create-spreadsheet';
|
||||
import createSpreadsheetRow from './create-spreadsheet-row';
|
||||
import createWorksheet from './create-worksheet';
|
||||
|
||||
export default [createSpreadsheet, createSpreadsheetRow, createWorksheet];
|
||||
export default [createSpreadsheet, createSpreadsheetRow];
|
||||
|
@@ -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,
|
||||
});
|
||||
},
|
||||
});
|
@@ -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,
|
||||
});
|
||||
},
|
||||
});
|
@@ -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],
|
||||
});
|
||||
},
|
||||
});
|
@@ -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];
|
@@ -1,4 +1,3 @@
|
||||
import listDatabases from './list-databases';
|
||||
import listParentPages from './list-parent-pages';
|
||||
|
||||
export default [listDatabases, listParentPages];
|
||||
export default [listDatabases];
|
||||
|
@@ -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;
|
||||
},
|
||||
};
|
@@ -3,7 +3,6 @@ import addAuthHeader from './common/add-auth-header';
|
||||
import addNotionVersionHeader from './common/add-notion-version-header';
|
||||
import auth from './auth';
|
||||
import triggers from './triggers';
|
||||
import actions from './actions';
|
||||
import dynamicData from './dynamic-data';
|
||||
|
||||
export default defineApp({
|
||||
@@ -15,9 +14,11 @@ export default defineApp({
|
||||
authDocUrl: 'https://automatisch.io/docs/apps/notion/connection',
|
||||
primaryColor: '000000',
|
||||
supportsConnections: true,
|
||||
beforeRequest: [addAuthHeader, addNotionVersionHeader],
|
||||
beforeRequest: [
|
||||
addAuthHeader,
|
||||
addNotionVersionHeader,
|
||||
],
|
||||
auth,
|
||||
triggers,
|
||||
actions,
|
||||
dynamicData,
|
||||
});
|
||||
|
@@ -16,27 +16,13 @@ export async function up(knex: Knex): Promise<void> {
|
||||
.select('role')
|
||||
.groupBy('role');
|
||||
|
||||
let shouldCreateAdminRole = true;
|
||||
for (const { role } of uniqueUserRoles) {
|
||||
// skip empty roles
|
||||
if (!role) continue;
|
||||
|
||||
const lowerCaseRole = lowerCase(role);
|
||||
|
||||
if (lowerCaseRole === 'admin') {
|
||||
shouldCreateAdminRole = false;
|
||||
}
|
||||
|
||||
await knex('roles').insert({
|
||||
name: capitalize(role),
|
||||
key: lowerCaseRole,
|
||||
});
|
||||
}
|
||||
|
||||
if (shouldCreateAdminRole) {
|
||||
await knex('roles').insert({
|
||||
name: 'Admin',
|
||||
key: 'admin',
|
||||
key: lowerCase(role),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -1,46 +1,23 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
const getPermissionForRole = (roleId: string, subject: string, actions: string[], conditions: string[] = []) => actions
|
||||
.map(action => ({
|
||||
role_id: roleId,
|
||||
subject,
|
||||
action,
|
||||
conditions,
|
||||
}));
|
||||
const getPermission = (subject: string, actions: string[]) => actions.map(action => ({ subject, action }));
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.createTable('permissions', (table) => {
|
||||
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
||||
table.uuid('role_id').references('id').inTable('roles');
|
||||
table.string('action').notNullable();
|
||||
table.string('subject').notNullable();
|
||||
table.jsonb('conditions').notNullable().defaultTo([]);
|
||||
|
||||
table.timestamps(true, true);
|
||||
});
|
||||
|
||||
const roles = await knex('roles').select(['id', 'key']) as { id: string, key: string }[];
|
||||
|
||||
for (const role of roles) {
|
||||
// `admin` role should have no conditions unlike others by default
|
||||
const isAdmin = role.key === 'admin';
|
||||
const roleConditions = isAdmin ? [] : ['isCreator'];
|
||||
|
||||
// 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']),
|
||||
]);
|
||||
}
|
||||
}
|
||||
await knex('permissions').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']),
|
||||
]);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
|
@@ -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');
|
||||
}
|
@@ -13,7 +13,6 @@ export async function up(knex: Knex): Promise<void> {
|
||||
table.text('email_attribute_name').notNullable();
|
||||
table.text('role_attribute_name').notNullable();
|
||||
table.uuid('default_role_id').references('id').inTable('roles');
|
||||
table.boolean('active').defaultTo(false);
|
||||
|
||||
table.timestamps(true, true);
|
||||
});
|
||||
|
@@ -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');
|
||||
});
|
||||
}
|
@@ -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();
|
||||
}
|
@@ -25,8 +25,6 @@ import updateRole from './mutations/update-role.ee';
|
||||
import updateStep from './mutations/update-step';
|
||||
import updateUser from './mutations/update-user.ee';
|
||||
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 = {
|
||||
createConnection,
|
||||
@@ -56,8 +54,6 @@ const mutationResolvers = {
|
||||
updateRole,
|
||||
updateStep,
|
||||
verifyConnection,
|
||||
createSamlAuthProvider,
|
||||
updateSamlAuthProvider,
|
||||
};
|
||||
|
||||
export default mutationResolvers;
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import kebabCase from 'lodash/kebabCase';
|
||||
import Permission from '../../models/permission';
|
||||
import Role from '../../models/role';
|
||||
import Context from '../../types/express/context';
|
||||
import Permission from '../../models/permission';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
@@ -11,9 +10,8 @@ type Params = {
|
||||
};
|
||||
};
|
||||
|
||||
const createRole = async (_parent: unknown, params: Params, context: Context) => {
|
||||
context.currentUser.can('create', 'Role');
|
||||
|
||||
// TODO: access
|
||||
const createRole = async (_parent: unknown, params: Params) => {
|
||||
const { name, description, permissions } = params.input;
|
||||
const key = kebabCase(name);
|
||||
|
||||
|
@@ -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;
|
@@ -1,22 +1,17 @@
|
||||
import User from '../../models/user';
|
||||
import Role from '../../models/role';
|
||||
import Context from '../../types/express/context';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
fullName: string;
|
||||
email: string;
|
||||
password: string;
|
||||
role: {
|
||||
id: string;
|
||||
};
|
||||
roleId: string;
|
||||
};
|
||||
};
|
||||
|
||||
const createUser = async (_parent: unknown, params: Params, context: Context) => {
|
||||
context.currentUser.can('create', 'User');
|
||||
|
||||
const { fullName, email, password } = params.input;
|
||||
// TODO: access
|
||||
const createUser = async (_parent: unknown, params: Params) => {
|
||||
const { fullName, email, password, roleId } = params.input;
|
||||
|
||||
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!');
|
||||
}
|
||||
|
||||
const userPayload: Partial<User> = {
|
||||
const user = await User.query().insert({
|
||||
fullName,
|
||||
email,
|
||||
password,
|
||||
};
|
||||
|
||||
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);
|
||||
roleId,
|
||||
});
|
||||
|
||||
return user;
|
||||
};
|
||||
|
@@ -2,7 +2,8 @@ import { Duration } from 'luxon';
|
||||
import Context from '../../types/express/context';
|
||||
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;
|
||||
|
||||
await context.currentUser.$query().delete();
|
||||
@@ -19,4 +20,4 @@ const deleteCurrentUser = async (_parent: unknown, params: never, context: Conte
|
||||
return true;
|
||||
};
|
||||
|
||||
export default deleteCurrentUser;
|
||||
export default deleteUser;
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import Role from '../../models/role';
|
||||
import SamlAuthProvider from '../../models/saml-auth-provider.ee';
|
||||
import Context from '../../types/express/context';
|
||||
|
||||
type Params = {
|
||||
@@ -8,37 +7,22 @@ type Params = {
|
||||
};
|
||||
};
|
||||
|
||||
// TODO: access
|
||||
const deleteRole = async (
|
||||
_parent: unknown,
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('delete', 'Role');
|
||||
|
||||
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) {
|
||||
throw new Error('Admin role cannot be deleted!');
|
||||
}
|
||||
|
||||
const samlAuthProviderUsingDefaultRole = await SamlAuthProvider.query()
|
||||
.where({ default_role_id: role.id })
|
||||
.limit(1)
|
||||
.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();
|
||||
/**
|
||||
* TODO: consider migrations for users that still have the role or
|
||||
* do not let the role get deleted if there are still associated users
|
||||
*/
|
||||
await role.$query().delete();
|
||||
|
||||
return true;
|
||||
|
@@ -9,13 +9,12 @@ type Params = {
|
||||
};
|
||||
};
|
||||
|
||||
// TODO: access
|
||||
const deleteUser = async (
|
||||
_parent: unknown,
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('delete', 'User');
|
||||
|
||||
const id = params.input.id;
|
||||
|
||||
await User.query().deleteById(id);
|
||||
|
@@ -13,7 +13,7 @@ const TOKEN_EXPIRES_IN = '14d';
|
||||
|
||||
const login = async (_parent: unknown, params: Params) => {
|
||||
const user = await User.query().findOne({
|
||||
email: params.input.email.toLowerCase(),
|
||||
email: params.input.email,
|
||||
});
|
||||
|
||||
if (user && (await user.login(params.input.password))) {
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import Context from '../../types/express/context';
|
||||
import Role from '../../models/role';
|
||||
import Permission from '../../models/permission';
|
||||
import permissionCatalog from '../../helpers/permission-catalog.ee';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
@@ -12,13 +11,12 @@ type Params = {
|
||||
};
|
||||
};
|
||||
|
||||
const updateRole = async (
|
||||
// TODO: access
|
||||
const updateUser = async (
|
||||
_parent: unknown,
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('update', 'Role');
|
||||
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
@@ -26,66 +24,21 @@ const updateRole = async (
|
||||
permissions,
|
||||
} = params.input;
|
||||
|
||||
const role = await Role
|
||||
.query()
|
||||
.findById(id)
|
||||
.throwIfNotFound();
|
||||
const role = await Role.query().findById(id).throwIfNotFound();
|
||||
|
||||
try {
|
||||
const updatedRole = await Role.transaction(async (trx) => {
|
||||
await role.$relatedQuery('permissions', trx).delete();
|
||||
// TODO: delete the unrelated items!
|
||||
await role.$relatedQuery('permissions').unrelate();
|
||||
|
||||
if (permissions?.length) {
|
||||
const sanitizedPermissions = permissions
|
||||
.filter((permission) => {
|
||||
const {
|
||||
action,
|
||||
subject,
|
||||
conditions,
|
||||
} = permission;
|
||||
|
||||
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);
|
||||
// TODO: possibly assert that given permissions do actually exist in catalog
|
||||
// TODO: possibly optimize it with patching the different permissions compared to current ones
|
||||
return await role.$query()
|
||||
.patchAndFetch(
|
||||
{
|
||||
name,
|
||||
description,
|
||||
permissions,
|
||||
}
|
||||
|
||||
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;
|
||||
|
@@ -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;
|
@@ -12,30 +12,21 @@ type Params = {
|
||||
};
|
||||
};
|
||||
|
||||
// TODO: access
|
||||
const updateUser = async (
|
||||
_parent: unknown,
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('update', 'User');
|
||||
|
||||
const userPayload: Partial<User> = {
|
||||
email: params.input.email,
|
||||
fullName: params.input.fullName,
|
||||
};
|
||||
|
||||
try {
|
||||
context.currentUser.can('update', 'Role');
|
||||
|
||||
userPayload.roleId = params.input.role.id;
|
||||
} catch {
|
||||
// void
|
||||
}
|
||||
|
||||
const user = await User.query().patchAndFetchById(
|
||||
params.input.id,
|
||||
userPayload
|
||||
);
|
||||
const user = await User.query()
|
||||
.patchAndFetchById(
|
||||
params.input.id,
|
||||
{
|
||||
email: params.input.email,
|
||||
fullName: params.input.fullName,
|
||||
roleId: params.input.role.id,
|
||||
}
|
||||
);
|
||||
|
||||
return user;
|
||||
};
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import App from '../../models/app';
|
||||
import Connection from '../../models/connection';
|
||||
import Context from '../../types/express/context';
|
||||
|
||||
type Params = {
|
||||
@@ -7,17 +6,13 @@ type Params = {
|
||||
};
|
||||
|
||||
const getApp = async (_parent: unknown, params: Params, context: Context) => {
|
||||
const conditions = context.currentUser.can('read', 'Connection');
|
||||
|
||||
const userConnections = context.currentUser.$relatedQuery('connections');
|
||||
const allConnections = Connection.query();
|
||||
const connectionBaseQuery = conditions.isCreator ? userConnections : allConnections;
|
||||
context.currentUser.can('read', 'Connection');
|
||||
|
||||
const app = await App.findOneByKey(params.key);
|
||||
|
||||
if (context.currentUser) {
|
||||
const connections = await connectionBaseQuery
|
||||
.clone()
|
||||
const connections = await context.currentUser
|
||||
.$relatedQuery('connections')
|
||||
.select('connections.*')
|
||||
.fullOuterJoinRelated('steps')
|
||||
.where({
|
||||
|
@@ -1,8 +1,6 @@
|
||||
import { IConnection } from '@automatisch/types';
|
||||
import App from '../../models/app';
|
||||
import Context from '../../types/express/context';
|
||||
import Flow from '../../models/flow';
|
||||
import Connection from '../../models/connection';
|
||||
|
||||
type Params = {
|
||||
name: string;
|
||||
@@ -13,27 +11,19 @@ const getConnectedApps = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
const conditions = 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;
|
||||
context.currentUser.can('read', 'Connection');
|
||||
|
||||
let apps = await App.findAll(params.name);
|
||||
|
||||
const connections = await connectionBaseQuery
|
||||
.clone()
|
||||
const connections = await context.currentUser
|
||||
.$relatedQuery('connections')
|
||||
.select('connections.key')
|
||||
.where({ draft: false })
|
||||
.count('connections.id as count')
|
||||
.groupBy('connections.key');
|
||||
|
||||
const flows = await flowBaseQuery
|
||||
.clone()
|
||||
const flows = await context.currentUser
|
||||
.$relatedQuery('flows')
|
||||
.withGraphJoined('steps')
|
||||
.orderBy('created_at', 'desc');
|
||||
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { IDynamicData, IJSONObject } from '@automatisch/types';
|
||||
import Context from '../../types/express/context';
|
||||
import App from '../../models/app';
|
||||
import Step from '../../models/step';
|
||||
import ExecutionStep from '../../models/execution-step';
|
||||
import globalVariable from '../../helpers/global-variable';
|
||||
import computeParameters from '../../helpers/compute-parameters';
|
||||
@@ -17,13 +16,10 @@ const getDynamicData = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
const conditions = context.currentUser.can('update', 'Flow');
|
||||
const userSteps = context.currentUser.$relatedQuery('steps');
|
||||
const allSteps = Step.query();
|
||||
const stepBaseQuery = conditions.isCreator ? userSteps : allSteps;
|
||||
context.currentUser.can('update', 'Flow');
|
||||
|
||||
const step = await stepBaseQuery
|
||||
.clone()
|
||||
const step = await context.currentUser
|
||||
.$relatedQuery('steps')
|
||||
.withGraphFetched({
|
||||
connection: true,
|
||||
flow: true,
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { IDynamicFields, IJSONObject } from '@automatisch/types';
|
||||
import Context from '../../types/express/context';
|
||||
import App from '../../models/app';
|
||||
import Step from '../../models/step';
|
||||
import globalVariable from '../../helpers/global-variable';
|
||||
|
||||
type Params = {
|
||||
@@ -15,13 +14,10 @@ const getDynamicFields = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
const conditions = context.currentUser.can('update', 'Flow');
|
||||
const userSteps = context.currentUser.$relatedQuery('steps');
|
||||
const allSteps = Step.query();
|
||||
const stepBaseQuery = conditions.isCreator ? userSteps : allSteps;
|
||||
context.currentUser.can('update', 'Flow');
|
||||
|
||||
const step = await stepBaseQuery
|
||||
.clone()
|
||||
const step = await context.currentUser
|
||||
.$relatedQuery('steps')
|
||||
.withGraphFetched({
|
||||
connection: true,
|
||||
flow: true,
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import Context from '../../types/express/context';
|
||||
import paginate from '../../helpers/pagination';
|
||||
import Execution from '../../models/execution';
|
||||
|
||||
type Params = {
|
||||
executionId: string;
|
||||
@@ -13,13 +12,10 @@ const getExecutionSteps = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
const conditions = context.currentUser.can('read', 'Execution');
|
||||
const userExecutions = context.currentUser.$relatedQuery('executions');
|
||||
const allExecutions = Execution.query();
|
||||
const executionBaseQuery = conditions.isCreator ? userExecutions : allExecutions;
|
||||
context.currentUser.can('read', 'Execution');
|
||||
|
||||
const execution = await executionBaseQuery
|
||||
.clone()
|
||||
const execution = await context.currentUser
|
||||
.$relatedQuery('executions')
|
||||
.withSoftDeleted()
|
||||
.findById(params.executionId)
|
||||
.throwIfNotFound();
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import Context from '../../types/express/context';
|
||||
import Execution from '../../models/execution';
|
||||
|
||||
type Params = {
|
||||
executionId: string;
|
||||
@@ -10,13 +9,10 @@ const getExecution = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
const conditions = context.currentUser.can('read', 'Execution');
|
||||
const userExecutions = context.currentUser.$relatedQuery('executions');
|
||||
const allExecutions = Execution.query();
|
||||
const executionBaseQuery = conditions.isCreator ? userExecutions : allExecutions;
|
||||
context.currentUser.can('read', 'Execution');
|
||||
|
||||
const execution = await executionBaseQuery
|
||||
.clone()
|
||||
const execution = await context.currentUser
|
||||
.$relatedQuery('executions')
|
||||
.withGraphFetched({
|
||||
flow: {
|
||||
steps: true,
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import { raw } from 'objection';
|
||||
import Context from '../../types/express/context';
|
||||
import Execution from '../../models/execution';
|
||||
import paginate from '../../helpers/pagination';
|
||||
|
||||
type Params = {
|
||||
@@ -13,11 +12,7 @@ const getExecutions = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
const conditions = context.currentUser.can('read', 'Execution');
|
||||
|
||||
const userExecutions = context.currentUser.$relatedQuery('executions');
|
||||
const allExecutions = Execution.query();
|
||||
const executionBaseQuery = conditions.isCreator ? userExecutions : allExecutions;
|
||||
context.currentUser.can('read', 'Execution');
|
||||
|
||||
const selectStatusStatement = `
|
||||
case
|
||||
@@ -28,8 +23,8 @@ const getExecutions = async (
|
||||
as status
|
||||
`;
|
||||
|
||||
const executions = executionBaseQuery
|
||||
.clone()
|
||||
const executions = context.currentUser
|
||||
.$relatedQuery('executions')
|
||||
.joinRelated('executionSteps as execution_steps')
|
||||
.select('executions.*', raw(selectStatusStatement))
|
||||
.withSoftDeleted()
|
||||
|
@@ -1,18 +1,14 @@
|
||||
import Context from '../../types/express/context';
|
||||
import Flow from '../../models/flow';
|
||||
|
||||
type Params = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const getFlow = async (_parent: unknown, params: Params, context: Context) => {
|
||||
const conditions = context.currentUser.can('read', 'Flow');
|
||||
const userFlows = context.currentUser.$relatedQuery('flows');
|
||||
const allFlows = Flow.query();
|
||||
const baseQuery = conditions.isCreator ? userFlows : allFlows;
|
||||
context.currentUser.can('read', 'Flow');
|
||||
|
||||
const flow = await baseQuery
|
||||
.clone()
|
||||
const flow = await context.currentUser
|
||||
.$relatedQuery('flows')
|
||||
.withGraphJoined('[steps.[connection]]')
|
||||
.orderBy('steps.position', 'asc')
|
||||
.findOne({ 'flows.id': params.id })
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import Flow from '../../models/flow';
|
||||
import Context from '../../types/express/context';
|
||||
import paginate from '../../helpers/pagination';
|
||||
|
||||
@@ -11,13 +10,10 @@ type Params = {
|
||||
};
|
||||
|
||||
const getFlows = async (_parent: unknown, params: Params, context: Context) => {
|
||||
const conditions = context.currentUser.can('read', 'Flow');
|
||||
const userFlows = context.currentUser.$relatedQuery('flows');
|
||||
const allFlows = Flow.query();
|
||||
const baseQuery = conditions.isCreator ? userFlows : allFlows;
|
||||
context.currentUser.can('read', 'Flow');
|
||||
|
||||
const flowsQuery = baseQuery
|
||||
.clone()
|
||||
const flowsQuery = context.currentUser
|
||||
.$relatedQuery('flows')
|
||||
.joinRelated({
|
||||
steps: true,
|
||||
})
|
||||
|
@@ -1,7 +0,0 @@
|
||||
import permissionCatalog from '../../helpers/permission-catalog.ee';
|
||||
|
||||
const getPermissionCatalog = async () => {
|
||||
return permissionCatalog;
|
||||
};
|
||||
|
||||
export default getPermissionCatalog;
|
76
packages/backend/src/graphql/queries/get-permissions.ee.ts
Normal file
76
packages/backend/src/graphql/queries/get-permissions.ee.ts
Normal 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;
|
@@ -5,19 +5,9 @@ type Params = {
|
||||
id: string
|
||||
};
|
||||
|
||||
// TODO: access
|
||||
const getRole = async (_parent: unknown, params: Params, context: Context) => {
|
||||
context.currentUser.can('read', 'Role');
|
||||
|
||||
return await Role
|
||||
.query()
|
||||
.leftJoinRelated({
|
||||
permissions: true
|
||||
})
|
||||
.withGraphFetched({
|
||||
permissions: true
|
||||
})
|
||||
.findById(params.id)
|
||||
.throwIfNotFound();
|
||||
return await Role.query().findById(params.id).throwIfNotFound();
|
||||
};
|
||||
|
||||
export default getRole;
|
||||
|
@@ -1,10 +1,9 @@
|
||||
import Context from '../../types/express/context';
|
||||
import Role from '../../models/role';
|
||||
|
||||
// TODO: access
|
||||
const getRoles = async (_parent: unknown, params: unknown, context: Context) => {
|
||||
context.currentUser.can('read', 'Role');
|
||||
|
||||
return await Role.query().orderBy('name');
|
||||
return await Role.query();
|
||||
};
|
||||
|
||||
export default getRoles;
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import SamlAuthProvider from '../../models/saml-auth-provider.ee';
|
||||
|
||||
const getSamlAuthProviders = async () => {
|
||||
const providers = await SamlAuthProvider.query().where({ active: true });
|
||||
const providers = await SamlAuthProvider.query();
|
||||
|
||||
return providers;
|
||||
};
|
||||
|
@@ -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 ExecutionStep from '../../models/execution-step';
|
||||
import { ref } from 'objection';
|
||||
|
||||
type Params = {
|
||||
stepId: string;
|
||||
@@ -12,18 +11,15 @@ const getStepWithTestExecutions = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
const conditions = context.currentUser.can('update', 'Flow');
|
||||
const userSteps = context.currentUser.$relatedQuery('steps');
|
||||
const allSteps = Step.query();
|
||||
const stepBaseQuery = conditions.isCreator ? userSteps : allSteps;
|
||||
context.currentUser.can('update', 'Flow');
|
||||
|
||||
const step = await stepBaseQuery
|
||||
.clone()
|
||||
const step = await context.currentUser
|
||||
.$relatedQuery('steps')
|
||||
.findOne({ 'steps.id': params.stepId })
|
||||
.throwIfNotFound();
|
||||
|
||||
const previousStepsWithCurrentStep = await stepBaseQuery
|
||||
.clone()
|
||||
const previousStepsWithCurrentStep = await context.currentUser
|
||||
.$relatedQuery('steps')
|
||||
.withGraphJoined('executionSteps')
|
||||
.where('flow_id', '=', step.flowId)
|
||||
.andWhere('position', '<', step.position)
|
||||
|
@@ -5,9 +5,8 @@ type Params = {
|
||||
id: string
|
||||
};
|
||||
|
||||
// TODO: access
|
||||
const getUser = async (_parent: unknown, params: Params, context: Context) => {
|
||||
context.currentUser.can('read', 'User');
|
||||
|
||||
return await User
|
||||
.query()
|
||||
.leftJoinRelated({
|
||||
|
@@ -7,9 +7,8 @@ type Params = {
|
||||
offset: number;
|
||||
};
|
||||
|
||||
// TODO: access
|
||||
const getUsers = async (_parent: unknown, params: Params, context: Context) => {
|
||||
context.currentUser.can('read', 'User');
|
||||
|
||||
const usersQuery = User
|
||||
.query()
|
||||
.leftJoinRelated({
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import Context from '../../types/express/context';
|
||||
import App from '../../models/app';
|
||||
import Connection from '../../models/connection';
|
||||
import globalVariable from '../../helpers/global-variable';
|
||||
|
||||
type Params = {
|
||||
@@ -13,13 +12,10 @@ const testConnection = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
const conditions = context.currentUser.can('update', 'Connection');
|
||||
const userConnections = context.currentUser.$relatedQuery('connections');
|
||||
const allConnections = Connection.query();
|
||||
const connectionBaseQuery = conditions.isCreator ? userConnections : allConnections;
|
||||
context.currentUser.can('update', 'Connection');
|
||||
|
||||
let connection = await connectionBaseQuery
|
||||
.clone()
|
||||
let connection = await context.currentUser
|
||||
.$relatedQuery('connections')
|
||||
.findOne({
|
||||
id: params.id,
|
||||
})
|
||||
|
@@ -16,7 +16,7 @@ import getUsers from './queries/get-users';
|
||||
import getInvoices from './queries/get-invoices.ee';
|
||||
import getPaddleInfo from './queries/get-paddle-info.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 getRoles from './queries/get-roles.ee';
|
||||
import getSamlAuthProviders from './queries/get-saml-auth-providers.ee';
|
||||
@@ -43,7 +43,7 @@ const queryResolvers = {
|
||||
getInvoices,
|
||||
getPaddleInfo,
|
||||
getPaymentPlans,
|
||||
getPermissionCatalog,
|
||||
getPermissions,
|
||||
getRole,
|
||||
getRoles,
|
||||
getSamlAuthProviders,
|
||||
|
@@ -42,11 +42,14 @@ type Query {
|
||||
getTrialStatus: GetTrialStatus
|
||||
getSubscriptionStatus: GetSubscriptionStatus
|
||||
getSamlAuthProviders: [GetSamlAuthProviders]
|
||||
getUsers(limit: Int!, offset: Int!): UserConnection
|
||||
getUsers(
|
||||
limit: Int!
|
||||
offset: Int!
|
||||
): UserConnection
|
||||
getUser(id: String!): User
|
||||
getRoles: [Role]
|
||||
getRole(id: String!): Role
|
||||
getPermissionCatalog: PermissionCatalog
|
||||
getPermissions: Permissions
|
||||
healthcheck: AppHealth
|
||||
}
|
||||
|
||||
@@ -78,8 +81,6 @@ type Mutation {
|
||||
updateStep(input: UpdateStepInput): Step
|
||||
updateUser(input: UpdateUserInput): User
|
||||
verifyConnection(input: VerifyConnectionInput): Connection
|
||||
createSamlAuthProvider(input: CreateSamlAuthProviderInput): SamlAuthProvider
|
||||
updateSamlAuthProvider(input: UpdateSamlAuthProviderInput): SamlAuthProvider
|
||||
}
|
||||
|
||||
"""
|
||||
@@ -291,20 +292,6 @@ type Execution {
|
||||
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 {
|
||||
edges: [UserEdge]
|
||||
pageInfo: PageInfo
|
||||
@@ -336,34 +323,6 @@ input VerifyConnectionInput {
|
||||
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 {
|
||||
id: String!
|
||||
}
|
||||
@@ -681,13 +640,13 @@ type GetSamlAuthProviders {
|
||||
}
|
||||
|
||||
type Permission {
|
||||
id: String
|
||||
action: String
|
||||
subject: String
|
||||
conditions: [String]
|
||||
}
|
||||
|
||||
type PermissionCatalog {
|
||||
# TODO: emphasize it's a catalog item
|
||||
type Permissions {
|
||||
actions: [Action]
|
||||
subjects: [Subject]
|
||||
conditions: [Condition]
|
||||
@@ -695,7 +654,7 @@ type PermissionCatalog {
|
||||
|
||||
type Action {
|
||||
label: String
|
||||
key: String
|
||||
action: String
|
||||
subjects: [String]
|
||||
}
|
||||
|
||||
|
@@ -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;
|
@@ -1,4 +1,3 @@
|
||||
// TODO: replace with axios-with-proxy when needed
|
||||
import axios from 'axios';
|
||||
import appConfig from '../../config/app';
|
||||
import { DateTime } from 'luxon';
|
||||
|
@@ -1,4 +1,3 @@
|
||||
// TODO: replace with axios-with-proxy
|
||||
import axios from 'axios';
|
||||
import appConfig from '../config/app';
|
||||
import memoryCache from 'memory-cache';
|
||||
|
@@ -3,7 +3,7 @@ import ExecutionStep from '../models/execution-step';
|
||||
import get from 'lodash.get';
|
||||
|
||||
// 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(
|
||||
parameters: Step['parameters'],
|
||||
@@ -42,7 +42,7 @@ export default function computeParameters(
|
||||
if (Array.isArray(value)) {
|
||||
return {
|
||||
...result,
|
||||
[key]: value.map((item) => computeParameters(item, executionSteps)),
|
||||
[key]: value.map(item => computeParameters(item, executionSteps)),
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -1,10 +1,8 @@
|
||||
import { IHttpClientParams } from '@automatisch/types';
|
||||
import { AxiosRequestConfig } from 'axios';
|
||||
import { URL } from 'node:url';
|
||||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
export { AxiosInstance as IHttpClient } from 'axios';
|
||||
|
||||
import { IHttpClientParams } from '@automatisch/types';
|
||||
import { URL } from 'url';
|
||||
import HttpError from '../../errors/http';
|
||||
import axios from '../axios-with-proxy';
|
||||
|
||||
const removeBaseUrlForAbsoluteUrls = (
|
||||
requestConfig: AxiosRequestConfig
|
||||
|
@@ -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;
|
@@ -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);
|
||||
}
|
@@ -2,7 +2,6 @@ import Base from './base';
|
||||
|
||||
class Permission extends Base {
|
||||
id: string;
|
||||
roleId: string;
|
||||
action: string;
|
||||
subject: string;
|
||||
conditions: string[];
|
||||
@@ -11,11 +10,10 @@ class Permission extends Base {
|
||||
|
||||
static jsonSchema = {
|
||||
type: 'object',
|
||||
required: ['roleId', 'action', 'subject'],
|
||||
required: ['action', 'subject'],
|
||||
|
||||
properties: {
|
||||
id: { type: 'string', format: 'uuid' },
|
||||
roleId: { type: 'string', format: 'uuid' },
|
||||
action: { type: 'string', minLength: 1 },
|
||||
subject: { type: 'string', minLength: 1 },
|
||||
conditions: { type: 'array', items: { type: 'string' } },
|
||||
|
@@ -20,7 +20,7 @@ class Role extends Base {
|
||||
id: { type: 'string', format: 'uuid' },
|
||||
name: { 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' },
|
||||
updatedAt: { type: 'string' },
|
||||
},
|
||||
@@ -40,11 +40,15 @@ class Role extends Base {
|
||||
},
|
||||
},
|
||||
permissions: {
|
||||
relation: Base.HasManyRelation,
|
||||
relation: Base.ManyToManyRelation,
|
||||
modelClass: Permission,
|
||||
join: {
|
||||
from: 'roles.id',
|
||||
to: 'permissions.role_id',
|
||||
through: {
|
||||
from: 'roles_permissions.role_id',
|
||||
to: 'roles_permissions.permission_id',
|
||||
},
|
||||
to: 'permissions.id',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@@ -8,7 +8,7 @@ class SamlAuthProvider extends Base {
|
||||
id!: string;
|
||||
name: string;
|
||||
certificate: string;
|
||||
signatureAlgorithm: SamlConfig['signatureAlgorithm'];
|
||||
signatureAlgorithm: SamlConfig["signatureAlgorithm"];
|
||||
issuer: string;
|
||||
entryPoint: string;
|
||||
firstnameAttributeName: string;
|
||||
@@ -16,7 +16,6 @@ class SamlAuthProvider extends Base {
|
||||
emailAttributeName: string;
|
||||
roleAttributeName: string;
|
||||
defaultRoleId: string;
|
||||
active: boolean;
|
||||
|
||||
static tableName = 'saml_auth_providers';
|
||||
|
||||
@@ -39,18 +38,14 @@ class SamlAuthProvider extends Base {
|
||||
id: { type: 'string', format: 'uuid' },
|
||||
name: { type: 'string', minLength: 1 },
|
||||
certificate: { type: 'string', minLength: 1 },
|
||||
signatureAlgorithm: {
|
||||
type: 'string',
|
||||
enum: ['sha1', 'sha256', 'sha512'],
|
||||
},
|
||||
signatureAlgorithm: { type: 'string', enum: ['sha1', 'sha256', 'sha512'] },
|
||||
issuer: { type: 'string', minLength: 1 },
|
||||
entryPoint: { type: 'string', minLength: 1 },
|
||||
firstnameAttributeName: { type: 'string', minLength: 1 },
|
||||
surnameAttributeName: { type: 'string', minLength: 1 },
|
||||
emailAttributeName: { type: 'string', minLength: 1 },
|
||||
roleAttributeName: { type: 'string', minLength: 1 },
|
||||
defaultRoleId: { type: 'string', format: 'uuid' },
|
||||
active: { type: 'boolean' },
|
||||
defaultRoleId: { type: 'string', format: 'uuid' }
|
||||
},
|
||||
};
|
||||
|
||||
@@ -77,7 +72,7 @@ class SamlAuthProvider extends Base {
|
||||
entryPoint: this.entryPoint,
|
||||
issuer: this.issuer,
|
||||
signatureAlgorithm: this.signatureAlgorithm,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,22 +1,22 @@
|
||||
import crypto from 'node:crypto';
|
||||
import { QueryContext, ModelOptions } from 'objection';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { DateTime } from 'luxon';
|
||||
import crypto from 'node:crypto';
|
||||
import { ModelOptions, QueryContext } from 'objection';
|
||||
import { PureAbility, fieldPatternMatcher, mongoQueryMatcher } from '@casl/ability';
|
||||
import type { Subject } from '@casl/ability';
|
||||
|
||||
import appConfig from '../config/app';
|
||||
import checkLicense from '../helpers/check-license.ee';
|
||||
import userAbility from '../helpers/user-ability';
|
||||
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 Role from './role';
|
||||
import Connection from './connection';
|
||||
import Flow from './flow';
|
||||
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 Subscription from './subscription.ee';
|
||||
|
||||
class User extends Base {
|
||||
id!: string;
|
||||
@@ -148,11 +148,15 @@ class User extends Base {
|
||||
},
|
||||
},
|
||||
permissions: {
|
||||
relation: Base.HasManyRelation,
|
||||
relation: Base.ManyToManyRelation,
|
||||
modelClass: Permission,
|
||||
join: {
|
||||
from: 'users.role_id',
|
||||
to: 'permissions.role_id',
|
||||
through: {
|
||||
from: 'roles_permissions.role_id',
|
||||
to: 'roles_permissions.permission_id',
|
||||
},
|
||||
to: 'permissions.id',
|
||||
},
|
||||
},
|
||||
identities: {
|
||||
@@ -161,8 +165,8 @@ class User extends Base {
|
||||
join: {
|
||||
from: 'identities.user_id',
|
||||
to: 'users.id',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
login(password: string) {
|
||||
@@ -288,44 +292,27 @@ class User extends Base {
|
||||
}
|
||||
}
|
||||
|
||||
async $afterFind(): Promise<any> {
|
||||
const hasValidLicense = await checkLicense();
|
||||
|
||||
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;
|
||||
});
|
||||
get ability() {
|
||||
if (!this.permissions) {
|
||||
throw new Error('User.permissions must be fetched!');
|
||||
}
|
||||
|
||||
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> {
|
||||
return userAbility(this);
|
||||
}
|
||||
|
||||
can(action: string, subject: string) {
|
||||
can(action: string, subject: Subject) {
|
||||
const can = this.ability.can(action, subject);
|
||||
|
||||
if (!can) throw new Error('Not authorized!');
|
||||
|
||||
const relevantRule = this.ability.relevantRuleFor(action, subject);
|
||||
|
||||
const conditions = (relevantRule?.conditions as string[]) || [];
|
||||
const conditionMap: Record<string, true> = Object.fromEntries(
|
||||
conditions.map((condition) => [condition, true])
|
||||
);
|
||||
|
||||
return conditionMap;
|
||||
return can;
|
||||
}
|
||||
|
||||
cannot(action: string, subject: string) {
|
||||
cannot(action: string, subject: Subject) {
|
||||
const cannot = this.ability.cannot(action, subject);
|
||||
|
||||
if (cannot) throw new Error('Not authorized!');
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@automatisch/cli",
|
||||
"version": "0.8.0",
|
||||
"version": "0.7.1",
|
||||
"license": "See LICENSE file",
|
||||
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
||||
"contributors": [
|
||||
@@ -33,7 +33,7 @@
|
||||
"version": "oclif readme && git add README.md"
|
||||
},
|
||||
"dependencies": {
|
||||
"@automatisch/backend": "^0.8.0",
|
||||
"@automatisch/backend": "^0.7.1",
|
||||
"@oclif/core": "^1",
|
||||
"@oclif/plugin-help": "^5",
|
||||
"@oclif/plugin-plugins": "^2.0.1",
|
||||
|
@@ -15593,6 +15593,11 @@ winston@^3.6.0, winston@^3.7.1:
|
||||
triple-beam "^1.3.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:
|
||||
version "6.5.4"
|
||||
resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-6.5.4.tgz#3141afba3cc8aa2ae14c24d0f6811374ba8ff6a9"
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@automatisch/docs",
|
||||
"version": "0.8.0",
|
||||
"version": "0.7.1",
|
||||
"license": "See LICENSE file",
|
||||
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
||||
"private": true,
|
||||
|
@@ -157,7 +157,6 @@ export default defineConfig({
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Triggers', link: '/apps/notion/triggers' },
|
||||
{ text: 'Actions', link: '/apps/notion/actions' },
|
||||
{ text: 'Connection', link: '/apps/notion/connection' },
|
||||
],
|
||||
},
|
||||
|
@@ -5,8 +5,6 @@ items:
|
||||
desc: Create a blank spreadsheet or duplicate an existing spreadsheet. Optionally, provide headers.
|
||||
- name: Create spreadsheet row
|
||||
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>
|
||||
|
@@ -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 />
|
@@ -1,5 +1,9 @@
|
||||
# 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.
|
||||
|
||||
- [DeepL](/apps/deepl/actions)
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@automatisch/e2e-tests",
|
||||
"version": "0.8.0",
|
||||
"version": "0.7.1",
|
||||
"license": "See LICENSE file",
|
||||
"private": true,
|
||||
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
||||
|
20
packages/types/index.d.ts
vendored
20
packages/types/index.d.ts
vendored
@@ -96,7 +96,6 @@ export interface IUser {
|
||||
flows: IFlow[];
|
||||
steps: IStep[];
|
||||
role: IRole;
|
||||
permissions: IPermission[];
|
||||
}
|
||||
|
||||
export interface IRole {
|
||||
@@ -105,20 +104,6 @@ export interface IRole {
|
||||
name: string;
|
||||
description: string;
|
||||
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 {
|
||||
@@ -432,11 +417,6 @@ declare module 'axios' {
|
||||
interface AxiosRequestConfig {
|
||||
additionalProperties?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// ref: https://github.com/axios/axios/issues/5095
|
||||
interface AxiosInstance {
|
||||
create(config?: CreateAxiosDefaults): AxiosInstance;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IRequest extends Request {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@automatisch/types",
|
||||
"version": "0.8.0",
|
||||
"version": "0.7.1",
|
||||
"license": "See LICENSE file",
|
||||
"description": "Type definitions for automatisch",
|
||||
"homepage": "https://github.com/automatisch/automatisch",
|
||||
|
@@ -1,13 +1,11 @@
|
||||
{
|
||||
"name": "@automatisch/web",
|
||||
"version": "0.8.0",
|
||||
"version": "0.7.1",
|
||||
"license": "See LICENSE file",
|
||||
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.6.9",
|
||||
"@automatisch/types": "^0.8.0",
|
||||
"@casl/ability": "^6.5.0",
|
||||
"@casl/react": "^3.1.0",
|
||||
"@automatisch/types": "^0.7.1",
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@hookform/resolvers": "^2.8.8",
|
||||
@@ -33,7 +31,7 @@
|
||||
"notistack": "^2.0.2",
|
||||
"react": "^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-json-tree": "^0.16.2",
|
||||
"react-router-dom": "^6.0.2",
|
||||
|
@@ -8,74 +8,60 @@ import CreateRole from 'pages/CreateRole/index.ee';
|
||||
import EditRole from 'pages/EditRole/index.ee';
|
||||
|
||||
import * as URLS from 'config/urls';
|
||||
import Can from 'components/Can';
|
||||
|
||||
// TODO: consider introducing redirections to `/` as fallback
|
||||
export default (
|
||||
<>
|
||||
<Route
|
||||
path={URLS.USERS}
|
||||
element={
|
||||
<Can I="read" a="User">
|
||||
<AdminSettingsLayout>
|
||||
<Users />
|
||||
</AdminSettingsLayout>
|
||||
</Can>
|
||||
<AdminSettingsLayout>
|
||||
<Users />
|
||||
</AdminSettingsLayout>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={URLS.CREATE_USER}
|
||||
element={
|
||||
<Can I="create" a="User">
|
||||
<AdminSettingsLayout>
|
||||
<CreateUser />
|
||||
</AdminSettingsLayout>
|
||||
</Can>
|
||||
<AdminSettingsLayout>
|
||||
<CreateUser />
|
||||
</AdminSettingsLayout>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={URLS.USER_PATTERN}
|
||||
element={
|
||||
<Can I="update" a="User">
|
||||
<AdminSettingsLayout>
|
||||
<EditUser />
|
||||
</AdminSettingsLayout>
|
||||
</Can>
|
||||
<AdminSettingsLayout>
|
||||
<EditUser />
|
||||
</AdminSettingsLayout>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={URLS.ROLES}
|
||||
element={
|
||||
<Can I="read" a="Role">
|
||||
<AdminSettingsLayout>
|
||||
<Roles />
|
||||
</AdminSettingsLayout>
|
||||
</Can>
|
||||
<AdminSettingsLayout>
|
||||
<Roles />
|
||||
</AdminSettingsLayout>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={URLS.CREATE_ROLE}
|
||||
element={
|
||||
<Can I="create" a="Role">
|
||||
<AdminSettingsLayout>
|
||||
<CreateRole />
|
||||
</AdminSettingsLayout>
|
||||
</Can>
|
||||
<AdminSettingsLayout>
|
||||
<CreateRole />
|
||||
</AdminSettingsLayout>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={URLS.ROLE_PATTERN}
|
||||
element={
|
||||
<Can I="update" a="Role">
|
||||
<AdminSettingsLayout>
|
||||
<EditRole />
|
||||
</AdminSettingsLayout>
|
||||
</Can>
|
||||
<AdminSettingsLayout>
|
||||
<EditRole />
|
||||
</AdminSettingsLayout>
|
||||
}
|
||||
/>
|
||||
|
||||
|
@@ -4,7 +4,6 @@ import MenuItem from '@mui/material/MenuItem';
|
||||
import Menu, { MenuProps } from '@mui/material/Menu';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import Can from 'components/Can';
|
||||
import apolloClient from 'graphql/client';
|
||||
import * as URLS from 'config/urls';
|
||||
import useAuthentication from 'hooks/useAuthentication';
|
||||
@@ -55,14 +54,9 @@ function AccountDropdownMenu(
|
||||
{formatMessage('accountDropdownMenu.settings')}
|
||||
</MenuItem>
|
||||
|
||||
<Can I="read" a="User">
|
||||
<MenuItem
|
||||
component={Link}
|
||||
to={URLS.ADMIN_SETTINGS_DASHBOARD}
|
||||
>
|
||||
{formatMessage('accountDropdownMenu.adminSettings')}
|
||||
</MenuItem>
|
||||
</Can>
|
||||
<MenuItem component={Link} to={URLS.ADMIN_SETTINGS_DASHBOARD}>
|
||||
{formatMessage('accountDropdownMenu.adminSettings')}
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem onClick={logout} data-test="logout-item">
|
||||
{formatMessage('accountDropdownMenu.logout')}
|
||||
|
@@ -12,7 +12,7 @@ import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import computeAuthStepVariables from 'helpers/computeAuthStepVariables';
|
||||
import { processStep } from 'helpers/authenticationSteps';
|
||||
import InputCreator from 'components/InputCreator';
|
||||
import { generateExternalLink } from '../../helpers/translationValues';
|
||||
import { generateExternalLink } from '../../helpers/translation-values';
|
||||
import { Form } from './style';
|
||||
|
||||
type AddAppConnectionProps = {
|
||||
|
@@ -1,42 +1,34 @@
|
||||
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
|
||||
import GroupIcon from '@mui/icons-material/Group';
|
||||
import GroupsIcon from '@mui/icons-material/Groups';
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import * 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 Drawer from 'components/Drawer';
|
||||
import * as URLS from 'config/urls';
|
||||
import useCurrentUserAbility from 'hooks/useCurrentUserAbility';
|
||||
|
||||
type SettingsLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
type DrawerLink = {
|
||||
Icon: SvgIconComponent,
|
||||
primary: string,
|
||||
to: string,
|
||||
}
|
||||
|
||||
function createDrawerLinks({ canReadRole, canReadUser }: { canReadRole: boolean; canReadUser: boolean; }) {
|
||||
function createDrawerLinks({ isCloud }: { isCloud: boolean }) {
|
||||
const items = [
|
||||
canReadUser ? {
|
||||
Icon: GroupIcon,
|
||||
primary: 'adminSettingsDrawer.users',
|
||||
to: URLS.USERS,
|
||||
} : null,
|
||||
canReadRole ? {
|
||||
Icon: GroupsIcon,
|
||||
primary: 'adminSettingsDrawer.roles',
|
||||
to: URLS.ROLES,
|
||||
} : null
|
||||
]
|
||||
.filter(Boolean) as DrawerLink[];
|
||||
{
|
||||
Icon: GroupIcon,
|
||||
primary: 'adminSettingsDrawer.users',
|
||||
to: URLS.USERS,
|
||||
},
|
||||
{
|
||||
Icon: GroupsIcon,
|
||||
primary: 'adminSettingsDrawer.roles',
|
||||
to: URLS.ROLES,
|
||||
}
|
||||
]
|
||||
|
||||
return items;
|
||||
}
|
||||
@@ -52,17 +44,14 @@ const drawerBottomLinks = [
|
||||
export default function SettingsLayout({
|
||||
children,
|
||||
}: SettingsLayoutProps): React.ReactElement {
|
||||
const { isCloud } = useAutomatischInfo();
|
||||
const theme = useTheme();
|
||||
const currentUserAbility = useCurrentUserAbility();
|
||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'));
|
||||
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
|
||||
|
||||
const openDrawer = () => setDrawerOpen(true);
|
||||
const closeDrawer = () => setDrawerOpen(false);
|
||||
const drawerLinks = createDrawerLinks({
|
||||
canReadUser: currentUserAbility.can('read', 'User'),
|
||||
canReadRole: currentUserAbility.can('read', 'Role'),
|
||||
});
|
||||
const drawerLinks = createDrawerLinks({ isCloud });
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@@ -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} />);
|
||||
};
|
@@ -20,7 +20,6 @@ export default function ConditionalIconButton(props: any): React.ReactElement {
|
||||
size={buttonProps.size}
|
||||
component={buttonProps.component}
|
||||
to={buttonProps.to}
|
||||
disabled={buttonProps.disabled}
|
||||
>
|
||||
{icon}
|
||||
</IconButton>
|
||||
|
@@ -2,7 +2,7 @@ import { styled } from '@mui/material/styles';
|
||||
import MuiIconButton, { iconButtonClasses } from '@mui/material/IconButton';
|
||||
|
||||
export const IconButton = styled(MuiIconButton)`
|
||||
&.${iconButtonClasses.colorPrimary}:not(.${iconButtonClasses.disabled}) {
|
||||
&.${iconButtonClasses.colorPrimary} {
|
||||
background: ${({ theme }) => theme.palette.primary.main};
|
||||
color: ${({ theme }) => theme.palette.primary.contrastText};
|
||||
|
||||
|
@@ -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}
|
||||
/>
|
||||
)}}
|
||||
/>
|
||||
);
|
||||
}
|
@@ -2,54 +2,35 @@ import * as React from 'react';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import { useSnackbar } from 'notistack';
|
||||
|
||||
import Can from 'components/Can';
|
||||
import ConfirmationDialog from 'components/ConfirmationDialog';
|
||||
import { DELETE_ROLE } from 'graphql/mutations/delete-role.ee';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
|
||||
type DeleteRoleButtonProps = {
|
||||
disabled?: boolean;
|
||||
roleId: string;
|
||||
};
|
||||
|
||||
export default function DeleteRoleButton(props: DeleteRoleButtonProps) {
|
||||
const { disabled, roleId } = props;
|
||||
const { roleId } = props;
|
||||
const [showConfirmation, setShowConfirmation] = React.useState(false);
|
||||
const [deleteRole] = useMutation(DELETE_ROLE, {
|
||||
variables: { input: { id: roleId } },
|
||||
refetchQueries: ['GetRoles'],
|
||||
});
|
||||
const formatMessage = useFormatMessage();
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const handleConfirm = React.useCallback(async () => {
|
||||
try {
|
||||
await deleteRole();
|
||||
await deleteRole();
|
||||
|
||||
setShowConfirmation(false);
|
||||
enqueueSnackbar(formatMessage('deleteRoleButton.successfullyDeleted'), {
|
||||
variant: 'success',
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error('Failed while deleting!');
|
||||
}
|
||||
setShowConfirmation(false);
|
||||
}, [deleteRole]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Can I="delete" a="Role" passThrough>
|
||||
{(allowed) => (
|
||||
<IconButton
|
||||
disabled={!allowed || disabled}
|
||||
onClick={() => setShowConfirmation(true)}
|
||||
size="small"
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</Can>
|
||||
<IconButton onClick={() => setShowConfirmation(true)} size="small">
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
|
||||
<ConfirmationDialog
|
||||
open={showConfirmation}
|
||||
|
@@ -2,7 +2,6 @@ import * as React from 'react';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import { useSnackbar } from 'notistack';
|
||||
|
||||
import ConfirmationDialog from 'components/ConfirmationDialog';
|
||||
import { DELETE_USER } from 'graphql/mutations/delete-user.ee';
|
||||
@@ -20,19 +19,11 @@ export default function DeleteUserButton(props: DeleteUserButtonProps) {
|
||||
refetchQueries: ['GetUsers'],
|
||||
});
|
||||
const formatMessage = useFormatMessage();
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const handleConfirm = React.useCallback(async () => {
|
||||
try {
|
||||
await deleteUser();
|
||||
await deleteUser();
|
||||
|
||||
setShowConfirmation(false);
|
||||
enqueueSnackbar(formatMessage('deleteUserButton.successfullyDeleted'), {
|
||||
variant: 'success',
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error('Failed while deleting!');
|
||||
}
|
||||
setShowConfirmation(false);
|
||||
}, [deleteUser]);
|
||||
|
||||
return (
|
||||
|
@@ -6,7 +6,6 @@ import type { PopoverProps } from '@mui/material/Popover';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import { useSnackbar } from 'notistack';
|
||||
|
||||
import Can from 'components/Can';
|
||||
import { DELETE_FLOW } from 'graphql/mutations/delete-flow';
|
||||
import { DUPLICATE_FLOW } from 'graphql/mutations/duplicate-flow';
|
||||
import * as URLS from 'config/urls';
|
||||
@@ -73,39 +72,13 @@ export default function ContextMenu(
|
||||
hideBackdrop={false}
|
||||
anchorEl={anchorEl}
|
||||
>
|
||||
<Can I="read" a="Flow" passThrough>
|
||||
{(allowed) => (
|
||||
<MenuItem
|
||||
disabled={!allowed}
|
||||
component={Link}
|
||||
to={URLS.FLOW(flowId)}
|
||||
>
|
||||
{formatMessage('flow.view')}
|
||||
</MenuItem>
|
||||
)}
|
||||
</Can>
|
||||
<MenuItem component={Link} to={URLS.FLOW(flowId)}>
|
||||
{formatMessage('flow.view')}
|
||||
</MenuItem>
|
||||
|
||||
<Can I="create" a="Flow" passThrough>
|
||||
{(allowed) => (
|
||||
<MenuItem
|
||||
disabled={!allowed}
|
||||
onClick={onFlowDuplicate}
|
||||
>
|
||||
{formatMessage('flow.duplicate')}
|
||||
</MenuItem>
|
||||
)}
|
||||
</Can>
|
||||
<MenuItem onClick={onFlowDuplicate}>{formatMessage('flow.duplicate')}</MenuItem>
|
||||
|
||||
<Can I="delete" a="Flow" passThrough>
|
||||
{(allowed) => (
|
||||
<MenuItem
|
||||
disabled={!allowed}
|
||||
onClick={onFlowDelete}
|
||||
>
|
||||
{formatMessage('flow.delete')}
|
||||
</MenuItem>
|
||||
)}
|
||||
</Can>
|
||||
<MenuItem onClick={onFlowDelete}>{formatMessage('flow.delete')}</MenuItem>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
47
packages/web/src/components/ListLoader/index.tsx
Normal file
47
packages/web/src/components/ListLoader/index.tsx
Normal 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;
|
@@ -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>
|
||||
)
|
||||
}
|
@@ -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;
|
@@ -13,14 +13,15 @@ import Typography from '@mui/material/Typography';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
|
||||
import DeleteRoleButton from 'components/DeleteRoleButton/index.ee';
|
||||
import ListLoader from 'components/ListLoader';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import useRoles from 'hooks/useRoles.ee';
|
||||
import * as URLS from 'config/urls';
|
||||
|
||||
// TODO: introduce loading bar
|
||||
// TODO: introduce interaction feedback upon deletion (successful + failure)
|
||||
export default function RoleList(): React.ReactElement {
|
||||
const formatMessage = useFormatMessage();
|
||||
const { roles } = useRoles();
|
||||
const { roles, loading } = useRoles();
|
||||
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
@@ -49,34 +50,40 @@ export default function RoleList(): React.ReactElement {
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{roles.map((role) => (
|
||||
<TableRow
|
||||
key={role.id}
|
||||
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
|
||||
>
|
||||
<TableCell scope="row">
|
||||
<Typography variant="subtitle2">{role.name}</Typography>
|
||||
</TableCell>
|
||||
{loading ? (
|
||||
<ListLoader rowsNumber={3} cellNumber={2} />
|
||||
) : (
|
||||
roles.map((role) => (
|
||||
<TableRow
|
||||
key={role.id}
|
||||
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
|
||||
>
|
||||
<TableCell scope="row">
|
||||
<Typography variant="subtitle2">{role.name}</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell scope="row">
|
||||
<Typography variant="subtitle2">{role.description}</Typography>
|
||||
</TableCell>
|
||||
<TableCell scope="row">
|
||||
<Typography variant="subtitle2">
|
||||
{role.description}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Stack direction="row" gap={1} justifyContent="right">
|
||||
<IconButton
|
||||
size="small"
|
||||
component={Link}
|
||||
to={URLS.ROLE(role.id)}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<TableCell>
|
||||
<Stack direction="row" gap={1} justifyContent="right">
|
||||
<IconButton
|
||||
size="small"
|
||||
component={Link}
|
||||
to={URLS.ROLE(role.id)}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
|
||||
<DeleteRoleButton disabled={role.isAdmin} roleId={role.id} />
|
||||
</Stack>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
<DeleteRoleButton roleId={role.id} />
|
||||
</Stack>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
@@ -4,7 +4,7 @@ import Button from '@mui/material/Button';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Divider from '@mui/material/Divider';
|
||||
|
||||
import * as URLS from 'config/urls';
|
||||
import appConfig from 'config/app';
|
||||
import useSamlAuthProviders from 'hooks/useSamlAuthProviders.ee';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
|
||||
@@ -24,12 +24,10 @@ function SsoProviders() {
|
||||
<Button
|
||||
key={provider.id}
|
||||
component="a"
|
||||
href={URLS.SSO_LOGIN(provider.issuer)}
|
||||
href={`${appConfig.apiUrl}/login/saml/${provider.issuer}`}
|
||||
variant="outlined"
|
||||
>
|
||||
{formatMessage('ssoProviders.loginWithProvider', {
|
||||
providerName: provider.name
|
||||
})}
|
||||
{provider.name}
|
||||
</Button>
|
||||
))}
|
||||
</Stack>
|
||||
|
@@ -3,7 +3,7 @@ import Alert from '@mui/material/Alert';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
import * as URLS from 'config/urls';
|
||||
import { generateInternalLink } from 'helpers/translationValues';
|
||||
import { generateInternalLink } from 'helpers/translation-values';
|
||||
import useTrialStatus from 'hooks/useTrialStatus.ee';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
|
||||
|
@@ -13,11 +13,13 @@ import Typography from '@mui/material/Typography';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
|
||||
import DeleteUserButton from 'components/DeleteUserButton/index.ee';
|
||||
import ListLoader from 'components/ListLoader';
|
||||
import useUsers from 'hooks/useUsers';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
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 {
|
||||
const formatMessage = useFormatMessage();
|
||||
const { users, loading } = useUsers();
|
||||
@@ -49,34 +51,38 @@ export default function UserList(): React.ReactElement {
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{users.map((user) => (
|
||||
<TableRow
|
||||
key={user.id}
|
||||
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
|
||||
>
|
||||
<TableCell scope="row">
|
||||
<Typography variant="subtitle2">{user.fullName}</Typography>
|
||||
</TableCell>
|
||||
{loading ? (
|
||||
<ListLoader rowsNumber={3} cellNumber={2} />
|
||||
) : (
|
||||
users.map((user) => (
|
||||
<TableRow
|
||||
key={user.id}
|
||||
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
|
||||
>
|
||||
<TableCell scope="row">
|
||||
<Typography variant="subtitle2">{user.fullName}</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Typography variant="subtitle2">{user.email}</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="subtitle2">{user.email}</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Stack direction="row" gap={1} justifyContent="right">
|
||||
<IconButton
|
||||
size="small"
|
||||
component={Link}
|
||||
to={URLS.USER(user.id)}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<TableCell>
|
||||
<Stack direction="row" gap={1} justifyContent="right">
|
||||
<IconButton
|
||||
size="small"
|
||||
component={Link}
|
||||
to={URLS.USER(user.id)}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
|
||||
<DeleteUserButton userId={user.id} />
|
||||
</Stack>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
<DeleteUserButton userId={user.id} />
|
||||
</Stack>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
@@ -3,7 +3,7 @@ import { FormattedMessage } from 'react-intl';
|
||||
import Typography from '@mui/material/Typography';
|
||||
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 TextField from '../TextField';
|
||||
import { Alert } from './style';
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { styled } from '@mui/material/styles';
|
||||
import MuiAlert, { alertClasses } from '@mui/material/Alert';
|
||||
|
||||
export const Alert = styled(MuiAlert)(() => ({
|
||||
export const Alert = styled(MuiAlert)(({ theme }) => ({
|
||||
[`&.${alertClasses.root}`]: {
|
||||
fontWeight: 300,
|
||||
width: '100%',
|
||||
|
@@ -17,9 +17,7 @@ const config: Config = {
|
||||
supportEmailAddress: 'support@automatisch.io'
|
||||
};
|
||||
|
||||
if (!config.apiUrl && !config.graphqlUrl) {
|
||||
config.apiUrl = '/';
|
||||
} else if (!config.apiUrl) {
|
||||
if (!config.apiUrl) {
|
||||
config.apiUrl = (new URL(config.graphqlUrl)).origin;
|
||||
}
|
||||
|
||||
|
@@ -1,5 +1,3 @@
|
||||
import appConfig from './app';
|
||||
|
||||
export const CONNECTIONS = '/connections';
|
||||
export const EXECUTIONS = '/executions';
|
||||
export const EXECUTION_PATTERN = '/executions/:executionId';
|
||||
@@ -8,7 +6,6 @@ export const EXECUTION = (executionId: string) =>
|
||||
|
||||
export const LOGIN = '/login';
|
||||
export const LOGIN_CALLBACK = `${LOGIN}/callback`;
|
||||
export const SSO_LOGIN = (issuer: string) => `${appConfig.apiUrl}/login/saml/${issuer}`;
|
||||
export const SIGNUP = '/sign-up';
|
||||
export const FORGOT_PASSWORD = '/forgot-password';
|
||||
export const RESET_PASSWORD = '/reset-password';
|
||||
|
@@ -6,12 +6,6 @@ export const UPDATE_ROLE = gql`
|
||||
id
|
||||
name
|
||||
description
|
||||
permissions {
|
||||
id
|
||||
action
|
||||
subject
|
||||
conditions
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@@ -7,11 +7,9 @@ export const GET_CURRENT_USER = gql`
|
||||
fullName
|
||||
email
|
||||
role {
|
||||
id
|
||||
isAdmin
|
||||
}
|
||||
permissions {
|
||||
id
|
||||
action
|
||||
subject
|
||||
conditions
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user