Compare commits
50 Commits
AUT-1324
...
dependabot
Author | SHA1 | Date | |
---|---|---|---|
![]() |
015d65ac98 | ||
![]() |
47510e24d5 | ||
![]() |
91c9ef3068 | ||
![]() |
240854e4ac | ||
![]() |
0e4fc7efbc | ||
![]() |
b47e859225 | ||
![]() |
62a1072682 | ||
![]() |
c6f2a97591 | ||
![]() |
d66be231b3 | ||
![]() |
f73ffc8711 | ||
![]() |
e4c17c1bc7 | ||
![]() |
997e729535 | ||
![]() |
e0e313b8d1 | ||
![]() |
f0bd763e72 | ||
![]() |
6a7a90536b | ||
![]() |
ac8ddedfb5 | ||
![]() |
6fcd126ff8 | ||
![]() |
55d0966d48 | ||
![]() |
2583e08f7a | ||
![]() |
de72e62470 | ||
![]() |
91993dbb07 | ||
![]() |
d87ee4daa3 | ||
![]() |
6791e002ff | ||
![]() |
4ca84aa515 | ||
![]() |
8189cbc171 | ||
![]() |
73edb45ff7 | ||
![]() |
0bbe362660 | ||
![]() |
a76bee51fc | ||
![]() |
6e42b52414 | ||
![]() |
aed61209fa | ||
![]() |
f5d796ea77 | ||
![]() |
ecb04b4ba9 | ||
![]() |
dabb01e237 | ||
![]() |
c2d27d0fd4 | ||
![]() |
e62bd75fdf | ||
![]() |
2e917bd62b | ||
![]() |
e0492c4264 | ||
![]() |
7db68e2f96 | ||
![]() |
e9b05a37d1 | ||
![]() |
5613259536 | ||
![]() |
3209ff16ac | ||
![]() |
a49c8602d1 | ||
![]() |
7caa055e00 | ||
![]() |
0d62bc6c78 | ||
![]() |
bc0861fd9e | ||
![]() |
f280052d93 | ||
![]() |
21da49f79d | ||
![]() |
19a5ccf942 | ||
![]() |
2981fa5946 | ||
![]() |
05a3016557 |
@@ -10,11 +10,11 @@ export default async (request, response) => {
|
||||
};
|
||||
|
||||
const appConfigParams = (request) => {
|
||||
const { allowCustomConnection, shared, disabled } = request.body;
|
||||
const { customConnectionAllowed, shared, disabled } = request.body;
|
||||
|
||||
return {
|
||||
key: request.params.appKey,
|
||||
allowCustomConnection,
|
||||
customConnectionAllowed,
|
||||
shared,
|
||||
disabled,
|
||||
};
|
||||
|
@@ -23,7 +23,7 @@ describe('POST /api/v1/admin/apps/:appKey/config', () => {
|
||||
|
||||
it('should return created app config', async () => {
|
||||
const appConfig = {
|
||||
allowCustomConnection: true,
|
||||
customConnectionAllowed: true,
|
||||
shared: true,
|
||||
disabled: false,
|
||||
};
|
||||
@@ -44,7 +44,7 @@ describe('POST /api/v1/admin/apps/:appKey/config', () => {
|
||||
it('should return HTTP 422 for already existing app config', async () => {
|
||||
const appConfig = {
|
||||
key: 'gitlab',
|
||||
allowCustomConnection: true,
|
||||
customConnectionAllowed: true,
|
||||
shared: true,
|
||||
disabled: false,
|
||||
};
|
||||
|
@@ -8,16 +8,19 @@ export default async (request, response) => {
|
||||
})
|
||||
.throwIfNotFound();
|
||||
|
||||
await appConfig.$query().patchAndFetch(appConfigParams(request));
|
||||
await appConfig.$query().patchAndFetch({
|
||||
...appConfigParams(request),
|
||||
key: request.params.appKey,
|
||||
});
|
||||
|
||||
renderObject(response, appConfig);
|
||||
};
|
||||
|
||||
const appConfigParams = (request) => {
|
||||
const { allowCustomConnection, shared, disabled } = request.body;
|
||||
const { customConnectionAllowed, shared, disabled } = request.body;
|
||||
|
||||
return {
|
||||
allowCustomConnection,
|
||||
customConnectionAllowed,
|
||||
shared,
|
||||
disabled,
|
||||
};
|
||||
|
@@ -24,7 +24,7 @@ describe('PATCH /api/v1/admin/apps/:appKey/config', () => {
|
||||
it('should return updated app config', async () => {
|
||||
const appConfig = {
|
||||
key: 'gitlab',
|
||||
allowCustomConnection: true,
|
||||
customConnectionAllowed: true,
|
||||
shared: true,
|
||||
disabled: false,
|
||||
};
|
||||
@@ -34,7 +34,7 @@ describe('PATCH /api/v1/admin/apps/:appKey/config', () => {
|
||||
const newAppConfigValues = {
|
||||
shared: false,
|
||||
disabled: true,
|
||||
allowCustomConnection: false,
|
||||
customConnectionAllowed: false,
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
@@ -55,7 +55,7 @@ describe('PATCH /api/v1/admin/apps/:appKey/config', () => {
|
||||
const appConfig = {
|
||||
shared: false,
|
||||
disabled: true,
|
||||
allowCustomConnection: false,
|
||||
customConnectionAllowed: false,
|
||||
};
|
||||
|
||||
await request(app)
|
||||
@@ -68,7 +68,7 @@ describe('PATCH /api/v1/admin/apps/:appKey/config', () => {
|
||||
it('should return HTTP 422 for invalid app config data', async () => {
|
||||
const appConfig = {
|
||||
key: 'gitlab',
|
||||
allowCustomConnection: true,
|
||||
customConnectionAllowed: true,
|
||||
shared: true,
|
||||
disabled: false,
|
||||
};
|
||||
|
@@ -155,7 +155,7 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
|
||||
await createAppConfig({
|
||||
key: 'gitlab',
|
||||
disabled: false,
|
||||
allowCustomConnection: true,
|
||||
customConnectionAllowed: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -218,7 +218,7 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
|
||||
await createAppConfig({
|
||||
key: 'gitlab',
|
||||
disabled: false,
|
||||
allowCustomConnection: false,
|
||||
customConnectionAllowed: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -17,7 +17,7 @@ describe('GET /api/v1/apps/:appKey/config', () => {
|
||||
|
||||
appConfig = await createAppConfig({
|
||||
key: 'deepl',
|
||||
allowCustomConnection: true,
|
||||
customConnectionAllowed: true,
|
||||
shared: true,
|
||||
disabled: false,
|
||||
});
|
||||
|
@@ -8,7 +8,7 @@ export default async (request, response) => {
|
||||
})
|
||||
.throwIfNotFound();
|
||||
|
||||
connection = await connection.update(connectionParams(request));
|
||||
connection = await connection.updateFormattedData(connectionParams(request));
|
||||
|
||||
renderObject(response, connection);
|
||||
};
|
||||
|
@@ -0,0 +1,37 @@
|
||||
export async function up(knex) {
|
||||
await knex.schema.alterTable('app_configs', (table) => {
|
||||
table.boolean('connection_allowed').defaultTo(false);
|
||||
});
|
||||
|
||||
const appConfigs = await knex('app_configs').select('*');
|
||||
|
||||
for (const appConfig of appConfigs) {
|
||||
const appAuthClients = await knex('app_auth_clients').where(
|
||||
'app_key',
|
||||
appConfig.key
|
||||
);
|
||||
|
||||
const hasSomeActiveAppAuthClients = !!appAuthClients?.some(
|
||||
(appAuthClient) => appAuthClient.active
|
||||
);
|
||||
const shared = appConfig.shared;
|
||||
const active = appConfig.disabled === false;
|
||||
|
||||
const connectionAllowedConditions = [
|
||||
hasSomeActiveAppAuthClients,
|
||||
shared,
|
||||
active,
|
||||
];
|
||||
const connectionAllowed = connectionAllowedConditions.every(Boolean);
|
||||
|
||||
await knex('app_configs')
|
||||
.where('id', appConfig.id)
|
||||
.update({ connection_allowed: connectionAllowed });
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex) {
|
||||
await knex.schema.alterTable('app_configs', (table) => {
|
||||
table.dropColumn('connection_allowed');
|
||||
});
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
export async function up(knex) {
|
||||
return knex.schema.alterTable('app_configs', (table) => {
|
||||
table.renameColumn('allow_custom_connection', 'custom_connection_allowed');
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex) {
|
||||
return knex.schema.alterTable('app_configs', (table) => {
|
||||
table.renameColumn('custom_connection_allowed', 'allow_custom_connection');
|
||||
});
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
export async function up(knex) {
|
||||
return knex.schema.alterTable('app_configs', function (table) {
|
||||
table.dropPrimary();
|
||||
table.primary('key');
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex) {
|
||||
return knex.schema.alterTable('app_configs', function (table) {
|
||||
table.dropPrimary();
|
||||
table.primary('id');
|
||||
});
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
export async function up(knex) {
|
||||
return knex.schema.alterTable('app_configs', function (table) {
|
||||
table.dropColumn('id');
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex) {
|
||||
return knex.schema.alterTable('app_configs', function (table) {
|
||||
table.uuid('id').defaultTo(knex.raw('gen_random_uuid()'));
|
||||
});
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
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",
|
||||
},
|
||||
"id": {
|
||||
"format": "uuid",
|
||||
"type": "string",
|
||||
},
|
||||
"key": {
|
||||
"type": "string",
|
||||
},
|
||||
"shared": {
|
||||
"default": false,
|
||||
"type": "boolean",
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"key",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
@@ -0,0 +1,42 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`Permission model > jsonSchema should have correct validations 1`] = `
|
||||
{
|
||||
"properties": {
|
||||
"action": {
|
||||
"minLength": 1,
|
||||
"type": "string",
|
||||
},
|
||||
"conditions": {
|
||||
"items": {
|
||||
"type": "string",
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
},
|
||||
"id": {
|
||||
"format": "uuid",
|
||||
"type": "string",
|
||||
},
|
||||
"roleId": {
|
||||
"format": "uuid",
|
||||
"type": "string",
|
||||
},
|
||||
"subject": {
|
||||
"minLength": 1,
|
||||
"type": "string",
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"roleId",
|
||||
"action",
|
||||
"subject",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
@@ -2,6 +2,7 @@ import AES from 'crypto-js/aes.js';
|
||||
import enc from 'crypto-js/enc-utf8.js';
|
||||
import appConfig from '../config/app.js';
|
||||
import Base from './base.js';
|
||||
import AppConfig from './app-config.js';
|
||||
|
||||
class AppAuthClient extends Base {
|
||||
static tableName = 'app_auth_clients';
|
||||
@@ -21,6 +22,17 @@ class AppAuthClient extends Base {
|
||||
},
|
||||
};
|
||||
|
||||
static relationMappings = () => ({
|
||||
appConfig: {
|
||||
relation: Base.BelongsToOneRelation,
|
||||
modelClass: AppConfig,
|
||||
join: {
|
||||
from: 'app_auth_clients.app_key',
|
||||
to: 'app_configs.key',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
encryptData() {
|
||||
if (!this.eligibleForEncryption()) return;
|
||||
|
||||
@@ -48,6 +60,17 @@ 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) {
|
||||
@@ -55,11 +78,23 @@ class AppAuthClient extends Base {
|
||||
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() {
|
||||
this.decryptData();
|
||||
}
|
||||
|
@@ -2,9 +2,12 @@ import { describe, it, expect, vi } from 'vitest';
|
||||
import AES from 'crypto-js/aes.js';
|
||||
import enc from 'crypto-js/enc-utf8.js';
|
||||
|
||||
import AppConfig from './app-config.js';
|
||||
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', () => {
|
||||
@@ -15,6 +18,23 @@ describe('AppAuthClient model', () => {
|
||||
expect(AppAuthClient.jsonSchema).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('relationMappings should return correct associations', () => {
|
||||
const relationMappings = AppAuthClient.relationMappings();
|
||||
|
||||
const expectedRelations = {
|
||||
appConfig: {
|
||||
relation: Base.BelongsToOneRelation,
|
||||
modelClass: AppConfig,
|
||||
join: {
|
||||
from: 'app_auth_clients.app_key',
|
||||
to: 'app_configs.key',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(relationMappings).toStrictEqual(expectedRelations);
|
||||
});
|
||||
|
||||
describe('encryptData', () => {
|
||||
it('should return undefined if eligibleForEncryption is not true', async () => {
|
||||
vi.spyOn(
|
||||
@@ -140,6 +160,63 @@ 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,
|
||||
@@ -151,6 +228,17 @@ 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();
|
||||
|
||||
@@ -164,6 +252,19 @@ 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();
|
||||
|
||||
|
@@ -5,6 +5,10 @@ import Base from './base.js';
|
||||
class AppConfig extends Base {
|
||||
static tableName = 'app_configs';
|
||||
|
||||
static get idColumn() {
|
||||
return 'key';
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: 'object',
|
||||
required: ['key'],
|
||||
@@ -12,7 +16,8 @@ class AppConfig extends Base {
|
||||
properties: {
|
||||
id: { type: 'string', format: 'uuid' },
|
||||
key: { type: 'string' },
|
||||
allowCustomConnection: { type: 'boolean', default: false },
|
||||
connectionAllowed: { type: 'boolean', default: false },
|
||||
customConnectionAllowed: { type: 'boolean', default: false },
|
||||
shared: { type: 'boolean', default: false },
|
||||
disabled: { type: 'boolean', default: false },
|
||||
createdAt: { type: 'string' },
|
||||
@@ -31,31 +36,44 @@ class AppConfig extends Base {
|
||||
},
|
||||
});
|
||||
|
||||
static get virtualAttributes() {
|
||||
return ['canConnect', 'canCustomConnect'];
|
||||
}
|
||||
|
||||
get canCustomConnect() {
|
||||
return !this.disabled && this.allowCustomConnection;
|
||||
}
|
||||
|
||||
get canConnect() {
|
||||
const hasSomeActiveAppAuthClients = !!this.appAuthClients?.some(
|
||||
(appAuthClient) => appAuthClient.active
|
||||
);
|
||||
const shared = this.shared;
|
||||
const active = this.disabled === false;
|
||||
|
||||
const conditions = [hasSomeActiveAppAuthClients, shared, active];
|
||||
|
||||
return conditions.every(Boolean);
|
||||
}
|
||||
|
||||
async getApp() {
|
||||
if (!this.key) return null;
|
||||
|
||||
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;
|
||||
|
180
packages/backend/src/models/app-config.test.js
Normal file
180
packages/backend/src/models/app-config.test.js
Normal file
@@ -0,0 +1,180 @@
|
||||
import { vi, 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', () => {
|
||||
expect(AppConfig.tableName).toBe('app_configs');
|
||||
});
|
||||
|
||||
it('idColumn should return key field', () => {
|
||||
expect(AppConfig.idColumn).toBe('key');
|
||||
});
|
||||
|
||||
it('jsonSchema should have correct validations', () => {
|
||||
expect(AppConfig.jsonSchema).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('relationMappings should return correct associations', () => {
|
||||
const relationMappings = AppConfig.relationMappings();
|
||||
|
||||
const expectedRelations = {
|
||||
appAuthClients: {
|
||||
relation: Base.HasManyRelation,
|
||||
modelClass: AppAuthClient,
|
||||
join: {
|
||||
from: 'app_configs.key',
|
||||
to: 'app_auth_clients.app_key',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(relationMappings).toStrictEqual(expectedRelations);
|
||||
});
|
||||
|
||||
describe('getApp', () => {
|
||||
it('getApp should return null if there is no key', async () => {
|
||||
const appConfig = new AppConfig();
|
||||
const app = await appConfig.getApp();
|
||||
|
||||
expect(app).toBeNull();
|
||||
});
|
||||
|
||||
it('getApp should return app with provided key', async () => {
|
||||
const appConfig = new AppConfig();
|
||||
appConfig.key = 'deepl';
|
||||
|
||||
const app = await appConfig.getApp();
|
||||
const expectedApp = await App.findOneByKey(appConfig.key);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
@@ -89,7 +89,7 @@ class Connection extends Base {
|
||||
}
|
||||
|
||||
if (this.appConfig) {
|
||||
return !this.appConfig.disabled && this.appConfig.allowCustomConnection;
|
||||
return !this.appConfig.disabled && this.appConfig.customConnectionAllowed;
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -122,10 +122,20 @@ class Connection extends Base {
|
||||
return this.data ? true : false;
|
||||
}
|
||||
|
||||
async checkEligibilityForCreation() {
|
||||
const app = await App.findOneByKey(this.key);
|
||||
async getApp() {
|
||||
if (!this.key) return null;
|
||||
|
||||
const appConfig = await AppConfig.query().findOne({ key: this.key });
|
||||
return await App.findOneByKey(this.key);
|
||||
}
|
||||
|
||||
async getAppConfig() {
|
||||
return await AppConfig.query().findOne({ key: this.key });
|
||||
}
|
||||
|
||||
async checkEligibilityForCreation() {
|
||||
const app = await this.getApp();
|
||||
|
||||
const appConfig = await this.getAppConfig();
|
||||
|
||||
if (appConfig) {
|
||||
if (appConfig.disabled) {
|
||||
@@ -134,7 +144,7 @@ class Connection extends Base {
|
||||
);
|
||||
}
|
||||
|
||||
if (!appConfig.allowCustomConnection && this.formattedData) {
|
||||
if (!appConfig.customConnectionAllowed && this.formattedData) {
|
||||
throw new NotAuthorizedError(
|
||||
`New custom connections have been disabled for ${app.name}!`
|
||||
);
|
||||
@@ -160,12 +170,6 @@ class Connection extends Base {
|
||||
return this;
|
||||
}
|
||||
|
||||
async getApp() {
|
||||
if (!this.key) return null;
|
||||
|
||||
return await App.findOneByKey(this.key);
|
||||
}
|
||||
|
||||
async testAndUpdateConnection() {
|
||||
const app = await this.getApp();
|
||||
const $ = await globalVariable({ connection: this, app });
|
||||
@@ -224,7 +228,7 @@ class Connection extends Base {
|
||||
async reset() {
|
||||
const formattedData = this?.formattedData?.screenName
|
||||
? { screenName: this.formattedData.screenName }
|
||||
: null;
|
||||
: {};
|
||||
|
||||
const updatedConnection = await this.$query().patchAndFetch({
|
||||
formattedData,
|
||||
@@ -233,7 +237,7 @@ class Connection extends Base {
|
||||
return updatedConnection;
|
||||
}
|
||||
|
||||
async update({ formattedData, appAuthClientId }) {
|
||||
async updateFormattedData({ formattedData, appAuthClientId }) {
|
||||
if (appAuthClientId) {
|
||||
const appAuthClient = await AppAuthClient.query()
|
||||
.findById(appAuthClientId)
|
||||
|
@@ -3,11 +3,16 @@ import AES from 'crypto-js/aes.js';
|
||||
import enc from 'crypto-js/enc-utf8.js';
|
||||
import appConfig from '../config/app.js';
|
||||
import AppAuthClient from './app-auth-client.js';
|
||||
import App from './app.js';
|
||||
import AppConfig from './app-config.js';
|
||||
import Base from './base.js';
|
||||
import Connection from './connection';
|
||||
import Step from './step.js';
|
||||
import User from './user.js';
|
||||
import Telemetry from '../helpers/telemetry/index.js';
|
||||
import { createConnection } from '../../test/factories/connection.js';
|
||||
import { createAppConfig } from '../../test/factories/app-config.js';
|
||||
import { createAppAuthClient } from '../../test/factories/app-auth-client.js';
|
||||
|
||||
describe('Connection model', () => {
|
||||
it('tableName should return correct name', () => {
|
||||
@@ -26,57 +31,138 @@ describe('Connection model', () => {
|
||||
expect(virtualAttributes).toStrictEqual(expectedAttributes);
|
||||
});
|
||||
|
||||
it('relationMappings should return correct associations', () => {
|
||||
const relationMappings = Connection.relationMappings();
|
||||
describe('relationMappings', () => {
|
||||
it('should return correct associations', () => {
|
||||
const relationMappings = Connection.relationMappings();
|
||||
|
||||
const expectedRelations = {
|
||||
user: {
|
||||
relation: Base.BelongsToOneRelation,
|
||||
modelClass: User,
|
||||
join: {
|
||||
from: 'connections.user_id',
|
||||
to: 'users.id',
|
||||
const expectedRelations = {
|
||||
user: {
|
||||
relation: Base.BelongsToOneRelation,
|
||||
modelClass: User,
|
||||
join: {
|
||||
from: 'connections.user_id',
|
||||
to: 'users.id',
|
||||
},
|
||||
},
|
||||
},
|
||||
steps: {
|
||||
relation: Base.HasManyRelation,
|
||||
modelClass: Step,
|
||||
join: {
|
||||
from: 'connections.id',
|
||||
to: 'steps.connection_id',
|
||||
steps: {
|
||||
relation: Base.HasManyRelation,
|
||||
modelClass: Step,
|
||||
join: {
|
||||
from: 'connections.id',
|
||||
to: 'steps.connection_id',
|
||||
},
|
||||
},
|
||||
},
|
||||
triggerSteps: {
|
||||
relation: Base.HasManyRelation,
|
||||
modelClass: Step,
|
||||
join: {
|
||||
from: 'connections.id',
|
||||
to: 'steps.connection_id',
|
||||
triggerSteps: {
|
||||
relation: Base.HasManyRelation,
|
||||
modelClass: Step,
|
||||
join: {
|
||||
from: 'connections.id',
|
||||
to: 'steps.connection_id',
|
||||
},
|
||||
filter: expect.any(Function),
|
||||
},
|
||||
filter: expect.any(Function),
|
||||
},
|
||||
appConfig: {
|
||||
relation: Base.BelongsToOneRelation,
|
||||
modelClass: AppConfig,
|
||||
join: {
|
||||
from: 'connections.key',
|
||||
to: 'app_configs.key',
|
||||
appConfig: {
|
||||
relation: Base.BelongsToOneRelation,
|
||||
modelClass: AppConfig,
|
||||
join: {
|
||||
from: 'connections.key',
|
||||
to: 'app_configs.key',
|
||||
},
|
||||
},
|
||||
},
|
||||
appAuthClient: {
|
||||
relation: Base.BelongsToOneRelation,
|
||||
modelClass: AppAuthClient,
|
||||
join: {
|
||||
from: 'connections.app_auth_client_id',
|
||||
to: 'app_auth_clients.id',
|
||||
appAuthClient: {
|
||||
relation: Base.BelongsToOneRelation,
|
||||
modelClass: AppAuthClient,
|
||||
join: {
|
||||
from: 'connections.app_auth_client_id',
|
||||
to: 'app_auth_clients.id',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
expect(relationMappings).toStrictEqual(expectedRelations);
|
||||
expect(relationMappings).toStrictEqual(expectedRelations);
|
||||
});
|
||||
|
||||
it('triggerSteps should return only trigger typed steps', () => {
|
||||
const relations = Connection.relationMappings();
|
||||
const whereSpy = vi.fn();
|
||||
|
||||
relations.triggerSteps.filter({ where: whereSpy });
|
||||
|
||||
expect(whereSpy).toHaveBeenCalledWith('type', '=', 'trigger');
|
||||
});
|
||||
});
|
||||
|
||||
describe.todo('reconnectable');
|
||||
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 () => {
|
||||
@@ -160,4 +246,558 @@ describe('Connection model', () => {
|
||||
expect(connection.data).not.toEqual(formattedData);
|
||||
});
|
||||
});
|
||||
|
||||
describe('eligibleForEncryption', () => {
|
||||
it('should return true when formattedData property exists', async () => {
|
||||
const connection = new Connection();
|
||||
connection.formattedData = { clientId: 'sample-id' };
|
||||
|
||||
expect(connection.eligibleForEncryption()).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when formattedData property doesn't exist", async () => {
|
||||
const connection = new Connection();
|
||||
connection.formattedData = undefined;
|
||||
|
||||
expect(connection.eligibleForEncryption()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('eligibleForDecryption', () => {
|
||||
it('should return true when data property exists', async () => {
|
||||
const connection = new Connection();
|
||||
connection.data = 'encrypted-data';
|
||||
|
||||
expect(connection.eligibleForDecryption()).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when data property doesn't exist", async () => {
|
||||
const connection = new Connection();
|
||||
connection.data = undefined;
|
||||
|
||||
expect(connection.eligibleForDecryption()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getApp', () => {
|
||||
it('should return connection app when valid key exists', async () => {
|
||||
const connection = new Connection();
|
||||
connection.key = 'gitlab';
|
||||
|
||||
const connectionApp = await connection.getApp();
|
||||
const app = await App.findOneByKey('gitlab');
|
||||
|
||||
expect(connectionApp).toStrictEqual(app);
|
||||
});
|
||||
|
||||
it('should throw an error when invalid key exists', async () => {
|
||||
const connection = new Connection();
|
||||
connection.key = 'invalid-key';
|
||||
|
||||
await expect(() => connection.getApp()).rejects.toThrowError(
|
||||
`An application with the "invalid-key" key couldn't be found.`
|
||||
);
|
||||
});
|
||||
|
||||
it('should return null when no key exists', async () => {
|
||||
const connection = new Connection();
|
||||
|
||||
await expect(connection.getApp()).resolves.toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
it('getAppConfig should return connection app config', async () => {
|
||||
const connection = new Connection();
|
||||
connection.key = 'gitlab';
|
||||
|
||||
const appConfig = await createAppConfig({ key: 'gitlab' });
|
||||
|
||||
const connectionAppConfig = await connection.getAppConfig();
|
||||
|
||||
expect(connectionAppConfig).toStrictEqual(appConfig);
|
||||
});
|
||||
|
||||
describe('checkEligibilityForCreation', () => {
|
||||
it('should return connection if no app config exists', async () => {
|
||||
vi.spyOn(Connection.prototype, 'getApp').mockResolvedValue({
|
||||
name: 'gitlab',
|
||||
});
|
||||
|
||||
vi.spyOn(Connection.prototype, 'getAppConfig').mockResolvedValue();
|
||||
|
||||
const connection = new Connection();
|
||||
|
||||
expect(await connection.checkEligibilityForCreation()).toBe(connection);
|
||||
});
|
||||
|
||||
it('should throw an error when app does not exist', async () => {
|
||||
vi.spyOn(Connection.prototype, 'getApp').mockRejectedValue(
|
||||
new Error(
|
||||
`An application with the "unexisting-app" key couldn't be found.`
|
||||
)
|
||||
);
|
||||
|
||||
vi.spyOn(Connection.prototype, 'getAppConfig').mockResolvedValue();
|
||||
|
||||
const connection = new Connection();
|
||||
|
||||
await expect(() =>
|
||||
connection.checkEligibilityForCreation()
|
||||
).rejects.toThrow(
|
||||
`An application with the "unexisting-app" key couldn't be found.`
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error when app config is disabled', async () => {
|
||||
vi.spyOn(Connection.prototype, 'getApp').mockResolvedValue({
|
||||
name: 'gitlab',
|
||||
});
|
||||
|
||||
vi.spyOn(Connection.prototype, 'getAppConfig').mockResolvedValue({
|
||||
disabled: true,
|
||||
});
|
||||
|
||||
const connection = new Connection();
|
||||
|
||||
await expect(() =>
|
||||
connection.checkEligibilityForCreation()
|
||||
).rejects.toThrow(
|
||||
'The application has been disabled for new connections!'
|
||||
);
|
||||
});
|
||||
|
||||
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',
|
||||
});
|
||||
|
||||
vi.spyOn(Connection.prototype, 'getAppConfig').mockResolvedValue({
|
||||
disabled: false,
|
||||
customConnectionAllowed: false,
|
||||
});
|
||||
|
||||
const connection = new Connection();
|
||||
connection.formattedData = {};
|
||||
|
||||
await expect(() =>
|
||||
connection.checkEligibilityForCreation()
|
||||
).rejects.toThrow(
|
||||
'New custom connections have been disabled for gitlab!'
|
||||
);
|
||||
});
|
||||
|
||||
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({
|
||||
appKey: 'gitlab',
|
||||
active: true,
|
||||
formattedAuthDefaults: {
|
||||
clientId: 'sample-id',
|
||||
},
|
||||
});
|
||||
|
||||
const connection = await createConnection({
|
||||
key: 'gitlab',
|
||||
appAuthClientId: appAuthClient.id,
|
||||
formattedData: null,
|
||||
});
|
||||
|
||||
await connection.checkEligibilityForCreation();
|
||||
|
||||
expect(connection.formattedData).toStrictEqual({
|
||||
clientId: 'sample-id',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('testAndUpdateConnection', () => {
|
||||
it('should verify connection and persist it', async () => {
|
||||
const connection = await createConnection({ verified: false });
|
||||
|
||||
const isStillVerifiedSpy = vi.fn().mockReturnValue(true);
|
||||
|
||||
const originalApp = await connection.getApp();
|
||||
|
||||
const getAppSpy = vi
|
||||
.spyOn(connection, 'getApp')
|
||||
.mockImplementation(() => {
|
||||
return {
|
||||
...originalApp,
|
||||
auth: {
|
||||
...originalApp.auth,
|
||||
isStillVerified: isStillVerifiedSpy,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const updatedConnection = await connection.testAndUpdateConnection();
|
||||
|
||||
expect(getAppSpy).toHaveBeenCalledOnce();
|
||||
expect(isStillVerifiedSpy).toHaveBeenCalledOnce();
|
||||
expect(updatedConnection.verified).toBe(true);
|
||||
});
|
||||
|
||||
it('should unverify connection and persist it', async () => {
|
||||
const connection = await createConnection({ verified: true });
|
||||
|
||||
const isStillVerifiedSpy = vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('Wrong credentials!'));
|
||||
|
||||
const originalApp = await connection.getApp();
|
||||
|
||||
const getAppSpy = vi
|
||||
.spyOn(connection, 'getApp')
|
||||
.mockImplementation(() => {
|
||||
return {
|
||||
...originalApp,
|
||||
auth: {
|
||||
...originalApp.auth,
|
||||
isStillVerified: isStillVerifiedSpy,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const updatedConnection = await connection.testAndUpdateConnection();
|
||||
|
||||
expect(getAppSpy).toHaveBeenCalledOnce();
|
||||
expect(isStillVerifiedSpy).toHaveBeenCalledOnce();
|
||||
expect(updatedConnection.verified).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyAndUpdateConnection', () => {
|
||||
it('should verify connection with valid token', async () => {
|
||||
const connection = await createConnection({
|
||||
verified: false,
|
||||
draft: true,
|
||||
});
|
||||
|
||||
const verifyCredentialsSpy = vi.fn().mockResolvedValue(true);
|
||||
|
||||
const originalApp = await connection.getApp();
|
||||
|
||||
vi.spyOn(connection, 'getApp').mockImplementation(() => {
|
||||
return {
|
||||
...originalApp,
|
||||
auth: {
|
||||
...originalApp.auth,
|
||||
verifyCredentials: verifyCredentialsSpy,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const updatedConnection = await connection.verifyAndUpdateConnection();
|
||||
|
||||
expect(verifyCredentialsSpy).toHaveBeenCalledOnce();
|
||||
expect(updatedConnection.verified).toBe(true);
|
||||
expect(updatedConnection.draft).toBe(false);
|
||||
});
|
||||
|
||||
it('should throw an error with invalid token', async () => {
|
||||
const connection = await createConnection({
|
||||
verified: false,
|
||||
draft: true,
|
||||
});
|
||||
|
||||
const verifyCredentialsSpy = vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('Invalid token!'));
|
||||
|
||||
const originalApp = await connection.getApp();
|
||||
|
||||
vi.spyOn(connection, 'getApp').mockImplementation(() => {
|
||||
return {
|
||||
...originalApp,
|
||||
auth: {
|
||||
...originalApp.auth,
|
||||
verifyCredentials: verifyCredentialsSpy,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
await expect(() =>
|
||||
connection.verifyAndUpdateConnection()
|
||||
).rejects.toThrowError('Invalid token!');
|
||||
expect(verifyCredentialsSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyWebhook', () => {
|
||||
it('should verify webhook on remote', async () => {
|
||||
const connection = await createConnection({ key: 'typeform' });
|
||||
|
||||
const verifyWebhookSpy = vi.fn().mockResolvedValue('verified-webhook');
|
||||
|
||||
const originalApp = await connection.getApp();
|
||||
|
||||
vi.spyOn(connection, 'getApp').mockImplementation(() => {
|
||||
return {
|
||||
...originalApp,
|
||||
auth: {
|
||||
...originalApp.auth,
|
||||
verifyWebhook: verifyWebhookSpy,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
expect(await connection.verifyWebhook()).toBe('verified-webhook');
|
||||
});
|
||||
|
||||
it('should return true if connection does not have value in key property', async () => {
|
||||
const connection = await createConnection({ key: null });
|
||||
|
||||
expect(await connection.verifyWebhook()).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw an error at failed webhook verification', async () => {
|
||||
const connection = await createConnection({ key: 'typeform' });
|
||||
|
||||
const verifyWebhookSpy = vi.fn().mockRejectedValue('unverified-webhook');
|
||||
|
||||
const originalApp = await connection.getApp();
|
||||
|
||||
vi.spyOn(connection, 'getApp').mockImplementation(() => {
|
||||
return {
|
||||
...originalApp,
|
||||
auth: {
|
||||
...originalApp.auth,
|
||||
verifyWebhook: verifyWebhookSpy,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
await expect(() => connection.verifyWebhook()).rejects.toThrowError(
|
||||
'unverified-webhook'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('generateAuthUrl should return authentication url', async () => {
|
||||
const connection = await createConnection({
|
||||
key: 'typeform',
|
||||
formattedData: {
|
||||
url: 'https://automatisch.io/authentication-url',
|
||||
},
|
||||
});
|
||||
|
||||
const generateAuthUrlSpy = vi.fn();
|
||||
|
||||
const originalApp = await connection.getApp();
|
||||
|
||||
vi.spyOn(connection, 'getApp').mockImplementation(() => {
|
||||
return {
|
||||
...originalApp,
|
||||
auth: {
|
||||
...originalApp.auth,
|
||||
generateAuthUrl: generateAuthUrlSpy,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
expect(await connection.generateAuthUrl()).toStrictEqual({
|
||||
url: 'https://automatisch.io/authentication-url',
|
||||
});
|
||||
});
|
||||
|
||||
describe('reset', () => {
|
||||
it('should keep screen name when exists and reset the rest of the formatted data', async () => {
|
||||
const connection = await createConnection({
|
||||
formattedData: {
|
||||
screenName: 'Sample connection',
|
||||
token: 'sample-token',
|
||||
},
|
||||
});
|
||||
|
||||
await connection.reset();
|
||||
|
||||
const refetchedConnection = await connection.$query();
|
||||
|
||||
expect(refetchedConnection.formattedData).toStrictEqual({
|
||||
screenName: 'Sample connection',
|
||||
});
|
||||
});
|
||||
|
||||
it('should empty formatted data object when screen name does not exist', async () => {
|
||||
const connection = await createConnection({
|
||||
formattedData: {
|
||||
token: 'sample-token',
|
||||
},
|
||||
});
|
||||
|
||||
await connection.reset();
|
||||
|
||||
const refetchedConnection = await connection.$query();
|
||||
|
||||
expect(refetchedConnection.formattedData).toStrictEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateFormattedData', () => {
|
||||
it('should extend connection data with app auth client auth defaults', async () => {
|
||||
const appAuthClient = await createAppAuthClient({
|
||||
formattedAuthDefaults: {
|
||||
clientId: 'sample-id',
|
||||
},
|
||||
});
|
||||
|
||||
const connection = await createConnection({
|
||||
appAuthClientId: appAuthClient.id,
|
||||
formattedData: {
|
||||
token: 'sample-token',
|
||||
},
|
||||
});
|
||||
|
||||
const updatedConnection = await connection.updateFormattedData({
|
||||
appAuthClientId: appAuthClient.id,
|
||||
});
|
||||
|
||||
expect(updatedConnection.formattedData).toStrictEqual({
|
||||
clientId: 'sample-id',
|
||||
token: 'sample-token',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('$beforeInsert', () => {
|
||||
it('should call super.$beforeInsert', async () => {
|
||||
const superBeforeInsertSpy = vi
|
||||
.spyOn(Base.prototype, '$beforeInsert')
|
||||
.mockResolvedValue();
|
||||
|
||||
await createConnection();
|
||||
|
||||
expect(superBeforeInsertSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should call checkEligibilityForCreation', async () => {
|
||||
const checkEligibilityForCreationSpy = vi
|
||||
.spyOn(Connection.prototype, 'checkEligibilityForCreation')
|
||||
.mockResolvedValue();
|
||||
|
||||
await createConnection();
|
||||
|
||||
expect(checkEligibilityForCreationSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should call encryptData', async () => {
|
||||
const encryptDataSpy = vi
|
||||
.spyOn(Connection.prototype, 'encryptData')
|
||||
.mockResolvedValue();
|
||||
|
||||
await createConnection();
|
||||
|
||||
expect(encryptDataSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
describe('$beforeUpdate', () => {
|
||||
it('should call super.$beforeUpdate', async () => {
|
||||
const superBeforeUpdateSpy = vi
|
||||
.spyOn(Base.prototype, '$beforeUpdate')
|
||||
.mockResolvedValue();
|
||||
|
||||
const connection = await createConnection();
|
||||
|
||||
await connection.$query().patch({ verified: false });
|
||||
|
||||
expect(superBeforeUpdateSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should call encryptData', async () => {
|
||||
const connection = await createConnection();
|
||||
|
||||
const encryptDataSpy = vi
|
||||
.spyOn(Connection.prototype, 'encryptData')
|
||||
.mockResolvedValue();
|
||||
|
||||
await connection.$query().patch({ verified: false });
|
||||
|
||||
expect(encryptDataSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
describe('$afterFind', () => {
|
||||
it('should call decryptData', async () => {
|
||||
const connection = await createConnection();
|
||||
|
||||
const decryptDataSpy = vi
|
||||
.spyOn(Connection.prototype, 'decryptData')
|
||||
.mockResolvedValue();
|
||||
|
||||
await connection.$query();
|
||||
|
||||
expect(decryptDataSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
describe('$afterInsert', () => {
|
||||
it('should call super.$afterInsert', async () => {
|
||||
const superAfterInsertSpy = vi.spyOn(Base.prototype, '$afterInsert');
|
||||
|
||||
await createConnection();
|
||||
|
||||
expect(superAfterInsertSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should call Telemetry.connectionCreated', async () => {
|
||||
const telemetryConnectionCreatedSpy = vi
|
||||
.spyOn(Telemetry, 'connectionCreated')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const connection = await createConnection();
|
||||
|
||||
expect(telemetryConnectionCreatedSpy).toHaveBeenCalledWith(connection);
|
||||
});
|
||||
});
|
||||
|
||||
describe('$afterUpdate', () => {
|
||||
it('should call super.$afterUpdate', async () => {
|
||||
const superAfterInsertSpy = vi.spyOn(Base.prototype, '$afterUpdate');
|
||||
|
||||
const connection = await createConnection();
|
||||
|
||||
await connection.$query().patch({ verified: false });
|
||||
|
||||
expect(superAfterInsertSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should call Telemetry.connectionUpdated', async () => {
|
||||
const telemetryconnectionUpdatedSpy = vi
|
||||
.spyOn(Telemetry, 'connectionCreated')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const connection = await createConnection();
|
||||
|
||||
await connection.$query().patch({ verified: false });
|
||||
|
||||
expect(telemetryconnectionUpdatedSpy).toHaveBeenCalledWith(connection);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -19,25 +19,39 @@ class Permission extends Base {
|
||||
},
|
||||
};
|
||||
|
||||
static sanitize(permissions) {
|
||||
static filter(permissions) {
|
||||
const sanitizedPermissions = permissions.filter((permission) => {
|
||||
const { action, subject, conditions } = permission;
|
||||
|
||||
const relevantAction = permissionCatalog.actions.find(
|
||||
(actionCatalogItem) => actionCatalogItem.key === action
|
||||
);
|
||||
const validSubject = relevantAction.subjects.includes(subject);
|
||||
const validConditions = conditions.every((condition) => {
|
||||
return !!permissionCatalog.conditions.find(
|
||||
(conditionCatalogItem) => conditionCatalogItem.key === condition
|
||||
);
|
||||
});
|
||||
const relevantAction = this.findAction(action);
|
||||
const validSubject = this.isSubjectValid(subject, relevantAction);
|
||||
const validConditions = this.areConditionsValid(conditions);
|
||||
|
||||
return validSubject && validConditions;
|
||||
return relevantAction && validSubject && validConditions;
|
||||
});
|
||||
|
||||
return sanitizedPermissions;
|
||||
}
|
||||
|
||||
static findAction(action) {
|
||||
return permissionCatalog.actions.find(
|
||||
(actionCatalogItem) => actionCatalogItem.key === action
|
||||
);
|
||||
}
|
||||
|
||||
static isSubjectValid(subject, action) {
|
||||
return action && action.subjects.includes(subject);
|
||||
}
|
||||
|
||||
static areConditionsValid(conditions) {
|
||||
return conditions.every((condition) => this.isConditionValid(condition));
|
||||
}
|
||||
|
||||
static isConditionValid(condition) {
|
||||
return !!permissionCatalog.conditions.find(
|
||||
(conditionCatalogItem) => conditionCatalogItem.key === condition
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Permission;
|
||||
|
95
packages/backend/src/models/permission.test.js
Normal file
95
packages/backend/src/models/permission.test.js
Normal file
@@ -0,0 +1,95 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import Permission from './permission';
|
||||
import permissionCatalog from '../helpers/permission-catalog.ee.js';
|
||||
|
||||
describe('Permission model', () => {
|
||||
it('tableName should return correct name', () => {
|
||||
expect(Permission.tableName).toBe('permissions');
|
||||
});
|
||||
|
||||
it('jsonSchema should have correct validations', () => {
|
||||
expect(Permission.jsonSchema).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('filter should return only valid permissions based on permission catalog', () => {
|
||||
const permissions = [
|
||||
{ action: 'read', subject: 'Flow', conditions: ['isCreator'] },
|
||||
{ action: 'delete', subject: 'Connection', conditions: [] },
|
||||
{ action: 'publish', subject: 'Flow', conditions: ['isCreator'] },
|
||||
{ action: 'update', subject: 'Execution', conditions: [] }, // Invalid subject
|
||||
{ action: 'read', subject: 'Execution', conditions: ['invalid'] }, // Invalid condition
|
||||
{ action: 'invalid', subject: 'Execution', conditions: [] }, // Invalid action
|
||||
];
|
||||
|
||||
const result = Permission.filter(permissions);
|
||||
|
||||
expect(result).toStrictEqual([
|
||||
{ action: 'read', subject: 'Flow', conditions: ['isCreator'] },
|
||||
{ action: 'delete', subject: 'Connection', conditions: [] },
|
||||
{ action: 'publish', subject: 'Flow', conditions: ['isCreator'] },
|
||||
]);
|
||||
});
|
||||
|
||||
describe('findAction', () => {
|
||||
it('should return action from permission catalog', () => {
|
||||
const action = Permission.findAction('create');
|
||||
expect(action.key).toStrictEqual('create');
|
||||
});
|
||||
|
||||
it('should return undefined for invalid actions', () => {
|
||||
const invalidAction = Permission.findAction('invalidAction');
|
||||
expect(invalidAction).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSubjectValid', () => {
|
||||
it('should return true for valid subjects', () => {
|
||||
const validAction = permissionCatalog.actions.find(
|
||||
(action) => action.key === 'create'
|
||||
);
|
||||
|
||||
const validSubject = Permission.isSubjectValid('Connection', validAction);
|
||||
expect(validSubject).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid subjects', () => {
|
||||
const validAction = permissionCatalog.actions.find(
|
||||
(action) => action.key === 'create'
|
||||
);
|
||||
|
||||
const invalidSubject = Permission.isSubjectValid(
|
||||
'Execution',
|
||||
validAction
|
||||
);
|
||||
|
||||
expect(invalidSubject).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('areConditionsValid', () => {
|
||||
it('should return true for valid conditions', () => {
|
||||
const validConditions = Permission.areConditionsValid(['isCreator']);
|
||||
expect(validConditions).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid conditions', () => {
|
||||
const invalidConditions = Permission.areConditionsValid([
|
||||
'invalidCondition',
|
||||
]);
|
||||
|
||||
expect(invalidConditions).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isConditionValid', () => {
|
||||
it('should return true for valid conditions', () => {
|
||||
const validCondition = Permission.isConditionValid('isCreator');
|
||||
expect(validCondition).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid conditions', () => {
|
||||
const invalidCondition = Permission.isConditionValid('invalidCondition');
|
||||
expect(invalidCondition).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
@@ -63,14 +63,14 @@ class Role extends Base {
|
||||
await this.$relatedQuery('permissions', trx).delete();
|
||||
|
||||
if (permissions?.length) {
|
||||
const sanitizedPermissions = Permission.sanitize(permissions).map(
|
||||
const validPermissions = Permission.filter(permissions).map(
|
||||
(permission) => ({
|
||||
...permission,
|
||||
roleId: this.id,
|
||||
})
|
||||
);
|
||||
|
||||
await Permission.query().insert(sanitizedPermissions);
|
||||
await Permission.query().insert(validPermissions);
|
||||
}
|
||||
|
||||
await this.$query(trx).patch({
|
||||
|
@@ -2,11 +2,10 @@ const appConfigSerializer = (appConfig) => {
|
||||
return {
|
||||
id: appConfig.id,
|
||||
key: appConfig.key,
|
||||
allowCustomConnection: appConfig.allowCustomConnection,
|
||||
customConnectionAllowed: appConfig.customConnectionAllowed,
|
||||
shared: appConfig.shared,
|
||||
disabled: appConfig.disabled,
|
||||
canConnect: appConfig.canConnect,
|
||||
canCustomConnect: appConfig.canCustomConnect,
|
||||
connectionAllowed: appConfig.connectionAllowed,
|
||||
createdAt: appConfig.createdAt.getTime(),
|
||||
updatedAt: appConfig.updatedAt.getTime(),
|
||||
};
|
||||
|
@@ -13,11 +13,10 @@ describe('appConfig serializer', () => {
|
||||
const expectedPayload = {
|
||||
id: appConfig.id,
|
||||
key: appConfig.key,
|
||||
allowCustomConnection: appConfig.allowCustomConnection,
|
||||
customConnectionAllowed: appConfig.customConnectionAllowed,
|
||||
shared: appConfig.shared,
|
||||
disabled: appConfig.disabled,
|
||||
canConnect: appConfig.canConnect,
|
||||
canCustomConnect: appConfig.canCustomConnect,
|
||||
connectionAllowed: appConfig.connectionAllowed,
|
||||
createdAt: appConfig.createdAt.getTime(),
|
||||
updatedAt: appConfig.updatedAt.getTime(),
|
||||
};
|
||||
|
@@ -10,7 +10,6 @@ const formattedAuthDefaults = {
|
||||
|
||||
export const createAppAuthClient = async (params = {}) => {
|
||||
params.name = params?.name || faker.person.fullName();
|
||||
params.id = params?.id || faker.string.uuid();
|
||||
params.appKey = params?.appKey || 'deepl';
|
||||
params.active = params?.active ?? true;
|
||||
params.formattedAuthDefaults =
|
||||
|
@@ -2,7 +2,7 @@ const createAppConfigMock = (appConfig) => {
|
||||
return {
|
||||
data: {
|
||||
key: appConfig.key,
|
||||
allowCustomConnection: appConfig.allowCustomConnection,
|
||||
customConnectionAllowed: appConfig.customConnectionAllowed,
|
||||
shared: appConfig.shared,
|
||||
disabled: appConfig.disabled,
|
||||
},
|
||||
|
@@ -3,11 +3,10 @@ const getAppConfigMock = (appConfig) => {
|
||||
data: {
|
||||
id: appConfig.id,
|
||||
key: appConfig.key,
|
||||
allowCustomConnection: appConfig.allowCustomConnection,
|
||||
customConnectionAllowed: appConfig.customConnectionAllowed,
|
||||
shared: appConfig.shared,
|
||||
disabled: appConfig.disabled,
|
||||
canConnect: appConfig.canConnect,
|
||||
canCustomConnect: appConfig.canCustomConnect,
|
||||
connectionAllowed: appConfig.connectionAllowed,
|
||||
createdAt: appConfig.createdAt.getTime(),
|
||||
updatedAt: appConfig.updatedAt.getTime(),
|
||||
},
|
||||
|
@@ -8,11 +8,15 @@ export class AdminApplicationSettingsPage extends AuthenticatedPage {
|
||||
constructor(page) {
|
||||
super(page);
|
||||
|
||||
this.allowCustomConnectionsSwitch = this.page.locator('[name="allowCustomConnection"]');
|
||||
this.allowCustomConnectionsSwitch = this.page.locator(
|
||||
'[name="customConnectionAllowed"]'
|
||||
);
|
||||
this.allowSharedConnectionsSwitch = this.page.locator('[name="shared"]');
|
||||
this.disableConnectionsSwitch = this.page.locator('[name="disabled"]');
|
||||
this.saveButton = this.page.getByTestId('submit-button');
|
||||
this.successSnackbar = this.page.getByTestId('snackbar-save-admin-apps-settings-success');
|
||||
this.successSnackbar = this.page.getByTestId(
|
||||
'snackbar-save-admin-apps-settings-success'
|
||||
);
|
||||
}
|
||||
|
||||
async allowCustomConnections() {
|
||||
|
@@ -20,7 +20,7 @@ function AdminApplicationCreateAuthClient(props) {
|
||||
const {
|
||||
mutateAsync: createAppConfig,
|
||||
isPending: isCreateAppConfigPending,
|
||||
error: createAppConfigError
|
||||
error: createAppConfigError,
|
||||
} = useAdminCreateAppConfig(props.appKey);
|
||||
|
||||
const {
|
||||
@@ -30,16 +30,15 @@ function AdminApplicationCreateAuthClient(props) {
|
||||
} = useAdminCreateAppAuthClient(appKey);
|
||||
|
||||
const submitHandler = async (values) => {
|
||||
let appConfigId = appConfig?.data?.id;
|
||||
let appConfigKey = appConfig?.data?.key;
|
||||
|
||||
if (!appConfigId) {
|
||||
if (!appConfigKey) {
|
||||
const { data: appConfigData } = await createAppConfig({
|
||||
allowCustomConnection: true,
|
||||
customConnectionAllowed: true,
|
||||
shared: false,
|
||||
disabled: false,
|
||||
});
|
||||
|
||||
appConfigId = appConfigData.id;
|
||||
appConfigKey = appConfigData.key;
|
||||
}
|
||||
|
||||
const { name, active, ...formattedAuthDefaults } = values;
|
||||
|
@@ -46,7 +46,8 @@ function AdminApplicationSettings(props) {
|
||||
|
||||
const defaultValues = useMemo(
|
||||
() => ({
|
||||
allowCustomConnection: appConfig?.data?.allowCustomConnection || false,
|
||||
customConnectionAllowed:
|
||||
appConfig?.data?.customConnectionAllowed || false,
|
||||
shared: appConfig?.data?.shared || false,
|
||||
disabled: appConfig?.data?.disabled || false,
|
||||
}),
|
||||
@@ -61,8 +62,8 @@ function AdminApplicationSettings(props) {
|
||||
<Paper sx={{ p: 2, mt: 4 }}>
|
||||
<Stack spacing={2} direction="column">
|
||||
<Switch
|
||||
name="allowCustomConnection"
|
||||
label={formatMessage('adminAppsSettings.allowCustomConnection')}
|
||||
name="customConnectionAllowed"
|
||||
label={formatMessage('adminAppsSettings.customConnectionAllowed')}
|
||||
FormControlLabelProps={{
|
||||
labelPlacement: 'start',
|
||||
}}
|
||||
|
@@ -93,14 +93,17 @@ function ChooseConnectionSubstep(props) {
|
||||
appWithConnections?.map((connection) => optionGenerator(connection)) ||
|
||||
[];
|
||||
|
||||
if (!appConfig?.data || appConfig?.data?.canCustomConnect) {
|
||||
if (
|
||||
!appConfig?.data ||
|
||||
(!appConfig.data?.disabled && appConfig.data?.customConnectionAllowed)
|
||||
) {
|
||||
options.push({
|
||||
label: formatMessage('chooseConnectionSubstep.addNewConnection'),
|
||||
value: ADD_CONNECTION_VALUE,
|
||||
});
|
||||
}
|
||||
|
||||
if (appConfig?.data?.canConnect) {
|
||||
if (appConfig?.data?.connectionAllowed) {
|
||||
options.push({
|
||||
label: formatMessage('chooseConnectionSubstep.addNewSharedConnection'),
|
||||
value: ADD_SHARED_CONNECTION_VALUE,
|
||||
|
@@ -40,7 +40,7 @@ SuggestionItem.propTypes = {
|
||||
data: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
label: PropTypes.string,
|
||||
sampleValue: PropTypes.string,
|
||||
sampleValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
}),
|
||||
).isRequired,
|
||||
onSuggestionClick: PropTypes.func.isRequired,
|
||||
|
@@ -53,15 +53,13 @@ function SignUpForm() {
|
||||
}, [authentication.isAuthenticated]);
|
||||
|
||||
const handleSubmit = async (values) => {
|
||||
const { fullName, email, password } = values;
|
||||
|
||||
await registerUser({
|
||||
fullName,
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
try {
|
||||
const { fullName, email, password } = values;
|
||||
await registerUser({
|
||||
fullName,
|
||||
email,
|
||||
password,
|
||||
});
|
||||
const { data } = await createAccessToken({
|
||||
email,
|
||||
password,
|
||||
@@ -69,9 +67,27 @@ function SignUpForm() {
|
||||
const { token } = data;
|
||||
authentication.updateToken(token);
|
||||
} catch (error) {
|
||||
enqueueSnackbar(error?.message || formatMessage('signupForm.error'), {
|
||||
variant: 'error',
|
||||
});
|
||||
const errors = error?.response?.data?.errors
|
||||
? Object.values(error.response.data.errors)
|
||||
: [];
|
||||
|
||||
if (errors.length) {
|
||||
for (const [error] of errors) {
|
||||
enqueueSnackbar(error, {
|
||||
variant: 'error',
|
||||
SnackbarProps: {
|
||||
'data-test': 'snackbar-sign-up-error',
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
enqueueSnackbar(error?.message || formatMessage('signupForm.error'), {
|
||||
variant: 'error',
|
||||
SnackbarProps: {
|
||||
'data-test': 'snackbar-sign-up-error',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -32,7 +32,8 @@ Variable.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
element: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
sampleValue: PropTypes.string.isRequired,
|
||||
sampleValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
|
||||
.isRequired,
|
||||
}),
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import api from 'helpers/api';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
|
||||
export default function useAdminCreateSamlAuthProvider() {
|
||||
const queryClient = useQueryClient();
|
||||
@@ -15,6 +16,20 @@ export default function useAdminCreateSamlAuthProvider() {
|
||||
queryKey: ['admin', 'samlAuthProviders'],
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
const errors = Object.entries(
|
||||
error.response.data.errors || [['', 'Failed while saving!']],
|
||||
);
|
||||
|
||||
for (const error of errors) {
|
||||
enqueueSnackbar(`${error[0] ? error[0] + ': ' : ''} ${error[1]}`, {
|
||||
variant: 'error',
|
||||
SnackbarProps: {
|
||||
'data-test': 'snackbar-create-saml-auth-provider-error',
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return query;
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import api from 'helpers/api';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
|
||||
export default function useAdminUpdateSamlAuthProvider(samlAuthProviderId) {
|
||||
const queryClient = useQueryClient();
|
||||
@@ -18,6 +19,20 @@ export default function useAdminUpdateSamlAuthProvider(samlAuthProviderId) {
|
||||
queryKey: ['admin', 'samlAuthProviders'],
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
const errors = Object.entries(
|
||||
error.response.data.errors || [['', 'Failed while saving!']],
|
||||
);
|
||||
|
||||
for (const error of errors) {
|
||||
enqueueSnackbar(`${error[0] ? error[0] + ': ' : ''} ${error[1]}`, {
|
||||
variant: 'error',
|
||||
SnackbarProps: {
|
||||
'data-test': 'snackbar-update-saml-auth-provider-error',
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return query;
|
||||
|
@@ -292,7 +292,7 @@
|
||||
"adminApps.connections": "Connections",
|
||||
"adminApps.authClients": "Auth clients",
|
||||
"adminApps.settings": "Settings",
|
||||
"adminAppsSettings.allowCustomConnection": "Allow custom connection",
|
||||
"adminAppsSettings.customConnectionAllowed": "Allow custom connection",
|
||||
"adminAppsSettings.shared": "Shared",
|
||||
"adminAppsSettings.disabled": "Disabled",
|
||||
"adminAppsSettings.save": "Save",
|
||||
|
@@ -77,14 +77,15 @@ export default function Application() {
|
||||
|
||||
const connectionOptions = React.useMemo(() => {
|
||||
const shouldHaveCustomConnection =
|
||||
appConfig?.data?.canConnect && appConfig?.data?.canCustomConnect;
|
||||
appConfig?.data?.connectionAllowed &&
|
||||
appConfig?.data?.customConnectionAllowed;
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: formatMessage('app.addConnection'),
|
||||
key: 'addConnection',
|
||||
'data-test': 'add-connection-button',
|
||||
to: URLS.APP_ADD_CONNECTION(appKey, appConfig?.data?.canConnect),
|
||||
to: URLS.APP_ADD_CONNECTION(appKey, appConfig?.data?.connectionAllowed),
|
||||
disabled: !currentUserAbility.can('create', 'Connection'),
|
||||
},
|
||||
];
|
||||
@@ -155,8 +156,9 @@ export default function Application() {
|
||||
disabled={
|
||||
!allowed ||
|
||||
(appConfig?.data &&
|
||||
!appConfig?.data?.canConnect &&
|
||||
!appConfig?.data?.canCustomConnect) ||
|
||||
!appConfig?.data?.disabled &&
|
||||
!appConfig?.data?.connectionAllowed &&
|
||||
!appConfig?.data?.customConnectionAllowed) ||
|
||||
connectionOptions.every(({ disabled }) => disabled)
|
||||
}
|
||||
options={connectionOptions}
|
||||
|
@@ -56,6 +56,19 @@ function RoleMappings({ provider, providerLoading }) {
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
const errors = Object.values(
|
||||
error.response.data.errors || [['Failed while saving!']],
|
||||
);
|
||||
|
||||
for (const [error] of errors) {
|
||||
enqueueSnackbar(error, {
|
||||
variant: 'error',
|
||||
SnackbarProps: {
|
||||
'data-test': 'snackbar-update-role-mappings-error',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error('Failed while saving!');
|
||||
}
|
||||
};
|
||||
|
@@ -66,10 +66,10 @@ function RoleMappingsFieldArray() {
|
||||
<MuiTextField
|
||||
{...params}
|
||||
label={formatMessage('roleMappingsForm.role')}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
loading={isRolesLoading}
|
||||
required
|
||||
/>
|
||||
</Stack>
|
||||
<IconButton
|
||||
|
@@ -65,7 +65,7 @@ function SamlConfiguration({ provider, providerLoading }) {
|
||||
'data-test': 'snackbar-save-saml-provider-success',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
} catch {
|
||||
throw new Error('Failed while saving!');
|
||||
}
|
||||
};
|
||||
|
@@ -459,9 +459,8 @@ export const SamlAuthProviderRolePropType = PropTypes.shape({
|
||||
export const AppConfigPropType = PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
key: PropTypes.string,
|
||||
allowCustomConnection: PropTypes.bool,
|
||||
canConnect: PropTypes.bool,
|
||||
canCustomConnect: PropTypes.bool,
|
||||
customConnectionAllowed: PropTypes.bool,
|
||||
connectionAllowed: PropTypes.bool,
|
||||
shared: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
});
|
||||
@@ -469,7 +468,7 @@ export const AppConfigPropType = PropTypes.shape({
|
||||
export const AppAuthClientPropType = PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
appConfigId: PropTypes.string,
|
||||
appConfigKey: PropTypes.string,
|
||||
authDefaults: PropTypes.string,
|
||||
formattedAuthDefaults: PropTypes.object,
|
||||
active: PropTypes.bool,
|
||||
|
16
yarn.lock
16
yarn.lock
@@ -4111,10 +4111,10 @@
|
||||
resolved "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz"
|
||||
integrity sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==
|
||||
|
||||
"@types/http-proxy@^1.17.5":
|
||||
version "1.17.8"
|
||||
resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.8.tgz"
|
||||
integrity sha512-5kPLG5BKpWYkw/LVOGWpiq3nEVqxiN32rTgI53Sk12/xHFQ2rG3ehI9IO+O3W2QoKeyB92dJkoka8SUm6BX1pA==
|
||||
"@types/http-proxy@^1.17.8":
|
||||
version "1.17.15"
|
||||
resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.15.tgz#12118141ce9775a6499ecb4c01d02f90fc839d36"
|
||||
integrity sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
@@ -9451,11 +9451,11 @@ http-proxy-agent@^7.0.0:
|
||||
debug "^4.3.4"
|
||||
|
||||
http-proxy-middleware@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.1.tgz"
|
||||
integrity sha512-cfaXRVoZxSed/BmkA7SwBVNI9Kj7HFltaE5rqYOub5kWzWZ+gofV2koVN1j2rMW7pEfSSlCHGJ31xmuyFyfLOg==
|
||||
version "2.0.7"
|
||||
resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz#915f236d92ae98ef48278a95dedf17e991936ec6"
|
||||
integrity sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==
|
||||
dependencies:
|
||||
"@types/http-proxy" "^1.17.5"
|
||||
"@types/http-proxy" "^1.17.8"
|
||||
http-proxy "^1.18.1"
|
||||
is-glob "^4.0.1"
|
||||
is-plain-obj "^3.0.0"
|
||||
|
Reference in New Issue
Block a user