Compare commits
53 Commits
test-cover
...
aut-1375
Author | SHA1 | Date | |
---|---|---|---|
![]() |
591e1444f9 | ||
![]() |
5d5aebdb41 | ||
![]() |
83d16e72e1 | ||
![]() |
2a77763c51 | ||
![]() |
17614d6d47 | ||
![]() |
b4fcdbd2c4 | ||
![]() |
8e89a103db | ||
![]() |
978ceaadb6 | ||
![]() |
770b07179f | ||
![]() |
6d15167ad9 | ||
![]() |
39cba6bc74 | ||
![]() |
9558e66abf | ||
![]() |
ff7908955e | ||
![]() |
26b095b835 | ||
![]() |
feba2a32f9 | ||
![]() |
5090ece9b6 | ||
![]() |
221b19586e | ||
![]() |
3346c14255 | ||
![]() |
6e97e023c9 | ||
![]() |
b26e2ecf2e | ||
![]() |
d896238f23 | ||
![]() |
d2c8f5a75c | ||
![]() |
ce430d238c | ||
![]() |
ee397441ed | ||
![]() |
ba82d986c1 | ||
![]() |
2361cb521e | ||
![]() |
05f8d95281 | ||
![]() |
6c60b1c263 | ||
![]() |
0c32a0693c | ||
![]() |
807faa3c93 | ||
![]() |
fb53e37f7a | ||
![]() |
4ffdf98e16 | ||
![]() |
b8da721e39 | ||
![]() |
db8b98ca16 | ||
![]() |
01b8c600fe | ||
![]() |
69bd5549a2 | ||
![]() |
bc631e3931 | ||
![]() |
8ca4bc5a33 | ||
![]() |
58a569afb0 | ||
![]() |
db718d6fc3 | ||
![]() |
ca9cb8b07b | ||
![]() |
ef14586412 | ||
![]() |
15f1fca6fe | ||
![]() |
a570b8eb7a | ||
![]() |
02e2735b7a | ||
![]() |
54fa347142 | ||
![]() |
0c752beace | ||
![]() |
c14f808d29 | ||
![]() |
ad71173671 | ||
![]() |
204325ef44 | ||
![]() |
7ce6117659 | ||
![]() |
551548400f | ||
![]() |
90a7b4c1c0 |
1
.github/workflows/playwright.yml
vendored
1
.github/workflows/playwright.yml
vendored
@@ -3,6 +3,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
# TODO: Add pull request after optimizing the total excecution time of the test suite.
|
||||
pull_request:
|
||||
paths:
|
||||
- 'packages/backend/**'
|
||||
|
@@ -10,12 +10,11 @@ export default async (request, response) => {
|
||||
};
|
||||
|
||||
const appConfigParams = (request) => {
|
||||
const { customConnectionAllowed, shared, disabled } = request.body;
|
||||
const { useOnlyPredefinedAuthClients, disabled } = request.body;
|
||||
|
||||
return {
|
||||
key: request.params.appKey,
|
||||
customConnectionAllowed,
|
||||
shared,
|
||||
useOnlyPredefinedAuthClients,
|
||||
disabled,
|
||||
};
|
||||
};
|
||||
|
@@ -23,8 +23,7 @@ describe('POST /api/v1/admin/apps/:appKey/config', () => {
|
||||
|
||||
it('should return created app config', async () => {
|
||||
const appConfig = {
|
||||
customConnectionAllowed: true,
|
||||
shared: true,
|
||||
useOnlyPredefinedAuthClients: false,
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
@@ -38,14 +37,14 @@ describe('POST /api/v1/admin/apps/:appKey/config', () => {
|
||||
...appConfig,
|
||||
key: 'gitlab',
|
||||
});
|
||||
|
||||
expect(response.body).toMatchObject(expectedPayload);
|
||||
});
|
||||
|
||||
it('should return HTTP 422 for already existing app config', async () => {
|
||||
const appConfig = {
|
||||
key: 'gitlab',
|
||||
customConnectionAllowed: true,
|
||||
shared: true,
|
||||
useOnlyPredefinedAuthClients: false,
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
|
@@ -17,11 +17,10 @@ export default async (request, response) => {
|
||||
};
|
||||
|
||||
const appConfigParams = (request) => {
|
||||
const { customConnectionAllowed, shared, disabled } = request.body;
|
||||
const { useOnlyPredefinedAuthClients, disabled } = request.body;
|
||||
|
||||
return {
|
||||
customConnectionAllowed,
|
||||
shared,
|
||||
useOnlyPredefinedAuthClients,
|
||||
disabled,
|
||||
};
|
||||
};
|
||||
|
@@ -24,17 +24,15 @@ describe('PATCH /api/v1/admin/apps/:appKey/config', () => {
|
||||
it('should return updated app config', async () => {
|
||||
const appConfig = {
|
||||
key: 'gitlab',
|
||||
customConnectionAllowed: true,
|
||||
shared: true,
|
||||
useOnlyPredefinedAuthClients: true,
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
await createAppConfig(appConfig);
|
||||
|
||||
const newAppConfigValues = {
|
||||
shared: false,
|
||||
disabled: true,
|
||||
customConnectionAllowed: false,
|
||||
useOnlyPredefinedAuthClients: false,
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
@@ -53,9 +51,8 @@ describe('PATCH /api/v1/admin/apps/:appKey/config', () => {
|
||||
|
||||
it('should return not found response for unexisting app config', async () => {
|
||||
const appConfig = {
|
||||
shared: false,
|
||||
disabled: true,
|
||||
customConnectionAllowed: false,
|
||||
useOnlyPredefinedAuthClients: false,
|
||||
};
|
||||
|
||||
await request(app)
|
||||
@@ -68,8 +65,7 @@ describe('PATCH /api/v1/admin/apps/:appKey/config', () => {
|
||||
it('should return HTTP 422 for invalid app config data', async () => {
|
||||
const appConfig = {
|
||||
key: 'gitlab',
|
||||
customConnectionAllowed: true,
|
||||
shared: true,
|
||||
useOnlyPredefinedAuthClients: true,
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
|
@@ -7,7 +7,7 @@ export default async (request, response) => {
|
||||
.throwIfNotFound();
|
||||
|
||||
const roleMappings = await samlAuthProvider
|
||||
.$relatedQuery('samlAuthProvidersRoleMappings')
|
||||
.$relatedQuery('roleMappings')
|
||||
.orderBy('remote_role_name', 'asc');
|
||||
|
||||
renderObject(response, roleMappings);
|
||||
|
@@ -8,15 +8,14 @@ export default async (request, response) => {
|
||||
.findById(samlAuthProviderId)
|
||||
.throwIfNotFound();
|
||||
|
||||
const samlAuthProvidersRoleMappings =
|
||||
await samlAuthProvider.updateRoleMappings(
|
||||
samlAuthProvidersRoleMappingsParams(request)
|
||||
);
|
||||
const roleMappings = await samlAuthProvider.updateRoleMappings(
|
||||
roleMappingsParams(request)
|
||||
);
|
||||
|
||||
renderObject(response, samlAuthProvidersRoleMappings);
|
||||
renderObject(response, roleMappings);
|
||||
};
|
||||
|
||||
const samlAuthProvidersRoleMappingsParams = (request) => {
|
||||
const roleMappingsParams = (request) => {
|
||||
const roleMappings = request.body;
|
||||
|
||||
return roleMappings.map(({ roleId, remoteRoleName }) => ({
|
||||
|
@@ -6,7 +6,7 @@ import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by
|
||||
import { createRole } from '../../../../../../test/factories/role.js';
|
||||
import { createUser } from '../../../../../../test/factories/user.js';
|
||||
import { createSamlAuthProvider } from '../../../../../../test/factories/saml-auth-provider.ee.js';
|
||||
import { createSamlAuthProvidersRoleMapping } from '../../../../../../test/factories/saml-auth-providers-role-mapping.js';
|
||||
import { createRoleMapping } from '../../../../../../test/factories/role-mapping.js';
|
||||
import createRoleMappingsMock from '../../../../../../test/mocks/rest/api/v1/admin/saml-auth-providers/update-role-mappings.ee.js';
|
||||
import * as license from '../../../../../helpers/license.ee.js';
|
||||
|
||||
@@ -21,12 +21,12 @@ describe('PATCH /api/v1/admin/saml-auth-providers/:samlAuthProviderId/role-mappi
|
||||
|
||||
samlAuthProvider = await createSamlAuthProvider();
|
||||
|
||||
await createSamlAuthProvidersRoleMapping({
|
||||
await createRoleMapping({
|
||||
samlAuthProviderId: samlAuthProvider.id,
|
||||
remoteRoleName: 'Viewer',
|
||||
});
|
||||
|
||||
await createSamlAuthProvidersRoleMapping({
|
||||
await createRoleMapping({
|
||||
samlAuthProviderId: samlAuthProvider.id,
|
||||
remoteRoleName: 'Editor',
|
||||
});
|
||||
@@ -64,7 +64,7 @@ describe('PATCH /api/v1/admin/saml-auth-providers/:samlAuthProviderId/role-mappi
|
||||
|
||||
it('should delete role mappings when given empty role mappings', async () => {
|
||||
const existingRoleMappings = await samlAuthProvider.$relatedQuery(
|
||||
'samlAuthProvidersRoleMappings'
|
||||
'roleMappings'
|
||||
);
|
||||
|
||||
expect(existingRoleMappings.length).toBe(2);
|
||||
@@ -149,34 +149,4 @@ describe('PATCH /api/v1/admin/saml-auth-providers/:samlAuthProviderId/role-mappi
|
||||
.send(roleMappings)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it('should not delete existing role mapping when error thrown', async () => {
|
||||
const roleMappings = [
|
||||
{
|
||||
roleId: userRole.id,
|
||||
remoteRoleName: {
|
||||
invalid: 'data',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const roleMappingsBeforeRequest = await samlAuthProvider.$relatedQuery(
|
||||
'samlAuthProvidersRoleMappings'
|
||||
);
|
||||
|
||||
await request(app)
|
||||
.patch(
|
||||
`/api/v1/admin/saml-auth-providers/${samlAuthProvider.id}/role-mappings`
|
||||
)
|
||||
.set('Authorization', token)
|
||||
.send(roleMappings)
|
||||
.expect(422);
|
||||
|
||||
const roleMappingsAfterRequest = await samlAuthProvider.$relatedQuery(
|
||||
'samlAuthProvidersRoleMappings'
|
||||
);
|
||||
|
||||
expect(roleMappingsBeforeRequest).toStrictEqual(roleMappingsAfterRequest);
|
||||
expect(roleMappingsAfterRequest.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
@@ -155,7 +155,7 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
|
||||
await createAppConfig({
|
||||
key: 'gitlab',
|
||||
disabled: false,
|
||||
customConnectionAllowed: true,
|
||||
useOnlyPredefinedAuthClients: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -218,7 +218,7 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
|
||||
await createAppConfig({
|
||||
key: 'gitlab',
|
||||
disabled: false,
|
||||
customConnectionAllowed: false,
|
||||
useOnlyPredefinedAuthClients: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -266,14 +266,14 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('with auth clients enabled', async () => {
|
||||
describe('with auth client enabled', async () => {
|
||||
let appAuthClient;
|
||||
|
||||
beforeEach(async () => {
|
||||
await createAppConfig({
|
||||
key: 'gitlab',
|
||||
disabled: false,
|
||||
shared: true,
|
||||
useOnlyPredefinedAuthClients: false,
|
||||
});
|
||||
|
||||
appAuthClient = await createAppAuthClient({
|
||||
@@ -310,19 +310,6 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
|
||||
expect(response.body).toStrictEqual(expectedPayload);
|
||||
});
|
||||
|
||||
it('should return not authorized response for appAuthClientId and formattedData together', async () => {
|
||||
const connectionData = {
|
||||
appAuthClientId: appAuthClient.id,
|
||||
formattedData: {},
|
||||
};
|
||||
|
||||
await request(app)
|
||||
.post('/api/v1/apps/gitlab/connections')
|
||||
.set('Authorization', token)
|
||||
.send(connectionData)
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
it('should return not found response for invalid app key', async () => {
|
||||
await request(app)
|
||||
.post('/api/v1/apps/invalid-app-key/connections')
|
||||
@@ -349,18 +336,20 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('with auth clients disabled', async () => {
|
||||
|
||||
describe('with auth client disabled', async () => {
|
||||
let appAuthClient;
|
||||
|
||||
beforeEach(async () => {
|
||||
await createAppConfig({
|
||||
key: 'gitlab',
|
||||
disabled: false,
|
||||
shared: false,
|
||||
useOnlyPredefinedAuthClients: false,
|
||||
});
|
||||
|
||||
appAuthClient = await createAppAuthClient({
|
||||
appKey: 'gitlab',
|
||||
active: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -373,7 +362,7 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
|
||||
.post('/api/v1/apps/gitlab/connections')
|
||||
.set('Authorization', token)
|
||||
.send(connectionData)
|
||||
.expect(403);
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it('should return not found response for invalid app key', async () => {
|
||||
|
@@ -17,8 +17,7 @@ describe('GET /api/v1/apps/:appKey/config', () => {
|
||||
|
||||
appConfig = await createAppConfig({
|
||||
key: 'deepl',
|
||||
customConnectionAllowed: true,
|
||||
shared: true,
|
||||
useOnlyPredefinedAuthClients: false,
|
||||
disabled: false,
|
||||
});
|
||||
|
||||
|
@@ -87,14 +87,14 @@ describe('GET /api/v1/apps/:appKey/connections', () => {
|
||||
|
||||
it('should return not found response for invalid connection UUID', async () => {
|
||||
await createPermission({
|
||||
action: 'update',
|
||||
action: 'read',
|
||||
subject: 'Connection',
|
||||
roleId: currentUserRole.id,
|
||||
conditions: ['isCreator'],
|
||||
});
|
||||
|
||||
await request(app)
|
||||
.get('/api/v1/connections/invalid-connection-id/connections')
|
||||
.get('/api/v1/apps/invalid-connection-id/connections')
|
||||
.set('Authorization', token)
|
||||
.expect(404);
|
||||
});
|
||||
|
@@ -47,7 +47,6 @@ describe('POST /api/v1/connections/:connectionId/reset', () => {
|
||||
|
||||
const expectedPayload = resetConnectionMock({
|
||||
...refetchedCurrentUserConnection,
|
||||
reconnectable: refetchedCurrentUserConnection.reconnectable,
|
||||
formattedData: {
|
||||
screenName: 'Connection name',
|
||||
},
|
||||
|
@@ -55,10 +55,9 @@ describe('PATCH /api/v1/connections/:connectionId', () => {
|
||||
|
||||
const refetchedCurrentUserConnection = await currentUserConnection.$query();
|
||||
|
||||
const expectedPayload = updateConnectionMock({
|
||||
...refetchedCurrentUserConnection,
|
||||
reconnectable: refetchedCurrentUserConnection.reconnectable,
|
||||
});
|
||||
const expectedPayload = updateConnectionMock(
|
||||
refetchedCurrentUserConnection
|
||||
);
|
||||
|
||||
expect(response.body).toStrictEqual(expectedPayload);
|
||||
});
|
||||
|
@@ -193,7 +193,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
|
||||
const notExistingStepUUID = Crypto.randomUUID();
|
||||
|
||||
await request(app)
|
||||
.get(`/api/v1/steps/${notExistingStepUUID}/dynamic-data`)
|
||||
.post(`/api/v1/steps/${notExistingStepUUID}/dynamic-data`)
|
||||
.set('Authorization', token)
|
||||
.expect(404);
|
||||
});
|
||||
@@ -216,7 +216,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
|
||||
const step = await createStep({ appKey: null });
|
||||
|
||||
await request(app)
|
||||
.get(`/api/v1/steps/${step.id}/dynamic-data`)
|
||||
.post(`/api/v1/steps/${step.id}/dynamic-data`)
|
||||
.set('Authorization', token)
|
||||
.expect(404);
|
||||
});
|
||||
|
@@ -118,7 +118,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-fields', () => {
|
||||
const notExistingStepUUID = Crypto.randomUUID();
|
||||
|
||||
await request(app)
|
||||
.get(`/api/v1/steps/${notExistingStepUUID}/dynamic-fields`)
|
||||
.post(`/api/v1/steps/${notExistingStepUUID}/dynamic-fields`)
|
||||
.set('Authorization', token)
|
||||
.expect(404);
|
||||
});
|
||||
@@ -138,10 +138,11 @@ describe('POST /api/v1/steps/:stepId/dynamic-fields', () => {
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
const step = await createStep({ appKey: null });
|
||||
const step = await createStep();
|
||||
await step.$query().patch({ appKey: null });
|
||||
|
||||
await request(app)
|
||||
.get(`/api/v1/steps/${step.id}/dynamic-fields`)
|
||||
.post(`/api/v1/steps/${step.id}/dynamic-fields`)
|
||||
.set('Authorization', token)
|
||||
.expect(404);
|
||||
});
|
||||
|
@@ -0,0 +1,52 @@
|
||||
export async function up(knex) {
|
||||
await knex.schema.createTable('role_mappings', (table) => {
|
||||
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
||||
table
|
||||
.uuid('saml_auth_provider_id')
|
||||
.references('id')
|
||||
.inTable('saml_auth_providers');
|
||||
table.uuid('role_id').references('id').inTable('roles');
|
||||
table.string('remote_role_name').notNullable();
|
||||
|
||||
table.unique(['saml_auth_provider_id', 'remote_role_name']);
|
||||
|
||||
table.timestamps(true, true);
|
||||
});
|
||||
|
||||
const existingRoleMappings = await knex('saml_auth_providers_role_mappings');
|
||||
|
||||
if (existingRoleMappings.length) {
|
||||
await knex('role_mappings').insert(existingRoleMappings);
|
||||
}
|
||||
|
||||
return await knex.schema.dropTable('saml_auth_providers_role_mappings');
|
||||
}
|
||||
|
||||
export async function down(knex) {
|
||||
await knex.schema.createTable(
|
||||
'saml_auth_providers_role_mappings',
|
||||
(table) => {
|
||||
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
||||
table
|
||||
.uuid('saml_auth_provider_id')
|
||||
.references('id')
|
||||
.inTable('saml_auth_providers');
|
||||
table.uuid('role_id').references('id').inTable('roles');
|
||||
table.string('remote_role_name').notNullable();
|
||||
|
||||
table.unique(['saml_auth_provider_id', 'remote_role_name']);
|
||||
|
||||
table.timestamps(true, true);
|
||||
}
|
||||
);
|
||||
|
||||
const existingRoleMappings = await knex('role_mappings');
|
||||
|
||||
if (existingRoleMappings.length) {
|
||||
await knex('saml_auth_providers_role_mappings').insert(
|
||||
existingRoleMappings
|
||||
);
|
||||
}
|
||||
|
||||
return await knex.schema.dropTable('role_mappings');
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
export async function up(knex) {
|
||||
return await knex.schema.alterTable('app_configs', (table) => {
|
||||
table.boolean('use_only_predefined_auth_clients').defaultTo(false);
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex) {
|
||||
return await knex.schema.alterTable('app_configs', (table) => {
|
||||
table.dropColumn('use_only_predefined_auth_clients');
|
||||
});
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
export async function up(knex) {
|
||||
return await knex.schema.alterTable('app_configs', (table) => {
|
||||
table.dropColumn('shared');
|
||||
table.dropColumn('connection_allowed');
|
||||
table.dropColumn('custom_connection_allowed');
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex) {
|
||||
return await knex.schema.alterTable('app_configs', (table) => {
|
||||
table.boolean('shared').defaultTo(false);
|
||||
table.boolean('connection_allowed').defaultTo(false);
|
||||
table.boolean('custom_connection_allowed').defaultTo(false);
|
||||
});
|
||||
}
|
@@ -30,7 +30,7 @@ const findOrCreateUserBySamlIdentity = async (
|
||||
: [mappedUser.role];
|
||||
|
||||
const samlAuthProviderRoleMapping = await samlAuthProvider
|
||||
.$relatedQuery('samlAuthProvidersRoleMappings')
|
||||
.$relatedQuery('roleMappings')
|
||||
.whereIn('remote_role_name', mappedRoles)
|
||||
.limit(1)
|
||||
.first();
|
||||
|
46
packages/backend/src/helpers/user-ability.test.js
Normal file
46
packages/backend/src/helpers/user-ability.test.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import userAbility from './user-ability.js';
|
||||
|
||||
describe('userAbility', () => {
|
||||
it('should return PureAbility instantiated with user permissions', () => {
|
||||
const user = {
|
||||
permissions: [
|
||||
{
|
||||
subject: 'Flow',
|
||||
action: 'read',
|
||||
conditions: ['isCreator'],
|
||||
},
|
||||
],
|
||||
role: {
|
||||
name: 'User',
|
||||
},
|
||||
};
|
||||
|
||||
const ability = userAbility(user);
|
||||
|
||||
expect(ability.rules).toStrictEqual(user.permissions);
|
||||
});
|
||||
|
||||
it('should return permission-less PureAbility for user with no role', () => {
|
||||
const user = {
|
||||
permissions: [
|
||||
{
|
||||
subject: 'Flow',
|
||||
action: 'read',
|
||||
conditions: ['isCreator'],
|
||||
},
|
||||
],
|
||||
role: null,
|
||||
};
|
||||
const ability = userAbility(user);
|
||||
|
||||
expect(ability.rules).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it('should return permission-less PureAbility for user with no permissions', () => {
|
||||
const user = { permissions: null, role: { name: 'User' } };
|
||||
const ability = userAbility(user);
|
||||
|
||||
expect(ability.rules).toStrictEqual([]);
|
||||
});
|
||||
});
|
@@ -3,17 +3,9 @@
|
||||
exports[`AppConfig model > jsonSchema should have correct validations 1`] = `
|
||||
{
|
||||
"properties": {
|
||||
"connectionAllowed": {
|
||||
"default": false,
|
||||
"type": "boolean",
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
},
|
||||
"customConnectionAllowed": {
|
||||
"default": false,
|
||||
"type": "boolean",
|
||||
},
|
||||
"disabled": {
|
||||
"default": false,
|
||||
"type": "boolean",
|
||||
@@ -25,13 +17,13 @@ exports[`AppConfig model > jsonSchema should have correct validations 1`] = `
|
||||
"key": {
|
||||
"type": "string",
|
||||
},
|
||||
"shared": {
|
||||
"default": false,
|
||||
"type": "boolean",
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string",
|
||||
},
|
||||
"useOnlyPredefinedAuthClients": {
|
||||
"default": false,
|
||||
"type": "boolean",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"key",
|
||||
|
@@ -0,0 +1,30 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`RoleMapping model > jsonSchema should have the correct schema 1`] = `
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"format": "uuid",
|
||||
"type": "string",
|
||||
},
|
||||
"remoteRoleName": {
|
||||
"minLength": 1,
|
||||
"type": "string",
|
||||
},
|
||||
"roleId": {
|
||||
"format": "uuid",
|
||||
"type": "string",
|
||||
},
|
||||
"samlAuthProviderId": {
|
||||
"format": "uuid",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"samlAuthProviderId",
|
||||
"roleId",
|
||||
"remoteRoleName",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
@@ -1,6 +1,6 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`SamlAuthProvidersRoleMapping model > jsonSchema should have the correct schema 1`] = `
|
||||
exports[`RoleMapping model > jsonSchema should have the correct schema 1`] = `
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
@@ -60,39 +60,26 @@ class AppAuthClient extends Base {
|
||||
return this.authDefaults ? true : false;
|
||||
}
|
||||
|
||||
async triggerAppConfigUpdate() {
|
||||
const appConfig = await this.$relatedQuery('appConfig');
|
||||
|
||||
// This is a workaround to update connection allowed column for AppConfig
|
||||
await appConfig?.$query().patch({
|
||||
key: appConfig.key,
|
||||
shared: appConfig.shared,
|
||||
disabled: appConfig.disabled,
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Make another abstraction like beforeSave instead of using
|
||||
// beforeInsert and beforeUpdate separately for the same operation.
|
||||
async $beforeInsert(queryContext) {
|
||||
await super.$beforeInsert(queryContext);
|
||||
|
||||
this.encryptData();
|
||||
}
|
||||
|
||||
async $afterInsert(queryContext) {
|
||||
await super.$afterInsert(queryContext);
|
||||
|
||||
await this.triggerAppConfigUpdate();
|
||||
}
|
||||
|
||||
async $beforeUpdate(opt, queryContext) {
|
||||
await super.$beforeUpdate(opt, queryContext);
|
||||
|
||||
this.encryptData();
|
||||
}
|
||||
|
||||
async $afterUpdate(opt, queryContext) {
|
||||
await super.$afterUpdate(opt, queryContext);
|
||||
|
||||
await this.triggerAppConfigUpdate();
|
||||
}
|
||||
|
||||
async $afterFind() {
|
||||
|
@@ -7,7 +7,6 @@ import AppAuthClient from './app-auth-client.js';
|
||||
import Base from './base.js';
|
||||
import appConfig from '../config/app.js';
|
||||
import { createAppAuthClient } from '../../test/factories/app-auth-client.js';
|
||||
import { createAppConfig } from '../../test/factories/app-config.js';
|
||||
|
||||
describe('AppAuthClient model', () => {
|
||||
it('tableName should return correct name', () => {
|
||||
@@ -164,63 +163,6 @@ describe('AppAuthClient model', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('triggerAppConfigUpdate', () => {
|
||||
it('should trigger an update in related app config', async () => {
|
||||
await createAppConfig({ key: 'gitlab' });
|
||||
|
||||
const appAuthClient = await createAppAuthClient({
|
||||
appKey: 'gitlab',
|
||||
});
|
||||
|
||||
const appConfigBeforeUpdateSpy = vi.spyOn(
|
||||
AppConfig.prototype,
|
||||
'$beforeUpdate'
|
||||
);
|
||||
|
||||
await appAuthClient.triggerAppConfigUpdate();
|
||||
|
||||
expect(appConfigBeforeUpdateSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should update related AppConfig after creating an instance', async () => {
|
||||
const appConfig = await createAppConfig({
|
||||
key: 'gitlab',
|
||||
disabled: false,
|
||||
shared: true,
|
||||
});
|
||||
|
||||
await createAppAuthClient({
|
||||
appKey: 'gitlab',
|
||||
active: true,
|
||||
});
|
||||
|
||||
const refetchedAppConfig = await appConfig.$query();
|
||||
|
||||
expect(refetchedAppConfig.connectionAllowed).toBe(true);
|
||||
});
|
||||
|
||||
it('should update related AppConfig after updating an instance', async () => {
|
||||
const appConfig = await createAppConfig({
|
||||
key: 'gitlab',
|
||||
disabled: false,
|
||||
shared: true,
|
||||
});
|
||||
|
||||
const appAuthClient = await createAppAuthClient({
|
||||
appKey: 'gitlab',
|
||||
active: false,
|
||||
});
|
||||
|
||||
let refetchedAppConfig = await appConfig.$query();
|
||||
expect(refetchedAppConfig.connectionAllowed).toBe(false);
|
||||
|
||||
await appAuthClient.$query().patchAndFetch({ active: true });
|
||||
|
||||
refetchedAppConfig = await appConfig.$query();
|
||||
expect(refetchedAppConfig.connectionAllowed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('$beforeInsert should call AppAuthClient.encryptData', async () => {
|
||||
const appAuthClientBeforeInsertSpy = vi.spyOn(
|
||||
AppAuthClient.prototype,
|
||||
@@ -232,17 +174,6 @@ describe('AppAuthClient model', () => {
|
||||
expect(appAuthClientBeforeInsertSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('$afterInsert should call AppAuthClient.triggerAppConfigUpdate', async () => {
|
||||
const appAuthClientAfterInsertSpy = vi.spyOn(
|
||||
AppAuthClient.prototype,
|
||||
'triggerAppConfigUpdate'
|
||||
);
|
||||
|
||||
await createAppAuthClient();
|
||||
|
||||
expect(appAuthClientAfterInsertSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('$beforeUpdate should call AppAuthClient.encryptData', async () => {
|
||||
const appAuthClient = await createAppAuthClient();
|
||||
|
||||
@@ -256,19 +187,6 @@ describe('AppAuthClient model', () => {
|
||||
expect(appAuthClientBeforeUpdateSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('$afterUpdate should call AppAuthClient.triggerAppConfigUpdate', async () => {
|
||||
const appAuthClient = await createAppAuthClient();
|
||||
|
||||
const appAuthClientAfterUpdateSpy = vi.spyOn(
|
||||
AppAuthClient.prototype,
|
||||
'triggerAppConfigUpdate'
|
||||
);
|
||||
|
||||
await appAuthClient.$query().patchAndFetch({ name: 'sample' });
|
||||
|
||||
expect(appAuthClientAfterUpdateSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('$afterFind should call AppAuthClient.decryptData', async () => {
|
||||
const appAuthClient = await createAppAuthClient();
|
||||
|
||||
|
@@ -16,9 +16,7 @@ class AppConfig extends Base {
|
||||
properties: {
|
||||
id: { type: 'string', format: 'uuid' },
|
||||
key: { type: 'string' },
|
||||
connectionAllowed: { type: 'boolean', default: false },
|
||||
customConnectionAllowed: { type: 'boolean', default: false },
|
||||
shared: { type: 'boolean', default: false },
|
||||
useOnlyPredefinedAuthClients: { type: 'boolean', default: false },
|
||||
disabled: { type: 'boolean', default: false },
|
||||
createdAt: { type: 'string' },
|
||||
updatedAt: { type: 'string' },
|
||||
@@ -41,39 +39,6 @@ class AppConfig extends Base {
|
||||
|
||||
return await App.findOneByKey(this.key);
|
||||
}
|
||||
|
||||
async computeAndAssignConnectionAllowedProperty() {
|
||||
this.connectionAllowed = await this.computeConnectionAllowedProperty();
|
||||
}
|
||||
|
||||
async computeConnectionAllowedProperty() {
|
||||
const appAuthClients = await this.$relatedQuery('appAuthClients');
|
||||
|
||||
const hasSomeActiveAppAuthClients =
|
||||
appAuthClients?.some((appAuthClient) => appAuthClient.active) || false;
|
||||
|
||||
const conditions = [
|
||||
hasSomeActiveAppAuthClients,
|
||||
this.shared,
|
||||
!this.disabled,
|
||||
];
|
||||
|
||||
const connectionAllowed = conditions.every(Boolean);
|
||||
|
||||
return connectionAllowed;
|
||||
}
|
||||
|
||||
async $beforeInsert(queryContext) {
|
||||
await super.$beforeInsert(queryContext);
|
||||
|
||||
await this.computeAndAssignConnectionAllowedProperty();
|
||||
}
|
||||
|
||||
async $beforeUpdate(opt, queryContext) {
|
||||
await super.$beforeUpdate(opt, queryContext);
|
||||
|
||||
await this.computeAndAssignConnectionAllowedProperty();
|
||||
}
|
||||
}
|
||||
|
||||
export default AppConfig;
|
||||
|
@@ -1,11 +1,9 @@
|
||||
import { vi, describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import Base from './base.js';
|
||||
import AppConfig from './app-config.js';
|
||||
import App from './app.js';
|
||||
import AppAuthClient from './app-auth-client.js';
|
||||
import { createAppConfig } from '../../test/factories/app-config.js';
|
||||
import { createAppAuthClient } from '../../test/factories/app-auth-client.js';
|
||||
|
||||
describe('AppConfig model', () => {
|
||||
it('tableName should return correct name', () => {
|
||||
@@ -55,126 +53,4 @@ describe('AppConfig model', () => {
|
||||
expect(app).toStrictEqual(expectedApp);
|
||||
});
|
||||
});
|
||||
|
||||
describe('computeAndAssignConnectionAllowedProperty', () => {
|
||||
it('should call computeConnectionAllowedProperty and assign the result', async () => {
|
||||
const appConfig = await createAppConfig();
|
||||
|
||||
const computeConnectionAllowedPropertySpy = vi
|
||||
.spyOn(appConfig, 'computeConnectionAllowedProperty')
|
||||
.mockResolvedValue(true);
|
||||
|
||||
await appConfig.computeAndAssignConnectionAllowedProperty();
|
||||
|
||||
expect(computeConnectionAllowedPropertySpy).toHaveBeenCalled();
|
||||
expect(appConfig.connectionAllowed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('computeConnectionAllowedProperty', () => {
|
||||
it('should return true when app is enabled, shared and allows custom connection with an active app auth client', async () => {
|
||||
await createAppAuthClient({
|
||||
appKey: 'deepl',
|
||||
active: true,
|
||||
});
|
||||
|
||||
await createAppAuthClient({
|
||||
appKey: 'deepl',
|
||||
active: false,
|
||||
});
|
||||
|
||||
const appConfig = await createAppConfig({
|
||||
disabled: false,
|
||||
customConnectionAllowed: true,
|
||||
shared: true,
|
||||
key: 'deepl',
|
||||
});
|
||||
|
||||
const connectionAllowed =
|
||||
await appConfig.computeConnectionAllowedProperty();
|
||||
|
||||
expect(connectionAllowed).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if there is no active app auth client', async () => {
|
||||
await createAppAuthClient({
|
||||
appKey: 'deepl',
|
||||
active: false,
|
||||
});
|
||||
|
||||
const appConfig = await createAppConfig({
|
||||
disabled: false,
|
||||
customConnectionAllowed: true,
|
||||
shared: true,
|
||||
key: 'deepl',
|
||||
});
|
||||
|
||||
const connectionAllowed =
|
||||
await appConfig.computeConnectionAllowedProperty();
|
||||
|
||||
expect(connectionAllowed).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if there is no app auth clients', async () => {
|
||||
const appConfig = await createAppConfig({
|
||||
disabled: false,
|
||||
customConnectionAllowed: true,
|
||||
shared: true,
|
||||
key: 'deepl',
|
||||
});
|
||||
|
||||
const connectionAllowed =
|
||||
await appConfig.computeConnectionAllowedProperty();
|
||||
|
||||
expect(connectionAllowed).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when app is disabled', async () => {
|
||||
const appConfig = await createAppConfig({
|
||||
disabled: true,
|
||||
customConnectionAllowed: true,
|
||||
});
|
||||
|
||||
const connectionAllowed =
|
||||
await appConfig.computeConnectionAllowedProperty();
|
||||
|
||||
expect(connectionAllowed).toBe(false);
|
||||
});
|
||||
|
||||
it(`should return false when app doesn't allow custom connection`, async () => {
|
||||
const appConfig = await createAppConfig({
|
||||
disabled: false,
|
||||
customConnectionAllowed: false,
|
||||
});
|
||||
|
||||
const connectionAllowed =
|
||||
await appConfig.computeConnectionAllowedProperty();
|
||||
|
||||
expect(connectionAllowed).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('$beforeInsert should call computeAndAssignConnectionAllowedProperty', async () => {
|
||||
const computeAndAssignConnectionAllowedPropertySpy = vi
|
||||
.spyOn(AppConfig.prototype, 'computeAndAssignConnectionAllowedProperty')
|
||||
.mockResolvedValue(true);
|
||||
|
||||
await createAppConfig();
|
||||
|
||||
expect(computeAndAssignConnectionAllowedPropertySpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('$beforeUpdate should call computeAndAssignConnectionAllowedProperty', async () => {
|
||||
const appConfig = await createAppConfig();
|
||||
|
||||
const computeAndAssignConnectionAllowedPropertySpy = vi
|
||||
.spyOn(AppConfig.prototype, 'computeAndAssignConnectionAllowedProperty')
|
||||
.mockResolvedValue(true);
|
||||
|
||||
await appConfig.$query().patch({
|
||||
key: 'deepl',
|
||||
});
|
||||
|
||||
expect(computeAndAssignConnectionAllowedPropertySpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
@@ -33,10 +33,6 @@ class Connection extends Base {
|
||||
},
|
||||
};
|
||||
|
||||
static get virtualAttributes() {
|
||||
return ['reconnectable'];
|
||||
}
|
||||
|
||||
static relationMappings = () => ({
|
||||
user: {
|
||||
relation: Base.BelongsToOneRelation,
|
||||
@@ -83,18 +79,6 @@ class Connection extends Base {
|
||||
},
|
||||
});
|
||||
|
||||
get reconnectable() {
|
||||
if (this.appAuthClientId) {
|
||||
return this.appAuthClient.active;
|
||||
}
|
||||
|
||||
if (this.appConfig) {
|
||||
return !this.appConfig.disabled && this.appConfig.customConnectionAllowed;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
encryptData() {
|
||||
if (!this.eligibleForEncryption()) return;
|
||||
|
||||
@@ -144,19 +128,13 @@ class Connection extends Base {
|
||||
);
|
||||
}
|
||||
|
||||
if (!appConfig.customConnectionAllowed && this.formattedData) {
|
||||
if (appConfig.useOnlyPredefinedAuthClients && this.formattedData) {
|
||||
throw new NotAuthorizedError(
|
||||
`New custom connections have been disabled for ${app.name}!`
|
||||
);
|
||||
}
|
||||
|
||||
if (!appConfig.shared && this.appAuthClientId) {
|
||||
throw new NotAuthorizedError(
|
||||
'The connection with the given app auth client is not allowed!'
|
||||
);
|
||||
}
|
||||
|
||||
if (appConfig.shared && !this.formattedData) {
|
||||
if (!this.formattedData) {
|
||||
const authClient = await appConfig
|
||||
.$relatedQuery('appAuthClients')
|
||||
.findById(this.appAuthClientId)
|
||||
|
@@ -23,14 +23,6 @@ describe('Connection model', () => {
|
||||
expect(Connection.jsonSchema).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('virtualAttributes should return correct attributes', () => {
|
||||
const virtualAttributes = Connection.virtualAttributes;
|
||||
|
||||
const expectedAttributes = ['reconnectable'];
|
||||
|
||||
expect(virtualAttributes).toStrictEqual(expectedAttributes);
|
||||
});
|
||||
|
||||
describe('relationMappings', () => {
|
||||
it('should return correct associations', () => {
|
||||
const relationMappings = Connection.relationMappings();
|
||||
@@ -92,78 +84,6 @@ describe('Connection model', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('reconnectable', () => {
|
||||
it('should return active status of app auth client when created via app auth client', async () => {
|
||||
const appAuthClient = await createAppAuthClient({
|
||||
active: true,
|
||||
formattedAuthDefaults: {
|
||||
clientId: 'sample-id',
|
||||
},
|
||||
});
|
||||
|
||||
const connection = await createConnection({
|
||||
appAuthClientId: appAuthClient.id,
|
||||
formattedData: {
|
||||
token: 'sample-token',
|
||||
},
|
||||
});
|
||||
|
||||
const connectionWithAppAuthClient = await connection
|
||||
.$query()
|
||||
.withGraphFetched({
|
||||
appAuthClient: true,
|
||||
});
|
||||
|
||||
expect(connectionWithAppAuthClient.reconnectable).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when app config is not disabled and allows custom connection', async () => {
|
||||
const appConfig = await createAppConfig({
|
||||
key: 'gitlab',
|
||||
disabled: false,
|
||||
customConnectionAllowed: true,
|
||||
});
|
||||
|
||||
const connection = await createConnection({
|
||||
key: appConfig.key,
|
||||
formattedData: {
|
||||
token: 'sample-token',
|
||||
},
|
||||
});
|
||||
|
||||
const connectionWithAppAuthClient = await connection
|
||||
.$query()
|
||||
.withGraphFetched({
|
||||
appConfig: true,
|
||||
});
|
||||
|
||||
expect(connectionWithAppAuthClient.reconnectable).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when app config is disabled or does not allow custom connection', async () => {
|
||||
const connection = await createConnection({
|
||||
key: 'gitlab',
|
||||
formattedData: {
|
||||
token: 'sample-token',
|
||||
},
|
||||
});
|
||||
|
||||
await createAppConfig({
|
||||
key: 'gitlab',
|
||||
disabled: true,
|
||||
customConnectionAllowed: false,
|
||||
});
|
||||
|
||||
const connectionWithAppAuthClient = await connection
|
||||
.$query()
|
||||
.withGraphFetched({
|
||||
appConfig: true,
|
||||
});
|
||||
|
||||
expect(connectionWithAppAuthClient.reconnectable).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('encryptData', () => {
|
||||
it('should return undefined if eligibleForEncryption is not true', async () => {
|
||||
vi.spyOn(Connection.prototype, 'eligibleForEncryption').mockReturnValue(
|
||||
@@ -366,6 +286,7 @@ describe('Connection model', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// TODO: update test case name
|
||||
it('should throw an error when app config does not allow custom connection with formatted data', async () => {
|
||||
vi.spyOn(Connection.prototype, 'getApp').mockResolvedValue({
|
||||
name: 'gitlab',
|
||||
@@ -373,7 +294,7 @@ describe('Connection model', () => {
|
||||
|
||||
vi.spyOn(Connection.prototype, 'getAppConfig').mockResolvedValue({
|
||||
disabled: false,
|
||||
customConnectionAllowed: false,
|
||||
useOnlyPredefinedAuthClients: true,
|
||||
});
|
||||
|
||||
const connection = new Connection();
|
||||
@@ -386,32 +307,10 @@ describe('Connection model', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error when app config is not shared with app auth client', async () => {
|
||||
vi.spyOn(Connection.prototype, 'getApp').mockResolvedValue({
|
||||
name: 'gitlab',
|
||||
});
|
||||
|
||||
vi.spyOn(Connection.prototype, 'getAppConfig').mockResolvedValue({
|
||||
disabled: false,
|
||||
shared: false,
|
||||
});
|
||||
|
||||
const connection = new Connection();
|
||||
connection.appAuthClientId = 'sample-id';
|
||||
|
||||
await expect(() =>
|
||||
connection.checkEligibilityForCreation()
|
||||
).rejects.toThrow(
|
||||
'The connection with the given app auth client is not allowed!'
|
||||
);
|
||||
});
|
||||
|
||||
it('should apply app auth client auth defaults when creating with shared app auth client', async () => {
|
||||
await createAppConfig({
|
||||
key: 'gitlab',
|
||||
disabled: false,
|
||||
customConnectionAllowed: true,
|
||||
shared: true,
|
||||
});
|
||||
|
||||
const appAuthClient = await createAppAuthClient({
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import Base from './base.js';
|
||||
import SamlAuthProvider from './saml-auth-provider.ee.js';
|
||||
|
||||
class SamlAuthProvidersRoleMapping extends Base {
|
||||
static tableName = 'saml_auth_providers_role_mappings';
|
||||
class RoleMapping extends Base {
|
||||
static tableName = 'role_mappings';
|
||||
|
||||
static jsonSchema = {
|
||||
type: 'object',
|
||||
@@ -21,11 +21,11 @@ class SamlAuthProvidersRoleMapping extends Base {
|
||||
relation: Base.BelongsToOneRelation,
|
||||
modelClass: SamlAuthProvider,
|
||||
join: {
|
||||
from: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
|
||||
from: 'role_mappings.saml_auth_provider_id',
|
||||
to: 'saml_auth_providers.id',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default SamlAuthProvidersRoleMapping;
|
||||
export default RoleMapping;
|
@@ -1,28 +1,26 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import SamlAuthProvidersRoleMapping from '../models/saml-auth-providers-role-mapping.ee';
|
||||
import RoleMapping from './role-mapping.ee';
|
||||
import SamlAuthProvider from './saml-auth-provider.ee';
|
||||
import Base from './base';
|
||||
|
||||
describe('SamlAuthProvidersRoleMapping model', () => {
|
||||
describe('RoleMapping model', () => {
|
||||
it('tableName should return correct name', () => {
|
||||
expect(SamlAuthProvidersRoleMapping.tableName).toBe(
|
||||
'saml_auth_providers_role_mappings'
|
||||
);
|
||||
expect(RoleMapping.tableName).toBe('role_mappings');
|
||||
});
|
||||
|
||||
it('jsonSchema should have the correct schema', () => {
|
||||
expect(SamlAuthProvidersRoleMapping.jsonSchema).toMatchSnapshot();
|
||||
expect(RoleMapping.jsonSchema).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('relationMappings should return correct associations', () => {
|
||||
const relationMappings = SamlAuthProvidersRoleMapping.relationMappings();
|
||||
const relationMappings = RoleMapping.relationMappings();
|
||||
|
||||
const expectedRelations = {
|
||||
samlAuthProvider: {
|
||||
relation: Base.BelongsToOneRelation,
|
||||
modelClass: SamlAuthProvider,
|
||||
join: {
|
||||
from: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
|
||||
from: 'role_mappings.saml_auth_provider_id',
|
||||
to: 'saml_auth_providers.id',
|
||||
},
|
||||
},
|
@@ -5,7 +5,7 @@ import appConfig from '../config/app.js';
|
||||
import axios from '../helpers/axios-with-proxy.js';
|
||||
import Base from './base.js';
|
||||
import Identity from './identity.ee.js';
|
||||
import SamlAuthProvidersRoleMapping from './saml-auth-providers-role-mapping.ee.js';
|
||||
import RoleMapping from './role-mapping.ee.js';
|
||||
|
||||
class SamlAuthProvider extends Base {
|
||||
static tableName = 'saml_auth_providers';
|
||||
@@ -53,12 +53,12 @@ class SamlAuthProvider extends Base {
|
||||
to: 'saml_auth_providers.id',
|
||||
},
|
||||
},
|
||||
samlAuthProvidersRoleMappings: {
|
||||
roleMappings: {
|
||||
relation: Base.HasManyRelation,
|
||||
modelClass: SamlAuthProvidersRoleMapping,
|
||||
modelClass: RoleMapping,
|
||||
join: {
|
||||
from: 'saml_auth_providers.id',
|
||||
to: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
|
||||
to: 'role_mappings.saml_auth_provider_id',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -133,27 +133,22 @@ class SamlAuthProvider extends Base {
|
||||
}
|
||||
|
||||
async updateRoleMappings(roleMappings) {
|
||||
return await SamlAuthProvider.transaction(async (trx) => {
|
||||
await this.$relatedQuery('samlAuthProvidersRoleMappings', trx).delete();
|
||||
await this.$relatedQuery('roleMappings').delete();
|
||||
|
||||
if (isEmpty(roleMappings)) {
|
||||
return [];
|
||||
}
|
||||
if (isEmpty(roleMappings)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const samlAuthProvidersRoleMappingsData = roleMappings.map(
|
||||
(samlAuthProvidersRoleMapping) => ({
|
||||
...samlAuthProvidersRoleMapping,
|
||||
samlAuthProviderId: this.id,
|
||||
})
|
||||
);
|
||||
const roleMappingsData = roleMappings.map((roleMapping) => ({
|
||||
...roleMapping,
|
||||
samlAuthProviderId: this.id,
|
||||
}));
|
||||
|
||||
const samlAuthProvidersRoleMappings =
|
||||
await SamlAuthProvidersRoleMapping.query(trx).insertAndFetch(
|
||||
samlAuthProvidersRoleMappingsData
|
||||
);
|
||||
const newRoleMappings = await RoleMapping.query().insertAndFetch(
|
||||
roleMappingsData
|
||||
);
|
||||
|
||||
return samlAuthProvidersRoleMappings;
|
||||
});
|
||||
return newRoleMappings;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,9 +1,14 @@
|
||||
import { vi, describe, it, expect } from 'vitest';
|
||||
import { vi, beforeEach, describe, it, expect } from 'vitest';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import SamlAuthProvider from '../models/saml-auth-provider.ee';
|
||||
import SamlAuthProvidersRoleMapping from '../models/saml-auth-providers-role-mapping.ee';
|
||||
import RoleMapping from '../models/role-mapping.ee';
|
||||
import axios from '../helpers/axios-with-proxy.js';
|
||||
import Identity from './identity.ee';
|
||||
import Base from './base';
|
||||
import appConfig from '../config/app';
|
||||
import { createSamlAuthProvider } from '../../test/factories/saml-auth-provider.ee.js';
|
||||
import { createRoleMapping } from '../../test/factories/role-mapping.js';
|
||||
import { createRole } from '../../test/factories/role.js';
|
||||
|
||||
describe('SamlAuthProvider model', () => {
|
||||
it('tableName should return correct name', () => {
|
||||
@@ -26,12 +31,12 @@ describe('SamlAuthProvider model', () => {
|
||||
to: 'saml_auth_providers.id',
|
||||
},
|
||||
},
|
||||
samlAuthProvidersRoleMappings: {
|
||||
roleMappings: {
|
||||
relation: Base.HasManyRelation,
|
||||
modelClass: SamlAuthProvidersRoleMapping,
|
||||
modelClass: RoleMapping,
|
||||
join: {
|
||||
from: 'saml_auth_providers.id',
|
||||
to: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
|
||||
to: 'role_mappings.saml_auth_provider_id',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -81,4 +86,146 @@ describe('SamlAuthProvider model', () => {
|
||||
'https://example.com/saml/logout'
|
||||
);
|
||||
});
|
||||
|
||||
it('config should return the correct configuration object', () => {
|
||||
const samlAuthProvider = new SamlAuthProvider();
|
||||
|
||||
samlAuthProvider.certificate = 'sample-certificate';
|
||||
samlAuthProvider.signatureAlgorithm = 'sha256';
|
||||
samlAuthProvider.entryPoint = 'https://example.com/saml';
|
||||
samlAuthProvider.issuer = 'sample-issuer';
|
||||
|
||||
vi.spyOn(appConfig, 'baseUrl', 'get').mockReturnValue(
|
||||
'https://automatisch.io'
|
||||
);
|
||||
|
||||
const expectedConfig = {
|
||||
callbackUrl: 'https://automatisch.io/login/saml/sample-issuer/callback',
|
||||
cert: 'sample-certificate',
|
||||
entryPoint: 'https://example.com/saml',
|
||||
issuer: 'sample-issuer',
|
||||
signatureAlgorithm: 'sha256',
|
||||
logoutUrl: 'https://example.com/saml',
|
||||
};
|
||||
|
||||
expect(samlAuthProvider.config).toStrictEqual(expectedConfig);
|
||||
});
|
||||
|
||||
it('generateLogoutRequestBody should return a correctly encoded SAML logout request', () => {
|
||||
vi.mock('uuid', () => ({
|
||||
v4: vi.fn(),
|
||||
}));
|
||||
|
||||
const samlAuthProvider = new SamlAuthProvider();
|
||||
|
||||
samlAuthProvider.entryPoint = 'https://example.com/saml';
|
||||
samlAuthProvider.issuer = 'sample-issuer';
|
||||
|
||||
const mockUuid = '123e4567-e89b-12d3-a456-426614174000';
|
||||
uuidv4.mockReturnValue(mockUuid);
|
||||
|
||||
const sessionId = 'test-session-id';
|
||||
|
||||
const logoutRequest = samlAuthProvider.generateLogoutRequestBody(sessionId);
|
||||
|
||||
const expectedLogoutRequest = `
|
||||
<samlp:LogoutRequest
|
||||
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
|
||||
ID="${mockUuid}"
|
||||
Version="2.0"
|
||||
IssueInstant="${new Date().toISOString()}"
|
||||
Destination="https://example.com/saml">
|
||||
|
||||
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">sample-issuer</saml:Issuer>
|
||||
<samlp:SessionIndex>test-session-id</samlp:SessionIndex>
|
||||
</samlp:LogoutRequest>
|
||||
`;
|
||||
|
||||
const expectedEncodedRequest = Buffer.from(expectedLogoutRequest).toString(
|
||||
'base64'
|
||||
);
|
||||
|
||||
expect(logoutRequest).toBe(expectedEncodedRequest);
|
||||
});
|
||||
|
||||
it('terminateRemoteSession should send the correct POST request and return the response', async () => {
|
||||
vi.mock('../helpers/axios-with-proxy.js', () => ({
|
||||
default: {
|
||||
post: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const samlAuthProvider = new SamlAuthProvider();
|
||||
|
||||
samlAuthProvider.entryPoint = 'https://example.com/saml';
|
||||
samlAuthProvider.generateLogoutRequestBody = vi
|
||||
.fn()
|
||||
.mockReturnValue('mockEncodedLogoutRequest');
|
||||
|
||||
const sessionId = 'test-session-id';
|
||||
|
||||
const mockResponse = { data: 'Logout Successful' };
|
||||
axios.post.mockResolvedValue(mockResponse);
|
||||
|
||||
const response = await samlAuthProvider.terminateRemoteSession(sessionId);
|
||||
|
||||
expect(samlAuthProvider.generateLogoutRequestBody).toHaveBeenCalledWith(
|
||||
sessionId
|
||||
);
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
'https://example.com/saml',
|
||||
'SAMLRequest=mockEncodedLogoutRequest',
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(response).toBe(mockResponse);
|
||||
});
|
||||
|
||||
describe('updateRoleMappings', () => {
|
||||
let samlAuthProvider;
|
||||
|
||||
beforeEach(async () => {
|
||||
samlAuthProvider = await createSamlAuthProvider();
|
||||
});
|
||||
|
||||
it('should remove all existing role mappings', async () => {
|
||||
await createRoleMapping({
|
||||
samlAuthProviderId: samlAuthProvider.id,
|
||||
remoteRoleName: 'Admin',
|
||||
});
|
||||
|
||||
await createRoleMapping({
|
||||
samlAuthProviderId: samlAuthProvider.id,
|
||||
remoteRoleName: 'User',
|
||||
});
|
||||
|
||||
await samlAuthProvider.updateRoleMappings([]);
|
||||
|
||||
const roleMappings = await samlAuthProvider.$relatedQuery('roleMappings');
|
||||
expect(roleMappings).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it('should return the updated role mappings when new ones are provided', async () => {
|
||||
const adminRole = await createRole({ name: 'Admin' });
|
||||
const userRole = await createRole({ name: 'User' });
|
||||
|
||||
const newRoleMappings = [
|
||||
{ remoteRoleName: 'Admin', roleId: adminRole.id },
|
||||
{ remoteRoleName: 'User', roleId: userRole.id },
|
||||
];
|
||||
|
||||
const result = await samlAuthProvider.updateRoleMappings(newRoleMappings);
|
||||
|
||||
const refetchedRoleMappings = await samlAuthProvider.$relatedQuery(
|
||||
'roleMappings'
|
||||
);
|
||||
|
||||
expect(result).toStrictEqual(refetchedRoleMappings);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -212,6 +212,10 @@ class User extends Base {
|
||||
return `${appConfig.webAppUrl}/accept-invitation?token=${this.invitationToken}`;
|
||||
}
|
||||
|
||||
get ability() {
|
||||
return userAbility(this);
|
||||
}
|
||||
|
||||
static async authenticate(email, password) {
|
||||
const user = await User.query().findOne({
|
||||
email: email?.toLowerCase() || null,
|
||||
@@ -366,18 +370,6 @@ class User extends Base {
|
||||
return now.getTime() - sentAt.getTime() < fourHoursInMilliseconds;
|
||||
}
|
||||
|
||||
toTestTestCoverage() {
|
||||
if (!this.resetPasswordTokenSentAt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const sentAt = new Date(this.resetPasswordTokenSentAt);
|
||||
const now = new Date();
|
||||
const fourHoursInMilliseconds = 1000 * 60 * 60 * 4;
|
||||
|
||||
return now.getTime() - sentAt.getTime() < fourHoursInMilliseconds;
|
||||
}
|
||||
|
||||
async sendInvitationEmail() {
|
||||
await this.generateInvitationToken();
|
||||
|
||||
@@ -595,62 +587,6 @@ class User extends Base {
|
||||
return user;
|
||||
}
|
||||
|
||||
async $beforeInsert(queryContext) {
|
||||
await super.$beforeInsert(queryContext);
|
||||
|
||||
this.email = this.email.toLowerCase();
|
||||
await this.generateHash();
|
||||
|
||||
if (appConfig.isCloud) {
|
||||
this.startTrialPeriod();
|
||||
}
|
||||
}
|
||||
|
||||
async $beforeUpdate(opt, queryContext) {
|
||||
await super.$beforeUpdate(opt, queryContext);
|
||||
|
||||
if (this.email) {
|
||||
this.email = this.email.toLowerCase();
|
||||
}
|
||||
|
||||
await this.generateHash();
|
||||
}
|
||||
|
||||
async $afterInsert(queryContext) {
|
||||
await super.$afterInsert(queryContext);
|
||||
|
||||
if (appConfig.isCloud) {
|
||||
await this.$relatedQuery('usageData').insert({
|
||||
userId: this.id,
|
||||
consumedTaskCount: 0,
|
||||
nextResetAt: DateTime.now().plus({ days: 30 }).toISODate(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async $afterFind() {
|
||||
if (await hasValidLicense()) return this;
|
||||
|
||||
if (Array.isArray(this.permissions)) {
|
||||
this.permissions = this.permissions.filter((permission) => {
|
||||
const restrictedSubjects = [
|
||||
'App',
|
||||
'Role',
|
||||
'SamlAuthProvider',
|
||||
'Config',
|
||||
];
|
||||
|
||||
return !restrictedSubjects.includes(permission.subject);
|
||||
});
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
get ability() {
|
||||
return userAbility(this);
|
||||
}
|
||||
|
||||
can(action, subject) {
|
||||
const can = this.ability.can(action, subject);
|
||||
|
||||
@@ -666,12 +602,68 @@ class User extends Base {
|
||||
return conditionMap;
|
||||
}
|
||||
|
||||
cannot(action, subject) {
|
||||
const cannot = this.ability.cannot(action, subject);
|
||||
lowercaseEmail() {
|
||||
if (this.email) {
|
||||
this.email = this.email.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
if (cannot) throw new NotAuthorizedError();
|
||||
async createUsageData() {
|
||||
if (appConfig.isCloud) {
|
||||
return await this.$relatedQuery('usageData').insertAndFetch({
|
||||
userId: this.id,
|
||||
consumedTaskCount: 0,
|
||||
nextResetAt: DateTime.now().plus({ days: 30 }).toISODate(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return cannot;
|
||||
async omitEnterprisePermissionsWithoutValidLicense() {
|
||||
if (await hasValidLicense()) {
|
||||
return this;
|
||||
}
|
||||
|
||||
if (Array.isArray(this.permissions)) {
|
||||
this.permissions = this.permissions.filter((permission) => {
|
||||
const restrictedSubjects = [
|
||||
'App',
|
||||
'Role',
|
||||
'SamlAuthProvider',
|
||||
'Config',
|
||||
];
|
||||
|
||||
return !restrictedSubjects.includes(permission.subject);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async $beforeInsert(queryContext) {
|
||||
await super.$beforeInsert(queryContext);
|
||||
|
||||
this.lowercaseEmail();
|
||||
await this.generateHash();
|
||||
|
||||
if (appConfig.isCloud) {
|
||||
this.startTrialPeriod();
|
||||
}
|
||||
}
|
||||
|
||||
async $beforeUpdate(opt, queryContext) {
|
||||
await super.$beforeUpdate(opt, queryContext);
|
||||
|
||||
this.lowercaseEmail();
|
||||
|
||||
await this.generateHash();
|
||||
}
|
||||
|
||||
async $afterInsert(queryContext) {
|
||||
await super.$afterInsert(queryContext);
|
||||
|
||||
await this.createUsageData();
|
||||
}
|
||||
|
||||
async $afterFind() {
|
||||
await this.omitEnterprisePermissionsWithoutValidLicense();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,8 +1,10 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { DateTime, Duration } from 'luxon';
|
||||
import appConfig from '../config/app.js';
|
||||
import * as licenseModule from '../helpers/license.ee.js';
|
||||
import Base from './base.js';
|
||||
import AccessToken from './access-token.js';
|
||||
import Config from './config.js';
|
||||
import Connection from './connection.js';
|
||||
import Execution from './execution.js';
|
||||
import Flow from './flow.js';
|
||||
@@ -19,6 +21,7 @@ import {
|
||||
REMOVE_AFTER_30_DAYS_OR_150_JOBS,
|
||||
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
|
||||
} from '../helpers/remove-job-configuration.js';
|
||||
import * as userAbilityModule from '../helpers/user-ability.js';
|
||||
import { createUser } from '../../test/factories/user.js';
|
||||
import { createConnection } from '../../test/factories/connection.js';
|
||||
import { createRole } from '../../test/factories/role.js';
|
||||
@@ -26,6 +29,9 @@ import { createPermission } from '../../test/factories/permission.js';
|
||||
import { createFlow } from '../../test/factories/flow.js';
|
||||
import { createStep } from '../../test/factories/step.js';
|
||||
import { createExecution } from '../../test/factories/execution.js';
|
||||
import { createSubscription } from '../../test/factories/subscription.js';
|
||||
import { createUsageData } from '../../test/factories/usage-data.js';
|
||||
import Billing from '../helpers/billing/index.ee.js';
|
||||
|
||||
describe('User model', () => {
|
||||
it('tableName should return correct name', () => {
|
||||
@@ -201,64 +207,6 @@ describe('User model', () => {
|
||||
expect(virtualAttributes).toStrictEqual(expectedAttributes);
|
||||
});
|
||||
|
||||
it('acceptInvitationUrl should return accept invitation page URL with invitation token', async () => {
|
||||
const user = new User();
|
||||
user.invitationToken = 'invitation-token';
|
||||
|
||||
vi.spyOn(appConfig, 'webAppUrl', 'get').mockReturnValue(
|
||||
'https://automatisch.io'
|
||||
);
|
||||
|
||||
expect(user.acceptInvitationUrl).toBe(
|
||||
'https://automatisch.io/accept-invitation?token=invitation-token'
|
||||
);
|
||||
});
|
||||
|
||||
describe('authenticate', () => {
|
||||
it('should create and return the token for correct email and password', async () => {
|
||||
const user = await createUser({
|
||||
email: 'test-user@automatisch.io',
|
||||
password: 'sample-password',
|
||||
});
|
||||
|
||||
const token = await User.authenticate(
|
||||
'test-user@automatisch.io',
|
||||
'sample-password'
|
||||
);
|
||||
|
||||
const persistedToken = await AccessToken.query().findOne({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
expect(token).toBe(persistedToken.token);
|
||||
});
|
||||
|
||||
it('should return undefined for existing email and incorrect password', async () => {
|
||||
await createUser({
|
||||
email: 'test-user@automatisch.io',
|
||||
password: 'sample-password',
|
||||
});
|
||||
|
||||
const token = await User.authenticate(
|
||||
'test-user@automatisch.io',
|
||||
'wrong-password'
|
||||
);
|
||||
|
||||
expect(token).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should return undefined for non-existing email', async () => {
|
||||
await createUser({
|
||||
email: 'test-user@automatisch.io',
|
||||
password: 'sample-password',
|
||||
});
|
||||
|
||||
const token = await User.authenticate('non-existing-user@automatisch.io');
|
||||
|
||||
expect(token).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('authorizedFlows', () => {
|
||||
it('should return user flows with isCreator condition', async () => {
|
||||
const userRole = await createRole({ name: 'User' });
|
||||
@@ -428,7 +376,10 @@ describe('User model', () => {
|
||||
const anotherUserConnection = await createConnection();
|
||||
|
||||
expect(
|
||||
await userWithRoleAndPermissions.authorizedConnections
|
||||
await userWithRoleAndPermissions.authorizedConnections.orderBy(
|
||||
'created_at',
|
||||
'asc'
|
||||
)
|
||||
).toStrictEqual([userConnection, anotherUserConnection]);
|
||||
});
|
||||
|
||||
@@ -501,6 +452,76 @@ describe('User model', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('acceptInvitationUrl should return accept invitation page URL with invitation token', async () => {
|
||||
const user = new User();
|
||||
user.invitationToken = 'invitation-token';
|
||||
|
||||
vi.spyOn(appConfig, 'webAppUrl', 'get').mockReturnValue(
|
||||
'https://automatisch.io'
|
||||
);
|
||||
|
||||
expect(user.acceptInvitationUrl).toBe(
|
||||
'https://automatisch.io/accept-invitation?token=invitation-token'
|
||||
);
|
||||
});
|
||||
|
||||
it('ability should return userAbility for the user', () => {
|
||||
const user = new User();
|
||||
user.fullName = 'Sample user';
|
||||
|
||||
const userAbilitySpy = vi
|
||||
.spyOn(userAbilityModule, 'default')
|
||||
.mockReturnValue('user-ability');
|
||||
|
||||
expect(user.ability).toStrictEqual('user-ability');
|
||||
expect(userAbilitySpy).toHaveBeenNthCalledWith(1, user);
|
||||
});
|
||||
|
||||
describe('authenticate', () => {
|
||||
it('should create and return the token for correct email and password', async () => {
|
||||
const user = await createUser({
|
||||
email: 'test-user@automatisch.io',
|
||||
password: 'sample-password',
|
||||
});
|
||||
|
||||
const token = await User.authenticate(
|
||||
'test-user@automatisch.io',
|
||||
'sample-password'
|
||||
);
|
||||
|
||||
const persistedToken = await AccessToken.query().findOne({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
expect(token).toBe(persistedToken.token);
|
||||
});
|
||||
|
||||
it('should return undefined for existing email and incorrect password', async () => {
|
||||
await createUser({
|
||||
email: 'test-user@automatisch.io',
|
||||
password: 'sample-password',
|
||||
});
|
||||
|
||||
const token = await User.authenticate(
|
||||
'test-user@automatisch.io',
|
||||
'wrong-password'
|
||||
);
|
||||
|
||||
expect(token).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should return undefined for non-existing email', async () => {
|
||||
await createUser({
|
||||
email: 'test-user@automatisch.io',
|
||||
password: 'sample-password',
|
||||
});
|
||||
|
||||
const token = await User.authenticate('non-existing-user@automatisch.io');
|
||||
|
||||
expect(token).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('login', () => {
|
||||
it('should return true when the given password matches with the user password', async () => {
|
||||
const user = await createUser({ password: 'sample-password' });
|
||||
@@ -875,4 +896,637 @@ describe('User model', () => {
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('isAllowedToRunFlows', () => {
|
||||
it('should return true when Automatisch is self hosted', async () => {
|
||||
const user = new User();
|
||||
|
||||
vi.spyOn(appConfig, 'isSelfHosted', 'get').mockReturnValue(true);
|
||||
|
||||
expect(await user.isAllowedToRunFlows()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when the user is in trial', async () => {
|
||||
const user = new User();
|
||||
|
||||
vi.spyOn(user, 'inTrial').mockResolvedValue(true);
|
||||
|
||||
expect(await user.isAllowedToRunFlows()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when the user has active subscription and within quota limits', async () => {
|
||||
const user = new User();
|
||||
|
||||
vi.spyOn(user, 'hasActiveSubscription').mockResolvedValue(true);
|
||||
vi.spyOn(user, 'withinLimits').mockResolvedValue(true);
|
||||
|
||||
expect(await user.isAllowedToRunFlows()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when the user has active subscription over quota limits', async () => {
|
||||
const user = new User();
|
||||
|
||||
vi.spyOn(user, 'hasActiveSubscription').mockResolvedValue(true);
|
||||
vi.spyOn(user, 'withinLimits').mockResolvedValue(false);
|
||||
|
||||
expect(await user.isAllowedToRunFlows()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false otherwise', async () => {
|
||||
const user = new User();
|
||||
|
||||
expect(await user.isAllowedToRunFlows()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('inTrial', () => {
|
||||
it('should return false when Automatisch is self hosted', async () => {
|
||||
const user = new User();
|
||||
|
||||
vi.spyOn(appConfig, 'isSelfHosted', 'get').mockReturnValue(true);
|
||||
|
||||
expect(await user.inTrial()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when the user does not have trial expiry date', async () => {
|
||||
const user = new User();
|
||||
|
||||
vi.spyOn(appConfig, 'isSelfHosted', 'get').mockReturnValue(false);
|
||||
|
||||
expect(await user.inTrial()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when the user has an active subscription', async () => {
|
||||
const user = new User();
|
||||
user.trialExpiryDate = '2024-12-14';
|
||||
|
||||
vi.spyOn(appConfig, 'isSelfHosted', 'get').mockReturnValue(false);
|
||||
|
||||
const hasActiveSubscriptionSpy = vi
|
||||
.spyOn(user, 'hasActiveSubscription')
|
||||
.mockResolvedValue(true);
|
||||
|
||||
expect(await user.inTrial()).toBe(false);
|
||||
expect(hasActiveSubscriptionSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should return true when trial expiry date is in future', async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const date = DateTime.fromObject(
|
||||
{ year: 2024, month: 11, day: 12, hour: 17, minute: 30 },
|
||||
{ zone: 'UTC+0' }
|
||||
);
|
||||
|
||||
vi.setSystemTime(date);
|
||||
|
||||
const user = await createUser();
|
||||
|
||||
await user.startTrialPeriod();
|
||||
|
||||
const refetchedUser = await user.$query();
|
||||
|
||||
vi.spyOn(appConfig, 'isSelfHosted', 'get').mockReturnValue(false);
|
||||
vi.spyOn(refetchedUser, 'hasActiveSubscription').mockResolvedValue(false);
|
||||
|
||||
expect(await refetchedUser.inTrial()).toBe(true);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should return false when trial expiry date is in past', async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const user = await createUser();
|
||||
|
||||
await user.startTrialPeriod();
|
||||
|
||||
vi.setSystemTime(DateTime.now().plus({ month: 1 }));
|
||||
|
||||
const refetchedUser = await user.$query();
|
||||
|
||||
vi.spyOn(appConfig, 'isSelfHosted', 'get').mockReturnValue(false);
|
||||
vi.spyOn(refetchedUser, 'hasActiveSubscription').mockResolvedValue(false);
|
||||
|
||||
expect(await refetchedUser.inTrial()).toBe(false);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasActiveSubscription', () => {
|
||||
it('should return true if current subscription is valid', async () => {
|
||||
const user = await createUser();
|
||||
await createSubscription({ userId: user.id, status: 'active' });
|
||||
|
||||
expect(await user.hasActiveSubscription()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if current subscription is not valid', async () => {
|
||||
const user = await createUser();
|
||||
|
||||
await createSubscription({
|
||||
userId: user.id,
|
||||
status: 'deleted',
|
||||
cancellationEffectiveDate: DateTime.now().minus({ day: 1 }).toString(),
|
||||
});
|
||||
|
||||
expect(await user.hasActiveSubscription()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if Automatisch is not a cloud installation', async () => {
|
||||
const user = new User();
|
||||
|
||||
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false);
|
||||
|
||||
expect(await user.hasActiveSubscription()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('withinLimits', () => {
|
||||
it('should return true when the consumed task count is less than the quota', async () => {
|
||||
const user = await createUser();
|
||||
const subscription = await createSubscription({ userId: user.id });
|
||||
|
||||
await createUsageData({
|
||||
subscriptionId: subscription.id,
|
||||
userId: user.id,
|
||||
consumedTaskCount: 100,
|
||||
});
|
||||
|
||||
expect(await user.withinLimits()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when the consumed task count is less than the quota', async () => {
|
||||
const user = await createUser();
|
||||
const subscription = await createSubscription({ userId: user.id });
|
||||
|
||||
await createUsageData({
|
||||
subscriptionId: subscription.id,
|
||||
userId: user.id,
|
||||
consumedTaskCount: 10000,
|
||||
});
|
||||
|
||||
expect(await user.withinLimits()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPlanAndUsage', () => {
|
||||
it('should return plan and usage', async () => {
|
||||
const user = await createUser();
|
||||
|
||||
const subscription = await createSubscription({ userId: user.id });
|
||||
|
||||
expect(await user.getPlanAndUsage()).toStrictEqual({
|
||||
usage: {
|
||||
task: 0,
|
||||
},
|
||||
plan: {
|
||||
id: subscription.paddlePlanId,
|
||||
name: '10k - monthly',
|
||||
limit: '10,000',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return trial plan and usage if no subscription exists', async () => {
|
||||
const user = await createUser();
|
||||
|
||||
expect(await user.getPlanAndUsage()).toStrictEqual({
|
||||
usage: {
|
||||
task: 0,
|
||||
},
|
||||
plan: {
|
||||
id: null,
|
||||
name: 'Free Trial',
|
||||
limit: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw not found when the current usage data does not exist', async () => {
|
||||
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false);
|
||||
|
||||
const user = await createUser();
|
||||
|
||||
await expect(() => user.getPlanAndUsage()).rejects.toThrow(
|
||||
'NotFoundError'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getInvoices', () => {
|
||||
it('should return invoices for the current subscription', async () => {
|
||||
const user = await createUser();
|
||||
const subscription = await createSubscription({ userId: user.id });
|
||||
|
||||
const getInvoicesSpy = vi
|
||||
.spyOn(Billing.paddleClient, 'getInvoices')
|
||||
.mockResolvedValue('dummy-invoices');
|
||||
|
||||
expect(await user.getInvoices()).toBe('dummy-invoices');
|
||||
expect(getInvoicesSpy).toHaveBeenCalledWith(
|
||||
Number(subscription.paddleSubscriptionId)
|
||||
);
|
||||
});
|
||||
|
||||
it('should return empty array without any subscriptions', async () => {
|
||||
const user = await createUser();
|
||||
|
||||
expect(await user.getInvoices()).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it.todo('getApps');
|
||||
|
||||
it('createAdmin should create admin with given data and mark the installation completed', async () => {
|
||||
const adminRole = await createRole({ name: 'Admin' });
|
||||
|
||||
const markInstallationCompletedSpy = vi
|
||||
.spyOn(Config, 'markInstallationCompleted')
|
||||
.mockResolvedValue();
|
||||
|
||||
const adminUser = await User.createAdmin({
|
||||
fullName: 'Sample admin',
|
||||
email: 'admin@automatisch.io',
|
||||
password: 'sample',
|
||||
});
|
||||
|
||||
expect(adminUser).toMatchObject({
|
||||
fullName: 'Sample admin',
|
||||
email: 'admin@automatisch.io',
|
||||
roleId: adminRole.id,
|
||||
});
|
||||
|
||||
expect(markInstallationCompletedSpy).toHaveBeenCalledOnce();
|
||||
expect(await adminUser.login('sample')).toBe(true);
|
||||
});
|
||||
|
||||
describe('registerUser', () => {
|
||||
it('should register user with user role and given data', async () => {
|
||||
const userRole = await createRole({ name: 'User' });
|
||||
|
||||
const user = await User.registerUser({
|
||||
fullName: 'Sample user',
|
||||
email: 'user@automatisch.io',
|
||||
password: 'sample-password',
|
||||
});
|
||||
|
||||
expect(user).toMatchObject({
|
||||
fullName: 'Sample user',
|
||||
email: 'user@automatisch.io',
|
||||
roleId: userRole.id,
|
||||
});
|
||||
|
||||
expect(await user.login('sample-password')).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw not found error when user role does not exist', async () => {
|
||||
await expect(() =>
|
||||
User.registerUser({
|
||||
fullName: 'Sample user',
|
||||
email: 'user@automatisch.io',
|
||||
password: 'sample-password',
|
||||
})
|
||||
).rejects.toThrowError('NotFoundError');
|
||||
});
|
||||
});
|
||||
|
||||
describe('can', () => {
|
||||
it('should return conditions for the given action and subject of the user', async () => {
|
||||
const userRole = await createRole({ name: 'User' });
|
||||
|
||||
await createPermission({
|
||||
roleId: userRole.id,
|
||||
subject: 'Flow',
|
||||
action: 'read',
|
||||
conditions: ['isCreator'],
|
||||
});
|
||||
|
||||
await createPermission({
|
||||
roleId: userRole.id,
|
||||
subject: 'Connection',
|
||||
action: 'read',
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
const user = await createUser({ roleId: userRole.id });
|
||||
|
||||
const userWithRoleAndPermissions = await user
|
||||
.$query()
|
||||
.withGraphFetched({ role: true, permissions: true });
|
||||
|
||||
expect(userWithRoleAndPermissions.can('read', 'Flow')).toStrictEqual({
|
||||
isCreator: true,
|
||||
});
|
||||
|
||||
expect(
|
||||
userWithRoleAndPermissions.can('read', 'Connection')
|
||||
).toStrictEqual({});
|
||||
});
|
||||
|
||||
it('should return not authorized error when the user is not permitted for the given action and subject', async () => {
|
||||
const userRole = await createRole({ name: 'User' });
|
||||
const user = await createUser({ roleId: userRole.id });
|
||||
|
||||
const userWithRoleAndPermissions = await user
|
||||
.$query()
|
||||
.withGraphFetched({ role: true, permissions: true });
|
||||
|
||||
expect(() => userWithRoleAndPermissions.can('read', 'Flow')).toThrowError(
|
||||
'The user is not authorized!'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('lowercaseEmail should lowercase the user email', () => {
|
||||
const user = new User();
|
||||
user.email = 'USER@AUTOMATISCH.IO';
|
||||
|
||||
user.lowercaseEmail();
|
||||
|
||||
expect(user.email).toBe('user@automatisch.io');
|
||||
});
|
||||
|
||||
describe('createUsageData', () => {
|
||||
it('should create usage data if Automatisch is a cloud installation', async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true);
|
||||
|
||||
const user = await createUser({
|
||||
fullName: 'Sample user',
|
||||
email: 'user@automatisch.io',
|
||||
});
|
||||
|
||||
vi.setSystemTime(DateTime.now().plus({ month: 1 }));
|
||||
|
||||
const usageData = await user.createUsageData();
|
||||
const currentUsageData = await user.$relatedQuery('currentUsageData');
|
||||
|
||||
expect(usageData).toStrictEqual(currentUsageData);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should not create usage data if Automatisch is not a cloud installation', async () => {
|
||||
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false);
|
||||
|
||||
const user = await createUser({
|
||||
fullName: 'Sample user',
|
||||
email: 'user@automatisch.io',
|
||||
});
|
||||
|
||||
const usageData = await user.createUsageData();
|
||||
|
||||
expect(usageData).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('omitEnterprisePermissionsWithoutValidLicense', () => {
|
||||
it('should return user as-is with valid license', async () => {
|
||||
const userRole = await createRole({ name: 'User' });
|
||||
const user = await createUser({
|
||||
fullName: 'Sample user',
|
||||
email: 'user@automatisch.io',
|
||||
roleId: userRole.id,
|
||||
});
|
||||
|
||||
const readFlowPermission = await createPermission({
|
||||
roleId: userRole.id,
|
||||
subject: 'Flow',
|
||||
action: 'read',
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
await createPermission({
|
||||
roleId: userRole.id,
|
||||
subject: 'App',
|
||||
action: 'read',
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
await createPermission({
|
||||
roleId: userRole.id,
|
||||
subject: 'Role',
|
||||
action: 'read',
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
await createPermission({
|
||||
roleId: userRole.id,
|
||||
subject: 'Config',
|
||||
action: 'read',
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
await createPermission({
|
||||
roleId: userRole.id,
|
||||
subject: 'SamlAuthProvider',
|
||||
action: 'read',
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
const userWithRoleAndPermissions = await user
|
||||
.$query()
|
||||
.withGraphFetched({ role: true, permissions: true });
|
||||
|
||||
expect(userWithRoleAndPermissions.permissions).toStrictEqual([
|
||||
readFlowPermission,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should omit enterprise permissions without valid license', async () => {
|
||||
vi.spyOn(licenseModule, 'hasValidLicense').mockResolvedValue(false);
|
||||
|
||||
const userRole = await createRole({ name: 'User' });
|
||||
const user = await createUser({
|
||||
fullName: 'Sample user',
|
||||
email: 'user@automatisch.io',
|
||||
roleId: userRole.id,
|
||||
});
|
||||
|
||||
const readFlowPermission = await createPermission({
|
||||
roleId: userRole.id,
|
||||
subject: 'Flow',
|
||||
action: 'read',
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
await createPermission({
|
||||
roleId: userRole.id,
|
||||
subject: 'App',
|
||||
action: 'read',
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
await createPermission({
|
||||
roleId: userRole.id,
|
||||
subject: 'Role',
|
||||
action: 'read',
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
await createPermission({
|
||||
roleId: userRole.id,
|
||||
subject: 'Config',
|
||||
action: 'read',
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
await createPermission({
|
||||
roleId: userRole.id,
|
||||
subject: 'SamlAuthProvider',
|
||||
action: 'read',
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
const userWithRoleAndPermissions = await user
|
||||
.$query()
|
||||
.withGraphFetched({ role: true, permissions: true });
|
||||
|
||||
expect(userWithRoleAndPermissions.permissions).toStrictEqual([
|
||||
readFlowPermission,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('$beforeInsert', () => {
|
||||
it('should call super.$beforeInsert', async () => {
|
||||
const superBeforeInsertSpy = vi
|
||||
.spyOn(User.prototype, '$beforeInsert')
|
||||
.mockResolvedValue();
|
||||
|
||||
await createUser();
|
||||
|
||||
expect(superBeforeInsertSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should lowercase the user email', async () => {
|
||||
const user = await createUser({
|
||||
fullName: 'Sample user',
|
||||
email: 'USER@AUTOMATISCH.IO',
|
||||
});
|
||||
|
||||
expect(user.email).toBe('user@automatisch.io');
|
||||
});
|
||||
|
||||
it('should generate password hash', async () => {
|
||||
const user = await createUser({
|
||||
fullName: 'Sample user',
|
||||
email: 'user@automatisch.io',
|
||||
password: 'sample-password',
|
||||
});
|
||||
|
||||
expect(user.password).not.toBe('sample-password');
|
||||
expect(await user.login('sample-password')).toBe(true);
|
||||
});
|
||||
|
||||
it('should start trial period if Automatisch is a cloud installation', async () => {
|
||||
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true);
|
||||
|
||||
const startTrialPeriodSpy = vi.spyOn(User.prototype, 'startTrialPeriod');
|
||||
|
||||
await createUser({
|
||||
fullName: 'Sample user',
|
||||
email: 'user@automatisch.io',
|
||||
});
|
||||
|
||||
expect(startTrialPeriodSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should not start trial period if Automatisch is not a cloud installation', async () => {
|
||||
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false);
|
||||
|
||||
const startTrialPeriodSpy = vi.spyOn(User.prototype, 'startTrialPeriod');
|
||||
|
||||
await createUser({
|
||||
fullName: 'Sample user',
|
||||
email: 'user@automatisch.io',
|
||||
});
|
||||
|
||||
expect(startTrialPeriodSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('$beforeUpdate', () => {
|
||||
it('should call super.$beforeUpdate', async () => {
|
||||
const superBeforeUpdateSpy = vi
|
||||
.spyOn(User.prototype, '$beforeUpdate')
|
||||
.mockResolvedValue();
|
||||
|
||||
const user = await createUser({
|
||||
fullName: 'Sample user',
|
||||
email: 'user@automatisch.io',
|
||||
});
|
||||
|
||||
await user.$query().patch({ fullName: 'Updated user name' });
|
||||
|
||||
expect(superBeforeUpdateSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should lowercase the user email if given', async () => {
|
||||
const user = await createUser({
|
||||
fullName: 'Sample user',
|
||||
email: 'user@automatisch.io',
|
||||
});
|
||||
|
||||
await user.$query().patchAndFetch({ email: 'NEW_EMAIL@AUTOMATISCH.IO' });
|
||||
|
||||
expect(user.email).toBe('new_email@automatisch.io');
|
||||
});
|
||||
|
||||
it('should generate password hash', async () => {
|
||||
const user = await createUser({
|
||||
fullName: 'Sample user',
|
||||
email: 'user@automatisch.io',
|
||||
password: 'sample-password',
|
||||
});
|
||||
|
||||
await user.$query().patchAndFetch({ password: 'new-password' });
|
||||
|
||||
expect(user.password).not.toBe('new-password');
|
||||
expect(await user.login('new-password')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('$afterInsert', () => {
|
||||
it('should call super.$afterInsert', async () => {
|
||||
const superAfterInsertSpy = vi.spyOn(User.prototype, '$afterInsert');
|
||||
|
||||
await createUser({
|
||||
fullName: 'Sample user',
|
||||
email: 'user@automatisch.io',
|
||||
});
|
||||
|
||||
expect(superAfterInsertSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should call createUsageData', async () => {
|
||||
const createUsageDataSpy = vi.spyOn(User.prototype, 'createUsageData');
|
||||
|
||||
await createUser({
|
||||
fullName: 'Sample user',
|
||||
email: 'user@automatisch.io',
|
||||
});
|
||||
|
||||
expect(createUsageDataSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
it('$afterFind should invoke omitEnterprisePermissionsWithoutValidLicense method', async () => {
|
||||
const omitEnterprisePermissionsWithoutValidLicenseSpy = vi.spyOn(
|
||||
User.prototype,
|
||||
'omitEnterprisePermissionsWithoutValidLicense'
|
||||
);
|
||||
|
||||
await createUser({
|
||||
fullName: 'Sample user',
|
||||
email: 'user@automatisch.io',
|
||||
});
|
||||
|
||||
expect(
|
||||
omitEnterprisePermissionsWithoutValidLicenseSpy
|
||||
).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
@@ -11,10 +11,6 @@ const redisConnection = {
|
||||
|
||||
const actionQueue = new Queue('action', redisConnection);
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await actionQueue.close();
|
||||
});
|
||||
|
||||
actionQueue.on('error', (error) => {
|
||||
if (error.code === CONNECTION_REFUSED) {
|
||||
logger.error(
|
||||
|
@@ -11,10 +11,6 @@ const redisConnection = {
|
||||
|
||||
const deleteUserQueue = new Queue('delete-user', redisConnection);
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await deleteUserQueue.close();
|
||||
});
|
||||
|
||||
deleteUserQueue.on('error', (error) => {
|
||||
if (error.code === CONNECTION_REFUSED) {
|
||||
logger.error(
|
||||
|
@@ -11,10 +11,6 @@ const redisConnection = {
|
||||
|
||||
const emailQueue = new Queue('email', redisConnection);
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await emailQueue.close();
|
||||
});
|
||||
|
||||
emailQueue.on('error', (error) => {
|
||||
if (error.code === CONNECTION_REFUSED) {
|
||||
logger.error(
|
||||
|
@@ -11,10 +11,6 @@ const redisConnection = {
|
||||
|
||||
const flowQueue = new Queue('flow', redisConnection);
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await flowQueue.close();
|
||||
});
|
||||
|
||||
flowQueue.on('error', (error) => {
|
||||
if (error.code === CONNECTION_REFUSED) {
|
||||
logger.error(
|
||||
|
21
packages/backend/src/queues/index.js
Normal file
21
packages/backend/src/queues/index.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import appConfig from '../config/app.js';
|
||||
import actionQueue from './action.js';
|
||||
import emailQueue from './email.js';
|
||||
import flowQueue from './flow.js';
|
||||
import triggerQueue from './trigger.js';
|
||||
import deleteUserQueue from './delete-user.ee.js';
|
||||
import removeCancelledSubscriptionsQueue from './remove-cancelled-subscriptions.ee.js';
|
||||
|
||||
const queues = [
|
||||
actionQueue,
|
||||
emailQueue,
|
||||
flowQueue,
|
||||
triggerQueue,
|
||||
deleteUserQueue,
|
||||
];
|
||||
|
||||
if (appConfig.isCloud) {
|
||||
queues.push(removeCancelledSubscriptionsQueue);
|
||||
}
|
||||
|
||||
export default queues;
|
@@ -14,10 +14,6 @@ const removeCancelledSubscriptionsQueue = new Queue(
|
||||
redisConnection
|
||||
);
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await removeCancelledSubscriptionsQueue.close();
|
||||
});
|
||||
|
||||
removeCancelledSubscriptionsQueue.on('error', (error) => {
|
||||
if (error.code === CONNECTION_REFUSED) {
|
||||
logger.error(
|
||||
|
@@ -11,10 +11,6 @@ const redisConnection = {
|
||||
|
||||
const triggerQueue = new Queue('trigger', redisConnection);
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await triggerQueue.close();
|
||||
});
|
||||
|
||||
triggerQueue.on('error', (error) => {
|
||||
if (error.code === CONNECTION_REFUSED) {
|
||||
logger.error(
|
||||
|
@@ -1,10 +1,8 @@
|
||||
const appConfigSerializer = (appConfig) => {
|
||||
return {
|
||||
key: appConfig.key,
|
||||
customConnectionAllowed: appConfig.customConnectionAllowed,
|
||||
shared: appConfig.shared,
|
||||
useOnlyPredefinedAuthClients: appConfig.useOnlyPredefinedAuthClients,
|
||||
disabled: appConfig.disabled,
|
||||
connectionAllowed: appConfig.connectionAllowed,
|
||||
createdAt: appConfig.createdAt.getTime(),
|
||||
updatedAt: appConfig.updatedAt.getTime(),
|
||||
};
|
||||
|
@@ -12,10 +12,8 @@ describe('appConfig serializer', () => {
|
||||
it('should return app config data', async () => {
|
||||
const expectedPayload = {
|
||||
key: appConfig.key,
|
||||
customConnectionAllowed: appConfig.customConnectionAllowed,
|
||||
shared: appConfig.shared,
|
||||
useOnlyPredefinedAuthClients: appConfig.useOnlyPredefinedAuthClients,
|
||||
disabled: appConfig.disabled,
|
||||
connectionAllowed: appConfig.connectionAllowed,
|
||||
createdAt: appConfig.createdAt.getTime(),
|
||||
updatedAt: appConfig.updatedAt.getTime(),
|
||||
};
|
||||
|
@@ -2,7 +2,9 @@ const authSerializer = (auth) => {
|
||||
return {
|
||||
fields: auth.fields,
|
||||
authenticationSteps: auth.authenticationSteps,
|
||||
sharedAuthenticationSteps: auth.sharedAuthenticationSteps,
|
||||
reconnectionSteps: auth.reconnectionSteps,
|
||||
sharedReconnectionSteps: auth.sharedReconnectionSteps,
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -10,6 +10,8 @@ describe('authSerializer', () => {
|
||||
fields: auth.fields,
|
||||
authenticationSteps: auth.authenticationSteps,
|
||||
reconnectionSteps: auth.reconnectionSteps,
|
||||
sharedAuthenticationSteps: auth.sharedAuthenticationSteps,
|
||||
sharedReconnectionSteps: auth.sharedReconnectionSteps,
|
||||
};
|
||||
|
||||
expect(authSerializer(auth)).toStrictEqual(expectedPayload);
|
||||
|
@@ -2,7 +2,6 @@ const connectionSerializer = (connection) => {
|
||||
return {
|
||||
id: connection.id,
|
||||
key: connection.key,
|
||||
reconnectable: connection.reconnectable,
|
||||
appAuthClientId: connection.appAuthClientId,
|
||||
formattedData: {
|
||||
screenName: connection.formattedData.screenName,
|
||||
|
@@ -13,7 +13,6 @@ describe('connectionSerializer', () => {
|
||||
const expectedPayload = {
|
||||
id: connection.id,
|
||||
key: connection.key,
|
||||
reconnectable: connection.reconnectable,
|
||||
appAuthClientId: connection.appAuthClientId,
|
||||
formattedData: {
|
||||
screenName: connection.formattedData.screenName,
|
||||
|
@@ -26,7 +26,7 @@ const serializers = {
|
||||
Permission: permissionSerializer,
|
||||
AdminSamlAuthProvider: adminSamlAuthProviderSerializer,
|
||||
SamlAuthProvider: samlAuthProviderSerializer,
|
||||
SamlAuthProvidersRoleMapping: samlAuthProviderRoleMappingSerializer,
|
||||
RoleMapping: samlAuthProviderRoleMappingSerializer,
|
||||
AppAuthClient: appAuthClientSerializer,
|
||||
AppConfig: appConfigSerializer,
|
||||
Flow: flowSerializer,
|
||||
|
@@ -1,20 +1,22 @@
|
||||
import * as Sentry from './helpers/sentry.ee.js';
|
||||
import appConfig from './config/app.js';
|
||||
import process from 'node:process';
|
||||
|
||||
Sentry.init();
|
||||
|
||||
import './config/orm.js';
|
||||
import './helpers/check-worker-readiness.js';
|
||||
import './workers/flow.js';
|
||||
import './workers/trigger.js';
|
||||
import './workers/action.js';
|
||||
import './workers/email.js';
|
||||
import './workers/delete-user.ee.js';
|
||||
import queues from './queues/index.js';
|
||||
import workers from './workers/index.js';
|
||||
|
||||
if (appConfig.isCloud) {
|
||||
import('./workers/remove-cancelled-subscriptions.ee.js');
|
||||
import('./queues/remove-cancelled-subscriptions.ee.js');
|
||||
}
|
||||
process.on('SIGTERM', async () => {
|
||||
for (const queue of queues) {
|
||||
await queue.close();
|
||||
}
|
||||
|
||||
for (const worker of workers) {
|
||||
await worker.close();
|
||||
}
|
||||
});
|
||||
|
||||
import telemetry from './helpers/telemetry/index.js';
|
||||
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { Worker } from 'bullmq';
|
||||
import process from 'node:process';
|
||||
|
||||
import * as Sentry from '../helpers/sentry.ee.js';
|
||||
import redisConfig from '../config/redis.js';
|
||||
@@ -15,7 +14,7 @@ import delayAsMilliseconds from '../helpers/delay-as-milliseconds.js';
|
||||
|
||||
const DEFAULT_DELAY_DURATION = 0;
|
||||
|
||||
export const worker = new Worker(
|
||||
const actionWorker = new Worker(
|
||||
'action',
|
||||
async (job) => {
|
||||
const { stepId, flowId, executionId, computedParameters, executionStep } =
|
||||
@@ -55,11 +54,11 @@ export const worker = new Worker(
|
||||
{ connection: redisConfig }
|
||||
);
|
||||
|
||||
worker.on('completed', (job) => {
|
||||
actionWorker.on('completed', (job) => {
|
||||
logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`);
|
||||
});
|
||||
|
||||
worker.on('failed', (job, err) => {
|
||||
actionWorker.on('failed', (job, err) => {
|
||||
const errorMessage = `
|
||||
JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has failed to start with ${err.message}
|
||||
\n ${err.stack}
|
||||
@@ -74,6 +73,4 @@ worker.on('failed', (job, err) => {
|
||||
});
|
||||
});
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await worker.close();
|
||||
});
|
||||
export default actionWorker;
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { Worker } from 'bullmq';
|
||||
import process from 'node:process';
|
||||
|
||||
import * as Sentry from '../helpers/sentry.ee.js';
|
||||
import redisConfig from '../config/redis.js';
|
||||
@@ -8,7 +7,7 @@ import appConfig from '../config/app.js';
|
||||
import User from '../models/user.js';
|
||||
import ExecutionStep from '../models/execution-step.js';
|
||||
|
||||
export const worker = new Worker(
|
||||
const deleteUserWorker = new Worker(
|
||||
'delete-user',
|
||||
async (job) => {
|
||||
const { id } = job.data;
|
||||
@@ -46,13 +45,13 @@ export const worker = new Worker(
|
||||
{ connection: redisConfig }
|
||||
);
|
||||
|
||||
worker.on('completed', (job) => {
|
||||
deleteUserWorker.on('completed', (job) => {
|
||||
logger.info(
|
||||
`JOB ID: ${job.id} - The user with the ID of '${job.data.id}' has been deleted!`
|
||||
);
|
||||
});
|
||||
|
||||
worker.on('failed', (job, err) => {
|
||||
deleteUserWorker.on('failed', (job, err) => {
|
||||
const errorMessage = `
|
||||
JOB ID: ${job.id} - The user with the ID of '${job.data.id}' has failed to be deleted! ${err.message}
|
||||
\n ${err.stack}
|
||||
@@ -67,6 +66,4 @@ worker.on('failed', (job, err) => {
|
||||
});
|
||||
});
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await worker.close();
|
||||
});
|
||||
export default deleteUserWorker;
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { Worker } from 'bullmq';
|
||||
import process from 'node:process';
|
||||
|
||||
import * as Sentry from '../helpers/sentry.ee.js';
|
||||
import redisConfig from '../config/redis.js';
|
||||
@@ -16,7 +15,7 @@ const isAutomatischEmail = (email) => {
|
||||
return email.endsWith('@automatisch.io');
|
||||
};
|
||||
|
||||
export const worker = new Worker(
|
||||
const emailWorker = new Worker(
|
||||
'email',
|
||||
async (job) => {
|
||||
const { email, subject, template, params } = job.data;
|
||||
@@ -39,13 +38,13 @@ export const worker = new Worker(
|
||||
{ connection: redisConfig }
|
||||
);
|
||||
|
||||
worker.on('completed', (job) => {
|
||||
emailWorker.on('completed', (job) => {
|
||||
logger.info(
|
||||
`JOB ID: ${job.id} - ${job.data.subject} email sent to ${job.data.email}!`
|
||||
);
|
||||
});
|
||||
|
||||
worker.on('failed', (job, err) => {
|
||||
emailWorker.on('failed', (job, err) => {
|
||||
const errorMessage = `
|
||||
JOB ID: ${job.id} - ${job.data.subject} email to ${job.data.email} has failed to send with ${err.message}
|
||||
\n ${err.stack}
|
||||
@@ -60,6 +59,4 @@ worker.on('failed', (job, err) => {
|
||||
});
|
||||
});
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await worker.close();
|
||||
});
|
||||
export default emailWorker;
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { Worker } from 'bullmq';
|
||||
import process from 'node:process';
|
||||
|
||||
import * as Sentry from '../helpers/sentry.ee.js';
|
||||
import redisConfig from '../config/redis.js';
|
||||
@@ -13,7 +12,7 @@ import {
|
||||
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
|
||||
} from '../helpers/remove-job-configuration.js';
|
||||
|
||||
export const worker = new Worker(
|
||||
const flowWorker = new Worker(
|
||||
'flow',
|
||||
async (job) => {
|
||||
const { flowId } = job.data;
|
||||
@@ -64,11 +63,11 @@ export const worker = new Worker(
|
||||
{ connection: redisConfig }
|
||||
);
|
||||
|
||||
worker.on('completed', (job) => {
|
||||
flowWorker.on('completed', (job) => {
|
||||
logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`);
|
||||
});
|
||||
|
||||
worker.on('failed', async (job, err) => {
|
||||
flowWorker.on('failed', async (job, err) => {
|
||||
const errorMessage = `
|
||||
JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has failed to start with ${err.message}
|
||||
\n ${err.stack}
|
||||
@@ -95,6 +94,4 @@ worker.on('failed', async (job, err) => {
|
||||
});
|
||||
});
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await worker.close();
|
||||
});
|
||||
export default flowWorker;
|
||||
|
21
packages/backend/src/workers/index.js
Normal file
21
packages/backend/src/workers/index.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import appConfig from '../config/app.js';
|
||||
import actionWorker from './action.js';
|
||||
import emailWorker from './email.js';
|
||||
import flowWorker from './flow.js';
|
||||
import triggerWorker from './trigger.js';
|
||||
import deleteUserWorker from './delete-user.ee.js';
|
||||
import removeCancelledSubscriptionsWorker from './remove-cancelled-subscriptions.ee.js';
|
||||
|
||||
const workers = [
|
||||
actionWorker,
|
||||
emailWorker,
|
||||
flowWorker,
|
||||
triggerWorker,
|
||||
deleteUserWorker,
|
||||
];
|
||||
|
||||
if (appConfig.isCloud) {
|
||||
workers.push(removeCancelledSubscriptionsWorker);
|
||||
}
|
||||
|
||||
export default workers;
|
@@ -1,12 +1,11 @@
|
||||
import { Worker } from 'bullmq';
|
||||
import process from 'node:process';
|
||||
import { DateTime } from 'luxon';
|
||||
import * as Sentry from '../helpers/sentry.ee.js';
|
||||
import redisConfig from '../config/redis.js';
|
||||
import logger from '../helpers/logger.js';
|
||||
import Subscription from '../models/subscription.ee.js';
|
||||
|
||||
export const worker = new Worker(
|
||||
const removeCancelledSubscriptionsWorker = new Worker(
|
||||
'remove-cancelled-subscriptions',
|
||||
async () => {
|
||||
await Subscription.query()
|
||||
@@ -23,13 +22,13 @@ export const worker = new Worker(
|
||||
{ connection: redisConfig }
|
||||
);
|
||||
|
||||
worker.on('completed', (job) => {
|
||||
removeCancelledSubscriptionsWorker.on('completed', (job) => {
|
||||
logger.info(
|
||||
`JOB ID: ${job.id} - The cancelled subscriptions have been removed!`
|
||||
);
|
||||
});
|
||||
|
||||
worker.on('failed', (job, err) => {
|
||||
removeCancelledSubscriptionsWorker.on('failed', (job, err) => {
|
||||
const errorMessage = `
|
||||
JOB ID: ${job.id} - ERROR: The cancelled subscriptions can not be removed! ${err.message}
|
||||
\n ${err.stack}
|
||||
@@ -42,6 +41,4 @@ worker.on('failed', (job, err) => {
|
||||
});
|
||||
});
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await worker.close();
|
||||
});
|
||||
export default removeCancelledSubscriptionsWorker;
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { Worker } from 'bullmq';
|
||||
import process from 'node:process';
|
||||
|
||||
import * as Sentry from '../helpers/sentry.ee.js';
|
||||
import redisConfig from '../config/redis.js';
|
||||
@@ -12,7 +11,7 @@ import {
|
||||
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
|
||||
} from '../helpers/remove-job-configuration.js';
|
||||
|
||||
export const worker = new Worker(
|
||||
const triggerWorker = new Worker(
|
||||
'trigger',
|
||||
async (job) => {
|
||||
const { flowId, executionId, stepId, executionStep } = await processTrigger(
|
||||
@@ -41,11 +40,11 @@ export const worker = new Worker(
|
||||
{ connection: redisConfig }
|
||||
);
|
||||
|
||||
worker.on('completed', (job) => {
|
||||
triggerWorker.on('completed', (job) => {
|
||||
logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`);
|
||||
});
|
||||
|
||||
worker.on('failed', (job, err) => {
|
||||
triggerWorker.on('failed', (job, err) => {
|
||||
const errorMessage = `
|
||||
JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has failed to start with ${err.message}
|
||||
\n ${err.stack}
|
||||
@@ -60,6 +59,4 @@ worker.on('failed', (job, err) => {
|
||||
});
|
||||
});
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await worker.close();
|
||||
});
|
||||
export default triggerWorker;
|
||||
|
@@ -1,16 +1,15 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { createRole } from './role.js';
|
||||
import RoleMapping from '../../src/models/role-mapping.ee.js';
|
||||
import { createSamlAuthProvider } from './saml-auth-provider.ee.js';
|
||||
import SamlAuthProviderRoleMapping from '../../src/models/saml-auth-providers-role-mapping.ee.js';
|
||||
|
||||
export const createRoleMapping = async (params = {}) => {
|
||||
params.roleId = params?.roleId || (await createRole()).id;
|
||||
params.roleId = params.roleId || (await createRole()).id;
|
||||
params.samlAuthProviderId =
|
||||
params?.samlAuthProviderId || (await createSamlAuthProvider()).id;
|
||||
params.samlAuthProviderId || (await createSamlAuthProvider()).id;
|
||||
params.remoteRoleName = params.remoteRoleName || faker.person.jobType();
|
||||
|
||||
params.remoteRoleName = params?.remoteRoleName || 'User';
|
||||
const roleMapping = await RoleMapping.query().insertAndFetch(params);
|
||||
|
||||
const samlAuthProviderRoleMapping =
|
||||
await SamlAuthProviderRoleMapping.query().insertAndFetch(params);
|
||||
|
||||
return samlAuthProviderRoleMapping;
|
||||
return roleMapping;
|
||||
};
|
||||
|
@@ -1,16 +0,0 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { createRole } from './role.js';
|
||||
import SamlAuthProvidersRoleMapping from '../../src/models/saml-auth-providers-role-mapping.ee.js';
|
||||
import { createSamlAuthProvider } from './saml-auth-provider.ee.js';
|
||||
|
||||
export const createSamlAuthProvidersRoleMapping = async (params = {}) => {
|
||||
params.roleId = params.roleId || (await createRole()).id;
|
||||
params.samlAuthProviderId =
|
||||
params.samlAuthProviderId || (await createSamlAuthProvider()).id;
|
||||
params.remoteRoleName = params.remoteRoleName || faker.person.jobType();
|
||||
|
||||
const samlAuthProvider =
|
||||
await SamlAuthProvidersRoleMapping.query().insertAndFetch(params);
|
||||
|
||||
return samlAuthProvider;
|
||||
};
|
@@ -2,8 +2,7 @@ const createAppConfigMock = (appConfig) => {
|
||||
return {
|
||||
data: {
|
||||
key: appConfig.key,
|
||||
customConnectionAllowed: appConfig.customConnectionAllowed,
|
||||
shared: appConfig.shared,
|
||||
useOnlyPredefinedAuthClients: appConfig.useOnlyPredefinedAuthClients,
|
||||
disabled: appConfig.disabled,
|
||||
},
|
||||
meta: {
|
||||
|
@@ -15,7 +15,7 @@ const getRoleMappingsMock = async (roleMappings) => {
|
||||
currentPage: null,
|
||||
isArray: true,
|
||||
totalPages: null,
|
||||
type: 'SamlAuthProvidersRoleMapping',
|
||||
type: 'RoleMapping',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@@ -15,7 +15,7 @@ const createRoleMappingsMock = async (roleMappings) => {
|
||||
currentPage: null,
|
||||
isArray: true,
|
||||
totalPages: null,
|
||||
type: 'SamlAuthProvidersRoleMapping',
|
||||
type: 'RoleMapping',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@@ -2,7 +2,6 @@ const createConnection = (connection) => {
|
||||
const connectionData = {
|
||||
id: connection.id,
|
||||
key: connection.key,
|
||||
reconnectable: connection.reconnectable || true,
|
||||
appAuthClientId: connection.appAuthClientId,
|
||||
formattedData: connection.formattedData,
|
||||
verified: connection.verified || false,
|
||||
|
@@ -4,6 +4,8 @@ const getAuthMock = (auth) => {
|
||||
fields: auth.fields,
|
||||
authenticationSteps: auth.authenticationSteps,
|
||||
reconnectionSteps: auth.reconnectionSteps,
|
||||
sharedReconnectionSteps: auth.sharedReconnectionSteps,
|
||||
sharedAuthenticationSteps: auth.sharedAuthenticationSteps,
|
||||
},
|
||||
meta: {
|
||||
count: 1,
|
||||
|
@@ -2,10 +2,8 @@ const getAppConfigMock = (appConfig) => {
|
||||
return {
|
||||
data: {
|
||||
key: appConfig.key,
|
||||
customConnectionAllowed: appConfig.customConnectionAllowed,
|
||||
shared: appConfig.shared,
|
||||
useOnlyPredefinedAuthClients: appConfig.useOnlyPredefinedAuthClients,
|
||||
disabled: appConfig.disabled,
|
||||
connectionAllowed: appConfig.connectionAllowed,
|
||||
createdAt: appConfig.createdAt.getTime(),
|
||||
updatedAt: appConfig.updatedAt.getTime(),
|
||||
},
|
||||
|
@@ -3,7 +3,6 @@ const getConnectionsMock = (connections) => {
|
||||
data: connections.map((connection) => ({
|
||||
id: connection.id,
|
||||
key: connection.key,
|
||||
reconnectable: connection.reconnectable,
|
||||
verified: connection.verified,
|
||||
appAuthClientId: connection.appAuthClientId,
|
||||
formattedData: {
|
||||
|
@@ -3,7 +3,6 @@ const resetConnectionMock = (connection) => {
|
||||
id: connection.id,
|
||||
key: connection.key,
|
||||
verified: connection.verified,
|
||||
reconnectable: connection.reconnectable,
|
||||
appAuthClientId: connection.appAuthClientId,
|
||||
formattedData: {
|
||||
screenName: connection.formattedData.screenName,
|
||||
|
@@ -3,7 +3,6 @@ const updateConnectionMock = (connection) => {
|
||||
id: connection.id,
|
||||
key: connection.key,
|
||||
verified: connection.verified,
|
||||
reconnectable: connection.reconnectable,
|
||||
appAuthClientId: connection.appAuthClientId,
|
||||
formattedData: {
|
||||
screenName: connection.formattedData.screenName,
|
||||
|
@@ -3,7 +3,6 @@ const getConnectionMock = async (connection) => {
|
||||
id: connection.id,
|
||||
key: connection.key,
|
||||
verified: connection.verified,
|
||||
reconnectable: connection.reconnectable,
|
||||
appAuthClientId: connection.appAuthClientId,
|
||||
formattedData: {
|
||||
screenName: connection.formattedData.screenName,
|
||||
|
@@ -16,10 +16,10 @@ export default defineConfig({
|
||||
include: ['**/src/models/**', '**/src/controllers/**'],
|
||||
thresholds: {
|
||||
autoUpdate: true,
|
||||
statements: 93.41,
|
||||
branches: 93.46,
|
||||
functions: 95.95,
|
||||
lines: 93.41,
|
||||
statements: 95.16,
|
||||
branches: 94.66,
|
||||
functions: 97.65,
|
||||
lines: 95.16,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@@ -83,6 +83,7 @@
|
||||
"access": "public"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@simbathesailor/use-what-changed": "^2.0.0",
|
||||
"@tanstack/eslint-plugin-query": "^5.20.1",
|
||||
"@tanstack/react-query-devtools": "^5.24.1",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
|
@@ -18,6 +18,7 @@ import { generateExternalLink } from 'helpers/translationValues';
|
||||
import { Form } from './style';
|
||||
import useAppAuth from 'hooks/useAppAuth';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useWhatChanged } from '@simbathesailor/use-what-changed';
|
||||
|
||||
function AddAppConnection(props) {
|
||||
const { application, connectionId, onClose } = props;
|
||||
@@ -64,7 +65,7 @@ function AddAppConnection(props) {
|
||||
|
||||
asyncAuthenticate();
|
||||
},
|
||||
[appAuthClientId, authenticate],
|
||||
[appAuthClientId, authenticate, key, navigate],
|
||||
);
|
||||
|
||||
const handleClientClick = (appAuthClientId) =>
|
||||
|
@@ -34,10 +34,10 @@ function AdminApplicationCreateAuthClient(props) {
|
||||
|
||||
if (!appConfigKey) {
|
||||
const { data: appConfigData } = await createAppConfig({
|
||||
customConnectionAllowed: true,
|
||||
shared: false,
|
||||
useOnlyPredefinedAuthClients: false,
|
||||
disabled: false,
|
||||
});
|
||||
|
||||
appConfigKey = appConfigData.key;
|
||||
}
|
||||
|
||||
|
@@ -46,9 +46,8 @@ function AdminApplicationSettings(props) {
|
||||
|
||||
const defaultValues = useMemo(
|
||||
() => ({
|
||||
customConnectionAllowed:
|
||||
appConfig?.data?.customConnectionAllowed || false,
|
||||
shared: appConfig?.data?.shared || false,
|
||||
useOnlyPredefinedAuthClients:
|
||||
appConfig?.data?.useOnlyPredefinedAuthClients || false,
|
||||
disabled: appConfig?.data?.disabled || false,
|
||||
}),
|
||||
[appConfig?.data],
|
||||
@@ -62,21 +61,17 @@ function AdminApplicationSettings(props) {
|
||||
<Paper sx={{ p: 2, mt: 4 }}>
|
||||
<Stack spacing={2} direction="column">
|
||||
<Switch
|
||||
name="customConnectionAllowed"
|
||||
label={formatMessage('adminAppsSettings.customConnectionAllowed')}
|
||||
FormControlLabelProps={{
|
||||
labelPlacement: 'start',
|
||||
}}
|
||||
/>
|
||||
<Divider />
|
||||
<Switch
|
||||
name="shared"
|
||||
label={formatMessage('adminAppsSettings.shared')}
|
||||
name="useOnlyPredefinedAuthClients"
|
||||
label={formatMessage(
|
||||
'adminAppsSettings.useOnlyPredefinedAuthClients',
|
||||
)}
|
||||
FormControlLabelProps={{
|
||||
labelPlacement: 'start',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Switch
|
||||
name="disabled"
|
||||
label={formatMessage('adminAppsSettings.disabled')}
|
||||
@@ -86,6 +81,7 @@ function AdminApplicationSettings(props) {
|
||||
/>
|
||||
<Divider />
|
||||
</Stack>
|
||||
|
||||
<Stack>
|
||||
<LoadingButton
|
||||
data-test="submit-button"
|
||||
|
@@ -15,17 +15,7 @@ function AppAuthClientsDialog(props) {
|
||||
|
||||
const formatMessage = useFormatMessage();
|
||||
|
||||
React.useEffect(
|
||||
function autoAuthenticateSingleClient() {
|
||||
if (appAuthClients?.data.length === 1) {
|
||||
onClientClick(appAuthClients.data[0].id);
|
||||
}
|
||||
},
|
||||
[appAuthClients?.data],
|
||||
);
|
||||
|
||||
if (!appAuthClients?.data.length || appAuthClients?.data.length === 1)
|
||||
return <React.Fragment />;
|
||||
if (!appAuthClients?.data.length) return <React.Fragment />;
|
||||
|
||||
return (
|
||||
<Dialog onClose={onClose} open={true}>
|
||||
|
@@ -11,14 +11,7 @@ import { useQueryClient } from '@tanstack/react-query';
|
||||
import Can from 'components/Can';
|
||||
|
||||
function ContextMenu(props) {
|
||||
const {
|
||||
appKey,
|
||||
connection,
|
||||
onClose,
|
||||
onMenuItemClick,
|
||||
anchorEl,
|
||||
disableReconnection,
|
||||
} = props;
|
||||
const { appKey, connection, onClose, onMenuItemClick, anchorEl } = props;
|
||||
const formatMessage = useFormatMessage();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -73,7 +66,7 @@ function ContextMenu(props) {
|
||||
{(allowed) => (
|
||||
<MenuItem
|
||||
component={Link}
|
||||
disabled={!allowed || disableReconnection}
|
||||
disabled={!allowed}
|
||||
to={URLS.APP_RECONNECT_CONNECTION(
|
||||
appKey,
|
||||
connection.id,
|
||||
@@ -109,7 +102,6 @@ ContextMenu.propTypes = {
|
||||
PropTypes.func,
|
||||
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
|
||||
]),
|
||||
disableReconnection: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default ContextMenu;
|
||||
|
@@ -30,8 +30,7 @@ const countTranslation = (value) => (
|
||||
function AppConnectionRow(props) {
|
||||
const formatMessage = useFormatMessage();
|
||||
const enqueueSnackbar = useEnqueueSnackbar();
|
||||
const { id, key, formattedData, verified, createdAt, reconnectable } =
|
||||
props.connection;
|
||||
const { id, key, formattedData, verified, createdAt } = props.connection;
|
||||
const [verificationVisible, setVerificationVisible] = React.useState(false);
|
||||
const contextButtonRef = React.useRef(null);
|
||||
const [anchorEl, setAnchorEl] = React.useState(null);
|
||||
@@ -174,7 +173,6 @@ function AppConnectionRow(props) {
|
||||
<ConnectionContextMenu
|
||||
appKey={key}
|
||||
connection={props.connection}
|
||||
disableReconnection={!reconnectable}
|
||||
onClose={handleClose}
|
||||
onMenuItemClick={onContextMenuAction}
|
||||
anchorEl={anchorEl}
|
||||
|
@@ -95,7 +95,8 @@ function ChooseConnectionSubstep(props) {
|
||||
|
||||
if (
|
||||
!appConfig?.data ||
|
||||
(!appConfig.data?.disabled && appConfig.data?.customConnectionAllowed)
|
||||
(!appConfig.data?.disabled === false &&
|
||||
appConfig.data?.useOnlyPredefinedAuthClients === false)
|
||||
) {
|
||||
options.push({
|
||||
label: formatMessage('chooseConnectionSubstep.addNewConnection'),
|
||||
@@ -103,12 +104,10 @@ function ChooseConnectionSubstep(props) {
|
||||
});
|
||||
}
|
||||
|
||||
if (appConfig?.data?.connectionAllowed) {
|
||||
options.push({
|
||||
label: formatMessage('chooseConnectionSubstep.addNewSharedConnection'),
|
||||
value: ADD_SHARED_CONNECTION_VALUE,
|
||||
});
|
||||
}
|
||||
options.push({
|
||||
label: formatMessage('chooseConnectionSubstep.addNewSharedConnection'),
|
||||
value: ADD_SHARED_CONNECTION_VALUE,
|
||||
});
|
||||
|
||||
return options;
|
||||
}, [data, formatMessage, appConfig?.data]);
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import LoadingButton from '@mui/lab/LoadingButton';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
|
||||
import useForgotPassword from 'hooks/useForgotPassword';
|
||||
import Form from 'components/Form';
|
||||
@@ -12,25 +12,17 @@ import useFormatMessage from 'hooks/useFormatMessage';
|
||||
export default function ForgotPasswordForm() {
|
||||
const formatMessage = useFormatMessage();
|
||||
const {
|
||||
mutateAsync: forgotPassword,
|
||||
mutate: forgotPassword,
|
||||
isPending: loading,
|
||||
isSuccess,
|
||||
isError,
|
||||
error,
|
||||
} = useForgotPassword();
|
||||
|
||||
const handleSubmit = async (values) => {
|
||||
const { email } = values;
|
||||
try {
|
||||
await forgotPassword({
|
||||
email,
|
||||
});
|
||||
} catch (error) {
|
||||
enqueueSnackbar(
|
||||
error?.message || formatMessage('forgotPasswordForm.error'),
|
||||
{
|
||||
variant: 'error',
|
||||
},
|
||||
);
|
||||
}
|
||||
const handleSubmit = ({ email }) => {
|
||||
forgotPassword({
|
||||
email,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -57,6 +49,16 @@ export default function ForgotPasswordForm() {
|
||||
margin="dense"
|
||||
autoComplete="username"
|
||||
/>
|
||||
{isError && (
|
||||
<Alert severity="error" sx={{ mt: 2 }}>
|
||||
{error?.message || formatMessage('forgotPasswordForm.error')}
|
||||
</Alert>
|
||||
)}
|
||||
{isSuccess && (
|
||||
<Alert severity="success" sx={{ mt: 2 }}>
|
||||
{formatMessage('forgotPasswordForm.instructionsSent')}
|
||||
</Alert>
|
||||
)}
|
||||
<LoadingButton
|
||||
type="submit"
|
||||
variant="contained"
|
||||
@@ -68,14 +70,6 @@ export default function ForgotPasswordForm() {
|
||||
>
|
||||
{formatMessage('forgotPasswordForm.submit')}
|
||||
</LoadingButton>
|
||||
{isSuccess && (
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{ color: (theme) => theme.palette.success.main }}
|
||||
>
|
||||
{formatMessage('forgotPasswordForm.instructionsSent')}
|
||||
</Typography>
|
||||
)}
|
||||
</Form>
|
||||
</Paper>
|
||||
);
|
||||
|
@@ -2,6 +2,7 @@ import * as React from 'react';
|
||||
import { useNavigate, Link as RouterLink } from 'react-router-dom';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Link from '@mui/material/Link';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import LoadingButton from '@mui/lab/LoadingButton';
|
||||
import useAuthentication from 'hooks/useAuthentication';
|
||||
@@ -11,7 +12,6 @@ import Form from 'components/Form';
|
||||
import TextField from 'components/TextField';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import useCreateAccessToken from 'hooks/useCreateAccessToken';
|
||||
import { Alert } from '@mui/material';
|
||||
|
||||
function LoginForm() {
|
||||
const isCloud = useCloud();
|
||||
@@ -45,7 +45,7 @@ function LoginForm() {
|
||||
|
||||
const renderError = () => {
|
||||
const errors = error?.response?.data?.errors?.general || [
|
||||
formatMessage('loginForm.error'),
|
||||
error?.message || formatMessage('loginForm.error'),
|
||||
];
|
||||
|
||||
return errors.map((error) => (
|
||||
|
@@ -2,6 +2,7 @@ import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import LoadingButton from '@mui/lab/LoadingButton';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||
import * as React from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
@@ -30,6 +31,8 @@ export default function ResetPasswordForm() {
|
||||
mutateAsync: resetPassword,
|
||||
isPending,
|
||||
isSuccess,
|
||||
error,
|
||||
isError,
|
||||
} = useResetPassword();
|
||||
const token = searchParams.get('token');
|
||||
|
||||
@@ -47,14 +50,23 @@ export default function ResetPasswordForm() {
|
||||
},
|
||||
});
|
||||
navigate(URLS.LOGIN);
|
||||
} catch (error) {
|
||||
enqueueSnackbar(
|
||||
error?.message || formatMessage('resetPasswordForm.error'),
|
||||
{
|
||||
variant: 'error',
|
||||
},
|
||||
);
|
||||
} catch {}
|
||||
};
|
||||
|
||||
const renderError = () => {
|
||||
if (!isError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const errors = error?.response?.data?.errors?.general || [
|
||||
error?.message || formatMessage('resetPasswordForm.error'),
|
||||
];
|
||||
|
||||
return errors.map((error) => (
|
||||
<Alert severity="error" sx={{ mt: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -96,7 +108,6 @@ export default function ResetPasswordForm() {
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label={formatMessage(
|
||||
'resetPasswordForm.confirmPasswordFieldLabel',
|
||||
@@ -117,7 +128,7 @@ export default function ResetPasswordForm() {
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
|
||||
{renderError()}
|
||||
<LoadingButton
|
||||
type="submit"
|
||||
variant="contained"
|
||||
|
@@ -67,17 +67,12 @@ export default function SplitButton(props) {
|
||||
}}
|
||||
open={open}
|
||||
anchorEl={anchorRef.current}
|
||||
placement="bottom-end"
|
||||
transition
|
||||
disablePortal
|
||||
>
|
||||
{({ TransitionProps, placement }) => (
|
||||
<Grow
|
||||
{...TransitionProps}
|
||||
style={{
|
||||
transformOrigin:
|
||||
placement === 'bottom' ? 'center top' : 'center bottom',
|
||||
}}
|
||||
>
|
||||
{({ TransitionProps }) => (
|
||||
<Grow {...TransitionProps}>
|
||||
<Paper>
|
||||
<ClickAwayListener onClickAway={handleClose}>
|
||||
<MenuList autoFocusItem>
|
||||
|
@@ -13,6 +13,7 @@ import useCreateConnectionAuthUrl from './useCreateConnectionAuthUrl';
|
||||
import useUpdateConnection from './useUpdateConnection';
|
||||
import useResetConnection from './useResetConnection';
|
||||
import useVerifyConnection from './useVerifyConnection';
|
||||
import { useWhatChanged } from '@simbathesailor/use-what-changed';
|
||||
|
||||
function getSteps(auth, hasConnection, useShared) {
|
||||
if (hasConnection) {
|
||||
@@ -37,11 +38,13 @@ export default function useAuthenticateApp(payload) {
|
||||
const { mutateAsync: createConnectionAuthUrl } = useCreateConnectionAuthUrl();
|
||||
const { mutateAsync: updateConnection } = useUpdateConnection();
|
||||
const { mutateAsync: resetConnection } = useResetConnection();
|
||||
const { mutateAsync: verifyConnection } = useVerifyConnection();
|
||||
const [authenticationInProgress, setAuthenticationInProgress] =
|
||||
React.useState(false);
|
||||
const formatMessage = useFormatMessage();
|
||||
const steps = getSteps(auth?.data, !!connectionId, useShared);
|
||||
const { mutateAsync: verifyConnection } = useVerifyConnection();
|
||||
const steps = React.useMemo(() => {
|
||||
return getSteps(auth?.data, !!connectionId, useShared);
|
||||
}, [auth, connectionId, useShared]);
|
||||
|
||||
const authenticate = React.useMemo(() => {
|
||||
if (!steps?.length) return;
|
||||
@@ -57,7 +60,6 @@ export default function useAuthenticateApp(payload) {
|
||||
fields,
|
||||
};
|
||||
let stepIndex = 0;
|
||||
|
||||
while (stepIndex < steps?.length) {
|
||||
const step = steps[stepIndex];
|
||||
const variables = computeAuthStepVariables(step.arguments, response);
|
||||
@@ -105,10 +107,10 @@ export default function useAuthenticateApp(payload) {
|
||||
response[step.name] = stepResponse;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
console.error(err);
|
||||
setAuthenticationInProgress(false);
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ['apps', appKey, 'connections'],
|
||||
});
|
||||
|
||||
@@ -126,13 +128,14 @@ export default function useAuthenticateApp(payload) {
|
||||
|
||||
return response;
|
||||
};
|
||||
// keep formatMessage out of it as it causes infinite loop.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
steps,
|
||||
appKey,
|
||||
appAuthClientId,
|
||||
connectionId,
|
||||
queryClient,
|
||||
formatMessage,
|
||||
createConnection,
|
||||
createConnectionAuthUrl,
|
||||
updateConnection,
|
||||
@@ -140,6 +143,24 @@ export default function useAuthenticateApp(payload) {
|
||||
verifyConnection,
|
||||
]);
|
||||
|
||||
useWhatChanged(
|
||||
[
|
||||
steps,
|
||||
appKey,
|
||||
appAuthClientId,
|
||||
connectionId,
|
||||
queryClient,
|
||||
createConnection,
|
||||
createConnectionAuthUrl,
|
||||
updateConnection,
|
||||
resetConnection,
|
||||
verifyConnection,
|
||||
],
|
||||
'steps, appKey, appAuthClientId, connectionId, queryClient, createConnection, createConnectionAuthUrl, updateConnection, resetConnection, verifyConnection',
|
||||
'',
|
||||
'useAuthenticate',
|
||||
);
|
||||
|
||||
return {
|
||||
authenticate,
|
||||
inProgress: authenticationInProgress,
|
||||
|
@@ -9,7 +9,7 @@ export default function useAutomatischInfo() {
|
||||
**/
|
||||
staleTime: Infinity,
|
||||
queryKey: ['automatisch', 'info'],
|
||||
queryFn: async (payload, signal) => {
|
||||
queryFn: async ({ signal }) => {
|
||||
const { data } = await api.get('/v1/automatisch/info', { signal });
|
||||
|
||||
return data;
|
||||
|
@@ -3,7 +3,7 @@ import { useMutation } from '@tanstack/react-query';
|
||||
import api from 'helpers/api';
|
||||
|
||||
export default function useCreateConnection(appKey) {
|
||||
const query = useMutation({
|
||||
const mutation = useMutation({
|
||||
mutationFn: async ({ appAuthClientId, formattedData }) => {
|
||||
const { data } = await api.post(`/v1/apps/${appKey}/connections`, {
|
||||
appAuthClientId,
|
||||
@@ -14,5 +14,5 @@ export default function useCreateConnection(appKey) {
|
||||
},
|
||||
});
|
||||
|
||||
return query;
|
||||
return mutation;
|
||||
}
|
||||
|
15
packages/web/src/hooks/useLicense.js
Normal file
15
packages/web/src/hooks/useLicense.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import api from 'helpers/api';
|
||||
|
||||
export default function useLicense() {
|
||||
const query = useQuery({
|
||||
queryKey: ['automatisch', 'license'],
|
||||
queryFn: async ({ signal }) => {
|
||||
const { data } = await api.get('/v1/automatisch/license', { signal });
|
||||
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
||||
return query;
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { Settings } from 'luxon';
|
||||
import { setUseWhatChange } from '@simbathesailor/use-what-changed';
|
||||
|
||||
import ThemeProvider from 'components/ThemeProvider';
|
||||
import IntlProvider from 'components/IntlProvider';
|
||||
@@ -14,6 +15,8 @@ import reportWebVitals from './reportWebVitals';
|
||||
// Sets the default locale to English for all luxon DateTime instances created afterwards.
|
||||
Settings.defaultLocale = 'en';
|
||||
|
||||
setUseWhatChange(process.env.NODE_ENV === 'development');
|
||||
|
||||
const container = document.getElementById('root');
|
||||
const root = createRoot(container);
|
||||
|
||||
|
@@ -22,7 +22,7 @@
|
||||
"app.connectionCount": "{count} connections",
|
||||
"app.flowCount": "{count} flows",
|
||||
"app.addConnection": "Add connection",
|
||||
"app.addCustomConnection": "Add custom connection",
|
||||
"app.addConnectionWithAuthClient": "Add connection with auth client",
|
||||
"app.reconnectConnection": "Reconnect connection",
|
||||
"app.createFlow": "Create flow",
|
||||
"app.settings": "Settings",
|
||||
@@ -292,7 +292,7 @@
|
||||
"adminApps.connections": "Connections",
|
||||
"adminApps.authClients": "Auth clients",
|
||||
"adminApps.settings": "Settings",
|
||||
"adminAppsSettings.customConnectionAllowed": "Allow custom connection",
|
||||
"adminAppsSettings.useOnlyPredefinedAuthClients": "Use only predefined auth clients",
|
||||
"adminAppsSettings.shared": "Shared",
|
||||
"adminAppsSettings.disabled": "Disabled",
|
||||
"adminAppsSettings.save": "Save",
|
||||
|
@@ -92,13 +92,6 @@ export default function AdminApplication() {
|
||||
value={URLS.ADMIN_APP_AUTH_CLIENTS_PATTERN}
|
||||
component={Link}
|
||||
/>
|
||||
<Tab
|
||||
label={formatMessage('adminApps.connections')}
|
||||
to={URLS.ADMIN_APP_CONNECTIONS(appKey)}
|
||||
value={URLS.ADMIN_APP_CONNECTIONS_PATTERN}
|
||||
disabled={!app.supportsConnections}
|
||||
component={Link}
|
||||
/>
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
@@ -111,10 +104,6 @@ export default function AdminApplication() {
|
||||
path={`/auth-clients/*`}
|
||||
element={<AdminApplicationAuthClients appKey={appKey} />}
|
||||
/>
|
||||
<Route
|
||||
path={`/connections/*`}
|
||||
element={<div>App connections</div>}
|
||||
/>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
|
@@ -6,7 +6,6 @@ import {
|
||||
Navigate,
|
||||
Routes,
|
||||
useParams,
|
||||
useSearchParams,
|
||||
useMatch,
|
||||
useNavigate,
|
||||
} from 'react-router-dom';
|
||||
@@ -31,6 +30,7 @@ import AppIcon from 'components/AppIcon';
|
||||
import Container from 'components/Container';
|
||||
import PageTitle from 'components/PageTitle';
|
||||
import useApp from 'hooks/useApp';
|
||||
import useAppAuthClients from 'hooks/useAppAuthClients';
|
||||
import Can from 'components/Can';
|
||||
import { AppPropType } from 'propTypes/propTypes';
|
||||
|
||||
@@ -61,47 +61,53 @@ export default function Application() {
|
||||
end: false,
|
||||
});
|
||||
const flowsPathMatch = useMatch({ path: URLS.APP_FLOWS_PATTERN, end: false });
|
||||
const [searchParams] = useSearchParams();
|
||||
const { appKey } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const { data: appAuthClients } = useAppAuthClients(appKey);
|
||||
|
||||
const { data, loading } = useApp(appKey);
|
||||
const app = data?.data || {};
|
||||
|
||||
const { data: appConfig } = useAppConfig(appKey);
|
||||
const connectionId = searchParams.get('connectionId') || undefined;
|
||||
|
||||
const currentUserAbility = useCurrentUserAbility();
|
||||
|
||||
const goToApplicationPage = () => navigate('connections');
|
||||
|
||||
const connectionOptions = React.useMemo(() => {
|
||||
const shouldHaveCustomConnection =
|
||||
appConfig?.data?.connectionAllowed &&
|
||||
appConfig?.data?.customConnectionAllowed;
|
||||
const addCustomConnection = {
|
||||
label: formatMessage('app.addConnection'),
|
||||
key: 'addConnection',
|
||||
'data-test': 'add-connection-button',
|
||||
to: URLS.APP_ADD_CONNECTION(appKey, false),
|
||||
disabled: !currentUserAbility.can('create', 'Connection'),
|
||||
};
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: formatMessage('app.addConnection'),
|
||||
key: 'addConnection',
|
||||
'data-test': 'add-connection-button',
|
||||
to: URLS.APP_ADD_CONNECTION(appKey, appConfig?.data?.connectionAllowed),
|
||||
disabled: !currentUserAbility.can('create', 'Connection'),
|
||||
},
|
||||
];
|
||||
const addConnectionWithAuthClient = {
|
||||
label: formatMessage('app.addConnectionWithAuthClient'),
|
||||
key: 'addConnectionWithAuthClient',
|
||||
'data-test': 'add-custom-connection-button',
|
||||
to: URLS.APP_ADD_CONNECTION(appKey, true),
|
||||
disabled: !currentUserAbility.can('create', 'Connection'),
|
||||
};
|
||||
|
||||
if (shouldHaveCustomConnection) {
|
||||
options.push({
|
||||
label: formatMessage('app.addCustomConnection'),
|
||||
key: 'addCustomConnection',
|
||||
'data-test': 'add-custom-connection-button',
|
||||
to: URLS.APP_ADD_CONNECTION(appKey),
|
||||
disabled: !currentUserAbility.can('create', 'Connection'),
|
||||
});
|
||||
// means there is no app config. defaulting to custom connections only
|
||||
if (!appConfig?.data) {
|
||||
return [addCustomConnection];
|
||||
}
|
||||
|
||||
return options;
|
||||
}, [appKey, appConfig?.data, currentUserAbility, formatMessage]);
|
||||
// means there is no app auth client. so we don't show the `addConnectionWithAuthClient`
|
||||
if (appAuthClients?.data?.length === 0) {
|
||||
return [addCustomConnection];
|
||||
}
|
||||
|
||||
// means only auth clients are allowed for connection creation
|
||||
if (appConfig?.data?.useOnlyPredefinedAuthClients === true) {
|
||||
return [addConnectionWithAuthClient];
|
||||
}
|
||||
|
||||
return [addCustomConnection, addConnectionWithAuthClient];
|
||||
}, [appKey, appConfig, appAuthClients, currentUserAbility, formatMessage]);
|
||||
|
||||
if (loading) return null;
|
||||
|
||||
@@ -154,12 +160,7 @@ export default function Application() {
|
||||
{(allowed) => (
|
||||
<SplitButton
|
||||
disabled={
|
||||
!allowed ||
|
||||
(appConfig?.data &&
|
||||
!appConfig?.data?.disabled &&
|
||||
!appConfig?.data?.connectionAllowed &&
|
||||
!appConfig?.data?.customConnectionAllowed) ||
|
||||
connectionOptions.every(({ disabled }) => disabled)
|
||||
!allowed || appConfig?.data?.disabled === true
|
||||
}
|
||||
options={connectionOptions}
|
||||
/>
|
||||
|
@@ -66,8 +66,8 @@ function RoleMappings({ provider, providerLoading }) {
|
||||
const enqueueSnackbar = useEnqueueSnackbar();
|
||||
|
||||
const {
|
||||
mutateAsync: updateSamlAuthProvidersRoleMappings,
|
||||
isPending: isUpdateSamlAuthProvidersRoleMappingsPending,
|
||||
mutateAsync: updateRoleMappings,
|
||||
isPending: isUpdateRoleMappingsPending,
|
||||
} = useAdminUpdateSamlAuthProviderRoleMappings(provider?.id);
|
||||
|
||||
const { data, isLoading: isAdminSamlAuthProviderRoleMappingsLoading } =
|
||||
@@ -79,7 +79,7 @@ function RoleMappings({ provider, providerLoading }) {
|
||||
const handleRoleMappingsUpdate = async (values) => {
|
||||
try {
|
||||
if (provider?.id) {
|
||||
await updateSamlAuthProvidersRoleMappings(
|
||||
await updateRoleMappings(
|
||||
values.roleMappings.map(({ roleId, remoteRoleName }) => ({
|
||||
roleId,
|
||||
remoteRoleName,
|
||||
@@ -148,7 +148,7 @@ function RoleMappings({ provider, providerLoading }) {
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ boxShadow: 2 }}
|
||||
loading={isUpdateSamlAuthProvidersRoleMappingsPending}
|
||||
loading={isUpdateRoleMappingsPending}
|
||||
>
|
||||
{formatMessage('roleMappingsForm.save')}
|
||||
</LoadingButton>
|
||||
|
@@ -211,7 +211,6 @@ export const ConnectionPropType = PropTypes.shape({
|
||||
flowCount: PropTypes.number,
|
||||
appData: AppPropType,
|
||||
createdAt: PropTypes.number,
|
||||
reconnectable: PropTypes.bool,
|
||||
appAuthClientId: PropTypes.string,
|
||||
});
|
||||
|
||||
@@ -459,8 +458,7 @@ export const SamlAuthProviderRolePropType = PropTypes.shape({
|
||||
export const AppConfigPropType = PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
key: PropTypes.string,
|
||||
customConnectionAllowed: PropTypes.bool,
|
||||
connectionAllowed: PropTypes.bool,
|
||||
useOnlyPredefinedAuthClients: PropTypes.bool,
|
||||
shared: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
});
|
||||
|
@@ -2126,6 +2126,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz#427d5549943a9c6fce808e39ea64dbe60d4047f1"
|
||||
integrity sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==
|
||||
|
||||
"@simbathesailor/use-what-changed@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@simbathesailor/use-what-changed/-/use-what-changed-2.0.0.tgz#7f82d78f92c8588b5fadd702065dde93bd781403"
|
||||
integrity sha512-ulBNrPSvfho9UN6zS2fii3AsdEcp2fMaKeqUZZeCNPaZbB6aXyTUhpEN9atjMAbu/eyK3AY8L4SYJUG62Ekocw==
|
||||
|
||||
"@sinclair/typebox@^0.24.1":
|
||||
version "0.24.51"
|
||||
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.51.tgz#645f33fe4e02defe26f2f5c0410e1c094eac7f5f"
|
||||
@@ -9784,7 +9789,16 @@ string-natural-compare@^3.0.1:
|
||||
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
|
||||
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0:
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^4.1.0, string-width@^4.2.0:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@@ -9888,7 +9902,14 @@ stringify-object@^3.3.0:
|
||||
is-obj "^1.0.1"
|
||||
is-regexp "^1.0.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
@@ -10952,7 +10973,16 @@ workbox-window@6.6.1:
|
||||
"@types/trusted-types" "^2.0.2"
|
||||
workbox-core "6.6.1"
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
|
Reference in New Issue
Block a user