Compare commits
23 Commits
v0.8.0
...
update-sam
Author | SHA1 | Date | |
---|---|---|---|
![]() |
934a525898 | ||
![]() |
40e10cc270 | ||
![]() |
41db227eb3 | ||
![]() |
43eea965c5 | ||
![]() |
8101c9f0bc | ||
![]() |
b4cda90338 | ||
![]() |
7ca37c412e | ||
![]() |
e4e3356dc9 | ||
![]() |
0deaa03218 | ||
![]() |
a7104c41a2 | ||
![]() |
5176b8c322 | ||
![]() |
c37c70446d | ||
![]() |
63abc8a2c8 | ||
![]() |
ba5c038e3b | ||
![]() |
a6669415f5 | ||
![]() |
4086fad867 | ||
![]() |
8a71c13078 | ||
![]() |
5d77f64e76 | ||
![]() |
0d092b977f | ||
![]() |
69582ff83d | ||
![]() |
a5c7da331a | ||
![]() |
8e842296b7 | ||
![]() |
7db14d1df7 |
@@ -33,7 +33,32 @@ services:
|
||||
- '6379:6379'
|
||||
expose:
|
||||
- 6379
|
||||
keycloak:
|
||||
image: quay.io/keycloak/keycloak:21.1
|
||||
restart: always
|
||||
container_name: keycloak
|
||||
environment:
|
||||
- KEYCLOAK_ADMIN=admin
|
||||
- KEYCLOAK_ADMIN_PASSWORD=admin
|
||||
- KC_DB=postgres
|
||||
- KC_DB_URL_HOST=postgres
|
||||
- KC_DB_URL_DATABASE=keycloak
|
||||
- KC_DB_USERNAME=automatisch_user
|
||||
- KC_DB_PASSWORD=automatisch_password
|
||||
- KC_HEALTH_ENABLED=true
|
||||
ports:
|
||||
- "8080:8080"
|
||||
command: start-dev
|
||||
depends_on:
|
||||
- postgres
|
||||
healthcheck:
|
||||
test: "curl -f http://localhost:8080/health/ready || exit 1"
|
||||
volumes:
|
||||
- keycloak:/opt/keycloak/data/
|
||||
expose:
|
||||
- 8080
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
keycloak:
|
||||
|
@@ -2,18 +2,33 @@ import appConfig from '../../src/config/app';
|
||||
import logger from '../../src/helpers/logger';
|
||||
import client from './client';
|
||||
import User from '../../src/models/user';
|
||||
import Role from '../../src/models/role';
|
||||
import '../../src/config/orm';
|
||||
|
||||
async function fetchAdminRole() {
|
||||
const role = await Role
|
||||
.query()
|
||||
.where({
|
||||
key: 'admin'
|
||||
})
|
||||
.limit(1)
|
||||
.first();
|
||||
|
||||
return role;
|
||||
}
|
||||
|
||||
export async function createUser(
|
||||
email = 'user@automatisch.io',
|
||||
password = 'sample'
|
||||
) {
|
||||
const UNIQUE_VIOLATION_CODE = '23505';
|
||||
|
||||
const role = await fetchAdminRole();
|
||||
const userParams = {
|
||||
email,
|
||||
password,
|
||||
fullName: 'Initial admin',
|
||||
role: 'admin',
|
||||
roleId: role.id,
|
||||
};
|
||||
|
||||
try {
|
||||
|
@@ -12,6 +12,7 @@ const knexConfig = {
|
||||
database: appConfig.postgresDatabase,
|
||||
ssl: appConfig.postgresEnableSsl,
|
||||
},
|
||||
asyncStackTraces: appConfig.isDev,
|
||||
searchPath: [appConfig.postgresSchema],
|
||||
pool: { min: 0, max: 20 },
|
||||
migrations: {
|
||||
|
@@ -4,7 +4,7 @@
|
||||
"license": "See LICENSE file",
|
||||
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
||||
"scripts": {
|
||||
"dev": "ts-node-dev --exit-child src/server.ts",
|
||||
"dev": "ts-node-dev --watch 'src/graphql/schema.graphql' --exit-child src/server.ts",
|
||||
"worker": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/worker.ts",
|
||||
"build": "tsc && yarn copy-statics",
|
||||
"build:watch": "nodemon --watch 'src/**/*.ts' --watch 'bin/**/*.ts' --exec yarn build --ext ts",
|
||||
@@ -24,12 +24,15 @@
|
||||
"dependencies": {
|
||||
"@automatisch/web": "^0.8.0",
|
||||
"@bull-board/express": "^3.10.1",
|
||||
"@casl/ability": "^6.5.0",
|
||||
"@graphql-tools/graphql-file-loader": "^7.3.4",
|
||||
"@graphql-tools/load": "^7.5.2",
|
||||
"@node-saml/passport-saml": "^4.0.4",
|
||||
"@rudderstack/rudder-sdk-node": "^1.1.2",
|
||||
"@sentry/node": "^7.42.0",
|
||||
"@sentry/tracing": "^7.42.0",
|
||||
"@types/luxon": "^2.3.1",
|
||||
"@types/passport": "^1.0.12",
|
||||
"@types/xmlrpc": "^1.3.7",
|
||||
"ajv-formats": "^2.1.1",
|
||||
"axios": "0.24.0",
|
||||
@@ -62,6 +65,7 @@
|
||||
"nodemailer": "6.7.0",
|
||||
"oauth-1.0a": "^2.2.6",
|
||||
"objection": "^3.0.0",
|
||||
"passport": "^0.6.0",
|
||||
"pg": "^8.7.1",
|
||||
"php-serialize": "^4.0.2",
|
||||
"stripe": "^11.13.0",
|
||||
|
@@ -17,6 +17,7 @@ import {
|
||||
} from './helpers/create-bull-board-handler';
|
||||
import injectBullBoardHandler from './helpers/inject-bull-board-handler';
|
||||
import router from './routes';
|
||||
import configurePassport from './helpers/passport';
|
||||
|
||||
createBullBoardHandler(serverAdapter);
|
||||
|
||||
@@ -50,6 +51,9 @@ app.use(
|
||||
})
|
||||
);
|
||||
app.use(cors(corsOptions));
|
||||
|
||||
configurePassport(app);
|
||||
|
||||
app.use('/', router);
|
||||
|
||||
webUIHandler(app);
|
||||
|
@@ -0,0 +1,46 @@
|
||||
import { Knex } from 'knex';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import lowerCase from 'lodash/lowerCase';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.createTable('roles', (table) => {
|
||||
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
||||
table.string('name').notNullable();
|
||||
table.string('key').notNullable();
|
||||
table.string('description');
|
||||
|
||||
table.timestamps(true, true);
|
||||
});
|
||||
|
||||
const uniqueUserRoles = await knex('users')
|
||||
.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',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
return knex.schema.dropTable('roles');
|
||||
}
|
@@ -0,0 +1,48 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
const getPermissionForRole = (roleId: string, subject: string, actions: string[], conditions: string[] = []) => actions
|
||||
.map(action => ({
|
||||
role_id: roleId,
|
||||
subject,
|
||||
action,
|
||||
conditions,
|
||||
}));
|
||||
|
||||
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']),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
return knex.schema.dropTable('permissions');
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.table('users', async (table) => {
|
||||
table.uuid('role_id').references('id').inTable('roles');
|
||||
});
|
||||
|
||||
const theRole = await knex('roles').select('id').limit(1).first();
|
||||
const roles = await knex('roles').select('id', 'key');
|
||||
|
||||
for (const role of roles) {
|
||||
await knex('users')
|
||||
.where({
|
||||
role: role.key
|
||||
})
|
||||
.update({
|
||||
role_id: role.id
|
||||
});
|
||||
}
|
||||
|
||||
// backfill not-migratables
|
||||
await knex('users').whereNull('role_id').update({ role_id: theRole.id });
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
return await knex.schema.table('users', (table) => {
|
||||
table.dropColumn('role_id');
|
||||
});
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.table('users', async (table) => {
|
||||
table.dropColumn('role');
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
return await knex.schema.table('users', (table) => {
|
||||
table.string('role').defaultTo('user');
|
||||
});
|
||||
}
|
@@ -0,0 +1,24 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
return knex.schema.createTable('saml_auth_providers', (table) => {
|
||||
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
||||
table.string('name').notNullable();
|
||||
table.text('certificate').notNullable();
|
||||
table.string('signature_algorithm').notNullable();
|
||||
table.string('issuer').notNullable();
|
||||
table.text('entry_point').notNullable();
|
||||
table.text('firstname_attribute_name').notNullable();
|
||||
table.text('surname_attribute_name').notNullable();
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
return knex.schema.dropTable('saml_auth_providers');
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
return knex.schema.createTable('identities', (table) => {
|
||||
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
||||
table.uuid('user_id').references('id').inTable('users');
|
||||
table.string('remote_id').notNullable();
|
||||
table.string('provider_id').notNullable();
|
||||
table.string('provider_type').notNullable();
|
||||
|
||||
table.timestamps(true, true);
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
return knex.schema.dropTable('identities');
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
return await knex.schema.alterTable('users', (table) => {
|
||||
table.string('password').nullable().alter();
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(): Promise<void> {
|
||||
// void
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
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();
|
||||
}
|
@@ -1,47 +1,63 @@
|
||||
import createConnection from './mutations/create-connection';
|
||||
import generateAuthUrl from './mutations/generate-auth-url';
|
||||
import updateConnection from './mutations/update-connection';
|
||||
import resetConnection from './mutations/reset-connection';
|
||||
import verifyConnection from './mutations/verify-connection';
|
||||
import deleteConnection from './mutations/delete-connection';
|
||||
import createFlow from './mutations/create-flow';
|
||||
import createRole from './mutations/create-role.ee';
|
||||
import createStep from './mutations/create-step';
|
||||
import createUser from './mutations/create-user.ee';
|
||||
import deleteConnection from './mutations/delete-connection';
|
||||
import deleteCurrentUser from './mutations/delete-current-user.ee';
|
||||
import deleteFlow from './mutations/delete-flow';
|
||||
import deleteRole from './mutations/delete-role.ee';
|
||||
import deleteStep from './mutations/delete-step';
|
||||
import deleteUser from './mutations/delete-user.ee';
|
||||
import duplicateFlow from './mutations/duplicate-flow';
|
||||
import executeFlow from './mutations/execute-flow';
|
||||
import forgotPassword from './mutations/forgot-password.ee';
|
||||
import generateAuthUrl from './mutations/generate-auth-url';
|
||||
import login from './mutations/login';
|
||||
import registerUser from './mutations/register-user.ee';
|
||||
import resetConnection from './mutations/reset-connection';
|
||||
import resetPassword from './mutations/reset-password.ee';
|
||||
import updateConnection from './mutations/update-connection';
|
||||
import updateCurrentUser from './mutations/update-current-user';
|
||||
import updateFlow from './mutations/update-flow';
|
||||
import updateFlowStatus from './mutations/update-flow-status';
|
||||
import executeFlow from './mutations/execute-flow';
|
||||
import deleteFlow from './mutations/delete-flow';
|
||||
import duplicateFlow from './mutations/duplicate-flow';
|
||||
import createStep from './mutations/create-step';
|
||||
import updateRole from './mutations/update-role.ee';
|
||||
import updateStep from './mutations/update-step';
|
||||
import deleteStep from './mutations/delete-step';
|
||||
import createUser from './mutations/create-user.ee';
|
||||
import deleteUser from './mutations/delete-user.ee';
|
||||
import updateUser from './mutations/update-user';
|
||||
import forgotPassword from './mutations/forgot-password.ee';
|
||||
import resetPassword from './mutations/reset-password.ee';
|
||||
import login from './mutations/login';
|
||||
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,
|
||||
generateAuthUrl,
|
||||
updateConnection,
|
||||
resetConnection,
|
||||
verifyConnection,
|
||||
deleteConnection,
|
||||
createFlow,
|
||||
createRole,
|
||||
createStep,
|
||||
createUser,
|
||||
deleteConnection,
|
||||
deleteCurrentUser,
|
||||
deleteFlow,
|
||||
deleteRole,
|
||||
deleteStep,
|
||||
deleteUser,
|
||||
duplicateFlow,
|
||||
executeFlow,
|
||||
forgotPassword,
|
||||
generateAuthUrl,
|
||||
login,
|
||||
registerUser,
|
||||
resetConnection,
|
||||
resetPassword,
|
||||
updateConnection,
|
||||
updateCurrentUser,
|
||||
updateUser,
|
||||
updateFlow,
|
||||
updateFlowStatus,
|
||||
executeFlow,
|
||||
deleteFlow,
|
||||
duplicateFlow,
|
||||
createStep,
|
||||
updateRole,
|
||||
updateStep,
|
||||
deleteStep,
|
||||
createUser,
|
||||
deleteUser,
|
||||
updateUser,
|
||||
forgotPassword,
|
||||
resetPassword,
|
||||
login,
|
||||
verifyConnection,
|
||||
createSamlAuthProvider,
|
||||
updateSamlAuthProvider,
|
||||
};
|
||||
|
||||
export default mutationResolvers;
|
||||
|
@@ -13,6 +13,8 @@ const createConnection = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('create', 'Connection');
|
||||
|
||||
await App.findOneByKey(params.input.key);
|
||||
|
||||
return await context.currentUser.$relatedQuery('connections').insert({
|
||||
|
@@ -14,6 +14,8 @@ const createFlow = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('create', 'Flow');
|
||||
|
||||
const connectionId = params?.input?.connectionId;
|
||||
const appKey = params?.input?.triggerAppKey;
|
||||
|
||||
|
34
packages/backend/src/graphql/mutations/create-role.ee.ts
Normal file
34
packages/backend/src/graphql/mutations/create-role.ee.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import kebabCase from 'lodash/kebabCase';
|
||||
import Permission from '../../models/permission';
|
||||
import Role from '../../models/role';
|
||||
import Context from '../../types/express/context';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
name: string;
|
||||
description: string;
|
||||
permissions: Permission[];
|
||||
};
|
||||
};
|
||||
|
||||
const createRole = async (_parent: unknown, params: Params, context: Context) => {
|
||||
context.currentUser.can('create', 'Role');
|
||||
|
||||
const { name, description, permissions } = params.input;
|
||||
const key = kebabCase(name);
|
||||
|
||||
const existingRole = await Role.query().findOne({ key });
|
||||
|
||||
if (existingRole) {
|
||||
throw new Error('Role already exists!');
|
||||
}
|
||||
|
||||
return await Role.query().insertGraph({
|
||||
key,
|
||||
name,
|
||||
description,
|
||||
permissions,
|
||||
}, { relate: ['permissions'] }).returning('*');
|
||||
};
|
||||
|
||||
export default createRole;
|
@@ -0,0 +1,54 @@
|
||||
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;
|
@@ -22,6 +22,8 @@ const createStep = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('update', 'Flow');
|
||||
|
||||
const { input } = params;
|
||||
|
||||
if (input.appKey && input.key) {
|
||||
|
@@ -1,14 +1,21 @@
|
||||
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;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const createUser = async (_parent: unknown, params: Params) => {
|
||||
const createUser = async (_parent: unknown, params: Params, context: Context) => {
|
||||
context.currentUser.can('create', 'User');
|
||||
|
||||
const { fullName, email, password } = params.input;
|
||||
|
||||
const existingUser = await User.query().findOne({ email });
|
||||
@@ -17,12 +24,23 @@ const createUser = async (_parent: unknown, params: Params) => {
|
||||
throw new Error('User already exists!');
|
||||
}
|
||||
|
||||
const user = await User.query().insert({
|
||||
const userPayload: Partial<User> = {
|
||||
fullName,
|
||||
email,
|
||||
password,
|
||||
role: 'user',
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
context.currentUser.can('update', 'Role');
|
||||
|
||||
userPayload.roleId = params.input.role.id;
|
||||
} catch {
|
||||
// void
|
||||
const role = await Role.query().findOne({ key: 'user' });
|
||||
userPayload.roleId = role.id;
|
||||
}
|
||||
|
||||
const user = await User.query().insert(userPayload);
|
||||
|
||||
return user;
|
||||
};
|
||||
|
@@ -11,6 +11,8 @@ const deleteConnection = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('delete', 'Connection');
|
||||
|
||||
await context.currentUser
|
||||
.$relatedQuery('connections')
|
||||
.delete()
|
||||
|
@@ -0,0 +1,22 @@
|
||||
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) => {
|
||||
const id = context.currentUser.id;
|
||||
|
||||
await context.currentUser.$query().delete();
|
||||
|
||||
const jobName = `Delete user - ${id}`;
|
||||
const jobPayload = { id };
|
||||
const millisecondsFor30Days = Duration.fromObject({ days: 30 }).toMillis();
|
||||
const jobOptions = {
|
||||
delay: millisecondsFor30Days
|
||||
};
|
||||
|
||||
await deleteUserQueue.add(jobName, jobPayload, jobOptions);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export default deleteCurrentUser;
|
@@ -13,6 +13,8 @@ const deleteFlow = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('delete', 'Flow');
|
||||
|
||||
const flow = await context.currentUser
|
||||
.$relatedQuery('flows')
|
||||
.findOne({
|
||||
|
47
packages/backend/src/graphql/mutations/delete-role.ee.ts
Normal file
47
packages/backend/src/graphql/mutations/delete-role.ee.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import Role from '../../models/role';
|
||||
import SamlAuthProvider from '../../models/saml-auth-provider.ee';
|
||||
import Context from '../../types/express/context';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
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();
|
||||
await role.$query().delete();
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export default deleteRole;
|
@@ -11,6 +11,8 @@ const deleteStep = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('update', 'Flow');
|
||||
|
||||
const step = await context.currentUser
|
||||
.$relatedQuery('steps')
|
||||
.withGraphFetched('flow')
|
||||
|
@@ -1,11 +1,24 @@
|
||||
import Context from '../../types/express/context';
|
||||
import deleteUserQueue from '../../queues/delete-user.ee';
|
||||
import { Duration } from 'luxon';
|
||||
import Context from '../../types/express/context';
|
||||
import User from '../../models/user';
|
||||
import deleteUserQueue from '../../queues/delete-user.ee';
|
||||
|
||||
const deleteUser = async (_parent: unknown, params: never, context: Context) => {
|
||||
const id = context.currentUser.id;
|
||||
type Params = {
|
||||
input: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
await context.currentUser.$query().delete();
|
||||
const deleteUser = async (
|
||||
_parent: unknown,
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('delete', 'User');
|
||||
|
||||
const id = params.input.id;
|
||||
|
||||
await User.query().deleteById(id);
|
||||
|
||||
const jobName = `Delete user - ${id}`;
|
||||
const jobPayload = { id };
|
||||
|
@@ -53,6 +53,8 @@ const duplicateFlow = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('create', 'Flow');
|
||||
|
||||
const flow = await context.currentUser
|
||||
.$relatedQuery('flows')
|
||||
.withGraphJoined('[steps]')
|
||||
|
@@ -12,6 +12,8 @@ const executeFlow = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('update', 'Flow');
|
||||
|
||||
const { stepId } = params.input;
|
||||
|
||||
const untilStep = await context.currentUser
|
||||
|
@@ -13,6 +13,8 @@ const generateAuthUrl = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('create', 'Connection');
|
||||
|
||||
const connection = await context.currentUser
|
||||
.$relatedQuery('connections')
|
||||
.findOne({
|
||||
|
33
packages/backend/src/graphql/mutations/register-user.ee.ts
Normal file
33
packages/backend/src/graphql/mutations/register-user.ee.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import User from '../../models/user';
|
||||
import Role from '../../models/role';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
fullName: string;
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
};
|
||||
|
||||
const registerUser = async (_parent: unknown, params: Params) => {
|
||||
const { fullName, email, password } = params.input;
|
||||
|
||||
const existingUser = await User.query().findOne({ email });
|
||||
|
||||
if (existingUser) {
|
||||
throw new Error('User already exists!');
|
||||
}
|
||||
|
||||
const role = await Role.query().findOne({ key: 'user' });
|
||||
|
||||
const user = await User.query().insert({
|
||||
fullName,
|
||||
email,
|
||||
password,
|
||||
roleId: role.id,
|
||||
});
|
||||
|
||||
return user;
|
||||
};
|
||||
|
||||
export default registerUser;
|
@@ -11,6 +11,8 @@ const resetConnection = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('create', 'Connection');
|
||||
|
||||
let connection = await context.currentUser
|
||||
.$relatedQuery('connections')
|
||||
.findOne({
|
||||
|
@@ -13,6 +13,8 @@ const updateConnection = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('create', 'Connection');
|
||||
|
||||
let connection = await context.currentUser
|
||||
.$relatedQuery('connections')
|
||||
.findOne({
|
||||
|
@@ -8,7 +8,7 @@ type Params = {
|
||||
};
|
||||
};
|
||||
|
||||
const updateUser = async (
|
||||
const updateCurrentUser = async (
|
||||
_parent: unknown,
|
||||
params: Params,
|
||||
context: Context
|
||||
@@ -22,4 +22,4 @@ const updateUser = async (
|
||||
return user;
|
||||
};
|
||||
|
||||
export default updateUser;
|
||||
export default updateCurrentUser;
|
@@ -18,6 +18,8 @@ const updateFlowStatus = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('publish', 'Flow');
|
||||
|
||||
let flow = await context.currentUser
|
||||
.$relatedQuery('flows')
|
||||
.findOne({
|
||||
@@ -55,7 +57,7 @@ const updateFlowStatus = async (
|
||||
} else {
|
||||
if (newActiveValue) {
|
||||
flow = await flow.$query().patchAndFetch({
|
||||
published_at: new Date().toISOString(),
|
||||
publishedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const jobName = `${JOB_NAME}-${flow.id}`;
|
||||
@@ -78,9 +80,12 @@ const updateFlowStatus = async (
|
||||
}
|
||||
}
|
||||
|
||||
flow = await flow.$query().withGraphFetched('steps').patchAndFetch({
|
||||
active: newActiveValue,
|
||||
});
|
||||
flow = await flow
|
||||
.$query()
|
||||
.withGraphFetched('steps')
|
||||
.patchAndFetch({
|
||||
active: newActiveValue,
|
||||
});
|
||||
|
||||
return flow;
|
||||
};
|
||||
|
@@ -12,6 +12,8 @@ const updateFlow = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('update', 'Flow');
|
||||
|
||||
let flow = await context.currentUser
|
||||
.$relatedQuery('flows')
|
||||
.findOne({
|
||||
|
91
packages/backend/src/graphql/mutations/update-role.ee.ts
Normal file
91
packages/backend/src/graphql/mutations/update-role.ee.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
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: {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
permissions: Permission[];
|
||||
};
|
||||
};
|
||||
|
||||
const updateRole = async (
|
||||
_parent: unknown,
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('update', 'Role');
|
||||
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
permissions,
|
||||
} = params.input;
|
||||
|
||||
const role = await Role
|
||||
.query()
|
||||
.findById(id)
|
||||
.throwIfNotFound();
|
||||
|
||||
try {
|
||||
const updatedRole = await Role.transaction(async (trx) => {
|
||||
await role.$relatedQuery('permissions', trx).delete();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
@@ -0,0 +1,45 @@
|
||||
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;
|
@@ -23,6 +23,8 @@ const updateStep = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('update', 'Flow');
|
||||
|
||||
const { input } = params;
|
||||
|
||||
let step = await context.currentUser
|
||||
|
43
packages/backend/src/graphql/mutations/update-user.ee.ts
Normal file
43
packages/backend/src/graphql/mutations/update-user.ee.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import Context from '../../types/express/context';
|
||||
import User from '../../models/user';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
id: string;
|
||||
email: string;
|
||||
fullName: string;
|
||||
role: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
return user;
|
||||
};
|
||||
|
||||
export default updateUser;
|
@@ -13,6 +13,8 @@ const verifyConnection = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('create', 'Connection');
|
||||
|
||||
let connection = await context.currentUser
|
||||
.$relatedQuery('connections')
|
||||
.findOne({
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import App from '../../models/app';
|
||||
import Connection from '../../models/connection';
|
||||
import Context from '../../types/express/context';
|
||||
|
||||
type Params = {
|
||||
@@ -6,11 +7,17 @@ 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;
|
||||
|
||||
const app = await App.findOneByKey(params.key);
|
||||
|
||||
if (context.currentUser) {
|
||||
const connections = await context.currentUser
|
||||
.$relatedQuery('connections')
|
||||
const connections = await connectionBaseQuery
|
||||
.clone()
|
||||
.select('connections.*')
|
||||
.fullOuterJoinRelated('steps')
|
||||
.where({
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import App from '../../models/app';
|
||||
import { IApp } from '@automatisch/types';
|
||||
import App from '../../models/app';
|
||||
|
||||
type Params = {
|
||||
name: string;
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import { IConnection } from '@automatisch/types';
|
||||
import App from '../../models/app';
|
||||
import Context from '../../types/express/context';
|
||||
import { IApp, IConnection } from '@automatisch/types';
|
||||
import Flow from '../../models/flow';
|
||||
import Connection from '../../models/connection';
|
||||
|
||||
type Params = {
|
||||
name: string;
|
||||
@@ -11,17 +13,27 @@ 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;
|
||||
|
||||
let apps = await App.findAll(params.name);
|
||||
|
||||
const connections = await context.currentUser
|
||||
.$relatedQuery('connections')
|
||||
const connections = await connectionBaseQuery
|
||||
.clone()
|
||||
.select('connections.key')
|
||||
.where({ draft: false })
|
||||
.count('connections.id as count')
|
||||
.groupBy('connections.key');
|
||||
|
||||
const flows = await context.currentUser
|
||||
.$relatedQuery('flows')
|
||||
const flows = await flowBaseQuery
|
||||
.clone()
|
||||
.withGraphJoined('steps')
|
||||
.orderBy('created_at', 'desc');
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
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';
|
||||
@@ -16,8 +17,13 @@ const getDynamicData = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
const step = await context.currentUser
|
||||
.$relatedQuery('steps')
|
||||
const conditions = context.currentUser.can('update', 'Flow');
|
||||
const userSteps = context.currentUser.$relatedQuery('steps');
|
||||
const allSteps = Step.query();
|
||||
const stepBaseQuery = conditions.isCreator ? userSteps : allSteps;
|
||||
|
||||
const step = await stepBaseQuery
|
||||
.clone()
|
||||
.withGraphFetched({
|
||||
connection: true,
|
||||
flow: true,
|
||||
|
@@ -1,6 +1,7 @@
|
||||
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 = {
|
||||
@@ -14,8 +15,13 @@ const getDynamicFields = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
const step = await context.currentUser
|
||||
.$relatedQuery('steps')
|
||||
const conditions = context.currentUser.can('update', 'Flow');
|
||||
const userSteps = context.currentUser.$relatedQuery('steps');
|
||||
const allSteps = Step.query();
|
||||
const stepBaseQuery = conditions.isCreator ? userSteps : allSteps;
|
||||
|
||||
const step = await stepBaseQuery
|
||||
.clone()
|
||||
.withGraphFetched({
|
||||
connection: true,
|
||||
flow: true,
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import Context from '../../types/express/context';
|
||||
import paginate from '../../helpers/pagination';
|
||||
import Execution from '../../models/execution';
|
||||
|
||||
type Params = {
|
||||
executionId: string;
|
||||
@@ -12,8 +13,13 @@ const getExecutionSteps = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
const execution = await context.currentUser
|
||||
.$relatedQuery('executions')
|
||||
const conditions = context.currentUser.can('read', 'Execution');
|
||||
const userExecutions = context.currentUser.$relatedQuery('executions');
|
||||
const allExecutions = Execution.query();
|
||||
const executionBaseQuery = conditions.isCreator ? userExecutions : allExecutions;
|
||||
|
||||
const execution = await executionBaseQuery
|
||||
.clone()
|
||||
.withSoftDeleted()
|
||||
.findById(params.executionId)
|
||||
.throwIfNotFound();
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import Context from '../../types/express/context';
|
||||
import Execution from '../../models/execution';
|
||||
|
||||
type Params = {
|
||||
executionId: string;
|
||||
@@ -9,8 +10,13 @@ const getExecution = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
const execution = await context.currentUser
|
||||
.$relatedQuery('executions')
|
||||
const conditions = context.currentUser.can('read', 'Execution');
|
||||
const userExecutions = context.currentUser.$relatedQuery('executions');
|
||||
const allExecutions = Execution.query();
|
||||
const executionBaseQuery = conditions.isCreator ? userExecutions : allExecutions;
|
||||
|
||||
const execution = await executionBaseQuery
|
||||
.clone()
|
||||
.withGraphFetched({
|
||||
flow: {
|
||||
steps: true,
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { raw } from 'objection';
|
||||
import Context from '../../types/express/context';
|
||||
import Execution from '../../models/execution';
|
||||
import paginate from '../../helpers/pagination';
|
||||
|
||||
type Params = {
|
||||
@@ -12,6 +13,12 @@ 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;
|
||||
|
||||
const selectStatusStatement = `
|
||||
case
|
||||
when count(*) filter (where execution_steps.status = 'failure') > 0
|
||||
@@ -21,8 +28,8 @@ const getExecutions = async (
|
||||
as status
|
||||
`;
|
||||
|
||||
const executions = context.currentUser
|
||||
.$relatedQuery('executions')
|
||||
const executions = executionBaseQuery
|
||||
.clone()
|
||||
.joinRelated('executionSteps as execution_steps')
|
||||
.select('executions.*', raw(selectStatusStatement))
|
||||
.withSoftDeleted()
|
||||
|
@@ -1,12 +1,18 @@
|
||||
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 flow = await context.currentUser
|
||||
.$relatedQuery('flows')
|
||||
const conditions = context.currentUser.can('read', 'Flow');
|
||||
const userFlows = context.currentUser.$relatedQuery('flows');
|
||||
const allFlows = Flow.query();
|
||||
const baseQuery = conditions.isCreator ? userFlows : allFlows;
|
||||
|
||||
const flow = await baseQuery
|
||||
.clone()
|
||||
.withGraphJoined('[steps.[connection]]')
|
||||
.orderBy('steps.position', 'asc')
|
||||
.findOne({ 'flows.id': params.id })
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import Flow from '../../models/flow';
|
||||
import Context from '../../types/express/context';
|
||||
import paginate from '../../helpers/pagination';
|
||||
|
||||
@@ -10,8 +11,13 @@ type Params = {
|
||||
};
|
||||
|
||||
const getFlows = async (_parent: unknown, params: Params, context: Context) => {
|
||||
const flowsQuery = context.currentUser
|
||||
.$relatedQuery('flows')
|
||||
const conditions = context.currentUser.can('read', 'Flow');
|
||||
const userFlows = context.currentUser.$relatedQuery('flows');
|
||||
const allFlows = Flow.query();
|
||||
const baseQuery = conditions.isCreator ? userFlows : allFlows;
|
||||
|
||||
const flowsQuery = baseQuery
|
||||
.clone()
|
||||
.joinRelated({
|
||||
steps: true,
|
||||
})
|
||||
|
@@ -0,0 +1,7 @@
|
||||
import permissionCatalog from '../../helpers/permission-catalog.ee';
|
||||
|
||||
const getPermissionCatalog = async () => {
|
||||
return permissionCatalog;
|
||||
};
|
||||
|
||||
export default getPermissionCatalog;
|
23
packages/backend/src/graphql/queries/get-role.ee.ts
Normal file
23
packages/backend/src/graphql/queries/get-role.ee.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import Context from '../../types/express/context';
|
||||
import Role from '../../models/role';
|
||||
|
||||
type Params = {
|
||||
id: string
|
||||
};
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
export default getRole;
|
10
packages/backend/src/graphql/queries/get-roles.ee.ts
Normal file
10
packages/backend/src/graphql/queries/get-roles.ee.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import Context from '../../types/express/context';
|
||||
import Role from '../../models/role';
|
||||
|
||||
const getRoles = async (_parent: unknown, params: unknown, context: Context) => {
|
||||
context.currentUser.can('read', 'Role');
|
||||
|
||||
return await Role.query().orderBy('name');
|
||||
};
|
||||
|
||||
export default getRoles;
|
@@ -0,0 +1,9 @@
|
||||
import SamlAuthProvider from '../../models/saml-auth-provider.ee';
|
||||
|
||||
const getSamlAuthProviders = async () => {
|
||||
const providers = await SamlAuthProvider.query().where({ active: true });
|
||||
|
||||
return providers;
|
||||
};
|
||||
|
||||
export default getSamlAuthProviders;
|
@@ -1,6 +1,7 @@
|
||||
import Context from '../../types/express/context';
|
||||
import ExecutionStep from '../../models/execution-step';
|
||||
import { ref } from 'objection';
|
||||
import ExecutionStep from '../../models/execution-step';
|
||||
import Step from '../../models/step';
|
||||
import Context from '../../types/express/context';
|
||||
|
||||
type Params = {
|
||||
stepId: string;
|
||||
@@ -11,13 +12,18 @@ const getStepWithTestExecutions = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
const step = await context.currentUser
|
||||
.$relatedQuery('steps')
|
||||
const conditions = context.currentUser.can('update', 'Flow');
|
||||
const userSteps = context.currentUser.$relatedQuery('steps');
|
||||
const allSteps = Step.query();
|
||||
const stepBaseQuery = conditions.isCreator ? userSteps : allSteps;
|
||||
|
||||
const step = await stepBaseQuery
|
||||
.clone()
|
||||
.findOne({ 'steps.id': params.stepId })
|
||||
.throwIfNotFound();
|
||||
|
||||
const previousStepsWithCurrentStep = await context.currentUser
|
||||
.$relatedQuery('steps')
|
||||
const previousStepsWithCurrentStep = await stepBaseQuery
|
||||
.clone()
|
||||
.withGraphJoined('executionSteps')
|
||||
.where('flow_id', '=', step.flowId)
|
||||
.andWhere('position', '<', step.position)
|
||||
|
23
packages/backend/src/graphql/queries/get-user.ts
Normal file
23
packages/backend/src/graphql/queries/get-user.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import Context from '../../types/express/context';
|
||||
import User from '../../models/user';
|
||||
|
||||
type Params = {
|
||||
id: string
|
||||
};
|
||||
|
||||
const getUser = async (_parent: unknown, params: Params, context: Context) => {
|
||||
context.currentUser.can('read', 'User');
|
||||
|
||||
return await User
|
||||
.query()
|
||||
.leftJoinRelated({
|
||||
role: true
|
||||
})
|
||||
.withGraphFetched({
|
||||
role: true
|
||||
})
|
||||
.findById(params.id)
|
||||
.throwIfNotFound();
|
||||
};
|
||||
|
||||
export default getUser;
|
26
packages/backend/src/graphql/queries/get-users.ts
Normal file
26
packages/backend/src/graphql/queries/get-users.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import Context from '../../types/express/context';
|
||||
import paginate from '../../helpers/pagination';
|
||||
import User from '../../models/user';
|
||||
|
||||
type Params = {
|
||||
limit: number;
|
||||
offset: number;
|
||||
};
|
||||
|
||||
const getUsers = async (_parent: unknown, params: Params, context: Context) => {
|
||||
context.currentUser.can('read', 'User');
|
||||
|
||||
const usersQuery = User
|
||||
.query()
|
||||
.leftJoinRelated({
|
||||
role: true
|
||||
})
|
||||
.withGraphFetched({
|
||||
role: true
|
||||
})
|
||||
.orderBy('full_name', 'desc');
|
||||
|
||||
return paginate(usersQuery, params.limit, params.offset);
|
||||
};
|
||||
|
||||
export default getUsers;
|
@@ -1,5 +1,6 @@
|
||||
import Context from '../../types/express/context';
|
||||
import App from '../../models/app';
|
||||
import Connection from '../../models/connection';
|
||||
import globalVariable from '../../helpers/global-variable';
|
||||
|
||||
type Params = {
|
||||
@@ -12,8 +13,13 @@ const testConnection = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
let connection = await context.currentUser
|
||||
.$relatedQuery('connections')
|
||||
const conditions = context.currentUser.can('update', 'Connection');
|
||||
const userConnections = context.currentUser.$relatedQuery('connections');
|
||||
const allConnections = Connection.query();
|
||||
const connectionBaseQuery = conditions.isCreator ? userConnections : allConnections;
|
||||
|
||||
let connection = await connectionBaseQuery
|
||||
.clone()
|
||||
.findOne({
|
||||
id: params.id,
|
||||
})
|
||||
|
@@ -1,47 +1,59 @@
|
||||
import getApps from './queries/get-apps';
|
||||
import getApp from './queries/get-app';
|
||||
import getApps from './queries/get-apps';
|
||||
import getAutomatischInfo from './queries/get-automatisch-info';
|
||||
import getBillingAndUsage from './queries/get-billing-and-usage.ee';
|
||||
import getConnectedApps from './queries/get-connected-apps';
|
||||
import testConnection from './queries/test-connection';
|
||||
import getFlow from './queries/get-flow';
|
||||
import getFlows from './queries/get-flows';
|
||||
import getStepWithTestExecutions from './queries/get-step-with-test-executions';
|
||||
import getExecution from './queries/get-execution';
|
||||
import getExecutions from './queries/get-executions';
|
||||
import getExecutionSteps from './queries/get-execution-steps';
|
||||
import getCurrentUser from './queries/get-current-user';
|
||||
import getDynamicData from './queries/get-dynamic-data';
|
||||
import getDynamicFields from './queries/get-dynamic-fields';
|
||||
import getCurrentUser from './queries/get-current-user';
|
||||
import getPaymentPlans from './queries/get-payment-plans.ee';
|
||||
import getPaddleInfo from './queries/get-paddle-info.ee';
|
||||
import getBillingAndUsage from './queries/get-billing-and-usage.ee';
|
||||
import getExecution from './queries/get-execution';
|
||||
import getExecutionSteps from './queries/get-execution-steps';
|
||||
import getExecutions from './queries/get-executions';
|
||||
import getFlow from './queries/get-flow';
|
||||
import getFlows from './queries/get-flows';
|
||||
import getUser from './queries/get-user';
|
||||
import getUsers from './queries/get-users';
|
||||
import getInvoices from './queries/get-invoices.ee';
|
||||
import getAutomatischInfo from './queries/get-automatisch-info';
|
||||
import getTrialStatus from './queries/get-trial-status.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 getRole from './queries/get-role.ee';
|
||||
import getRoles from './queries/get-roles.ee';
|
||||
import getSamlAuthProviders from './queries/get-saml-auth-providers.ee';
|
||||
import getStepWithTestExecutions from './queries/get-step-with-test-executions';
|
||||
import getSubscriptionStatus from './queries/get-subscription-status.ee';
|
||||
import getTrialStatus from './queries/get-trial-status.ee';
|
||||
import healthcheck from './queries/healthcheck';
|
||||
import testConnection from './queries/test-connection';
|
||||
|
||||
const queryResolvers = {
|
||||
getApps,
|
||||
getApp,
|
||||
getApps,
|
||||
getAutomatischInfo,
|
||||
getBillingAndUsage,
|
||||
getConnectedApps,
|
||||
testConnection,
|
||||
getFlow,
|
||||
getFlows,
|
||||
getStepWithTestExecutions,
|
||||
getCurrentUser,
|
||||
getDynamicData,
|
||||
getDynamicFields,
|
||||
getExecution,
|
||||
getExecutions,
|
||||
getExecutionSteps,
|
||||
getDynamicData,
|
||||
getDynamicFields,
|
||||
getCurrentUser,
|
||||
getPaymentPlans,
|
||||
getPaddleInfo,
|
||||
getBillingAndUsage,
|
||||
getFlow,
|
||||
getFlows,
|
||||
getInvoices,
|
||||
getAutomatischInfo,
|
||||
getTrialStatus,
|
||||
getPaddleInfo,
|
||||
getPaymentPlans,
|
||||
getPermissionCatalog,
|
||||
getRole,
|
||||
getRoles,
|
||||
getSamlAuthProviders,
|
||||
getStepWithTestExecutions,
|
||||
getSubscriptionStatus,
|
||||
getTrialStatus,
|
||||
getUser,
|
||||
getUsers,
|
||||
healthcheck,
|
||||
testConnection,
|
||||
};
|
||||
|
||||
export default queryResolvers;
|
||||
|
@@ -41,31 +41,45 @@ type Query {
|
||||
getAutomatischInfo: GetAutomatischInfo
|
||||
getTrialStatus: GetTrialStatus
|
||||
getSubscriptionStatus: GetSubscriptionStatus
|
||||
getSamlAuthProviders: [GetSamlAuthProviders]
|
||||
getUsers(limit: Int!, offset: Int!): UserConnection
|
||||
getUser(id: String!): User
|
||||
getRoles: [Role]
|
||||
getRole(id: String!): Role
|
||||
getPermissionCatalog: PermissionCatalog
|
||||
healthcheck: AppHealth
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
createConnection(input: CreateConnectionInput): Connection
|
||||
generateAuthUrl(input: GenerateAuthUrlInput): AuthLink
|
||||
updateConnection(input: UpdateConnectionInput): Connection
|
||||
resetConnection(input: ResetConnectionInput): Connection
|
||||
verifyConnection(input: VerifyConnectionInput): Connection
|
||||
deleteConnection(input: DeleteConnectionInput): Boolean
|
||||
createFlow(input: CreateFlowInput): Flow
|
||||
createRole(input: CreateRoleInput): Role
|
||||
createStep(input: CreateStepInput): Step
|
||||
createUser(input: CreateUserInput): User
|
||||
deleteConnection(input: DeleteConnectionInput): Boolean
|
||||
deleteCurrentUser: Boolean
|
||||
deleteFlow(input: DeleteFlowInput): Boolean
|
||||
deleteRole(input: DeleteRoleInput): Boolean
|
||||
deleteStep(input: DeleteStepInput): Step
|
||||
deleteUser(input: DeleteUserInput): Boolean
|
||||
duplicateFlow(input: DuplicateFlowInput): Flow
|
||||
executeFlow(input: ExecuteFlowInput): executeFlowType
|
||||
forgotPassword(input: ForgotPasswordInput): Boolean
|
||||
generateAuthUrl(input: GenerateAuthUrlInput): AuthLink
|
||||
login(input: LoginInput): Auth
|
||||
registerUser(input: RegisterUserInput): User
|
||||
resetConnection(input: ResetConnectionInput): Connection
|
||||
resetPassword(input: ResetPasswordInput): Boolean
|
||||
updateConnection(input: UpdateConnectionInput): Connection
|
||||
updateCurrentUser(input: UpdateCurrentUserInput): User
|
||||
updateFlow(input: UpdateFlowInput): Flow
|
||||
updateFlowStatus(input: UpdateFlowStatusInput): Flow
|
||||
executeFlow(input: ExecuteFlowInput): executeFlowType
|
||||
deleteFlow(input: DeleteFlowInput): Boolean
|
||||
duplicateFlow(input: DuplicateFlowInput): Flow
|
||||
createStep(input: CreateStepInput): Step
|
||||
updateRole(input: UpdateRoleInput): Role
|
||||
updateStep(input: UpdateStepInput): Step
|
||||
deleteStep(input: DeleteStepInput): Step
|
||||
createUser(input: CreateUserInput): User
|
||||
deleteUser: Boolean
|
||||
updateUser(input: UpdateUserInput): User
|
||||
forgotPassword(input: ForgotPasswordInput): Boolean
|
||||
resetPassword(input: ResetPasswordInput): Boolean
|
||||
login(input: LoginInput): Auth
|
||||
verifyConnection(input: VerifyConnectionInput): Connection
|
||||
createSamlAuthProvider(input: CreateSamlAuthProviderInput): SamlAuthProvider
|
||||
updateSamlAuthProvider(input: UpdateSamlAuthProviderInput): SamlAuthProvider
|
||||
}
|
||||
|
||||
"""
|
||||
@@ -277,6 +291,29 @@ 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
|
||||
}
|
||||
|
||||
type UserEdge {
|
||||
node: User
|
||||
}
|
||||
|
||||
input CreateConnectionInput {
|
||||
key: String!
|
||||
formattedData: JSONObject!
|
||||
@@ -299,6 +336,34 @@ 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!
|
||||
}
|
||||
@@ -360,9 +425,31 @@ input CreateUserInput {
|
||||
fullName: String!
|
||||
email: String!
|
||||
password: String!
|
||||
role: UserRoleInput!
|
||||
}
|
||||
|
||||
input UserRoleInput {
|
||||
id: String
|
||||
}
|
||||
|
||||
input UpdateUserInput {
|
||||
id: String!
|
||||
fullName: String
|
||||
email: String
|
||||
role: UserRoleInput
|
||||
}
|
||||
|
||||
input DeleteUserInput {
|
||||
id: String!
|
||||
}
|
||||
|
||||
input RegisterUserInput {
|
||||
fullName: String!
|
||||
email: String!
|
||||
password: String!
|
||||
}
|
||||
|
||||
input UpdateCurrentUserInput {
|
||||
email: String
|
||||
password: String
|
||||
fullName: String
|
||||
@@ -382,6 +469,29 @@ input LoginInput {
|
||||
password: String!
|
||||
}
|
||||
|
||||
input PermissionInput {
|
||||
action: String!
|
||||
subject: String!
|
||||
conditions: [String]
|
||||
}
|
||||
|
||||
input CreateRoleInput {
|
||||
name: String!
|
||||
description: String
|
||||
permissions: [PermissionInput]
|
||||
}
|
||||
|
||||
input UpdateRoleInput {
|
||||
id: String!
|
||||
name: String!
|
||||
description: String
|
||||
permissions: [PermissionInput]
|
||||
}
|
||||
|
||||
input DeleteRoleInput {
|
||||
id: String!
|
||||
}
|
||||
|
||||
"""
|
||||
The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).
|
||||
"""
|
||||
@@ -453,11 +563,21 @@ type User {
|
||||
id: String
|
||||
fullName: String
|
||||
email: String
|
||||
role: String
|
||||
role: Role
|
||||
permissions: [Permission]
|
||||
createdAt: String
|
||||
updatedAt: String
|
||||
}
|
||||
|
||||
type Role {
|
||||
id: String
|
||||
name: String
|
||||
key: String
|
||||
description: String
|
||||
isAdmin: Boolean
|
||||
permissions: [Permission]
|
||||
}
|
||||
|
||||
type PageInfo {
|
||||
currentPage: Int!
|
||||
totalPages: Int!
|
||||
@@ -554,6 +674,41 @@ type PaymentPlan {
|
||||
productId: String
|
||||
}
|
||||
|
||||
type GetSamlAuthProviders {
|
||||
id: String
|
||||
name: String
|
||||
issuer: String
|
||||
}
|
||||
|
||||
type Permission {
|
||||
id: String
|
||||
action: String
|
||||
subject: String
|
||||
conditions: [String]
|
||||
}
|
||||
|
||||
type PermissionCatalog {
|
||||
actions: [Action]
|
||||
subjects: [Subject]
|
||||
conditions: [Condition]
|
||||
}
|
||||
|
||||
type Action {
|
||||
label: String
|
||||
key: String
|
||||
subjects: [String]
|
||||
}
|
||||
|
||||
type Condition {
|
||||
key: String
|
||||
label: String
|
||||
}
|
||||
|
||||
type Subject {
|
||||
label: String
|
||||
key: String
|
||||
}
|
||||
|
||||
schema {
|
||||
query: Query
|
||||
mutation: Mutation
|
||||
|
@@ -12,7 +12,17 @@ const isAuthenticated = rule()(async (_parent, _args, req) => {
|
||||
const { userId } = jwt.verify(token, appConfig.appSecretKey) as {
|
||||
userId: string;
|
||||
};
|
||||
req.currentUser = await User.query().findById(userId).throwIfNotFound();
|
||||
req.currentUser = await User
|
||||
.query()
|
||||
.findById(userId)
|
||||
.leftJoinRelated({
|
||||
role: true,
|
||||
permissions: true,
|
||||
})
|
||||
.withGraphFetched({
|
||||
role: true,
|
||||
permissions: true,
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
@@ -25,13 +35,14 @@ const authentication = shield(
|
||||
Query: {
|
||||
'*': isAuthenticated,
|
||||
getAutomatischInfo: allow,
|
||||
getSamlAuthProviders: allow,
|
||||
healthcheck: allow,
|
||||
},
|
||||
Mutation: {
|
||||
'*': isAuthenticated,
|
||||
login: allow,
|
||||
createUser: allow,
|
||||
registerUser: allow,
|
||||
forgotPassword: allow,
|
||||
login: allow,
|
||||
resetPassword: allow,
|
||||
},
|
||||
},
|
||||
|
14
packages/backend/src/helpers/create-auth-token-by-user-id.ts
Normal file
14
packages/backend/src/helpers/create-auth-token-by-user-id.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
import appConfig from '../config/app';
|
||||
|
||||
const TOKEN_EXPIRES_IN = '14d';
|
||||
|
||||
const createAuthTokenByUserId = (userId: string) => {
|
||||
const token = jwt.sign({ userId }, appConfig.appSecretKey, {
|
||||
expiresIn: TOKEN_EXPIRES_IN,
|
||||
});
|
||||
|
||||
return token;
|
||||
};
|
||||
|
||||
export default createAuthTokenByUserId;
|
@@ -0,0 +1,48 @@
|
||||
import SamlAuthProvider from '../models/saml-auth-provider.ee';
|
||||
import User from '../models/user';
|
||||
import Identity from '../models/identity.ee';
|
||||
|
||||
const getUser = (user: Record<string, unknown>, providerConfig: SamlAuthProvider) => ({
|
||||
name: user[providerConfig.firstnameAttributeName],
|
||||
surname: user[providerConfig.surnameAttributeName],
|
||||
id: user.nameID,
|
||||
email: user[providerConfig.emailAttributeName],
|
||||
role: user[providerConfig.roleAttributeName],
|
||||
})
|
||||
|
||||
const findOrCreateUserBySamlIdentity = async (userIdentity: Record<string, unknown>, samlAuthProvider: SamlAuthProvider) => {
|
||||
const mappedUser = getUser(userIdentity, samlAuthProvider);
|
||||
const identity = await Identity.query().findOne({
|
||||
remote_id: mappedUser.id,
|
||||
});
|
||||
|
||||
if (identity) {
|
||||
const user = await identity.$relatedQuery('user');
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
const createdUser = await User.query().insertGraph({
|
||||
fullName: [
|
||||
mappedUser.name,
|
||||
mappedUser.surname
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' '),
|
||||
email: mappedUser.email as string,
|
||||
roleId: samlAuthProvider.defaultRoleId,
|
||||
identities: [
|
||||
{
|
||||
remoteId: mappedUser.id as string,
|
||||
providerId: samlAuthProvider.id,
|
||||
providerType: 'saml'
|
||||
}
|
||||
]
|
||||
}, {
|
||||
relate: ['identities']
|
||||
}).returning('*');
|
||||
|
||||
return createdUser;
|
||||
};
|
||||
|
||||
export default findOrCreateUserBySamlIdentity;
|
@@ -1,10 +1,11 @@
|
||||
import { Model } from 'objection';
|
||||
import ExtendedQueryBuilder from '../models/query-builder';
|
||||
import type Base from '../models/base';
|
||||
|
||||
const paginate = async (
|
||||
query: ExtendedQueryBuilder<Model, Model[]>,
|
||||
limit: number,
|
||||
offset: number
|
||||
offset: number,
|
||||
) => {
|
||||
if (limit < 1 || limit > 100) {
|
||||
throw new Error('Limit must be between 1 and 100');
|
||||
@@ -20,11 +21,9 @@ const paginate = async (
|
||||
currentPage: Math.ceil(offset / limit + 1),
|
||||
totalPages: Math.ceil(count / limit),
|
||||
},
|
||||
edges: records.map((record: Model) => {
|
||||
return {
|
||||
node: record,
|
||||
};
|
||||
}),
|
||||
edges: records.map((record: Base) => ({
|
||||
node: record,
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
|
84
packages/backend/src/helpers/passport.ts
Normal file
84
packages/backend/src/helpers/passport.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { URL } from 'node:url';
|
||||
import { IRequest } from '@automatisch/types';
|
||||
import { MultiSamlStrategy } from '@node-saml/passport-saml';
|
||||
import { Express } from 'express';
|
||||
import passport from 'passport';
|
||||
|
||||
import appConfig from '../config/app';
|
||||
import createAuthTokenByUserId from '../helpers/create-auth-token-by-user-id';
|
||||
import SamlAuthProvider from '../models/saml-auth-provider.ee';
|
||||
import findOrCreateUserBySamlIdentity from './find-or-create-user-by-saml-identity.ee'
|
||||
|
||||
export default function configurePassport(app: Express) {
|
||||
app.use(passport.initialize({
|
||||
userProperty: 'currentUser',
|
||||
}));
|
||||
|
||||
passport.use(new MultiSamlStrategy(
|
||||
{
|
||||
passReqToCallback: true,
|
||||
getSamlOptions: async function (request, done) {
|
||||
const { issuer } = request.params;
|
||||
const notFoundIssuer = new Error('Issuer cannot be found!');
|
||||
|
||||
if (!issuer) return done(notFoundIssuer);
|
||||
|
||||
const authProvider = await SamlAuthProvider.query().findOne({
|
||||
issuer: request.params.issuer as string,
|
||||
});
|
||||
|
||||
if (!authProvider) {
|
||||
return done(notFoundIssuer);
|
||||
}
|
||||
|
||||
return done(null, authProvider.config);
|
||||
},
|
||||
},
|
||||
async function (request, user: Record<string, unknown>, done) {
|
||||
const { issuer } = request.params;
|
||||
const notFoundIssuer = new Error('Issuer cannot be found!');
|
||||
|
||||
if (!issuer) return done(notFoundIssuer);
|
||||
|
||||
const authProvider = await SamlAuthProvider.query().findOne({
|
||||
issuer: request.params.issuer as string,
|
||||
});
|
||||
|
||||
if (!authProvider) {
|
||||
return done(notFoundIssuer);
|
||||
}
|
||||
|
||||
const foundUserWithIdentity = await findOrCreateUserBySamlIdentity(user, authProvider);
|
||||
return done(null, foundUserWithIdentity as unknown as Record<string, unknown>);
|
||||
},
|
||||
function (request, user: Record<string, unknown>, done: (error: any, user: Record<string, unknown>) => void) {
|
||||
return done(null, null);
|
||||
}
|
||||
));
|
||||
|
||||
app.get('/login/saml/:issuer',
|
||||
passport.authenticate('saml',
|
||||
{
|
||||
session: false,
|
||||
successRedirect: '/',
|
||||
})
|
||||
);
|
||||
|
||||
app.post(
|
||||
'/login/saml/:issuer/callback',
|
||||
passport.authenticate('saml', {
|
||||
session: false,
|
||||
failureRedirect: '/',
|
||||
failureFlash: true,
|
||||
}),
|
||||
(req: IRequest, res) => {
|
||||
const token = createAuthTokenByUserId(req.currentUser.id);
|
||||
|
||||
const redirectUrl = new URL(
|
||||
`/login/callback?token=${token}`,
|
||||
appConfig.webAppUrl,
|
||||
).toString();
|
||||
res.redirect(redirectUrl);
|
||||
}
|
||||
);
|
||||
};
|
72
packages/backend/src/helpers/permission-catalog.ee.ts
Normal file
72
packages/backend/src/helpers/permission-catalog.ee.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
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;
|
20
packages/backend/src/helpers/user-ability.ts
Normal file
20
packages/backend/src/helpers/user-ability.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
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);
|
||||
}
|
@@ -40,6 +40,9 @@ class Connection extends Base {
|
||||
userId: { type: 'string', format: 'uuid' },
|
||||
verified: { type: 'boolean', default: false },
|
||||
draft: { type: 'boolean' },
|
||||
deletedAt: { type: 'string' },
|
||||
createdAt: { type: 'string' },
|
||||
updatedAt: { type: 'string' },
|
||||
},
|
||||
};
|
||||
|
||||
|
@@ -31,6 +31,9 @@ class ExecutionStep extends Base {
|
||||
dataOut: { type: ['object', 'null'] },
|
||||
status: { type: 'string', enum: ['success', 'failure'] },
|
||||
errorDetails: { type: ['object', 'null'] },
|
||||
deletedAt: { type: 'string' },
|
||||
createdAt: { type: 'string' },
|
||||
updatedAt: { type: 'string' },
|
||||
},
|
||||
};
|
||||
|
||||
|
@@ -22,6 +22,9 @@ class Execution extends Base {
|
||||
flowId: { type: 'string', format: 'uuid' },
|
||||
testRun: { type: 'boolean', default: false },
|
||||
internalId: { type: 'string' },
|
||||
deletedAt: { type: 'string' },
|
||||
createdAt: { type: 'string' },
|
||||
updatedAt: { type: 'string' },
|
||||
},
|
||||
};
|
||||
|
||||
|
@@ -19,7 +19,7 @@ class Flow extends Base {
|
||||
status: 'paused' | 'published' | 'draft';
|
||||
steps: Step[];
|
||||
triggerStep: Step;
|
||||
published_at: string;
|
||||
publishedAt: string;
|
||||
remoteWebhookId: string;
|
||||
executions?: Execution[];
|
||||
lastExecution?: Execution;
|
||||
@@ -37,6 +37,10 @@ class Flow extends Base {
|
||||
userId: { type: 'string', format: 'uuid' },
|
||||
remoteWebhookId: { type: 'string' },
|
||||
active: { type: 'boolean' },
|
||||
publishedAt: { type: 'string' },
|
||||
deletedAt: { type: 'string' },
|
||||
createdAt: { type: 'string' },
|
||||
updatedAt: { type: 'string' },
|
||||
},
|
||||
};
|
||||
|
||||
|
53
packages/backend/src/models/identity.ee.ts
Normal file
53
packages/backend/src/models/identity.ee.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import Base from './base';
|
||||
import SamlAuthProvider from './saml-auth-provider.ee';
|
||||
import User from './user';
|
||||
|
||||
class Identity extends Base {
|
||||
id!: string;
|
||||
remoteId!: string;
|
||||
userId!: string;
|
||||
providerId!: string;
|
||||
providerType!: 'saml';
|
||||
|
||||
static tableName = 'identities';
|
||||
|
||||
static jsonSchema = {
|
||||
type: 'object',
|
||||
required: [
|
||||
'providerId',
|
||||
'remoteId',
|
||||
'userId',
|
||||
'providerType',
|
||||
],
|
||||
|
||||
properties: {
|
||||
id: { type: 'string', format: 'uuid' },
|
||||
userId: { type: 'string', format: 'uuid' },
|
||||
remoteId: { type: 'string', minLength: 1 },
|
||||
providerId: { type: 'string', format: 'uuid' },
|
||||
providerType: { type: 'string', enum: ['saml'] },
|
||||
},
|
||||
};
|
||||
|
||||
static relationMappings = () => ({
|
||||
user: {
|
||||
relation: Base.BelongsToOneRelation,
|
||||
modelClass: User,
|
||||
join: {
|
||||
from: 'users.id',
|
||||
to: 'identities.user_id',
|
||||
},
|
||||
},
|
||||
samlAuthProvider: {
|
||||
relation: Base.BelongsToOneRelation,
|
||||
modelClass: SamlAuthProvider,
|
||||
join: {
|
||||
from: 'saml_auth_providers.id',
|
||||
to: 'identities.provider_id'
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
export default Identity;
|
28
packages/backend/src/models/permission.ts
Normal file
28
packages/backend/src/models/permission.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import Base from './base';
|
||||
|
||||
class Permission extends Base {
|
||||
id: string;
|
||||
roleId: string;
|
||||
action: string;
|
||||
subject: string;
|
||||
conditions: string[];
|
||||
|
||||
static tableName = 'permissions';
|
||||
|
||||
static jsonSchema = {
|
||||
type: 'object',
|
||||
required: ['roleId', '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' } },
|
||||
createdAt: { type: 'string' },
|
||||
updatedAt: { type: 'string' },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default Permission;
|
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
Model,
|
||||
Page,
|
||||
ModelClass,
|
||||
PartialModelObject,
|
||||
ForClassMethod,
|
||||
AnyQueryBuilder,
|
||||
@@ -8,6 +9,10 @@ import {
|
||||
|
||||
const DELETED_COLUMN_NAME = 'deleted_at';
|
||||
|
||||
const supportsSoftDeletion = (modelClass: ModelClass<any>) => {
|
||||
return modelClass.jsonSchema.properties.deletedAt;
|
||||
}
|
||||
|
||||
const buildQueryBuidlerForClass = (): ForClassMethod => {
|
||||
return (modelClass) => {
|
||||
const qb: AnyQueryBuilder = Model.QueryBuilder.forClass.call(
|
||||
@@ -15,7 +20,7 @@ const buildQueryBuidlerForClass = (): ForClassMethod => {
|
||||
modelClass
|
||||
);
|
||||
qb.onBuild((builder) => {
|
||||
if (!builder.context().withSoftDeleted) {
|
||||
if (!builder.context().withSoftDeleted && supportsSoftDeletion(qb.modelClass())) {
|
||||
builder.whereNull(
|
||||
`${qb.modelClass().tableName}.${DELETED_COLUMN_NAME}`
|
||||
);
|
||||
@@ -38,9 +43,13 @@ class ExtendedQueryBuilder<M extends Model, R = M[]> extends Model.QueryBuilder<
|
||||
static forClass: ForClassMethod = buildQueryBuidlerForClass();
|
||||
|
||||
delete() {
|
||||
return this.patch({
|
||||
[DELETED_COLUMN_NAME]: new Date().toISOString(),
|
||||
} as unknown as PartialModelObject<M>);
|
||||
if (supportsSoftDeletion(this.modelClass())) {
|
||||
return this.patch({
|
||||
[DELETED_COLUMN_NAME]: new Date().toISOString(),
|
||||
} as unknown as PartialModelObject<M>);
|
||||
}
|
||||
|
||||
return super.delete();
|
||||
}
|
||||
|
||||
hardDelete() {
|
||||
|
57
packages/backend/src/models/role.ts
Normal file
57
packages/backend/src/models/role.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import Base from './base';
|
||||
import Permission from './permission';
|
||||
import User from './user';
|
||||
|
||||
class Role extends Base {
|
||||
id!: string;
|
||||
name!: string;
|
||||
key: string;
|
||||
description: string;
|
||||
users?: User[];
|
||||
permissions?: Permission[];
|
||||
|
||||
static tableName = 'roles';
|
||||
|
||||
static jsonSchema = {
|
||||
type: 'object',
|
||||
required: ['name', 'key'],
|
||||
|
||||
properties: {
|
||||
id: { type: 'string', format: 'uuid' },
|
||||
name: { type: 'string', minLength: 1 },
|
||||
key: { type: 'string', minLength: 1 },
|
||||
description: { type: ['string', 'null'], maxLength: 255 },
|
||||
createdAt: { type: 'string' },
|
||||
updatedAt: { type: 'string' },
|
||||
},
|
||||
};
|
||||
|
||||
static get virtualAttributes() {
|
||||
return ['isAdmin'];
|
||||
}
|
||||
|
||||
static relationMappings = () => ({
|
||||
users: {
|
||||
relation: Base.HasManyRelation,
|
||||
modelClass: User,
|
||||
join: {
|
||||
from: 'roles.id',
|
||||
to: 'users.role_id',
|
||||
},
|
||||
},
|
||||
permissions: {
|
||||
relation: Base.HasManyRelation,
|
||||
modelClass: Permission,
|
||||
join: {
|
||||
from: 'roles.id',
|
||||
to: 'permissions.role_id',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
get isAdmin() {
|
||||
return this.key === 'admin';
|
||||
}
|
||||
}
|
||||
|
||||
export default Role;
|
84
packages/backend/src/models/saml-auth-provider.ee.ts
Normal file
84
packages/backend/src/models/saml-auth-provider.ee.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { URL } from 'node:url';
|
||||
import type { SamlConfig } from '@node-saml/passport-saml';
|
||||
import appConfig from '../config/app';
|
||||
import Base from './base';
|
||||
import Identity from './identity.ee';
|
||||
|
||||
class SamlAuthProvider extends Base {
|
||||
id!: string;
|
||||
name: string;
|
||||
certificate: string;
|
||||
signatureAlgorithm: SamlConfig['signatureAlgorithm'];
|
||||
issuer: string;
|
||||
entryPoint: string;
|
||||
firstnameAttributeName: string;
|
||||
surnameAttributeName: string;
|
||||
emailAttributeName: string;
|
||||
roleAttributeName: string;
|
||||
defaultRoleId: string;
|
||||
active: boolean;
|
||||
|
||||
static tableName = 'saml_auth_providers';
|
||||
|
||||
static jsonSchema = {
|
||||
type: 'object',
|
||||
required: [
|
||||
'name',
|
||||
'certificate',
|
||||
'signatureAlgorithm',
|
||||
'entryPoint',
|
||||
'issuer',
|
||||
'firstnameAttributeName',
|
||||
'surnameAttributeName',
|
||||
'emailAttributeName',
|
||||
'roleAttributeName',
|
||||
'defaultRoleId',
|
||||
],
|
||||
|
||||
properties: {
|
||||
id: { type: 'string', format: 'uuid' },
|
||||
name: { type: 'string', minLength: 1 },
|
||||
certificate: { type: 'string', minLength: 1 },
|
||||
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' },
|
||||
},
|
||||
};
|
||||
|
||||
static relationMappings = () => ({
|
||||
identities: {
|
||||
relation: Base.HasOneRelation,
|
||||
modelClass: Identity,
|
||||
join: {
|
||||
from: 'identities.provider_id',
|
||||
to: 'saml_auth_providers.id',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
get config(): SamlConfig {
|
||||
const callbackUrl = new URL(
|
||||
`/login/saml/${this.issuer}/callback`,
|
||||
appConfig.baseUrl
|
||||
).toString();
|
||||
|
||||
return {
|
||||
callbackUrl,
|
||||
cert: this.certificate,
|
||||
entryPoint: this.entryPoint,
|
||||
issuer: this.issuer,
|
||||
signatureAlgorithm: this.signatureAlgorithm,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default SamlAuthProvider;
|
@@ -46,6 +46,9 @@ class Step extends Base {
|
||||
position: { type: 'integer' },
|
||||
parameters: { type: 'object' },
|
||||
webhookPath: { type: ['string', 'null'] },
|
||||
deletedAt: { type: 'string' },
|
||||
createdAt: { type: 'string' },
|
||||
updatedAt: { type: 'string' },
|
||||
},
|
||||
};
|
||||
|
||||
|
@@ -46,6 +46,9 @@ class Subscription extends Base {
|
||||
nextBillDate: { type: 'string' },
|
||||
lastBillDate: { type: 'string' },
|
||||
cancellationEffectiveDate: { type: 'string' },
|
||||
deletedAt: { type: 'string' },
|
||||
createdAt: { type: 'string' },
|
||||
updatedAt: { type: 'string' },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -84,7 +87,7 @@ class Subscription extends Base {
|
||||
return (
|
||||
this.status === 'deleted' &&
|
||||
Number(this.cancellationEffectiveDate) >
|
||||
DateTime.now().startOf('day').toMillis()
|
||||
DateTime.now().startOf('day').toMillis()
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -24,6 +24,9 @@ class UsageData extends Base {
|
||||
subscriptionId: { type: 'string', format: 'uuid' },
|
||||
consumedTaskCount: { type: 'integer' },
|
||||
nextResetAt: { type: 'string' },
|
||||
deletedAt: { type: 'string' },
|
||||
createdAt: { type: 'string' },
|
||||
updatedAt: { type: 'string' },
|
||||
},
|
||||
};
|
||||
|
||||
|
@@ -1,23 +1,29 @@
|
||||
import { QueryContext, ModelOptions } from 'objection';
|
||||
import bcrypt from 'bcrypt';
|
||||
import crypto from 'crypto';
|
||||
import { DateTime } from 'luxon';
|
||||
import crypto from 'node:crypto';
|
||||
import { ModelOptions, QueryContext } from 'objection';
|
||||
|
||||
import appConfig from '../config/app';
|
||||
import checkLicense from '../helpers/check-license.ee';
|
||||
import userAbility from '../helpers/user-ability';
|
||||
import Base from './base';
|
||||
import ExtendedQueryBuilder from './query-builder';
|
||||
import Connection from './connection';
|
||||
import Flow from './flow';
|
||||
import Step from './step';
|
||||
import Execution from './execution';
|
||||
import UsageData from './usage-data.ee';
|
||||
import Flow from './flow';
|
||||
import Identity from './identity.ee';
|
||||
import Permission from './permission';
|
||||
import ExtendedQueryBuilder from './query-builder';
|
||||
import Role from './role';
|
||||
import Step from './step';
|
||||
import Subscription from './subscription.ee';
|
||||
import UsageData from './usage-data.ee';
|
||||
|
||||
class User extends Base {
|
||||
id!: string;
|
||||
fullName!: string;
|
||||
email!: string;
|
||||
roleId: string;
|
||||
password!: string;
|
||||
role: string;
|
||||
resetPasswordToken: string;
|
||||
resetPasswordTokenSentAt: string;
|
||||
trialExpiryDate: string;
|
||||
@@ -29,19 +35,28 @@ class User extends Base {
|
||||
currentUsageData?: UsageData;
|
||||
subscriptions?: Subscription[];
|
||||
currentSubscription?: Subscription;
|
||||
role: Role;
|
||||
permissions: Permission[];
|
||||
identities: Identity[];
|
||||
|
||||
static tableName = 'users';
|
||||
|
||||
static jsonSchema = {
|
||||
type: 'object',
|
||||
required: ['fullName', 'email', 'password'],
|
||||
required: ['fullName', 'email'],
|
||||
|
||||
properties: {
|
||||
id: { type: 'string', format: 'uuid' },
|
||||
fullName: { type: 'string', minLength: 1 },
|
||||
email: { type: 'string', format: 'email', minLength: 1, maxLength: 255 },
|
||||
password: { type: 'string', minLength: 1, maxLength: 255 },
|
||||
role: { type: 'string', enum: ['admin', 'user'] },
|
||||
password: { type: 'string' },
|
||||
resetPasswordToken: { type: 'string' },
|
||||
resetPasswordTokenSentAt: { type: 'string' },
|
||||
trialExpiryDate: { type: 'string' },
|
||||
roleId: { type: 'string', format: 'uuid' },
|
||||
deletedAt: { type: 'string' },
|
||||
createdAt: { type: 'string' },
|
||||
updatedAt: { type: 'string' },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -124,6 +139,30 @@ class User extends Base {
|
||||
builder.orderBy('created_at', 'desc').limit(1).first();
|
||||
},
|
||||
},
|
||||
role: {
|
||||
relation: Base.HasOneRelation,
|
||||
modelClass: Role,
|
||||
join: {
|
||||
from: 'roles.id',
|
||||
to: 'users.role_id',
|
||||
},
|
||||
},
|
||||
permissions: {
|
||||
relation: Base.HasManyRelation,
|
||||
modelClass: Permission,
|
||||
join: {
|
||||
from: 'users.role_id',
|
||||
to: 'permissions.role_id',
|
||||
},
|
||||
},
|
||||
identities: {
|
||||
relation: Base.HasManyRelation,
|
||||
modelClass: Identity,
|
||||
join: {
|
||||
from: 'identities.user_id',
|
||||
to: 'users.id',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
login(password: string) {
|
||||
@@ -158,7 +197,9 @@ class User extends Base {
|
||||
}
|
||||
|
||||
async generateHash() {
|
||||
this.password = await bcrypt.hash(this.password, 10);
|
||||
if (this.password) {
|
||||
this.password = await bcrypt.hash(this.password, 10);
|
||||
}
|
||||
}
|
||||
|
||||
async startTrialPeriod() {
|
||||
@@ -232,9 +273,7 @@ class User extends Base {
|
||||
async $beforeUpdate(opt: ModelOptions, queryContext: QueryContext) {
|
||||
await super.$beforeUpdate(opt, queryContext);
|
||||
|
||||
if (this.password) {
|
||||
await this.generateHash();
|
||||
}
|
||||
await this.generateHash();
|
||||
}
|
||||
|
||||
async $afterInsert(queryContext: QueryContext) {
|
||||
@@ -248,6 +287,51 @@ 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;
|
||||
});
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
get ability(): ReturnType<typeof userAbility> {
|
||||
return userAbility(this);
|
||||
}
|
||||
|
||||
can(action: string, subject: string) {
|
||||
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;
|
||||
}
|
||||
|
||||
cannot(action: string, subject: string) {
|
||||
const cannot = this.ability.cannot(action, subject);
|
||||
|
||||
if (cannot) throw new Error('Not authorized!');
|
||||
|
||||
return cannot;
|
||||
}
|
||||
}
|
||||
|
||||
export default User;
|
||||
|
@@ -1,9 +1,5 @@
|
||||
# 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)
|
||||
|
38
packages/types/index.d.ts
vendored
38
packages/types/index.d.ts
vendored
@@ -95,6 +95,30 @@ export interface IUser {
|
||||
connections: IConnection[];
|
||||
flows: IFlow[];
|
||||
steps: IStep[];
|
||||
role: IRole;
|
||||
permissions: IPermission[];
|
||||
}
|
||||
|
||||
export interface IRole {
|
||||
id: string;
|
||||
key: string;
|
||||
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 {
|
||||
@@ -386,6 +410,20 @@ type TInvoice = {
|
||||
receipt_url: string;
|
||||
};
|
||||
|
||||
type TSamlAuthProvider = {
|
||||
id: string;
|
||||
name: string;
|
||||
certificate: string;
|
||||
signatureAlgorithm: "sha1" | "sha256" | "sha512";
|
||||
issuer: string;
|
||||
entryPoint: string;
|
||||
firstnameAttributeName: string;
|
||||
surnameAttributeName: string;
|
||||
emailAttributeName: string;
|
||||
roleAttributeName: string;
|
||||
defaultRoleId: string;
|
||||
}
|
||||
|
||||
declare module 'axios' {
|
||||
interface AxiosResponse {
|
||||
httpError?: IJSONObject;
|
||||
|
@@ -1,4 +1,5 @@
|
||||
PORT=3001
|
||||
REACT_APP_API_URL=http://localhost:3000
|
||||
REACT_APP_GRAPHQL_URL=http://localhost:3000/graphql
|
||||
# HTTPS=true
|
||||
REACT_APP_BASE_URL=http://localhost:3001
|
||||
|
@@ -6,6 +6,8 @@
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.6.9",
|
||||
"@automatisch/types": "^0.8.0",
|
||||
"@casl/ability": "^6.5.0",
|
||||
"@casl/react": "^3.1.0",
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@hookform/resolvers": "^2.8.8",
|
||||
@@ -31,7 +33,7 @@
|
||||
"notistack": "^2.0.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-hook-form": "^7.43.9",
|
||||
"react-hook-form": "^7.45.2",
|
||||
"react-intl": "^5.20.12",
|
||||
"react-json-tree": "^0.16.2",
|
||||
"react-router-dom": "^6.0.2",
|
||||
|
87
packages/web/src/adminSettingsRoutes.tsx
Normal file
87
packages/web/src/adminSettingsRoutes.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { Route, Navigate } from 'react-router-dom';
|
||||
import AdminSettingsLayout from 'components/AdminSettingsLayout';
|
||||
import Users from 'pages/Users';
|
||||
import EditUser from 'pages/EditUser';
|
||||
import CreateUser from 'pages/CreateUser';
|
||||
import Roles from 'pages/Roles/index.ee';
|
||||
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>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={URLS.CREATE_USER}
|
||||
element={
|
||||
<Can I="create" a="User">
|
||||
<AdminSettingsLayout>
|
||||
<CreateUser />
|
||||
</AdminSettingsLayout>
|
||||
</Can>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={URLS.USER_PATTERN}
|
||||
element={
|
||||
<Can I="update" a="User">
|
||||
<AdminSettingsLayout>
|
||||
<EditUser />
|
||||
</AdminSettingsLayout>
|
||||
</Can>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={URLS.ROLES}
|
||||
element={
|
||||
<Can I="read" a="Role">
|
||||
<AdminSettingsLayout>
|
||||
<Roles />
|
||||
</AdminSettingsLayout>
|
||||
</Can>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={URLS.CREATE_ROLE}
|
||||
element={
|
||||
<Can I="create" a="Role">
|
||||
<AdminSettingsLayout>
|
||||
<CreateRole />
|
||||
</AdminSettingsLayout>
|
||||
</Can>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={URLS.ROLE_PATTERN}
|
||||
element={
|
||||
<Can I="update" a="Role">
|
||||
<AdminSettingsLayout>
|
||||
<EditRole />
|
||||
</AdminSettingsLayout>
|
||||
</Can>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={URLS.ADMIN_SETTINGS}
|
||||
element={<Navigate to={URLS.USERS} replace />}
|
||||
/>
|
||||
</>
|
||||
);
|
@@ -4,6 +4,7 @@ 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';
|
||||
@@ -54,6 +55,15 @@ 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 onClick={logout} data-test="logout-item">
|
||||
{formatMessage('accountDropdownMenu.logout')}
|
||||
</MenuItem>
|
||||
|
@@ -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/translation-values';
|
||||
import { generateExternalLink } from '../../helpers/translationValues';
|
||||
import { Form } from './style';
|
||||
|
||||
type AddAppConnectionProps = {
|
||||
|
92
packages/web/src/components/AdminSettingsLayout/index.tsx
Normal file
92
packages/web/src/components/AdminSettingsLayout/index.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
|
||||
import GroupIcon from '@mui/icons-material/Group';
|
||||
import GroupsIcon from '@mui/icons-material/Groups';
|
||||
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 { SvgIconComponent } from '@mui/icons-material';
|
||||
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; }) {
|
||||
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[];
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
const drawerBottomLinks = [
|
||||
{
|
||||
Icon: ArrowBackIosNewIcon,
|
||||
primary: 'adminSettingsDrawer.goBack',
|
||||
to: '/',
|
||||
},
|
||||
];
|
||||
|
||||
export default function SettingsLayout({
|
||||
children,
|
||||
}: SettingsLayoutProps): React.ReactElement {
|
||||
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'),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppBar
|
||||
drawerOpen={isDrawerOpen}
|
||||
onDrawerOpen={openDrawer}
|
||||
onDrawerClose={closeDrawer}
|
||||
/>
|
||||
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<Drawer
|
||||
links={drawerLinks}
|
||||
bottomLinks={drawerBottomLinks}
|
||||
open={isDrawerOpen}
|
||||
onOpen={openDrawer}
|
||||
onClose={closeDrawer}
|
||||
/>
|
||||
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Toolbar />
|
||||
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
22
packages/web/src/components/Can/index.tsx
Normal file
22
packages/web/src/components/Can/index.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
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} />);
|
||||
};
|
@@ -19,6 +19,8 @@ export default function ConditionalIconButton(props: any): React.ReactElement {
|
||||
type={buttonProps.type}
|
||||
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} {
|
||||
&.${iconButtonClasses.colorPrimary}:not(.${iconButtonClasses.disabled}) {
|
||||
background: ${({ theme }) => theme.palette.primary.main};
|
||||
color: ${({ theme }) => theme.palette.primary.contrastText};
|
||||
|
||||
|
58
packages/web/src/components/ConfirmationDialog/index.tsx
Normal file
58
packages/web/src/components/ConfirmationDialog/index.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import * as React from 'react';
|
||||
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 DialogContentText from '@mui/material/DialogContentText';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
|
||||
type ConfirmationDialogProps = {
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
title: React.ReactNode;
|
||||
description: React.ReactNode;
|
||||
cancelButtonChildren: React.ReactNode;
|
||||
confirmButtionChildren: React.ReactNode;
|
||||
open?: boolean;
|
||||
}
|
||||
|
||||
export default function ConfirmationDialog(props: ConfirmationDialogProps) {
|
||||
const {
|
||||
onClose,
|
||||
onConfirm,
|
||||
title,
|
||||
description,
|
||||
cancelButtonChildren,
|
||||
confirmButtionChildren,
|
||||
open = true,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
{title && (
|
||||
<DialogTitle>
|
||||
{title}
|
||||
</DialogTitle>
|
||||
)}
|
||||
{description && (
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{description}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
)}
|
||||
|
||||
<DialogActions>
|
||||
{(cancelButtonChildren && onClose) && (
|
||||
<Button onClick={onClose}>{cancelButtonChildren}</Button>
|
||||
)}
|
||||
|
||||
{(confirmButtionChildren && onConfirm) && (
|
||||
<Button onClick={onConfirm} color="error">
|
||||
{confirmButtionChildren}
|
||||
</Button>
|
||||
)}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
57
packages/web/src/components/ControlledCheckbox/index.tsx
Normal file
57
packages/web/src/components/ControlledCheckbox/index.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
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}
|
||||
/>
|
||||
)}}
|
||||
/>
|
||||
);
|
||||
}
|
@@ -1,16 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useMutation } from '@apollo/client';
|
||||
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 DialogContentText from '@mui/material/DialogContentText';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
|
||||
import * as URLS from 'config/urls';
|
||||
import ConfirmationDialog from 'components/ConfirmationDialog';
|
||||
import apolloClient from 'graphql/client';
|
||||
import { DELETE_USER } from 'graphql/mutations/delete-user.ee';
|
||||
import { DELETE_CURRENT_USER } from 'graphql/mutations/delete-current-user.ee';
|
||||
import useAuthentication from 'hooks/useAuthentication';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import useCurrentUser from 'hooks/useCurrentUser';
|
||||
@@ -20,37 +15,29 @@ type DeleteAccountDialogProps = {
|
||||
}
|
||||
|
||||
export default function DeleteAccountDialog(props: DeleteAccountDialogProps) {
|
||||
const [deleteUser] = useMutation(DELETE_USER);
|
||||
const [deleteCurrentUser] = useMutation(DELETE_CURRENT_USER);
|
||||
const formatMessage = useFormatMessage();
|
||||
const currentUser = useCurrentUser();
|
||||
const authentication = useAuthentication();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleConfirm = React.useCallback(async () => {
|
||||
await deleteUser();
|
||||
await deleteCurrentUser();
|
||||
|
||||
authentication.updateToken('');
|
||||
await apolloClient.clearStore();
|
||||
|
||||
navigate(URLS.LOGIN);
|
||||
}, [deleteUser, currentUser]);
|
||||
}, [deleteCurrentUser, currentUser]);
|
||||
|
||||
return (
|
||||
<Dialog open onClose={props.onClose}>
|
||||
<DialogTitle >
|
||||
{formatMessage('deleteAccountDialog.title')}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id="alert-dialog-description">
|
||||
{formatMessage('deleteAccountDialog.description')}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={props.onClose}>{formatMessage('deleteAccountDialog.cancel')}</Button>
|
||||
<Button onClick={handleConfirm} color="error">
|
||||
{formatMessage('deleteAccountDialog.confirm')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
<ConfirmationDialog
|
||||
title={formatMessage('deleteAccountDialog.title')}
|
||||
description={formatMessage('deleteAccountDialog.description')}
|
||||
onClose={props.onClose}
|
||||
onConfirm={handleConfirm}
|
||||
cancelButtonChildren={formatMessage('deleteAccountDialog.cancel')}
|
||||
confirmButtionChildren={formatMessage('deleteAccountDialog.confirm')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
65
packages/web/src/components/DeleteRoleButton/index.ee.tsx
Normal file
65
packages/web/src/components/DeleteRoleButton/index.ee.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
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 [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();
|
||||
|
||||
setShowConfirmation(false);
|
||||
enqueueSnackbar(formatMessage('deleteRoleButton.successfullyDeleted'), {
|
||||
variant: 'success',
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error('Failed while deleting!');
|
||||
}
|
||||
}, [deleteRole]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Can I="delete" a="Role" passThrough>
|
||||
{(allowed) => (
|
||||
<IconButton
|
||||
disabled={!allowed || disabled}
|
||||
onClick={() => setShowConfirmation(true)}
|
||||
size="small"
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</Can>
|
||||
|
||||
<ConfirmationDialog
|
||||
open={showConfirmation}
|
||||
title={formatMessage('deleteRoleButton.title')}
|
||||
description={formatMessage('deleteRoleButton.description')}
|
||||
onClose={() => setShowConfirmation(false)}
|
||||
onConfirm={handleConfirm}
|
||||
cancelButtonChildren={formatMessage('deleteRoleButton.cancel')}
|
||||
confirmButtionChildren={formatMessage('deleteRoleButton.confirm')}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
55
packages/web/src/components/DeleteUserButton/index.ee.tsx
Normal file
55
packages/web/src/components/DeleteUserButton/index.ee.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
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';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
|
||||
type DeleteUserButtonProps = {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export default function DeleteUserButton(props: DeleteUserButtonProps) {
|
||||
const { userId } = props;
|
||||
const [showConfirmation, setShowConfirmation] = React.useState(false);
|
||||
const [deleteUser] = useMutation(DELETE_USER, {
|
||||
variables: { input: { id: userId } },
|
||||
refetchQueries: ['GetUsers'],
|
||||
});
|
||||
const formatMessage = useFormatMessage();
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const handleConfirm = React.useCallback(async () => {
|
||||
try {
|
||||
await deleteUser();
|
||||
|
||||
setShowConfirmation(false);
|
||||
enqueueSnackbar(formatMessage('deleteUserButton.successfullyDeleted'), {
|
||||
variant: 'success',
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error('Failed while deleting!');
|
||||
}
|
||||
}, [deleteUser]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton onClick={() => setShowConfirmation(true)} size="small">
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
|
||||
<ConfirmationDialog
|
||||
open={showConfirmation}
|
||||
title={formatMessage('deleteUserButton.title')}
|
||||
description={formatMessage('deleteUserButton.description')}
|
||||
onClose={() => setShowConfirmation(false)}
|
||||
onConfirm={handleConfirm}
|
||||
cancelButtonChildren={formatMessage('deleteUserButton.cancel')}
|
||||
confirmButtionChildren={formatMessage('deleteUserButton.confirm')}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -6,6 +6,7 @@ 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';
|
||||
@@ -72,13 +73,39 @@ export default function ContextMenu(
|
||||
hideBackdrop={false}
|
||||
anchorEl={anchorEl}
|
||||
>
|
||||
<MenuItem component={Link} to={URLS.FLOW(flowId)}>
|
||||
{formatMessage('flow.view')}
|
||||
</MenuItem>
|
||||
<Can I="read" a="Flow" passThrough>
|
||||
{(allowed) => (
|
||||
<MenuItem
|
||||
disabled={!allowed}
|
||||
component={Link}
|
||||
to={URLS.FLOW(flowId)}
|
||||
>
|
||||
{formatMessage('flow.view')}
|
||||
</MenuItem>
|
||||
)}
|
||||
</Can>
|
||||
|
||||
<MenuItem onClick={onFlowDuplicate}>{formatMessage('flow.duplicate')}</MenuItem>
|
||||
<Can I="create" a="Flow" passThrough>
|
||||
{(allowed) => (
|
||||
<MenuItem
|
||||
disabled={!allowed}
|
||||
onClick={onFlowDuplicate}
|
||||
>
|
||||
{formatMessage('flow.duplicate')}
|
||||
</MenuItem>
|
||||
)}
|
||||
</Can>
|
||||
|
||||
<MenuItem onClick={onFlowDelete}>{formatMessage('flow.delete')}</MenuItem>
|
||||
<Can I="delete" a="Flow" passThrough>
|
||||
{(allowed) => (
|
||||
<MenuItem
|
||||
disabled={!allowed}
|
||||
onClick={onFlowDelete}
|
||||
>
|
||||
{formatMessage('flow.delete')}
|
||||
</MenuItem>
|
||||
)}
|
||||
</Can>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
@@ -0,0 +1,142 @@
|
||||
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>
|
||||
)
|
||||
}
|
122
packages/web/src/components/PermissionCatalogField/index.ee.tsx
Normal file
122
packages/web/src/components/PermissionCatalogField/index.ee.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
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;
|
84
packages/web/src/components/RoleList/index.ee.tsx
Normal file
84
packages/web/src/components/RoleList/index.ee.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import * as React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
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 Paper from '@mui/material/Paper';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
|
||||
import DeleteRoleButton from 'components/DeleteRoleButton/index.ee';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import useRoles from 'hooks/useRoles.ee';
|
||||
import * as URLS from 'config/urls';
|
||||
|
||||
// TODO: introduce loading bar
|
||||
export default function RoleList(): React.ReactElement {
|
||||
const formatMessage = useFormatMessage();
|
||||
const { roles } = useRoles();
|
||||
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell component="th">
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{ color: 'text.secondary', fontWeight: 700 }}
|
||||
>
|
||||
{formatMessage('roleList.name')}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell component="th">
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{ color: 'text.secondary', fontWeight: 700 }}
|
||||
>
|
||||
{formatMessage('roleList.description')}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell component="th" />
|
||||
</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>
|
||||
|
||||
<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>
|
||||
|
||||
<DeleteRoleButton disabled={role.isAdmin} roleId={role.id} />
|
||||
</Stack>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user