Compare commits
13 Commits
AUT-794
...
add-loadin
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d3bc3a796b | ||
![]() |
9e64af4793 | ||
![]() |
b581f539e2 | ||
![]() |
aac1295c10 | ||
![]() |
e8f2802ee0 | ||
![]() |
75b3730a70 | ||
![]() |
af29dc9c3f | ||
![]() |
181cb5f335 | ||
![]() |
94e560c262 | ||
![]() |
f802061722 | ||
![]() |
58a7f6eec6 | ||
![]() |
5e11d3cc4d | ||
![]() |
399fb8312a |
@@ -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,55 @@ 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 Permission from '../../src/models/permission';
|
||||
import '../../src/config/orm';
|
||||
|
||||
async function seedPermissionsIfNeeded() {
|
||||
const existingPermissions = await Permission.query().limit(1).first();
|
||||
|
||||
if (!existingPermissions) return;
|
||||
|
||||
const getPermission = (subject: string, actions: string[]) => actions.map(action => ({ subject, action }));
|
||||
|
||||
await Permission.query().insert([
|
||||
...getPermission('Connection', ['create', 'read', 'delete', 'update']),
|
||||
...getPermission('Execution', ['read']),
|
||||
...getPermission('Flow', ['create', 'delete', 'publish', 'read', 'update']),
|
||||
...getPermission('Role', ['create', 'delete', 'read', 'update']),
|
||||
...getPermission('User', ['create', 'delete', 'read', 'update']),
|
||||
])
|
||||
}
|
||||
|
||||
async function createOrFetchRole() {
|
||||
const role = await Role.query().limit(1).first();
|
||||
|
||||
if (!role) {
|
||||
const createdRole = await Role.query().insertAndFetch({
|
||||
name: 'Admin',
|
||||
key: 'admin',
|
||||
});
|
||||
|
||||
return createdRole;
|
||||
}
|
||||
|
||||
return role;
|
||||
}
|
||||
|
||||
export async function createUser(
|
||||
email = 'user@automatisch.io',
|
||||
password = 'sample'
|
||||
) {
|
||||
const UNIQUE_VIOLATION_CODE = '23505';
|
||||
|
||||
await seedPermissionsIfNeeded();
|
||||
|
||||
const role = await createOrFetchRole();
|
||||
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.7.1",
|
||||
"@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",
|
||||
@@ -60,6 +63,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,32 @@
|
||||
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');
|
||||
|
||||
for (const { role } of uniqueUserRoles) {
|
||||
// skip empty roles
|
||||
if (!role) continue;
|
||||
|
||||
await knex('roles').insert({
|
||||
name: capitalize(role),
|
||||
key: lowerCase(role),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
return knex.schema.dropTable('roles');
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
const getPermission = (subject: string, actions: string[]) => actions.map(action => ({ subject, action }));
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.createTable('permissions', (table) => {
|
||||
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
||||
table.string('action').notNullable();
|
||||
table.string('subject').notNullable();
|
||||
|
||||
table.timestamps(true, true);
|
||||
});
|
||||
|
||||
await knex('permissions').insert([
|
||||
...getPermission('Connection', ['create', 'read', 'delete', 'update']),
|
||||
...getPermission('Execution', ['read']),
|
||||
...getPermission('Flow', ['create', 'delete', 'publish', 'read', 'update']),
|
||||
...getPermission('Role', ['create', 'delete', 'read', 'update']),
|
||||
...getPermission('User', ['create', 'delete', 'read', 'update']),
|
||||
]);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
return knex.schema.dropTable('permissions');
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.createTable('roles_permissions', (table) => {
|
||||
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
||||
table.uuid('role_id').references('id').inTable('roles');
|
||||
table.uuid('permission_id').references('id').inTable('permissions');
|
||||
});
|
||||
|
||||
const roles = await knex('roles').select('id');
|
||||
const permissions = await knex('permissions').select('id');
|
||||
|
||||
for (const role of roles) {
|
||||
for (const permission of permissions) {
|
||||
await knex('roles_permissions').insert({
|
||||
role_id: role.id,
|
||||
permission_id: permission.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
return knex.schema.dropTable('roles_permissions');
|
||||
}
|
@@ -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,23 @@
|
||||
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.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,13 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
return knex.schema.alterTable('permissions', (table) => {
|
||||
table.jsonb('conditions').notNullable().defaultTo([]);
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
return knex.schema.alterTable('permissions', (table) => {
|
||||
table.dropColumn('conditions');
|
||||
});
|
||||
}
|
@@ -1,47 +1,59 @@
|
||||
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';
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
|
32
packages/backend/src/graphql/mutations/create-role.ee.ts
Normal file
32
packages/backend/src/graphql/mutations/create-role.ee.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import kebabCase from 'lodash/kebabCase';
|
||||
import Role from '../../models/role';
|
||||
import Permission from '../../models/permission';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
name: string;
|
||||
description: string;
|
||||
permissions: Permission[];
|
||||
};
|
||||
};
|
||||
|
||||
// TODO: access
|
||||
const createRole = async (_parent: unknown, params: Params) => {
|
||||
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;
|
@@ -22,6 +22,8 @@ const createStep = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('update', 'Flow');
|
||||
|
||||
const { input } = params;
|
||||
|
||||
if (input.appKey && input.key) {
|
||||
|
@@ -5,11 +5,13 @@ type Params = {
|
||||
fullName: string;
|
||||
email: string;
|
||||
password: string;
|
||||
roleId: string;
|
||||
};
|
||||
};
|
||||
|
||||
// TODO: access
|
||||
const createUser = async (_parent: unknown, params: Params) => {
|
||||
const { fullName, email, password } = params.input;
|
||||
const { fullName, email, password, roleId } = params.input;
|
||||
|
||||
const existingUser = await User.query().findOne({ email });
|
||||
|
||||
@@ -21,7 +23,7 @@ const createUser = async (_parent: unknown, params: Params) => {
|
||||
fullName,
|
||||
email,
|
||||
password,
|
||||
role: 'user',
|
||||
roleId,
|
||||
});
|
||||
|
||||
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,23 @@
|
||||
import { Duration } from 'luxon';
|
||||
import Context from '../../types/express/context';
|
||||
import deleteUserQueue from '../../queues/delete-user.ee';
|
||||
|
||||
// TODO: access
|
||||
const deleteUser = 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 deleteUser;
|
@@ -13,6 +13,8 @@ const deleteFlow = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('delete', 'Flow');
|
||||
|
||||
const flow = await context.currentUser
|
||||
.$relatedQuery('flows')
|
||||
.findOne({
|
||||
|
31
packages/backend/src/graphql/mutations/delete-role.ee.ts
Normal file
31
packages/backend/src/graphql/mutations/delete-role.ee.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import Role from '../../models/role';
|
||||
import Context from '../../types/express/context';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
// TODO: access
|
||||
const deleteRole = async (
|
||||
_parent: unknown,
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
const role = await Role.query().findById(params.input.id).throwIfNotFound();
|
||||
|
||||
if (role.isAdmin) {
|
||||
throw new Error('Admin role cannot be deleted!');
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: consider migrations for users that still have the role or
|
||||
* do not let the role get deleted if there are still associated users
|
||||
*/
|
||||
await role.$query().delete();
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
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,23 @@
|
||||
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();
|
||||
// TODO: access
|
||||
const deleteUser = async (
|
||||
_parent: unknown,
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
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({
|
||||
|
44
packages/backend/src/graphql/mutations/update-role.ee.ts
Normal file
44
packages/backend/src/graphql/mutations/update-role.ee.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import Context from '../../types/express/context';
|
||||
import Role from '../../models/role';
|
||||
import Permission from '../../models/permission';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
permissions: Permission[];
|
||||
};
|
||||
};
|
||||
|
||||
// TODO: access
|
||||
const updateUser = async (
|
||||
_parent: unknown,
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
permissions,
|
||||
} = params.input;
|
||||
|
||||
const role = await Role.query().findById(id).throwIfNotFound();
|
||||
|
||||
// TODO: delete the unrelated items!
|
||||
await role.$relatedQuery('permissions').unrelate();
|
||||
|
||||
// TODO: possibly assert that given permissions do actually exist in catalog
|
||||
// TODO: possibly optimize it with patching the different permissions compared to current ones
|
||||
return await role.$query()
|
||||
.patchAndFetch(
|
||||
{
|
||||
name,
|
||||
description,
|
||||
permissions,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default updateUser;
|
@@ -23,6 +23,8 @@ const updateStep = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('update', 'Flow');
|
||||
|
||||
const { input } = params;
|
||||
|
||||
let step = await context.currentUser
|
||||
|
34
packages/backend/src/graphql/mutations/update-user.ee.ts
Normal file
34
packages/backend/src/graphql/mutations/update-user.ee.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import Context from '../../types/express/context';
|
||||
import User from '../../models/user';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
id: string;
|
||||
email: string;
|
||||
fullName: string;
|
||||
role: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
// TODO: access
|
||||
const updateUser = async (
|
||||
_parent: unknown,
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
const user = await User.query()
|
||||
.patchAndFetchById(
|
||||
params.input.id,
|
||||
{
|
||||
email: params.input.email,
|
||||
fullName: params.input.fullName,
|
||||
roleId: params.input.role.id,
|
||||
}
|
||||
);
|
||||
|
||||
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({
|
||||
|
@@ -6,6 +6,8 @@ type Params = {
|
||||
};
|
||||
|
||||
const getApp = async (_parent: unknown, params: Params, context: Context) => {
|
||||
context.currentUser.can('read', 'Connection');
|
||||
|
||||
const app = await App.findOneByKey(params.key);
|
||||
|
||||
if (context.currentUser) {
|
||||
|
@@ -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,6 @@
|
||||
import { IConnection } from '@automatisch/types';
|
||||
import App from '../../models/app';
|
||||
import Context from '../../types/express/context';
|
||||
import { IApp, IConnection } from '@automatisch/types';
|
||||
|
||||
type Params = {
|
||||
name: string;
|
||||
@@ -11,6 +11,8 @@ const getConnectedApps = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('read', 'Connection');
|
||||
|
||||
let apps = await App.findAll(params.name);
|
||||
|
||||
const connections = await context.currentUser
|
||||
|
@@ -16,6 +16,8 @@ const getDynamicData = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('update', 'Flow');
|
||||
|
||||
const step = await context.currentUser
|
||||
.$relatedQuery('steps')
|
||||
.withGraphFetched({
|
||||
|
@@ -14,6 +14,8 @@ const getDynamicFields = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('update', 'Flow');
|
||||
|
||||
const step = await context.currentUser
|
||||
.$relatedQuery('steps')
|
||||
.withGraphFetched({
|
||||
|
@@ -12,6 +12,8 @@ const getExecutionSteps = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('read', 'Execution');
|
||||
|
||||
const execution = await context.currentUser
|
||||
.$relatedQuery('executions')
|
||||
.withSoftDeleted()
|
||||
|
@@ -9,6 +9,8 @@ const getExecution = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('read', 'Execution');
|
||||
|
||||
const execution = await context.currentUser
|
||||
.$relatedQuery('executions')
|
||||
.withGraphFetched({
|
||||
|
@@ -12,6 +12,8 @@ const getExecutions = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('read', 'Execution');
|
||||
|
||||
const selectStatusStatement = `
|
||||
case
|
||||
when count(*) filter (where execution_steps.status = 'failure') > 0
|
||||
|
@@ -5,6 +5,8 @@ type Params = {
|
||||
};
|
||||
|
||||
const getFlow = async (_parent: unknown, params: Params, context: Context) => {
|
||||
context.currentUser.can('read', 'Flow');
|
||||
|
||||
const flow = await context.currentUser
|
||||
.$relatedQuery('flows')
|
||||
.withGraphJoined('[steps.[connection]]')
|
||||
|
@@ -10,6 +10,8 @@ type Params = {
|
||||
};
|
||||
|
||||
const getFlows = async (_parent: unknown, params: Params, context: Context) => {
|
||||
context.currentUser.can('read', 'Flow');
|
||||
|
||||
const flowsQuery = context.currentUser
|
||||
.$relatedQuery('flows')
|
||||
.joinRelated({
|
||||
|
76
packages/backend/src/graphql/queries/get-permissions.ee.ts
Normal file
76
packages/backend/src/graphql/queries/get-permissions.ee.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
const getPermissions = async () => {
|
||||
const Connection = {
|
||||
label: 'Connection',
|
||||
key: 'Connection',
|
||||
};
|
||||
|
||||
const Flow = {
|
||||
label: 'Flow',
|
||||
key: 'Flow',
|
||||
};
|
||||
|
||||
const Execution = {
|
||||
label: 'Execution',
|
||||
key: 'Execution',
|
||||
};
|
||||
|
||||
const permissions = {
|
||||
conditions: [
|
||||
{
|
||||
key: 'isCreator',
|
||||
label: 'Is creator'
|
||||
}
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
label: 'Create',
|
||||
action: 'create',
|
||||
subjects: [
|
||||
Connection.key,
|
||||
Flow.key,
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Read',
|
||||
action: 'read',
|
||||
subjects: [
|
||||
Connection.key,
|
||||
Execution.key,
|
||||
Flow.key,
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Update',
|
||||
action: 'update',
|
||||
subjects: [
|
||||
Connection.key,
|
||||
Flow.key,
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
action: 'delete',
|
||||
subjects: [
|
||||
Connection.key,
|
||||
Flow.key,
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Publish',
|
||||
action: 'publish',
|
||||
subjects: [
|
||||
Flow.key,
|
||||
]
|
||||
}
|
||||
],
|
||||
subjects: [
|
||||
Connection,
|
||||
Flow,
|
||||
Execution
|
||||
]
|
||||
};
|
||||
|
||||
return permissions;
|
||||
};
|
||||
|
||||
export default getPermissions;
|
13
packages/backend/src/graphql/queries/get-role.ee.ts
Normal file
13
packages/backend/src/graphql/queries/get-role.ee.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import Context from '../../types/express/context';
|
||||
import Role from '../../models/role';
|
||||
|
||||
type Params = {
|
||||
id: string
|
||||
};
|
||||
|
||||
// TODO: access
|
||||
const getRole = async (_parent: unknown, params: Params, context: Context) => {
|
||||
return await Role.query().findById(params.id).throwIfNotFound();
|
||||
};
|
||||
|
||||
export default getRole;
|
9
packages/backend/src/graphql/queries/get-roles.ee.ts
Normal file
9
packages/backend/src/graphql/queries/get-roles.ee.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import Context from '../../types/express/context';
|
||||
import Role from '../../models/role';
|
||||
|
||||
// TODO: access
|
||||
const getRoles = async (_parent: unknown, params: unknown, context: Context) => {
|
||||
return await Role.query();
|
||||
};
|
||||
|
||||
export default getRoles;
|
@@ -0,0 +1,9 @@
|
||||
import SamlAuthProvider from '../../models/saml-auth-provider.ee';
|
||||
|
||||
const getSamlAuthProviders = async () => {
|
||||
const providers = await SamlAuthProvider.query();
|
||||
|
||||
return providers;
|
||||
};
|
||||
|
||||
export default getSamlAuthProviders;
|
@@ -11,6 +11,8 @@ const getStepWithTestExecutions = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('update', 'Flow');
|
||||
|
||||
const step = await context.currentUser
|
||||
.$relatedQuery('steps')
|
||||
.findOne({ 'steps.id': params.stepId })
|
||||
|
22
packages/backend/src/graphql/queries/get-user.ts
Normal file
22
packages/backend/src/graphql/queries/get-user.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import Context from '../../types/express/context';
|
||||
import User from '../../models/user';
|
||||
|
||||
type Params = {
|
||||
id: string
|
||||
};
|
||||
|
||||
// TODO: access
|
||||
const getUser = async (_parent: unknown, params: Params, context: Context) => {
|
||||
return await User
|
||||
.query()
|
||||
.leftJoinRelated({
|
||||
role: true
|
||||
})
|
||||
.withGraphFetched({
|
||||
role: true
|
||||
})
|
||||
.findById(params.id)
|
||||
.throwIfNotFound();
|
||||
};
|
||||
|
||||
export default getUser;
|
25
packages/backend/src/graphql/queries/get-users.ts
Normal file
25
packages/backend/src/graphql/queries/get-users.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import Context from '../../types/express/context';
|
||||
import paginate from '../../helpers/pagination';
|
||||
import User from '../../models/user';
|
||||
|
||||
type Params = {
|
||||
limit: number;
|
||||
offset: number;
|
||||
};
|
||||
|
||||
// TODO: access
|
||||
const getUsers = async (_parent: unknown, params: Params, context: Context) => {
|
||||
const usersQuery = User
|
||||
.query()
|
||||
.leftJoinRelated({
|
||||
role: true
|
||||
})
|
||||
.withGraphFetched({
|
||||
role: true
|
||||
})
|
||||
.orderBy('full_name', 'desc');
|
||||
|
||||
return paginate(usersQuery, params.limit, params.offset);
|
||||
};
|
||||
|
||||
export default getUsers;
|
@@ -12,6 +12,8 @@ const testConnection = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('update', 'Connection');
|
||||
|
||||
let connection = await context.currentUser
|
||||
.$relatedQuery('connections')
|
||||
.findOne({
|
||||
|
@@ -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 getPermissions from './queries/get-permissions.ee';
|
||||
import getRole from './queries/get-role.ee';
|
||||
import getRoles from './queries/get-roles.ee';
|
||||
import getSamlAuthProviders from './queries/get-saml-auth-providers.ee';
|
||||
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,
|
||||
getPermissions,
|
||||
getRole,
|
||||
getRoles,
|
||||
getSamlAuthProviders,
|
||||
getStepWithTestExecutions,
|
||||
getSubscriptionStatus,
|
||||
getTrialStatus,
|
||||
getUser,
|
||||
getUsers,
|
||||
healthcheck,
|
||||
testConnection,
|
||||
};
|
||||
|
||||
export default queryResolvers;
|
||||
|
@@ -41,31 +41,46 @@ 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
|
||||
getPermissions: Permissions
|
||||
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
|
||||
}
|
||||
|
||||
"""
|
||||
@@ -277,6 +292,15 @@ type Execution {
|
||||
flow: Flow
|
||||
}
|
||||
|
||||
type UserConnection {
|
||||
edges: [UserEdge]
|
||||
pageInfo: PageInfo
|
||||
}
|
||||
|
||||
type UserEdge {
|
||||
node: User
|
||||
}
|
||||
|
||||
input CreateConnectionInput {
|
||||
key: String!
|
||||
formattedData: JSONObject!
|
||||
@@ -360,9 +384,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 +428,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 +522,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 +633,41 @@ type PaymentPlan {
|
||||
productId: String
|
||||
}
|
||||
|
||||
type GetSamlAuthProviders {
|
||||
id: String
|
||||
name: String
|
||||
issuer: String
|
||||
}
|
||||
|
||||
type Permission {
|
||||
action: String
|
||||
subject: String
|
||||
conditions: [String]
|
||||
}
|
||||
|
||||
# TODO: emphasize it's a catalog item
|
||||
type Permissions {
|
||||
actions: [Action]
|
||||
subjects: [Subject]
|
||||
conditions: [Condition]
|
||||
}
|
||||
|
||||
type Action {
|
||||
label: String
|
||||
action: 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);
|
||||
}
|
||||
);
|
||||
};
|
@@ -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;
|
26
packages/backend/src/models/permission.ts
Normal file
26
packages/backend/src/models/permission.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import Base from './base';
|
||||
|
||||
class Permission extends Base {
|
||||
id: string;
|
||||
action: string;
|
||||
subject: string;
|
||||
conditions: string[];
|
||||
|
||||
static tableName = 'permissions';
|
||||
|
||||
static jsonSchema = {
|
||||
type: 'object',
|
||||
required: ['action', 'subject'],
|
||||
|
||||
properties: {
|
||||
id: { 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() {
|
||||
|
61
packages/backend/src/models/role.ts
Normal file
61
packages/backend/src/models/role.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
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'], minLength: 1, 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.ManyToManyRelation,
|
||||
modelClass: Permission,
|
||||
join: {
|
||||
from: 'roles.id',
|
||||
through: {
|
||||
from: 'roles_permissions.role_id',
|
||||
to: 'roles_permissions.permission_id',
|
||||
},
|
||||
to: 'permissions.id',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
get isAdmin() {
|
||||
return this.key === 'admin';
|
||||
}
|
||||
}
|
||||
|
||||
export default Role;
|
79
packages/backend/src/models/saml-auth-provider.ee.ts
Normal file
79
packages/backend/src/models/saml-auth-provider.ee.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
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;
|
||||
|
||||
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' }
|
||||
},
|
||||
};
|
||||
|
||||
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,14 +1,20 @@
|
||||
import crypto from 'node:crypto';
|
||||
import { QueryContext, ModelOptions } from 'objection';
|
||||
import bcrypt from 'bcrypt';
|
||||
import crypto from 'crypto';
|
||||
import { DateTime } from 'luxon';
|
||||
import { PureAbility, fieldPatternMatcher, mongoQueryMatcher } from '@casl/ability';
|
||||
import type { Subject } from '@casl/ability';
|
||||
|
||||
import appConfig from '../config/app';
|
||||
import Base from './base';
|
||||
import ExtendedQueryBuilder from './query-builder';
|
||||
import Connection from './connection';
|
||||
import Flow from './flow';
|
||||
import Step from './step';
|
||||
import Role from './role';
|
||||
import Permission from './permission';
|
||||
import Execution from './execution';
|
||||
import Identity from './identity.ee';
|
||||
import UsageData from './usage-data.ee';
|
||||
import Subscription from './subscription.ee';
|
||||
|
||||
@@ -16,8 +22,8 @@ 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,34 @@ 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.ManyToManyRelation,
|
||||
modelClass: Permission,
|
||||
join: {
|
||||
from: 'users.role_id',
|
||||
through: {
|
||||
from: 'roles_permissions.role_id',
|
||||
to: 'roles_permissions.permission_id',
|
||||
},
|
||||
to: 'permissions.id',
|
||||
},
|
||||
},
|
||||
identities: {
|
||||
relation: Base.HasManyRelation,
|
||||
modelClass: Identity,
|
||||
join: {
|
||||
from: 'identities.user_id',
|
||||
to: 'users.id',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
login(password: string) {
|
||||
@@ -158,7 +201,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 +277,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 +291,34 @@ class User extends Base {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get ability() {
|
||||
if (!this.permissions) {
|
||||
throw new Error('User.permissions must be fetched!');
|
||||
}
|
||||
|
||||
// We're not using mongo, but our fields, conditions match
|
||||
return new PureAbility(this.permissions, {
|
||||
conditionsMatcher: mongoQueryMatcher,
|
||||
fieldMatcher: fieldPatternMatcher
|
||||
});
|
||||
}
|
||||
|
||||
can(action: string, subject: Subject) {
|
||||
const can = this.ability.can(action, subject);
|
||||
|
||||
if (!can) throw new Error('Not authorized!');
|
||||
|
||||
return can;
|
||||
}
|
||||
|
||||
cannot(action: string, subject: Subject) {
|
||||
const cannot = this.ability.cannot(action, subject);
|
||||
|
||||
if (cannot) throw new Error('Not authorized!');
|
||||
|
||||
return cannot;
|
||||
}
|
||||
}
|
||||
|
||||
export default User;
|
||||
|
23
packages/types/index.d.ts
vendored
23
packages/types/index.d.ts
vendored
@@ -95,6 +95,15 @@ export interface IUser {
|
||||
connections: IConnection[];
|
||||
flows: IFlow[];
|
||||
steps: IStep[];
|
||||
role: IRole;
|
||||
}
|
||||
|
||||
export interface IRole {
|
||||
id: string;
|
||||
key: string;
|
||||
name: string;
|
||||
description: string;
|
||||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
export interface IFieldDropdown {
|
||||
@@ -386,6 +395,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
|
||||
|
73
packages/web/src/adminSettingsRoutes.tsx
Normal file
73
packages/web/src/adminSettingsRoutes.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
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';
|
||||
|
||||
export default (
|
||||
<>
|
||||
<Route
|
||||
path={URLS.USERS}
|
||||
element={
|
||||
<AdminSettingsLayout>
|
||||
<Users />
|
||||
</AdminSettingsLayout>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={URLS.CREATE_USER}
|
||||
element={
|
||||
<AdminSettingsLayout>
|
||||
<CreateUser />
|
||||
</AdminSettingsLayout>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={URLS.USER_PATTERN}
|
||||
element={
|
||||
<AdminSettingsLayout>
|
||||
<EditUser />
|
||||
</AdminSettingsLayout>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={URLS.ROLES}
|
||||
element={
|
||||
<AdminSettingsLayout>
|
||||
<Roles />
|
||||
</AdminSettingsLayout>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={URLS.CREATE_ROLE}
|
||||
element={
|
||||
<AdminSettingsLayout>
|
||||
<CreateRole />
|
||||
</AdminSettingsLayout>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={URLS.ROLE_PATTERN}
|
||||
element={
|
||||
<AdminSettingsLayout>
|
||||
<EditRole />
|
||||
</AdminSettingsLayout>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={URLS.ADMIN_SETTINGS}
|
||||
element={<Navigate to={URLS.USERS} replace />}
|
||||
/>
|
||||
</>
|
||||
);
|
@@ -54,6 +54,10 @@ function AccountDropdownMenu(
|
||||
{formatMessage('accountDropdownMenu.settings')}
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem component={Link} to={URLS.ADMIN_SETTINGS_DASHBOARD}>
|
||||
{formatMessage('accountDropdownMenu.adminSettings')}
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem onClick={logout} data-test="logout-item">
|
||||
{formatMessage('accountDropdownMenu.logout')}
|
||||
</MenuItem>
|
||||
|
81
packages/web/src/components/AdminSettingsLayout/index.tsx
Normal file
81
packages/web/src/components/AdminSettingsLayout/index.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import GroupIcon from '@mui/icons-material/Group';
|
||||
import GroupsIcon from '@mui/icons-material/Groups';
|
||||
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
|
||||
|
||||
import * as URLS from 'config/urls';
|
||||
import useAutomatischInfo from 'hooks/useAutomatischInfo';
|
||||
import AppBar from 'components/AppBar';
|
||||
import Drawer from 'components/Drawer';
|
||||
|
||||
type SettingsLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
function createDrawerLinks({ isCloud }: { isCloud: boolean }) {
|
||||
const items = [
|
||||
{
|
||||
Icon: GroupIcon,
|
||||
primary: 'adminSettingsDrawer.users',
|
||||
to: URLS.USERS,
|
||||
},
|
||||
{
|
||||
Icon: GroupsIcon,
|
||||
primary: 'adminSettingsDrawer.roles',
|
||||
to: URLS.ROLES,
|
||||
}
|
||||
]
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
const drawerBottomLinks = [
|
||||
{
|
||||
Icon: ArrowBackIosNewIcon,
|
||||
primary: 'adminSettingsDrawer.goBack',
|
||||
to: '/',
|
||||
},
|
||||
];
|
||||
|
||||
export default function SettingsLayout({
|
||||
children,
|
||||
}: SettingsLayoutProps): React.ReactElement {
|
||||
const { isCloud } = useAutomatischInfo();
|
||||
const theme = useTheme();
|
||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'));
|
||||
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
|
||||
|
||||
const openDrawer = () => setDrawerOpen(true);
|
||||
const closeDrawer = () => setDrawerOpen(false);
|
||||
const drawerLinks = createDrawerLinks({ isCloud });
|
||||
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -19,6 +19,7 @@ export default function ConditionalIconButton(props: any): React.ReactElement {
|
||||
type={buttonProps.type}
|
||||
size={buttonProps.size}
|
||||
component={buttonProps.component}
|
||||
to={buttonProps.to}
|
||||
>
|
||||
{icon}
|
||||
</IconButton>
|
||||
|
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>
|
||||
);
|
||||
}
|
@@ -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')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
46
packages/web/src/components/DeleteRoleButton/index.ee.tsx
Normal file
46
packages/web/src/components/DeleteRoleButton/index.ee.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import * as React from 'react';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
|
||||
import ConfirmationDialog from 'components/ConfirmationDialog';
|
||||
import { DELETE_ROLE } from 'graphql/mutations/delete-role.ee';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
|
||||
type DeleteRoleButtonProps = {
|
||||
roleId: string;
|
||||
};
|
||||
|
||||
export default function DeleteRoleButton(props: DeleteRoleButtonProps) {
|
||||
const { roleId } = props;
|
||||
const [showConfirmation, setShowConfirmation] = React.useState(false);
|
||||
const [deleteRole] = useMutation(DELETE_ROLE, {
|
||||
variables: { input: { id: roleId } },
|
||||
refetchQueries: ['GetRoles'],
|
||||
});
|
||||
const formatMessage = useFormatMessage();
|
||||
|
||||
const handleConfirm = React.useCallback(async () => {
|
||||
await deleteRole();
|
||||
|
||||
setShowConfirmation(false);
|
||||
}, [deleteRole]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton onClick={() => setShowConfirmation(true)} size="small">
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
|
||||
<ConfirmationDialog
|
||||
open={showConfirmation}
|
||||
title={formatMessage('deleteRoleButton.title')}
|
||||
description={formatMessage('deleteRoleButton.description')}
|
||||
onClose={() => setShowConfirmation(false)}
|
||||
onConfirm={handleConfirm}
|
||||
cancelButtonChildren={formatMessage('deleteRoleButton.cancel')}
|
||||
confirmButtionChildren={formatMessage('deleteRoleButton.confirm')}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
46
packages/web/src/components/DeleteUserButton/index.ee.tsx
Normal file
46
packages/web/src/components/DeleteUserButton/index.ee.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import * as React from 'react';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
|
||||
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 handleConfirm = React.useCallback(async () => {
|
||||
await deleteUser();
|
||||
|
||||
setShowConfirmation(false);
|
||||
}, [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')}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
47
packages/web/src/components/ListLoader/index.tsx
Normal file
47
packages/web/src/components/ListLoader/index.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import {
|
||||
IconButton,
|
||||
Skeleton,
|
||||
Stack,
|
||||
TableCell,
|
||||
TableRow,
|
||||
} from '@mui/material';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
|
||||
type ListLoaderProps = {
|
||||
rowsNumber: number;
|
||||
cellNumber: number;
|
||||
};
|
||||
|
||||
const ListLoader = ({ rowsNumber, cellNumber }: ListLoaderProps) => {
|
||||
return (
|
||||
<>
|
||||
{[...Array(rowsNumber)].map((row, index) => (
|
||||
<TableRow
|
||||
key={index}
|
||||
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
|
||||
>
|
||||
{[...Array(cellNumber)].map((cell, index) => (
|
||||
<TableCell key={index} scope="row">
|
||||
<Skeleton />
|
||||
</TableCell>
|
||||
))}
|
||||
|
||||
<TableCell>
|
||||
<Stack direction="row" gap={1} justifyContent="right">
|
||||
<IconButton size="small">
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
|
||||
<IconButton size="small">
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListLoader;
|
91
packages/web/src/components/RoleList/index.ee.tsx
Normal file
91
packages/web/src/components/RoleList/index.ee.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
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 ListLoader from 'components/ListLoader';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import useRoles from 'hooks/useRoles.ee';
|
||||
import * as URLS from 'config/urls';
|
||||
|
||||
// TODO: introduce interaction feedback upon deletion (successful + failure)
|
||||
export default function RoleList(): React.ReactElement {
|
||||
const formatMessage = useFormatMessage();
|
||||
const { roles, loading } = 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>
|
||||
{loading ? (
|
||||
<ListLoader rowsNumber={3} cellNumber={2} />
|
||||
) : (
|
||||
roles.map((role) => (
|
||||
<TableRow
|
||||
key={role.id}
|
||||
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
|
||||
>
|
||||
<TableCell scope="row">
|
||||
<Typography variant="subtitle2">{role.name}</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell scope="row">
|
||||
<Typography variant="subtitle2">
|
||||
{role.description}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Stack direction="row" gap={1} justifyContent="right">
|
||||
<IconButton
|
||||
size="small"
|
||||
component={Link}
|
||||
to={URLS.ROLE(role.id)}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
|
||||
<DeleteRoleButton roleId={role.id} />
|
||||
</Stack>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
@@ -9,7 +9,7 @@ import { yupResolver } from '@hookform/resolvers/yup';
|
||||
|
||||
import useAuthentication from 'hooks/useAuthentication';
|
||||
import * as URLS from 'config/urls';
|
||||
import { CREATE_USER } from 'graphql/mutations/create-user.ee';
|
||||
import { REGISTER_USER } from 'graphql/mutations/register-user.ee';
|
||||
import Form from 'components/Form';
|
||||
import TextField from 'components/TextField';
|
||||
import { LOGIN } from 'graphql/mutations/login';
|
||||
@@ -40,7 +40,7 @@ function SignUpForm() {
|
||||
const navigate = useNavigate();
|
||||
const authentication = useAuthentication();
|
||||
const formatMessage = useFormatMessage();
|
||||
const [createUser, { loading: createUserLoading }] = useMutation(CREATE_USER);
|
||||
const [registerUser, { loading: registerUserLoading }] = useMutation(REGISTER_USER);
|
||||
const [login, { loading: loginLoading }] = useMutation(LOGIN);
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -51,7 +51,7 @@ function SignUpForm() {
|
||||
|
||||
const handleSubmit = async (values: any) => {
|
||||
const { fullName, email, password } = values;
|
||||
await createUser({
|
||||
await registerUser({
|
||||
variables: {
|
||||
input: { fullName, email, password },
|
||||
},
|
||||
@@ -165,7 +165,7 @@ function SignUpForm() {
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ boxShadow: 2, mt: 3 }}
|
||||
loading={createUserLoading || loginLoading}
|
||||
loading={registerUserLoading || loginLoading}
|
||||
fullWidth
|
||||
data-test="signUp-button"
|
||||
>
|
||||
|
39
packages/web/src/components/SsoProviders/index.ee.tsx
Normal file
39
packages/web/src/components/SsoProviders/index.ee.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import * as React from 'react';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Button from '@mui/material/Button';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Divider from '@mui/material/Divider';
|
||||
|
||||
import appConfig from 'config/app';
|
||||
import useSamlAuthProviders from 'hooks/useSamlAuthProviders.ee';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
|
||||
function SsoProviders() {
|
||||
const formatMessage = useFormatMessage();
|
||||
const { providers, loading } = useSamlAuthProviders();
|
||||
|
||||
if (!loading && providers.length === 0) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Divider>{formatMessage('loginPage.divider')}</Divider>
|
||||
|
||||
<Paper sx={{ px: 2, py: 4 }}>
|
||||
<Stack direction="column" gap={1}>
|
||||
{providers.map((provider) => (
|
||||
<Button
|
||||
key={provider.id}
|
||||
component="a"
|
||||
href={`${appConfig.apiUrl}/login/saml/${provider.issuer}`}
|
||||
variant="outlined"
|
||||
>
|
||||
{provider.name}
|
||||
</Button>
|
||||
))}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SsoProviders;
|
@@ -58,7 +58,7 @@ export default function UpgradeFreeTrial() {
|
||||
alignItems="stretch"
|
||||
>
|
||||
<TableContainer component={Paper}>
|
||||
<Table aria-label="simple table">
|
||||
<Table>
|
||||
<TableHead
|
||||
sx={{
|
||||
backgroundColor: (theme) =>
|
||||
|
90
packages/web/src/components/UserList/index.tsx
Normal file
90
packages/web/src/components/UserList/index.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
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 DeleteUserButton from 'components/DeleteUserButton/index.ee';
|
||||
import ListLoader from 'components/ListLoader';
|
||||
import useUsers from 'hooks/useUsers';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import * as URLS from 'config/urls';
|
||||
|
||||
// TODO: introduce translation entries
|
||||
// TODO: introduce interaction feedback upon deletion (successful + failure)
|
||||
export default function UserList(): React.ReactElement {
|
||||
const formatMessage = useFormatMessage();
|
||||
const { users, loading } = useUsers();
|
||||
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell component="th">
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{ color: 'text.secondary', fontWeight: 700 }}
|
||||
>
|
||||
{formatMessage('userList.fullName')}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell component="th">
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{ color: 'text.secondary', fontWeight: 700 }}
|
||||
>
|
||||
{formatMessage('userList.email')}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell component="th" />
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<ListLoader rowsNumber={3} cellNumber={2} />
|
||||
) : (
|
||||
users.map((user) => (
|
||||
<TableRow
|
||||
key={user.id}
|
||||
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
|
||||
>
|
||||
<TableCell scope="row">
|
||||
<Typography variant="subtitle2">{user.fullName}</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Typography variant="subtitle2">{user.email}</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Stack direction="row" gap={1} justifyContent="right">
|
||||
<IconButton
|
||||
size="small"
|
||||
component={Link}
|
||||
to={URLS.USER(user.id)}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
|
||||
<DeleteUserButton userId={user.id} />
|
||||
</Stack>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
@@ -1,13 +1,24 @@
|
||||
type Config = {
|
||||
[key: string]: string;
|
||||
baseUrl: string;
|
||||
apiUrl: string;
|
||||
graphqlUrl: string;
|
||||
notificationsUrl: string;
|
||||
chatwootBaseUrl: string;
|
||||
supportEmailAddress: string;
|
||||
};
|
||||
|
||||
const config: Config = {
|
||||
baseUrl: process.env.REACT_APP_BASE_URL as string,
|
||||
apiUrl: process.env.REACT_APP_API_URL as string,
|
||||
graphqlUrl: process.env.REACT_APP_GRAPHQL_URL as string,
|
||||
notificationsUrl: process.env.REACT_APP_NOTIFICATIONS_URL as string,
|
||||
chatwootBaseUrl: 'https://app.chatwoot.com',
|
||||
supportEmailAddress: 'support@automatisch.io'
|
||||
};
|
||||
|
||||
if (!config.apiUrl) {
|
||||
config.apiUrl = (new URL(config.graphqlUrl)).origin;
|
||||
}
|
||||
|
||||
export default config;
|
||||
|
@@ -1,35 +1,36 @@
|
||||
export const CONNECTIONS = '/connections';
|
||||
export const EXECUTIONS = '/executions';
|
||||
export const EXECUTION_PATTERN = '/executions/:executionId';
|
||||
export const EXECUTION = (executionId: string): string =>
|
||||
export const EXECUTION = (executionId: string) =>
|
||||
`/executions/${executionId}`;
|
||||
|
||||
export const LOGIN = '/login';
|
||||
export const LOGIN_CALLBACK = `${LOGIN}/callback`;
|
||||
export const SIGNUP = '/sign-up';
|
||||
export const FORGOT_PASSWORD = '/forgot-password';
|
||||
export const RESET_PASSWORD = '/reset-password';
|
||||
|
||||
export const APPS = '/apps';
|
||||
export const NEW_APP_CONNECTION = '/apps/new';
|
||||
export const APP = (appKey: string): string => `/app/${appKey}`;
|
||||
export const APP = (appKey: string) => `/app/${appKey}`;
|
||||
export const APP_PATTERN = '/app/:appKey';
|
||||
export const APP_CONNECTIONS = (appKey: string): string =>
|
||||
export const APP_CONNECTIONS = (appKey: string) =>
|
||||
`/app/${appKey}/connections`;
|
||||
export const APP_CONNECTIONS_PATTERN = '/app/:appKey/connections';
|
||||
export const APP_ADD_CONNECTION = (appKey: string): string =>
|
||||
export const APP_ADD_CONNECTION = (appKey: string) =>
|
||||
`/app/${appKey}/connections/add`;
|
||||
export const APP_ADD_CONNECTION_PATTERN = '/app/:appKey/connections/add';
|
||||
export const APP_RECONNECT_CONNECTION = (
|
||||
appKey: string,
|
||||
connectionId: string
|
||||
): string => `/app/${appKey}/connections/${connectionId}/reconnect`;
|
||||
) => `/app/${appKey}/connections/${connectionId}/reconnect`;
|
||||
export const APP_RECONNECT_CONNECTION_PATTERN =
|
||||
'/app/:appKey/connections/:connectionId/reconnect';
|
||||
export const APP_FLOWS = (appKey: string): string => `/app/${appKey}/flows`;
|
||||
export const APP_FLOWS = (appKey: string) => `/app/${appKey}/flows`;
|
||||
export const APP_FLOWS_FOR_CONNECTION = (
|
||||
appKey: string,
|
||||
connectionId: string
|
||||
): string => `/app/${appKey}/flows?connectionId=${connectionId}`;
|
||||
) => `/app/${appKey}/flows?connectionId=${connectionId}`;
|
||||
export const APP_FLOWS_PATTERN = '/app/:appKey/flows';
|
||||
|
||||
export const EDITOR = '/editor';
|
||||
@@ -54,11 +55,11 @@ export const CREATE_FLOW_WITH_APP_AND_CONNECTION = (
|
||||
|
||||
return `/editor/create?${searchParams}`;
|
||||
};
|
||||
export const FLOW_EDITOR = (flowId: string): string => `/editor/${flowId}`;
|
||||
export const FLOW_EDITOR = (flowId: string) => `/editor/${flowId}`;
|
||||
|
||||
export const FLOWS = '/flows';
|
||||
// TODO: revert this back to /flows/:flowId once we have a proper single flow page
|
||||
export const FLOW = (flowId: string): string => `/editor/${flowId}`;
|
||||
export const FLOW = (flowId: string) => `/editor/${flowId}`;
|
||||
export const FLOW_PATTERN = '/flows/:flowId';
|
||||
|
||||
export const SETTINGS = '/settings';
|
||||
@@ -71,6 +72,17 @@ export const SETTINGS_PROFILE = `${SETTINGS}/${PROFILE}`;
|
||||
export const SETTINGS_BILLING_AND_USAGE = `${SETTINGS}/${BILLING_AND_USAGE}`;
|
||||
export const SETTINGS_PLAN_UPGRADE = `${SETTINGS_BILLING_AND_USAGE}/${PLAN_UPGRADE}`;
|
||||
|
||||
export const ADMIN_SETTINGS = '/admin-settings';
|
||||
export const ADMIN_SETTINGS_DASHBOARD = ADMIN_SETTINGS;
|
||||
export const USERS = `${ADMIN_SETTINGS}/users`;
|
||||
export const USER = (userId: string) => `${USERS}/${userId}`;
|
||||
export const USER_PATTERN = `${USERS}/:userId`;
|
||||
export const CREATE_USER = `${USERS}/create`;
|
||||
export const ROLES = `${ADMIN_SETTINGS}/roles`;
|
||||
export const ROLE = (roleId: string) => `${ROLES}/${roleId}`;
|
||||
export const ROLE_PATTERN = `${ROLES}/:roleId`;
|
||||
export const CREATE_ROLE = `${ROLES}/create`;
|
||||
|
||||
export const DASHBOARD = FLOWS;
|
||||
|
||||
// External links
|
||||
|
12
packages/web/src/graphql/mutations/create-role.ee.ts
Normal file
12
packages/web/src/graphql/mutations/create-role.ee.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const CREATE_ROLE = gql`
|
||||
mutation CreateRole($input: CreateRoleInput) {
|
||||
createRole(input: $input) {
|
||||
id
|
||||
key
|
||||
name
|
||||
description
|
||||
}
|
||||
}
|
||||
`;
|
@@ -3,8 +3,12 @@ import { gql } from '@apollo/client';
|
||||
export const CREATE_USER = gql`
|
||||
mutation CreateUser($input: CreateUserInput) {
|
||||
createUser(input: $input) {
|
||||
id
|
||||
email
|
||||
fullName
|
||||
role {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@@ -0,0 +1,7 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const DELETE_CURRENT_USER = gql`
|
||||
mutation DeleteCurrentUser {
|
||||
deleteCurrentUser
|
||||
}
|
||||
`;
|
7
packages/web/src/graphql/mutations/delete-role.ee.ts
Normal file
7
packages/web/src/graphql/mutations/delete-role.ee.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const DELETE_ROLE = gql`
|
||||
mutation DeleteRole($input: DeleteRoleInput) {
|
||||
deleteRole(input: $input)
|
||||
}
|
||||
`;
|
@@ -1,7 +1,7 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const DELETE_USER = gql`
|
||||
mutation DeleteUser {
|
||||
deleteUser
|
||||
mutation DeleteUser($input: DeleteUserInput) {
|
||||
deleteUser(input: $input)
|
||||
}
|
||||
`;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user