Compare commits
13 Commits
AUT-740
...
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'
|
- '6379:6379'
|
||||||
expose:
|
expose:
|
||||||
- 6379
|
- 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:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
redis_data:
|
redis_data:
|
||||||
|
keycloak:
|
||||||
|
@@ -2,18 +2,55 @@ import appConfig from '../../src/config/app';
|
|||||||
import logger from '../../src/helpers/logger';
|
import logger from '../../src/helpers/logger';
|
||||||
import client from './client';
|
import client from './client';
|
||||||
import User from '../../src/models/user';
|
import User from '../../src/models/user';
|
||||||
|
import Role from '../../src/models/role';
|
||||||
|
import Permission from '../../src/models/permission';
|
||||||
import '../../src/config/orm';
|
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(
|
export async function createUser(
|
||||||
email = 'user@automatisch.io',
|
email = 'user@automatisch.io',
|
||||||
password = 'sample'
|
password = 'sample'
|
||||||
) {
|
) {
|
||||||
const UNIQUE_VIOLATION_CODE = '23505';
|
const UNIQUE_VIOLATION_CODE = '23505';
|
||||||
|
|
||||||
|
await seedPermissionsIfNeeded();
|
||||||
|
|
||||||
|
const role = await createOrFetchRole();
|
||||||
const userParams = {
|
const userParams = {
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
fullName: 'Initial admin',
|
fullName: 'Initial admin',
|
||||||
role: 'admin',
|
roleId: role.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@@ -12,6 +12,7 @@ const knexConfig = {
|
|||||||
database: appConfig.postgresDatabase,
|
database: appConfig.postgresDatabase,
|
||||||
ssl: appConfig.postgresEnableSsl,
|
ssl: appConfig.postgresEnableSsl,
|
||||||
},
|
},
|
||||||
|
asyncStackTraces: appConfig.isDev,
|
||||||
searchPath: [appConfig.postgresSchema],
|
searchPath: [appConfig.postgresSchema],
|
||||||
pool: { min: 0, max: 20 },
|
pool: { min: 0, max: 20 },
|
||||||
migrations: {
|
migrations: {
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
"license": "See LICENSE file",
|
"license": "See LICENSE file",
|
||||||
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"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",
|
"worker": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/worker.ts",
|
||||||
"build": "tsc && yarn copy-statics",
|
"build": "tsc && yarn copy-statics",
|
||||||
"build:watch": "nodemon --watch 'src/**/*.ts' --watch 'bin/**/*.ts' --exec yarn build --ext ts",
|
"build:watch": "nodemon --watch 'src/**/*.ts' --watch 'bin/**/*.ts' --exec yarn build --ext ts",
|
||||||
@@ -24,12 +24,15 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@automatisch/web": "^0.7.1",
|
"@automatisch/web": "^0.7.1",
|
||||||
"@bull-board/express": "^3.10.1",
|
"@bull-board/express": "^3.10.1",
|
||||||
|
"@casl/ability": "^6.5.0",
|
||||||
"@graphql-tools/graphql-file-loader": "^7.3.4",
|
"@graphql-tools/graphql-file-loader": "^7.3.4",
|
||||||
"@graphql-tools/load": "^7.5.2",
|
"@graphql-tools/load": "^7.5.2",
|
||||||
|
"@node-saml/passport-saml": "^4.0.4",
|
||||||
"@rudderstack/rudder-sdk-node": "^1.1.2",
|
"@rudderstack/rudder-sdk-node": "^1.1.2",
|
||||||
"@sentry/node": "^7.42.0",
|
"@sentry/node": "^7.42.0",
|
||||||
"@sentry/tracing": "^7.42.0",
|
"@sentry/tracing": "^7.42.0",
|
||||||
"@types/luxon": "^2.3.1",
|
"@types/luxon": "^2.3.1",
|
||||||
|
"@types/passport": "^1.0.12",
|
||||||
"@types/xmlrpc": "^1.3.7",
|
"@types/xmlrpc": "^1.3.7",
|
||||||
"ajv-formats": "^2.1.1",
|
"ajv-formats": "^2.1.1",
|
||||||
"axios": "0.24.0",
|
"axios": "0.24.0",
|
||||||
@@ -60,6 +63,7 @@
|
|||||||
"nodemailer": "6.7.0",
|
"nodemailer": "6.7.0",
|
||||||
"oauth-1.0a": "^2.2.6",
|
"oauth-1.0a": "^2.2.6",
|
||||||
"objection": "^3.0.0",
|
"objection": "^3.0.0",
|
||||||
|
"passport": "^0.6.0",
|
||||||
"pg": "^8.7.1",
|
"pg": "^8.7.1",
|
||||||
"php-serialize": "^4.0.2",
|
"php-serialize": "^4.0.2",
|
||||||
"stripe": "^11.13.0",
|
"stripe": "^11.13.0",
|
||||||
|
@@ -17,6 +17,7 @@ import {
|
|||||||
} from './helpers/create-bull-board-handler';
|
} from './helpers/create-bull-board-handler';
|
||||||
import injectBullBoardHandler from './helpers/inject-bull-board-handler';
|
import injectBullBoardHandler from './helpers/inject-bull-board-handler';
|
||||||
import router from './routes';
|
import router from './routes';
|
||||||
|
import configurePassport from './helpers/passport';
|
||||||
|
|
||||||
createBullBoardHandler(serverAdapter);
|
createBullBoardHandler(serverAdapter);
|
||||||
|
|
||||||
@@ -50,6 +51,9 @@ app.use(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
app.use(cors(corsOptions));
|
app.use(cors(corsOptions));
|
||||||
|
|
||||||
|
configurePassport(app);
|
||||||
|
|
||||||
app.use('/', router);
|
app.use('/', router);
|
||||||
|
|
||||||
webUIHandler(app);
|
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 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 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 updateFlow from './mutations/update-flow';
|
||||||
import updateFlowStatus from './mutations/update-flow-status';
|
import updateFlowStatus from './mutations/update-flow-status';
|
||||||
import executeFlow from './mutations/execute-flow';
|
import updateRole from './mutations/update-role.ee';
|
||||||
import deleteFlow from './mutations/delete-flow';
|
|
||||||
import duplicateFlow from './mutations/duplicate-flow';
|
|
||||||
import createStep from './mutations/create-step';
|
|
||||||
import updateStep from './mutations/update-step';
|
import updateStep from './mutations/update-step';
|
||||||
import deleteStep from './mutations/delete-step';
|
import updateUser from './mutations/update-user.ee';
|
||||||
import createUser from './mutations/create-user.ee';
|
import verifyConnection from './mutations/verify-connection';
|
||||||
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';
|
|
||||||
|
|
||||||
const mutationResolvers = {
|
const mutationResolvers = {
|
||||||
createConnection,
|
createConnection,
|
||||||
generateAuthUrl,
|
|
||||||
updateConnection,
|
|
||||||
resetConnection,
|
|
||||||
verifyConnection,
|
|
||||||
deleteConnection,
|
|
||||||
createFlow,
|
createFlow,
|
||||||
|
createRole,
|
||||||
|
createStep,
|
||||||
|
createUser,
|
||||||
|
deleteConnection,
|
||||||
|
deleteCurrentUser,
|
||||||
|
deleteFlow,
|
||||||
|
deleteRole,
|
||||||
|
deleteStep,
|
||||||
|
deleteUser,
|
||||||
|
duplicateFlow,
|
||||||
|
executeFlow,
|
||||||
|
forgotPassword,
|
||||||
|
generateAuthUrl,
|
||||||
|
login,
|
||||||
|
registerUser,
|
||||||
|
resetConnection,
|
||||||
|
resetPassword,
|
||||||
|
updateConnection,
|
||||||
|
updateCurrentUser,
|
||||||
|
updateUser,
|
||||||
updateFlow,
|
updateFlow,
|
||||||
updateFlowStatus,
|
updateFlowStatus,
|
||||||
executeFlow,
|
updateRole,
|
||||||
deleteFlow,
|
|
||||||
duplicateFlow,
|
|
||||||
createStep,
|
|
||||||
updateStep,
|
updateStep,
|
||||||
deleteStep,
|
verifyConnection,
|
||||||
createUser,
|
|
||||||
deleteUser,
|
|
||||||
updateUser,
|
|
||||||
forgotPassword,
|
|
||||||
resetPassword,
|
|
||||||
login,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default mutationResolvers;
|
export default mutationResolvers;
|
||||||
|
@@ -13,6 +13,8 @@ const createConnection = async (
|
|||||||
params: Params,
|
params: Params,
|
||||||
context: Context
|
context: Context
|
||||||
) => {
|
) => {
|
||||||
|
context.currentUser.can('create', 'Connection');
|
||||||
|
|
||||||
await App.findOneByKey(params.input.key);
|
await App.findOneByKey(params.input.key);
|
||||||
|
|
||||||
return await context.currentUser.$relatedQuery('connections').insert({
|
return await context.currentUser.$relatedQuery('connections').insert({
|
||||||
|
@@ -14,6 +14,8 @@ const createFlow = async (
|
|||||||
params: Params,
|
params: Params,
|
||||||
context: Context
|
context: Context
|
||||||
) => {
|
) => {
|
||||||
|
context.currentUser.can('create', 'Flow');
|
||||||
|
|
||||||
const connectionId = params?.input?.connectionId;
|
const connectionId = params?.input?.connectionId;
|
||||||
const appKey = params?.input?.triggerAppKey;
|
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,
|
params: Params,
|
||||||
context: Context
|
context: Context
|
||||||
) => {
|
) => {
|
||||||
|
context.currentUser.can('update', 'Flow');
|
||||||
|
|
||||||
const { input } = params;
|
const { input } = params;
|
||||||
|
|
||||||
if (input.appKey && input.key) {
|
if (input.appKey && input.key) {
|
||||||
|
@@ -5,11 +5,13 @@ type Params = {
|
|||||||
fullName: string;
|
fullName: string;
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
roleId: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: access
|
||||||
const createUser = async (_parent: unknown, params: Params) => {
|
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 });
|
const existingUser = await User.query().findOne({ email });
|
||||||
|
|
||||||
@@ -21,7 +23,7 @@ const createUser = async (_parent: unknown, params: Params) => {
|
|||||||
fullName,
|
fullName,
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
role: 'user',
|
roleId,
|
||||||
});
|
});
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
|
@@ -11,6 +11,8 @@ const deleteConnection = async (
|
|||||||
params: Params,
|
params: Params,
|
||||||
context: Context
|
context: Context
|
||||||
) => {
|
) => {
|
||||||
|
context.currentUser.can('delete', 'Connection');
|
||||||
|
|
||||||
await context.currentUser
|
await context.currentUser
|
||||||
.$relatedQuery('connections')
|
.$relatedQuery('connections')
|
||||||
.delete()
|
.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,
|
params: Params,
|
||||||
context: Context
|
context: Context
|
||||||
) => {
|
) => {
|
||||||
|
context.currentUser.can('delete', 'Flow');
|
||||||
|
|
||||||
const flow = await context.currentUser
|
const flow = await context.currentUser
|
||||||
.$relatedQuery('flows')
|
.$relatedQuery('flows')
|
||||||
.findOne({
|
.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,
|
params: Params,
|
||||||
context: Context
|
context: Context
|
||||||
) => {
|
) => {
|
||||||
|
context.currentUser.can('update', 'Flow');
|
||||||
|
|
||||||
const step = await context.currentUser
|
const step = await context.currentUser
|
||||||
.$relatedQuery('steps')
|
.$relatedQuery('steps')
|
||||||
.withGraphFetched('flow')
|
.withGraphFetched('flow')
|
||||||
|
@@ -1,11 +1,23 @@
|
|||||||
import Context from '../../types/express/context';
|
|
||||||
import deleteUserQueue from '../../queues/delete-user.ee';
|
|
||||||
import { Duration } from 'luxon';
|
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) => {
|
type Params = {
|
||||||
const id = context.currentUser.id;
|
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 jobName = `Delete user - ${id}`;
|
||||||
const jobPayload = { id };
|
const jobPayload = { id };
|
||||||
|
@@ -53,6 +53,8 @@ const duplicateFlow = async (
|
|||||||
params: Params,
|
params: Params,
|
||||||
context: Context
|
context: Context
|
||||||
) => {
|
) => {
|
||||||
|
context.currentUser.can('create', 'Flow');
|
||||||
|
|
||||||
const flow = await context.currentUser
|
const flow = await context.currentUser
|
||||||
.$relatedQuery('flows')
|
.$relatedQuery('flows')
|
||||||
.withGraphJoined('[steps]')
|
.withGraphJoined('[steps]')
|
||||||
|
@@ -12,6 +12,8 @@ const executeFlow = async (
|
|||||||
params: Params,
|
params: Params,
|
||||||
context: Context
|
context: Context
|
||||||
) => {
|
) => {
|
||||||
|
context.currentUser.can('update', 'Flow');
|
||||||
|
|
||||||
const { stepId } = params.input;
|
const { stepId } = params.input;
|
||||||
|
|
||||||
const untilStep = await context.currentUser
|
const untilStep = await context.currentUser
|
||||||
|
@@ -13,6 +13,8 @@ const generateAuthUrl = async (
|
|||||||
params: Params,
|
params: Params,
|
||||||
context: Context
|
context: Context
|
||||||
) => {
|
) => {
|
||||||
|
context.currentUser.can('create', 'Connection');
|
||||||
|
|
||||||
const connection = await context.currentUser
|
const connection = await context.currentUser
|
||||||
.$relatedQuery('connections')
|
.$relatedQuery('connections')
|
||||||
.findOne({
|
.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,
|
params: Params,
|
||||||
context: Context
|
context: Context
|
||||||
) => {
|
) => {
|
||||||
|
context.currentUser.can('create', 'Connection');
|
||||||
|
|
||||||
let connection = await context.currentUser
|
let connection = await context.currentUser
|
||||||
.$relatedQuery('connections')
|
.$relatedQuery('connections')
|
||||||
.findOne({
|
.findOne({
|
||||||
|
@@ -13,6 +13,8 @@ const updateConnection = async (
|
|||||||
params: Params,
|
params: Params,
|
||||||
context: Context
|
context: Context
|
||||||
) => {
|
) => {
|
||||||
|
context.currentUser.can('create', 'Connection');
|
||||||
|
|
||||||
let connection = await context.currentUser
|
let connection = await context.currentUser
|
||||||
.$relatedQuery('connections')
|
.$relatedQuery('connections')
|
||||||
.findOne({
|
.findOne({
|
||||||
|
@@ -8,7 +8,7 @@ type Params = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateUser = async (
|
const updateCurrentUser = async (
|
||||||
_parent: unknown,
|
_parent: unknown,
|
||||||
params: Params,
|
params: Params,
|
||||||
context: Context
|
context: Context
|
||||||
@@ -22,4 +22,4 @@ const updateUser = async (
|
|||||||
return user;
|
return user;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default updateUser;
|
export default updateCurrentUser;
|
@@ -18,6 +18,8 @@ const updateFlowStatus = async (
|
|||||||
params: Params,
|
params: Params,
|
||||||
context: Context
|
context: Context
|
||||||
) => {
|
) => {
|
||||||
|
context.currentUser.can('publish', 'Flow');
|
||||||
|
|
||||||
let flow = await context.currentUser
|
let flow = await context.currentUser
|
||||||
.$relatedQuery('flows')
|
.$relatedQuery('flows')
|
||||||
.findOne({
|
.findOne({
|
||||||
@@ -55,7 +57,7 @@ const updateFlowStatus = async (
|
|||||||
} else {
|
} else {
|
||||||
if (newActiveValue) {
|
if (newActiveValue) {
|
||||||
flow = await flow.$query().patchAndFetch({
|
flow = await flow.$query().patchAndFetch({
|
||||||
published_at: new Date().toISOString(),
|
publishedAt: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const jobName = `${JOB_NAME}-${flow.id}`;
|
const jobName = `${JOB_NAME}-${flow.id}`;
|
||||||
@@ -78,9 +80,12 @@ const updateFlowStatus = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flow = await flow.$query().withGraphFetched('steps').patchAndFetch({
|
flow = await flow
|
||||||
active: newActiveValue,
|
.$query()
|
||||||
});
|
.withGraphFetched('steps')
|
||||||
|
.patchAndFetch({
|
||||||
|
active: newActiveValue,
|
||||||
|
});
|
||||||
|
|
||||||
return flow;
|
return flow;
|
||||||
};
|
};
|
||||||
|
@@ -12,6 +12,8 @@ const updateFlow = async (
|
|||||||
params: Params,
|
params: Params,
|
||||||
context: Context
|
context: Context
|
||||||
) => {
|
) => {
|
||||||
|
context.currentUser.can('update', 'Flow');
|
||||||
|
|
||||||
let flow = await context.currentUser
|
let flow = await context.currentUser
|
||||||
.$relatedQuery('flows')
|
.$relatedQuery('flows')
|
||||||
.findOne({
|
.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,
|
params: Params,
|
||||||
context: Context
|
context: Context
|
||||||
) => {
|
) => {
|
||||||
|
context.currentUser.can('update', 'Flow');
|
||||||
|
|
||||||
const { input } = params;
|
const { input } = params;
|
||||||
|
|
||||||
let step = await context.currentUser
|
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,
|
params: Params,
|
||||||
context: Context
|
context: Context
|
||||||
) => {
|
) => {
|
||||||
|
context.currentUser.can('create', 'Connection');
|
||||||
|
|
||||||
let connection = await context.currentUser
|
let connection = await context.currentUser
|
||||||
.$relatedQuery('connections')
|
.$relatedQuery('connections')
|
||||||
.findOne({
|
.findOne({
|
||||||
|
@@ -6,6 +6,8 @@ type Params = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getApp = async (_parent: unknown, params: Params, context: Context) => {
|
const getApp = async (_parent: unknown, params: Params, context: Context) => {
|
||||||
|
context.currentUser.can('read', 'Connection');
|
||||||
|
|
||||||
const app = await App.findOneByKey(params.key);
|
const app = await App.findOneByKey(params.key);
|
||||||
|
|
||||||
if (context.currentUser) {
|
if (context.currentUser) {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import App from '../../models/app';
|
|
||||||
import { IApp } from '@automatisch/types';
|
import { IApp } from '@automatisch/types';
|
||||||
|
import App from '../../models/app';
|
||||||
|
|
||||||
type Params = {
|
type Params = {
|
||||||
name: string;
|
name: string;
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
|
import { IConnection } from '@automatisch/types';
|
||||||
import App from '../../models/app';
|
import App from '../../models/app';
|
||||||
import Context from '../../types/express/context';
|
import Context from '../../types/express/context';
|
||||||
import { IApp, IConnection } from '@automatisch/types';
|
|
||||||
|
|
||||||
type Params = {
|
type Params = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -11,6 +11,8 @@ const getConnectedApps = async (
|
|||||||
params: Params,
|
params: Params,
|
||||||
context: Context
|
context: Context
|
||||||
) => {
|
) => {
|
||||||
|
context.currentUser.can('read', 'Connection');
|
||||||
|
|
||||||
let apps = await App.findAll(params.name);
|
let apps = await App.findAll(params.name);
|
||||||
|
|
||||||
const connections = await context.currentUser
|
const connections = await context.currentUser
|
||||||
|
@@ -16,6 +16,8 @@ const getDynamicData = async (
|
|||||||
params: Params,
|
params: Params,
|
||||||
context: Context
|
context: Context
|
||||||
) => {
|
) => {
|
||||||
|
context.currentUser.can('update', 'Flow');
|
||||||
|
|
||||||
const step = await context.currentUser
|
const step = await context.currentUser
|
||||||
.$relatedQuery('steps')
|
.$relatedQuery('steps')
|
||||||
.withGraphFetched({
|
.withGraphFetched({
|
||||||
|
@@ -14,6 +14,8 @@ const getDynamicFields = async (
|
|||||||
params: Params,
|
params: Params,
|
||||||
context: Context
|
context: Context
|
||||||
) => {
|
) => {
|
||||||
|
context.currentUser.can('update', 'Flow');
|
||||||
|
|
||||||
const step = await context.currentUser
|
const step = await context.currentUser
|
||||||
.$relatedQuery('steps')
|
.$relatedQuery('steps')
|
||||||
.withGraphFetched({
|
.withGraphFetched({
|
||||||
|
@@ -12,6 +12,8 @@ const getExecutionSteps = async (
|
|||||||
params: Params,
|
params: Params,
|
||||||
context: Context
|
context: Context
|
||||||
) => {
|
) => {
|
||||||
|
context.currentUser.can('read', 'Execution');
|
||||||
|
|
||||||
const execution = await context.currentUser
|
const execution = await context.currentUser
|
||||||
.$relatedQuery('executions')
|
.$relatedQuery('executions')
|
||||||
.withSoftDeleted()
|
.withSoftDeleted()
|
||||||
|
@@ -9,6 +9,8 @@ const getExecution = async (
|
|||||||
params: Params,
|
params: Params,
|
||||||
context: Context
|
context: Context
|
||||||
) => {
|
) => {
|
||||||
|
context.currentUser.can('read', 'Execution');
|
||||||
|
|
||||||
const execution = await context.currentUser
|
const execution = await context.currentUser
|
||||||
.$relatedQuery('executions')
|
.$relatedQuery('executions')
|
||||||
.withGraphFetched({
|
.withGraphFetched({
|
||||||
|
@@ -12,6 +12,8 @@ const getExecutions = async (
|
|||||||
params: Params,
|
params: Params,
|
||||||
context: Context
|
context: Context
|
||||||
) => {
|
) => {
|
||||||
|
context.currentUser.can('read', 'Execution');
|
||||||
|
|
||||||
const selectStatusStatement = `
|
const selectStatusStatement = `
|
||||||
case
|
case
|
||||||
when count(*) filter (where execution_steps.status = 'failure') > 0
|
when count(*) filter (where execution_steps.status = 'failure') > 0
|
||||||
|
@@ -5,6 +5,8 @@ type Params = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getFlow = async (_parent: unknown, params: Params, context: Context) => {
|
const getFlow = async (_parent: unknown, params: Params, context: Context) => {
|
||||||
|
context.currentUser.can('read', 'Flow');
|
||||||
|
|
||||||
const flow = await context.currentUser
|
const flow = await context.currentUser
|
||||||
.$relatedQuery('flows')
|
.$relatedQuery('flows')
|
||||||
.withGraphJoined('[steps.[connection]]')
|
.withGraphJoined('[steps.[connection]]')
|
||||||
|
@@ -10,6 +10,8 @@ type Params = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getFlows = async (_parent: unknown, params: Params, context: Context) => {
|
const getFlows = async (_parent: unknown, params: Params, context: Context) => {
|
||||||
|
context.currentUser.can('read', 'Flow');
|
||||||
|
|
||||||
const flowsQuery = context.currentUser
|
const flowsQuery = context.currentUser
|
||||||
.$relatedQuery('flows')
|
.$relatedQuery('flows')
|
||||||
.joinRelated({
|
.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,
|
params: Params,
|
||||||
context: Context
|
context: Context
|
||||||
) => {
|
) => {
|
||||||
|
context.currentUser.can('update', 'Flow');
|
||||||
|
|
||||||
const step = await context.currentUser
|
const step = await context.currentUser
|
||||||
.$relatedQuery('steps')
|
.$relatedQuery('steps')
|
||||||
.findOne({ 'steps.id': params.stepId })
|
.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,
|
params: Params,
|
||||||
context: Context
|
context: Context
|
||||||
) => {
|
) => {
|
||||||
|
context.currentUser.can('update', 'Connection');
|
||||||
|
|
||||||
let connection = await context.currentUser
|
let connection = await context.currentUser
|
||||||
.$relatedQuery('connections')
|
.$relatedQuery('connections')
|
||||||
.findOne({
|
.findOne({
|
||||||
|
@@ -1,47 +1,59 @@
|
|||||||
import getApps from './queries/get-apps';
|
|
||||||
import getApp from './queries/get-app';
|
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 getConnectedApps from './queries/get-connected-apps';
|
||||||
import testConnection from './queries/test-connection';
|
import getCurrentUser from './queries/get-current-user';
|
||||||
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 getDynamicData from './queries/get-dynamic-data';
|
import getDynamicData from './queries/get-dynamic-data';
|
||||||
import getDynamicFields from './queries/get-dynamic-fields';
|
import getDynamicFields from './queries/get-dynamic-fields';
|
||||||
import getCurrentUser from './queries/get-current-user';
|
import getExecution from './queries/get-execution';
|
||||||
import getPaymentPlans from './queries/get-payment-plans.ee';
|
import getExecutionSteps from './queries/get-execution-steps';
|
||||||
import getPaddleInfo from './queries/get-paddle-info.ee';
|
import getExecutions from './queries/get-executions';
|
||||||
import getBillingAndUsage from './queries/get-billing-and-usage.ee';
|
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 getInvoices from './queries/get-invoices.ee';
|
||||||
import getAutomatischInfo from './queries/get-automatisch-info';
|
import getPaddleInfo from './queries/get-paddle-info.ee';
|
||||||
import getTrialStatus from './queries/get-trial-status.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 getSubscriptionStatus from './queries/get-subscription-status.ee';
|
||||||
|
import getTrialStatus from './queries/get-trial-status.ee';
|
||||||
import healthcheck from './queries/healthcheck';
|
import healthcheck from './queries/healthcheck';
|
||||||
|
import testConnection from './queries/test-connection';
|
||||||
|
|
||||||
const queryResolvers = {
|
const queryResolvers = {
|
||||||
getApps,
|
|
||||||
getApp,
|
getApp,
|
||||||
|
getApps,
|
||||||
|
getAutomatischInfo,
|
||||||
|
getBillingAndUsage,
|
||||||
getConnectedApps,
|
getConnectedApps,
|
||||||
testConnection,
|
getCurrentUser,
|
||||||
getFlow,
|
getDynamicData,
|
||||||
getFlows,
|
getDynamicFields,
|
||||||
getStepWithTestExecutions,
|
|
||||||
getExecution,
|
getExecution,
|
||||||
getExecutions,
|
getExecutions,
|
||||||
getExecutionSteps,
|
getExecutionSteps,
|
||||||
getDynamicData,
|
getFlow,
|
||||||
getDynamicFields,
|
getFlows,
|
||||||
getCurrentUser,
|
|
||||||
getPaymentPlans,
|
|
||||||
getPaddleInfo,
|
|
||||||
getBillingAndUsage,
|
|
||||||
getInvoices,
|
getInvoices,
|
||||||
getAutomatischInfo,
|
getPaddleInfo,
|
||||||
getTrialStatus,
|
getPaymentPlans,
|
||||||
|
getPermissions,
|
||||||
|
getRole,
|
||||||
|
getRoles,
|
||||||
|
getSamlAuthProviders,
|
||||||
|
getStepWithTestExecutions,
|
||||||
getSubscriptionStatus,
|
getSubscriptionStatus,
|
||||||
|
getTrialStatus,
|
||||||
|
getUser,
|
||||||
|
getUsers,
|
||||||
healthcheck,
|
healthcheck,
|
||||||
|
testConnection,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default queryResolvers;
|
export default queryResolvers;
|
||||||
|
@@ -41,31 +41,46 @@ type Query {
|
|||||||
getAutomatischInfo: GetAutomatischInfo
|
getAutomatischInfo: GetAutomatischInfo
|
||||||
getTrialStatus: GetTrialStatus
|
getTrialStatus: GetTrialStatus
|
||||||
getSubscriptionStatus: GetSubscriptionStatus
|
getSubscriptionStatus: GetSubscriptionStatus
|
||||||
|
getSamlAuthProviders: [GetSamlAuthProviders]
|
||||||
|
getUsers(
|
||||||
|
limit: Int!
|
||||||
|
offset: Int!
|
||||||
|
): UserConnection
|
||||||
|
getUser(id: String!): User
|
||||||
|
getRoles: [Role]
|
||||||
|
getRole(id: String!): Role
|
||||||
|
getPermissions: Permissions
|
||||||
healthcheck: AppHealth
|
healthcheck: AppHealth
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
createConnection(input: CreateConnectionInput): Connection
|
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
|
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
|
updateFlow(input: UpdateFlowInput): Flow
|
||||||
updateFlowStatus(input: UpdateFlowStatusInput): Flow
|
updateFlowStatus(input: UpdateFlowStatusInput): Flow
|
||||||
executeFlow(input: ExecuteFlowInput): executeFlowType
|
updateRole(input: UpdateRoleInput): Role
|
||||||
deleteFlow(input: DeleteFlowInput): Boolean
|
|
||||||
duplicateFlow(input: DuplicateFlowInput): Flow
|
|
||||||
createStep(input: CreateStepInput): Step
|
|
||||||
updateStep(input: UpdateStepInput): Step
|
updateStep(input: UpdateStepInput): Step
|
||||||
deleteStep(input: DeleteStepInput): Step
|
|
||||||
createUser(input: CreateUserInput): User
|
|
||||||
deleteUser: Boolean
|
|
||||||
updateUser(input: UpdateUserInput): User
|
updateUser(input: UpdateUserInput): User
|
||||||
forgotPassword(input: ForgotPasswordInput): Boolean
|
verifyConnection(input: VerifyConnectionInput): Connection
|
||||||
resetPassword(input: ResetPasswordInput): Boolean
|
|
||||||
login(input: LoginInput): Auth
|
|
||||||
}
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -277,6 +292,15 @@ type Execution {
|
|||||||
flow: Flow
|
flow: Flow
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UserConnection {
|
||||||
|
edges: [UserEdge]
|
||||||
|
pageInfo: PageInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserEdge {
|
||||||
|
node: User
|
||||||
|
}
|
||||||
|
|
||||||
input CreateConnectionInput {
|
input CreateConnectionInput {
|
||||||
key: String!
|
key: String!
|
||||||
formattedData: JSONObject!
|
formattedData: JSONObject!
|
||||||
@@ -360,9 +384,31 @@ input CreateUserInput {
|
|||||||
fullName: String!
|
fullName: String!
|
||||||
email: String!
|
email: String!
|
||||||
password: String!
|
password: String!
|
||||||
|
role: UserRoleInput!
|
||||||
|
}
|
||||||
|
|
||||||
|
input UserRoleInput {
|
||||||
|
id: String
|
||||||
}
|
}
|
||||||
|
|
||||||
input UpdateUserInput {
|
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
|
email: String
|
||||||
password: String
|
password: String
|
||||||
fullName: String
|
fullName: String
|
||||||
@@ -382,6 +428,29 @@ input LoginInput {
|
|||||||
password: String!
|
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).
|
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
|
id: String
|
||||||
fullName: String
|
fullName: String
|
||||||
email: String
|
email: String
|
||||||
role: String
|
role: Role
|
||||||
|
permissions: [Permission]
|
||||||
createdAt: String
|
createdAt: String
|
||||||
updatedAt: String
|
updatedAt: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Role {
|
||||||
|
id: String
|
||||||
|
name: String
|
||||||
|
key: String
|
||||||
|
description: String
|
||||||
|
isAdmin: Boolean
|
||||||
|
permissions: [Permission]
|
||||||
|
}
|
||||||
|
|
||||||
type PageInfo {
|
type PageInfo {
|
||||||
currentPage: Int!
|
currentPage: Int!
|
||||||
totalPages: Int!
|
totalPages: Int!
|
||||||
@@ -554,6 +633,41 @@ type PaymentPlan {
|
|||||||
productId: String
|
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 {
|
schema {
|
||||||
query: Query
|
query: Query
|
||||||
mutation: Mutation
|
mutation: Mutation
|
||||||
|
@@ -12,7 +12,17 @@ const isAuthenticated = rule()(async (_parent, _args, req) => {
|
|||||||
const { userId } = jwt.verify(token, appConfig.appSecretKey) as {
|
const { userId } = jwt.verify(token, appConfig.appSecretKey) as {
|
||||||
userId: string;
|
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;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -25,13 +35,14 @@ const authentication = shield(
|
|||||||
Query: {
|
Query: {
|
||||||
'*': isAuthenticated,
|
'*': isAuthenticated,
|
||||||
getAutomatischInfo: allow,
|
getAutomatischInfo: allow,
|
||||||
|
getSamlAuthProviders: allow,
|
||||||
healthcheck: allow,
|
healthcheck: allow,
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
'*': isAuthenticated,
|
'*': isAuthenticated,
|
||||||
login: allow,
|
registerUser: allow,
|
||||||
createUser: allow,
|
|
||||||
forgotPassword: allow,
|
forgotPassword: allow,
|
||||||
|
login: allow,
|
||||||
resetPassword: 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 { Model } from 'objection';
|
||||||
import ExtendedQueryBuilder from '../models/query-builder';
|
import ExtendedQueryBuilder from '../models/query-builder';
|
||||||
|
import type Base from '../models/base';
|
||||||
|
|
||||||
const paginate = async (
|
const paginate = async (
|
||||||
query: ExtendedQueryBuilder<Model, Model[]>,
|
query: ExtendedQueryBuilder<Model, Model[]>,
|
||||||
limit: number,
|
limit: number,
|
||||||
offset: number
|
offset: number,
|
||||||
) => {
|
) => {
|
||||||
if (limit < 1 || limit > 100) {
|
if (limit < 1 || limit > 100) {
|
||||||
throw new Error('Limit must be between 1 and 100');
|
throw new Error('Limit must be between 1 and 100');
|
||||||
@@ -20,11 +21,9 @@ const paginate = async (
|
|||||||
currentPage: Math.ceil(offset / limit + 1),
|
currentPage: Math.ceil(offset / limit + 1),
|
||||||
totalPages: Math.ceil(count / limit),
|
totalPages: Math.ceil(count / limit),
|
||||||
},
|
},
|
||||||
edges: records.map((record: Model) => {
|
edges: records.map((record: Base) => ({
|
||||||
return {
|
node: record,
|
||||||
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' },
|
userId: { type: 'string', format: 'uuid' },
|
||||||
verified: { type: 'boolean', default: false },
|
verified: { type: 'boolean', default: false },
|
||||||
draft: { type: 'boolean' },
|
draft: { type: 'boolean' },
|
||||||
|
deletedAt: { type: 'string' },
|
||||||
|
createdAt: { type: 'string' },
|
||||||
|
updatedAt: { type: 'string' },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -31,6 +31,9 @@ class ExecutionStep extends Base {
|
|||||||
dataOut: { type: ['object', 'null'] },
|
dataOut: { type: ['object', 'null'] },
|
||||||
status: { type: 'string', enum: ['success', 'failure'] },
|
status: { type: 'string', enum: ['success', 'failure'] },
|
||||||
errorDetails: { type: ['object', 'null'] },
|
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' },
|
flowId: { type: 'string', format: 'uuid' },
|
||||||
testRun: { type: 'boolean', default: false },
|
testRun: { type: 'boolean', default: false },
|
||||||
internalId: { type: 'string' },
|
internalId: { type: 'string' },
|
||||||
|
deletedAt: { type: 'string' },
|
||||||
|
createdAt: { type: 'string' },
|
||||||
|
updatedAt: { type: 'string' },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -19,7 +19,7 @@ class Flow extends Base {
|
|||||||
status: 'paused' | 'published' | 'draft';
|
status: 'paused' | 'published' | 'draft';
|
||||||
steps: Step[];
|
steps: Step[];
|
||||||
triggerStep: Step;
|
triggerStep: Step;
|
||||||
published_at: string;
|
publishedAt: string;
|
||||||
remoteWebhookId: string;
|
remoteWebhookId: string;
|
||||||
executions?: Execution[];
|
executions?: Execution[];
|
||||||
lastExecution?: Execution;
|
lastExecution?: Execution;
|
||||||
@@ -37,6 +37,10 @@ class Flow extends Base {
|
|||||||
userId: { type: 'string', format: 'uuid' },
|
userId: { type: 'string', format: 'uuid' },
|
||||||
remoteWebhookId: { type: 'string' },
|
remoteWebhookId: { type: 'string' },
|
||||||
active: { type: 'boolean' },
|
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 {
|
import {
|
||||||
Model,
|
Model,
|
||||||
Page,
|
Page,
|
||||||
|
ModelClass,
|
||||||
PartialModelObject,
|
PartialModelObject,
|
||||||
ForClassMethod,
|
ForClassMethod,
|
||||||
AnyQueryBuilder,
|
AnyQueryBuilder,
|
||||||
@@ -8,6 +9,10 @@ import {
|
|||||||
|
|
||||||
const DELETED_COLUMN_NAME = 'deleted_at';
|
const DELETED_COLUMN_NAME = 'deleted_at';
|
||||||
|
|
||||||
|
const supportsSoftDeletion = (modelClass: ModelClass<any>) => {
|
||||||
|
return modelClass.jsonSchema.properties.deletedAt;
|
||||||
|
}
|
||||||
|
|
||||||
const buildQueryBuidlerForClass = (): ForClassMethod => {
|
const buildQueryBuidlerForClass = (): ForClassMethod => {
|
||||||
return (modelClass) => {
|
return (modelClass) => {
|
||||||
const qb: AnyQueryBuilder = Model.QueryBuilder.forClass.call(
|
const qb: AnyQueryBuilder = Model.QueryBuilder.forClass.call(
|
||||||
@@ -15,7 +20,7 @@ const buildQueryBuidlerForClass = (): ForClassMethod => {
|
|||||||
modelClass
|
modelClass
|
||||||
);
|
);
|
||||||
qb.onBuild((builder) => {
|
qb.onBuild((builder) => {
|
||||||
if (!builder.context().withSoftDeleted) {
|
if (!builder.context().withSoftDeleted && supportsSoftDeletion(qb.modelClass())) {
|
||||||
builder.whereNull(
|
builder.whereNull(
|
||||||
`${qb.modelClass().tableName}.${DELETED_COLUMN_NAME}`
|
`${qb.modelClass().tableName}.${DELETED_COLUMN_NAME}`
|
||||||
);
|
);
|
||||||
@@ -38,9 +43,13 @@ class ExtendedQueryBuilder<M extends Model, R = M[]> extends Model.QueryBuilder<
|
|||||||
static forClass: ForClassMethod = buildQueryBuidlerForClass();
|
static forClass: ForClassMethod = buildQueryBuidlerForClass();
|
||||||
|
|
||||||
delete() {
|
delete() {
|
||||||
return this.patch({
|
if (supportsSoftDeletion(this.modelClass())) {
|
||||||
[DELETED_COLUMN_NAME]: new Date().toISOString(),
|
return this.patch({
|
||||||
} as unknown as PartialModelObject<M>);
|
[DELETED_COLUMN_NAME]: new Date().toISOString(),
|
||||||
|
} as unknown as PartialModelObject<M>);
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
hardDelete() {
|
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' },
|
position: { type: 'integer' },
|
||||||
parameters: { type: 'object' },
|
parameters: { type: 'object' },
|
||||||
webhookPath: { type: ['string', 'null'] },
|
webhookPath: { type: ['string', 'null'] },
|
||||||
|
deletedAt: { type: 'string' },
|
||||||
|
createdAt: { type: 'string' },
|
||||||
|
updatedAt: { type: 'string' },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -46,6 +46,9 @@ class Subscription extends Base {
|
|||||||
nextBillDate: { type: 'string' },
|
nextBillDate: { type: 'string' },
|
||||||
lastBillDate: { type: 'string' },
|
lastBillDate: { type: 'string' },
|
||||||
cancellationEffectiveDate: { type: 'string' },
|
cancellationEffectiveDate: { type: 'string' },
|
||||||
|
deletedAt: { type: 'string' },
|
||||||
|
createdAt: { type: 'string' },
|
||||||
|
updatedAt: { type: 'string' },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -84,7 +87,7 @@ class Subscription extends Base {
|
|||||||
return (
|
return (
|
||||||
this.status === 'deleted' &&
|
this.status === 'deleted' &&
|
||||||
Number(this.cancellationEffectiveDate) >
|
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' },
|
subscriptionId: { type: 'string', format: 'uuid' },
|
||||||
consumedTaskCount: { type: 'integer' },
|
consumedTaskCount: { type: 'integer' },
|
||||||
nextResetAt: { type: 'string' },
|
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 { QueryContext, ModelOptions } from 'objection';
|
||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
import crypto from 'crypto';
|
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
import { PureAbility, fieldPatternMatcher, mongoQueryMatcher } from '@casl/ability';
|
||||||
|
import type { Subject } from '@casl/ability';
|
||||||
|
|
||||||
import appConfig from '../config/app';
|
import appConfig from '../config/app';
|
||||||
import Base from './base';
|
import Base from './base';
|
||||||
import ExtendedQueryBuilder from './query-builder';
|
import ExtendedQueryBuilder from './query-builder';
|
||||||
import Connection from './connection';
|
import Connection from './connection';
|
||||||
import Flow from './flow';
|
import Flow from './flow';
|
||||||
import Step from './step';
|
import Step from './step';
|
||||||
|
import Role from './role';
|
||||||
|
import Permission from './permission';
|
||||||
import Execution from './execution';
|
import Execution from './execution';
|
||||||
|
import Identity from './identity.ee';
|
||||||
import UsageData from './usage-data.ee';
|
import UsageData from './usage-data.ee';
|
||||||
import Subscription from './subscription.ee';
|
import Subscription from './subscription.ee';
|
||||||
|
|
||||||
@@ -16,8 +22,8 @@ class User extends Base {
|
|||||||
id!: string;
|
id!: string;
|
||||||
fullName!: string;
|
fullName!: string;
|
||||||
email!: string;
|
email!: string;
|
||||||
|
roleId: string;
|
||||||
password!: string;
|
password!: string;
|
||||||
role: string;
|
|
||||||
resetPasswordToken: string;
|
resetPasswordToken: string;
|
||||||
resetPasswordTokenSentAt: string;
|
resetPasswordTokenSentAt: string;
|
||||||
trialExpiryDate: string;
|
trialExpiryDate: string;
|
||||||
@@ -29,19 +35,28 @@ class User extends Base {
|
|||||||
currentUsageData?: UsageData;
|
currentUsageData?: UsageData;
|
||||||
subscriptions?: Subscription[];
|
subscriptions?: Subscription[];
|
||||||
currentSubscription?: Subscription;
|
currentSubscription?: Subscription;
|
||||||
|
role: Role;
|
||||||
|
permissions: Permission[];
|
||||||
|
identities: Identity[];
|
||||||
|
|
||||||
static tableName = 'users';
|
static tableName = 'users';
|
||||||
|
|
||||||
static jsonSchema = {
|
static jsonSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
required: ['fullName', 'email', 'password'],
|
required: ['fullName', 'email'],
|
||||||
|
|
||||||
properties: {
|
properties: {
|
||||||
id: { type: 'string', format: 'uuid' },
|
id: { type: 'string', format: 'uuid' },
|
||||||
fullName: { type: 'string', minLength: 1 },
|
fullName: { type: 'string', minLength: 1 },
|
||||||
email: { type: 'string', format: 'email', minLength: 1, maxLength: 255 },
|
email: { type: 'string', format: 'email', minLength: 1, maxLength: 255 },
|
||||||
password: { type: 'string', minLength: 1, maxLength: 255 },
|
password: { type: 'string' },
|
||||||
role: { type: 'string', enum: ['admin', 'user'] },
|
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();
|
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) {
|
login(password: string) {
|
||||||
@@ -158,7 +201,9 @@ class User extends Base {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async generateHash() {
|
async generateHash() {
|
||||||
this.password = await bcrypt.hash(this.password, 10);
|
if (this.password) {
|
||||||
|
this.password = await bcrypt.hash(this.password, 10);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async startTrialPeriod() {
|
async startTrialPeriod() {
|
||||||
@@ -232,9 +277,7 @@ class User extends Base {
|
|||||||
async $beforeUpdate(opt: ModelOptions, queryContext: QueryContext) {
|
async $beforeUpdate(opt: ModelOptions, queryContext: QueryContext) {
|
||||||
await super.$beforeUpdate(opt, queryContext);
|
await super.$beforeUpdate(opt, queryContext);
|
||||||
|
|
||||||
if (this.password) {
|
await this.generateHash();
|
||||||
await this.generateHash();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async $afterInsert(queryContext: QueryContext) {
|
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;
|
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[];
|
connections: IConnection[];
|
||||||
flows: IFlow[];
|
flows: IFlow[];
|
||||||
steps: IStep[];
|
steps: IStep[];
|
||||||
|
role: IRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRole {
|
||||||
|
id: string;
|
||||||
|
key: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
isAdmin: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IFieldDropdown {
|
export interface IFieldDropdown {
|
||||||
@@ -386,6 +395,20 @@ type TInvoice = {
|
|||||||
receipt_url: string;
|
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' {
|
declare module 'axios' {
|
||||||
interface AxiosResponse {
|
interface AxiosResponse {
|
||||||
httpError?: IJSONObject;
|
httpError?: IJSONObject;
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
PORT=3001
|
PORT=3001
|
||||||
|
REACT_APP_API_URL=http://localhost:3000
|
||||||
REACT_APP_GRAPHQL_URL=http://localhost:3000/graphql
|
REACT_APP_GRAPHQL_URL=http://localhost:3000/graphql
|
||||||
# HTTPS=true
|
# HTTPS=true
|
||||||
REACT_APP_BASE_URL=http://localhost:3001
|
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')}
|
{formatMessage('accountDropdownMenu.settings')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
|
<MenuItem component={Link} to={URLS.ADMIN_SETTINGS_DASHBOARD}>
|
||||||
|
{formatMessage('accountDropdownMenu.adminSettings')}
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
<MenuItem onClick={logout} data-test="logout-item">
|
<MenuItem onClick={logout} data-test="logout-item">
|
||||||
{formatMessage('accountDropdownMenu.logout')}
|
{formatMessage('accountDropdownMenu.logout')}
|
||||||
</MenuItem>
|
</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}
|
type={buttonProps.type}
|
||||||
size={buttonProps.size}
|
size={buttonProps.size}
|
||||||
component={buttonProps.component}
|
component={buttonProps.component}
|
||||||
|
to={buttonProps.to}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
</IconButton>
|
</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 * as React from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useMutation } from '@apollo/client';
|
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 * as URLS from 'config/urls';
|
||||||
|
import ConfirmationDialog from 'components/ConfirmationDialog';
|
||||||
import apolloClient from 'graphql/client';
|
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 useAuthentication from 'hooks/useAuthentication';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import useCurrentUser from 'hooks/useCurrentUser';
|
import useCurrentUser from 'hooks/useCurrentUser';
|
||||||
@@ -20,37 +15,29 @@ type DeleteAccountDialogProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function DeleteAccountDialog(props: DeleteAccountDialogProps) {
|
export default function DeleteAccountDialog(props: DeleteAccountDialogProps) {
|
||||||
const [deleteUser] = useMutation(DELETE_USER);
|
const [deleteCurrentUser] = useMutation(DELETE_CURRENT_USER);
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const currentUser = useCurrentUser();
|
const currentUser = useCurrentUser();
|
||||||
const authentication = useAuthentication();
|
const authentication = useAuthentication();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleConfirm = React.useCallback(async () => {
|
const handleConfirm = React.useCallback(async () => {
|
||||||
await deleteUser();
|
await deleteCurrentUser();
|
||||||
|
|
||||||
authentication.updateToken('');
|
authentication.updateToken('');
|
||||||
await apolloClient.clearStore();
|
await apolloClient.clearStore();
|
||||||
|
|
||||||
navigate(URLS.LOGIN);
|
navigate(URLS.LOGIN);
|
||||||
}, [deleteUser, currentUser]);
|
}, [deleteCurrentUser, currentUser]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open onClose={props.onClose}>
|
<ConfirmationDialog
|
||||||
<DialogTitle >
|
title={formatMessage('deleteAccountDialog.title')}
|
||||||
{formatMessage('deleteAccountDialog.title')}
|
description={formatMessage('deleteAccountDialog.description')}
|
||||||
</DialogTitle>
|
onClose={props.onClose}
|
||||||
<DialogContent>
|
onConfirm={handleConfirm}
|
||||||
<DialogContentText id="alert-dialog-description">
|
cancelButtonChildren={formatMessage('deleteAccountDialog.cancel')}
|
||||||
{formatMessage('deleteAccountDialog.description')}
|
confirmButtionChildren={formatMessage('deleteAccountDialog.confirm')}
|
||||||
</DialogContentText>
|
/>
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button onClick={props.onClose}>{formatMessage('deleteAccountDialog.cancel')}</Button>
|
|
||||||
<Button onClick={handleConfirm} color="error">
|
|
||||||
{formatMessage('deleteAccountDialog.confirm')}
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
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 useAuthentication from 'hooks/useAuthentication';
|
||||||
import * as URLS from 'config/urls';
|
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 Form from 'components/Form';
|
||||||
import TextField from 'components/TextField';
|
import TextField from 'components/TextField';
|
||||||
import { LOGIN } from 'graphql/mutations/login';
|
import { LOGIN } from 'graphql/mutations/login';
|
||||||
@@ -40,7 +40,7 @@ function SignUpForm() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const authentication = useAuthentication();
|
const authentication = useAuthentication();
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const [createUser, { loading: createUserLoading }] = useMutation(CREATE_USER);
|
const [registerUser, { loading: registerUserLoading }] = useMutation(REGISTER_USER);
|
||||||
const [login, { loading: loginLoading }] = useMutation(LOGIN);
|
const [login, { loading: loginLoading }] = useMutation(LOGIN);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -51,7 +51,7 @@ function SignUpForm() {
|
|||||||
|
|
||||||
const handleSubmit = async (values: any) => {
|
const handleSubmit = async (values: any) => {
|
||||||
const { fullName, email, password } = values;
|
const { fullName, email, password } = values;
|
||||||
await createUser({
|
await registerUser({
|
||||||
variables: {
|
variables: {
|
||||||
input: { fullName, email, password },
|
input: { fullName, email, password },
|
||||||
},
|
},
|
||||||
@@ -165,7 +165,7 @@ function SignUpForm() {
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
sx={{ boxShadow: 2, mt: 3 }}
|
sx={{ boxShadow: 2, mt: 3 }}
|
||||||
loading={createUserLoading || loginLoading}
|
loading={registerUserLoading || loginLoading}
|
||||||
fullWidth
|
fullWidth
|
||||||
data-test="signUp-button"
|
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"
|
alignItems="stretch"
|
||||||
>
|
>
|
||||||
<TableContainer component={Paper}>
|
<TableContainer component={Paper}>
|
||||||
<Table aria-label="simple table">
|
<Table>
|
||||||
<TableHead
|
<TableHead
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: (theme) =>
|
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 = {
|
type Config = {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
|
baseUrl: string;
|
||||||
|
apiUrl: string;
|
||||||
|
graphqlUrl: string;
|
||||||
|
notificationsUrl: string;
|
||||||
|
chatwootBaseUrl: string;
|
||||||
|
supportEmailAddress: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const config: Config = {
|
const config: Config = {
|
||||||
baseUrl: process.env.REACT_APP_BASE_URL as string,
|
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,
|
graphqlUrl: process.env.REACT_APP_GRAPHQL_URL as string,
|
||||||
notificationsUrl: process.env.REACT_APP_NOTIFICATIONS_URL as string,
|
notificationsUrl: process.env.REACT_APP_NOTIFICATIONS_URL as string,
|
||||||
chatwootBaseUrl: 'https://app.chatwoot.com',
|
chatwootBaseUrl: 'https://app.chatwoot.com',
|
||||||
supportEmailAddress: 'support@automatisch.io'
|
supportEmailAddress: 'support@automatisch.io'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!config.apiUrl) {
|
||||||
|
config.apiUrl = (new URL(config.graphqlUrl)).origin;
|
||||||
|
}
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
@@ -1,35 +1,36 @@
|
|||||||
export const CONNECTIONS = '/connections';
|
export const CONNECTIONS = '/connections';
|
||||||
export const EXECUTIONS = '/executions';
|
export const EXECUTIONS = '/executions';
|
||||||
export const EXECUTION_PATTERN = '/executions/:executionId';
|
export const EXECUTION_PATTERN = '/executions/:executionId';
|
||||||
export const EXECUTION = (executionId: string): string =>
|
export const EXECUTION = (executionId: string) =>
|
||||||
`/executions/${executionId}`;
|
`/executions/${executionId}`;
|
||||||
|
|
||||||
export const LOGIN = '/login';
|
export const LOGIN = '/login';
|
||||||
|
export const LOGIN_CALLBACK = `${LOGIN}/callback`;
|
||||||
export const SIGNUP = '/sign-up';
|
export const SIGNUP = '/sign-up';
|
||||||
export const FORGOT_PASSWORD = '/forgot-password';
|
export const FORGOT_PASSWORD = '/forgot-password';
|
||||||
export const RESET_PASSWORD = '/reset-password';
|
export const RESET_PASSWORD = '/reset-password';
|
||||||
|
|
||||||
export const APPS = '/apps';
|
export const APPS = '/apps';
|
||||||
export const NEW_APP_CONNECTION = '/apps/new';
|
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_PATTERN = '/app/:appKey';
|
||||||
export const APP_CONNECTIONS = (appKey: string): string =>
|
export const APP_CONNECTIONS = (appKey: string) =>
|
||||||
`/app/${appKey}/connections`;
|
`/app/${appKey}/connections`;
|
||||||
export const APP_CONNECTIONS_PATTERN = '/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`;
|
`/app/${appKey}/connections/add`;
|
||||||
export const APP_ADD_CONNECTION_PATTERN = '/app/:appKey/connections/add';
|
export const APP_ADD_CONNECTION_PATTERN = '/app/:appKey/connections/add';
|
||||||
export const APP_RECONNECT_CONNECTION = (
|
export const APP_RECONNECT_CONNECTION = (
|
||||||
appKey: string,
|
appKey: string,
|
||||||
connectionId: string
|
connectionId: string
|
||||||
): string => `/app/${appKey}/connections/${connectionId}/reconnect`;
|
) => `/app/${appKey}/connections/${connectionId}/reconnect`;
|
||||||
export const APP_RECONNECT_CONNECTION_PATTERN =
|
export const APP_RECONNECT_CONNECTION_PATTERN =
|
||||||
'/app/:appKey/connections/:connectionId/reconnect';
|
'/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 = (
|
export const APP_FLOWS_FOR_CONNECTION = (
|
||||||
appKey: string,
|
appKey: string,
|
||||||
connectionId: string
|
connectionId: string
|
||||||
): string => `/app/${appKey}/flows?connectionId=${connectionId}`;
|
) => `/app/${appKey}/flows?connectionId=${connectionId}`;
|
||||||
export const APP_FLOWS_PATTERN = '/app/:appKey/flows';
|
export const APP_FLOWS_PATTERN = '/app/:appKey/flows';
|
||||||
|
|
||||||
export const EDITOR = '/editor';
|
export const EDITOR = '/editor';
|
||||||
@@ -54,11 +55,11 @@ export const CREATE_FLOW_WITH_APP_AND_CONNECTION = (
|
|||||||
|
|
||||||
return `/editor/create?${searchParams}`;
|
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';
|
export const FLOWS = '/flows';
|
||||||
// TODO: revert this back to /flows/:flowId once we have a proper single flow page
|
// 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 FLOW_PATTERN = '/flows/:flowId';
|
||||||
|
|
||||||
export const SETTINGS = '/settings';
|
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_BILLING_AND_USAGE = `${SETTINGS}/${BILLING_AND_USAGE}`;
|
||||||
export const SETTINGS_PLAN_UPGRADE = `${SETTINGS_BILLING_AND_USAGE}/${PLAN_UPGRADE}`;
|
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;
|
export const DASHBOARD = FLOWS;
|
||||||
|
|
||||||
// External links
|
// 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`
|
export const CREATE_USER = gql`
|
||||||
mutation CreateUser($input: CreateUserInput) {
|
mutation CreateUser($input: CreateUserInput) {
|
||||||
createUser(input: $input) {
|
createUser(input: $input) {
|
||||||
|
id
|
||||||
email
|
email
|
||||||
fullName
|
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';
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
export const DELETE_USER = gql`
|
export const DELETE_USER = gql`
|
||||||
mutation DeleteUser {
|
mutation DeleteUser($input: DeleteUserInput) {
|
||||||
deleteUser
|
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