Merge branch 'main' into AUT-1379
This commit is contained in:
@@ -35,9 +35,6 @@ export default defineTrigger({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
useSingletonWebhook: true,
|
|
||||||
singletonWebhookRefValueParameter: 'phoneNumberSid',
|
|
||||||
|
|
||||||
async run($) {
|
async run($) {
|
||||||
const dataItem = {
|
const dataItem = {
|
||||||
raw: $.request.body,
|
raw: $.request.body,
|
||||||
|
|||||||
@@ -10,12 +10,11 @@ export default async (request, response) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const appConfigParams = (request) => {
|
const appConfigParams = (request) => {
|
||||||
const { customConnectionAllowed, shared, disabled } = request.body;
|
const { useOnlyPredefinedAuthClients, disabled } = request.body;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key: request.params.appKey,
|
key: request.params.appKey,
|
||||||
customConnectionAllowed,
|
useOnlyPredefinedAuthClients,
|
||||||
shared,
|
|
||||||
disabled,
|
disabled,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,8 +23,7 @@ describe('POST /api/v1/admin/apps/:appKey/config', () => {
|
|||||||
|
|
||||||
it('should return created app config', async () => {
|
it('should return created app config', async () => {
|
||||||
const appConfig = {
|
const appConfig = {
|
||||||
customConnectionAllowed: true,
|
useOnlyPredefinedAuthClients: false,
|
||||||
shared: true,
|
|
||||||
disabled: false,
|
disabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -38,14 +37,14 @@ describe('POST /api/v1/admin/apps/:appKey/config', () => {
|
|||||||
...appConfig,
|
...appConfig,
|
||||||
key: 'gitlab',
|
key: 'gitlab',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(response.body).toMatchObject(expectedPayload);
|
expect(response.body).toMatchObject(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return HTTP 422 for already existing app config', async () => {
|
it('should return HTTP 422 for already existing app config', async () => {
|
||||||
const appConfig = {
|
const appConfig = {
|
||||||
key: 'gitlab',
|
key: 'gitlab',
|
||||||
customConnectionAllowed: true,
|
useOnlyPredefinedAuthClients: false,
|
||||||
shared: true,
|
|
||||||
disabled: false,
|
disabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ export default async (request, response) => {
|
|||||||
.findOne({ key: request.params.appKey })
|
.findOne({ key: request.params.appKey })
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
const appAuthClient = await appConfig
|
const oauthClient = await appConfig
|
||||||
.$relatedQuery('appAuthClients')
|
.$relatedQuery('oauthClients')
|
||||||
.insert(appAuthClientParams(request));
|
.insert(oauthClientParams(request));
|
||||||
|
|
||||||
renderObject(response, appAuthClient, { status: 201 });
|
renderObject(response, oauthClient, { status: 201 });
|
||||||
};
|
};
|
||||||
|
|
||||||
const appAuthClientParams = (request) => {
|
const oauthClientParams = (request) => {
|
||||||
const { active, appKey, name, formattedAuthDefaults } = request.body;
|
const { active, appKey, name, formattedAuthDefaults } = request.body;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -5,11 +5,11 @@ import app from '../../../../../app.js';
|
|||||||
import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js';
|
import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js';
|
||||||
import { createUser } from '../../../../../../test/factories/user.js';
|
import { createUser } from '../../../../../../test/factories/user.js';
|
||||||
import { createRole } from '../../../../../../test/factories/role.js';
|
import { createRole } from '../../../../../../test/factories/role.js';
|
||||||
import createAppAuthClientMock from '../../../../../../test/mocks/rest/api/v1/admin/apps/create-auth-client.js';
|
import createOAuthClientMock from '../../../../../../test/mocks/rest/api/v1/admin/apps/create-oauth-client.js';
|
||||||
import { createAppConfig } from '../../../../../../test/factories/app-config.js';
|
import { createAppConfig } from '../../../../../../test/factories/app-config.js';
|
||||||
import * as license from '../../../../../helpers/license.ee.js';
|
import * as license from '../../../../../helpers/license.ee.js';
|
||||||
|
|
||||||
describe('POST /api/v1/admin/apps/:appKey/auth-clients', () => {
|
describe('POST /api/v1/admin/apps/:appKey/oauth-clients', () => {
|
||||||
let currentUser, adminRole, token;
|
let currentUser, adminRole, token;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -26,7 +26,7 @@ describe('POST /api/v1/admin/apps/:appKey/auth-clients', () => {
|
|||||||
key: 'gitlab',
|
key: 'gitlab',
|
||||||
});
|
});
|
||||||
|
|
||||||
const appAuthClient = {
|
const oauthClient = {
|
||||||
active: true,
|
active: true,
|
||||||
appKey: 'gitlab',
|
appKey: 'gitlab',
|
||||||
name: 'First auth client',
|
name: 'First auth client',
|
||||||
@@ -39,17 +39,17 @@ describe('POST /api/v1/admin/apps/:appKey/auth-clients', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
.post('/api/v1/admin/apps/gitlab/auth-clients')
|
.post('/api/v1/admin/apps/gitlab/oauth-clients')
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.send(appAuthClient)
|
.send(oauthClient)
|
||||||
.expect(201);
|
.expect(201);
|
||||||
|
|
||||||
const expectedPayload = createAppAuthClientMock(appAuthClient);
|
const expectedPayload = createOAuthClientMock(oauthClient);
|
||||||
expect(response.body).toMatchObject(expectedPayload);
|
expect(response.body).toMatchObject(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return not found response for not existing app config', async () => {
|
it('should return not found response for not existing app config', async () => {
|
||||||
const appAuthClient = {
|
const oauthClient = {
|
||||||
active: true,
|
active: true,
|
||||||
appKey: 'gitlab',
|
appKey: 'gitlab',
|
||||||
name: 'First auth client',
|
name: 'First auth client',
|
||||||
@@ -62,9 +62,9 @@ describe('POST /api/v1/admin/apps/:appKey/auth-clients', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
await request(app)
|
await request(app)
|
||||||
.post('/api/v1/admin/apps/gitlab/auth-clients')
|
.post('/api/v1/admin/apps/gitlab/oauth-clients')
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.send(appAuthClient)
|
.send(oauthClient)
|
||||||
.expect(404);
|
.expect(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -73,14 +73,14 @@ describe('POST /api/v1/admin/apps/:appKey/auth-clients', () => {
|
|||||||
key: 'gitlab',
|
key: 'gitlab',
|
||||||
});
|
});
|
||||||
|
|
||||||
const appAuthClient = {
|
const oauthClient = {
|
||||||
appKey: 'gitlab',
|
appKey: 'gitlab',
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
.post('/api/v1/admin/apps/gitlab/auth-clients')
|
.post('/api/v1/admin/apps/gitlab/oauth-clients')
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.send(appAuthClient)
|
.send(oauthClient)
|
||||||
.expect(422);
|
.expect(422);
|
||||||
|
|
||||||
expect(response.body.meta.type).toStrictEqual('ModelValidation');
|
expect(response.body.meta.type).toStrictEqual('ModelValidation');
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { renderObject } from '../../../../../helpers/renderer.js';
|
|
||||||
import AppAuthClient from '../../../../../models/app-auth-client.js';
|
|
||||||
|
|
||||||
export default async (request, response) => {
|
|
||||||
const appAuthClient = await AppAuthClient.query()
|
|
||||||
.findById(request.params.appAuthClientId)
|
|
||||||
.where({ app_key: request.params.appKey })
|
|
||||||
.throwIfNotFound();
|
|
||||||
|
|
||||||
renderObject(response, appAuthClient);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { renderObject } from '../../../../../helpers/renderer.js';
|
||||||
|
import OAuthClient from '../../../../../models/oauth-client.js';
|
||||||
|
|
||||||
|
export default async (request, response) => {
|
||||||
|
const oauthClient = await OAuthClient.query()
|
||||||
|
.findById(request.params.oauthClientId)
|
||||||
|
.where({ app_key: request.params.appKey })
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
renderObject(response, oauthClient);
|
||||||
|
};
|
||||||
@@ -5,12 +5,12 @@ import app from '../../../../../app.js';
|
|||||||
import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js';
|
import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js';
|
||||||
import { createUser } from '../../../../../../test/factories/user.js';
|
import { createUser } from '../../../../../../test/factories/user.js';
|
||||||
import { createRole } from '../../../../../../test/factories/role.js';
|
import { createRole } from '../../../../../../test/factories/role.js';
|
||||||
import getAppAuthClientMock from '../../../../../../test/mocks/rest/api/v1/admin/apps/get-auth-client.js';
|
import getOAuthClientMock from '../../../../../../test/mocks/rest/api/v1/admin/apps/get-oauth-client.js';
|
||||||
import { createAppAuthClient } from '../../../../../../test/factories/app-auth-client.js';
|
import { createOAuthClient } from '../../../../../../test/factories/oauth-client.js';
|
||||||
import * as license from '../../../../../helpers/license.ee.js';
|
import * as license from '../../../../../helpers/license.ee.js';
|
||||||
|
|
||||||
describe('GET /api/v1/admin/apps/:appKey/auth-clients/:appAuthClientId', () => {
|
describe('GET /api/v1/admin/apps/:appKey/oauth-clients/:oauthClientId', () => {
|
||||||
let currentUser, adminRole, currentAppAuthClient, token;
|
let currentUser, adminRole, currentOAuthClient, token;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
|
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
|
||||||
@@ -18,29 +18,29 @@ describe('GET /api/v1/admin/apps/:appKey/auth-clients/:appAuthClientId', () => {
|
|||||||
adminRole = await createRole({ name: 'Admin' });
|
adminRole = await createRole({ name: 'Admin' });
|
||||||
currentUser = await createUser({ roleId: adminRole.id });
|
currentUser = await createUser({ roleId: adminRole.id });
|
||||||
|
|
||||||
currentAppAuthClient = await createAppAuthClient({
|
currentOAuthClient = await createOAuthClient({
|
||||||
appKey: 'deepl',
|
appKey: 'deepl',
|
||||||
});
|
});
|
||||||
|
|
||||||
token = await createAuthTokenByUserId(currentUser.id);
|
token = await createAuthTokenByUserId(currentUser.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return specified app auth client', async () => {
|
it('should return specified oauth client', async () => {
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
.get(`/api/v1/admin/apps/deepl/auth-clients/${currentAppAuthClient.id}`)
|
.get(`/api/v1/admin/apps/deepl/oauth-clients/${currentOAuthClient.id}`)
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const expectedPayload = getAppAuthClientMock(currentAppAuthClient);
|
const expectedPayload = getOAuthClientMock(currentOAuthClient);
|
||||||
expect(response.body).toStrictEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return not found response for not existing app auth client ID', async () => {
|
it('should return not found response for not existing oauth client ID', async () => {
|
||||||
const notExistingAppAuthClientUUID = Crypto.randomUUID();
|
const notExistingOAuthClientUUID = Crypto.randomUUID();
|
||||||
|
|
||||||
await request(app)
|
await request(app)
|
||||||
.get(
|
.get(
|
||||||
`/api/v1/admin/apps/deepl/auth-clients/${notExistingAppAuthClientUUID}`
|
`/api/v1/admin/apps/deepl/oauth-clients/${notExistingOAuthClientUUID}`
|
||||||
)
|
)
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(404);
|
.expect(404);
|
||||||
@@ -48,7 +48,7 @@ describe('GET /api/v1/admin/apps/:appKey/auth-clients/:appAuthClientId', () => {
|
|||||||
|
|
||||||
it('should return bad request response for invalid UUID', async () => {
|
it('should return bad request response for invalid UUID', async () => {
|
||||||
await request(app)
|
await request(app)
|
||||||
.get('/api/v1/admin/apps/deepl/auth-clients/invalidAppAuthClientUUID')
|
.get('/api/v1/admin/apps/deepl/oauth-clients/invalidOAuthClientUUID')
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(400);
|
.expect(400);
|
||||||
});
|
});
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { renderObject } from '../../../../../helpers/renderer.js';
|
import { renderObject } from '../../../../../helpers/renderer.js';
|
||||||
import AppAuthClient from '../../../../../models/app-auth-client.js';
|
import OAuthClient from '../../../../../models/oauth-client.js';
|
||||||
|
|
||||||
export default async (request, response) => {
|
export default async (request, response) => {
|
||||||
const appAuthClients = await AppAuthClient.query()
|
const oauthClients = await OAuthClient.query()
|
||||||
.where({ app_key: request.params.appKey })
|
.where({ app_key: request.params.appKey })
|
||||||
.orderBy('created_at', 'desc');
|
.orderBy('created_at', 'desc');
|
||||||
|
|
||||||
renderObject(response, appAuthClients);
|
renderObject(response, oauthClients);
|
||||||
};
|
};
|
||||||
@@ -4,11 +4,11 @@ import app from '../../../../../app.js';
|
|||||||
import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js';
|
import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js';
|
||||||
import { createUser } from '../../../../../../test/factories/user.js';
|
import { createUser } from '../../../../../../test/factories/user.js';
|
||||||
import { createRole } from '../../../../../../test/factories/role.js';
|
import { createRole } from '../../../../../../test/factories/role.js';
|
||||||
import getAuthClientsMock from '../../../../../../test/mocks/rest/api/v1/admin/apps/get-auth-clients.js';
|
import getAdminOAuthClientsMock from '../../../../../../test/mocks/rest/api/v1/admin/apps/get-oauth-clients.js';
|
||||||
import { createAppAuthClient } from '../../../../../../test/factories/app-auth-client.js';
|
import { createOAuthClient } from '../../../../../../test/factories/oauth-client.js';
|
||||||
import * as license from '../../../../../helpers/license.ee.js';
|
import * as license from '../../../../../helpers/license.ee.js';
|
||||||
|
|
||||||
describe('GET /api/v1/admin/apps/:appKey/auth-clients', () => {
|
describe('GET /api/v1/admin/apps/:appKey/oauth-clients', () => {
|
||||||
let currentUser, adminRole, token;
|
let currentUser, adminRole, token;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -20,23 +20,23 @@ describe('GET /api/v1/admin/apps/:appKey/auth-clients', () => {
|
|||||||
token = await createAuthTokenByUserId(currentUser.id);
|
token = await createAuthTokenByUserId(currentUser.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return specified app auth client info', async () => {
|
it('should return specified oauth client info', async () => {
|
||||||
const appAuthClientOne = await createAppAuthClient({
|
const oauthClientOne = await createOAuthClient({
|
||||||
appKey: 'deepl',
|
appKey: 'deepl',
|
||||||
});
|
});
|
||||||
|
|
||||||
const appAuthClientTwo = await createAppAuthClient({
|
const oauthClientTwo = await createOAuthClient({
|
||||||
appKey: 'deepl',
|
appKey: 'deepl',
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
.get('/api/v1/admin/apps/deepl/auth-clients')
|
.get('/api/v1/admin/apps/deepl/oauth-clients')
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const expectedPayload = getAuthClientsMock([
|
const expectedPayload = getAdminOAuthClientsMock([
|
||||||
appAuthClientTwo,
|
oauthClientTwo,
|
||||||
appAuthClientOne,
|
oauthClientOne,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(response.body).toStrictEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { renderObject } from '../../../../../helpers/renderer.js';
|
|
||||||
import AppAuthClient from '../../../../../models/app-auth-client.js';
|
|
||||||
|
|
||||||
export default async (request, response) => {
|
|
||||||
const appAuthClient = await AppAuthClient.query()
|
|
||||||
.findById(request.params.appAuthClientId)
|
|
||||||
.throwIfNotFound();
|
|
||||||
|
|
||||||
await appAuthClient.$query().patchAndFetch(appAuthClientParams(request));
|
|
||||||
|
|
||||||
renderObject(response, appAuthClient);
|
|
||||||
};
|
|
||||||
|
|
||||||
const appAuthClientParams = (request) => {
|
|
||||||
const { active, name, formattedAuthDefaults } = request.body;
|
|
||||||
|
|
||||||
return {
|
|
||||||
active,
|
|
||||||
name,
|
|
||||||
formattedAuthDefaults,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -17,11 +17,10 @@ export default async (request, response) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const appConfigParams = (request) => {
|
const appConfigParams = (request) => {
|
||||||
const { customConnectionAllowed, shared, disabled } = request.body;
|
const { useOnlyPredefinedAuthClients, disabled } = request.body;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
customConnectionAllowed,
|
useOnlyPredefinedAuthClients,
|
||||||
shared,
|
|
||||||
disabled,
|
disabled,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,17 +24,15 @@ describe('PATCH /api/v1/admin/apps/:appKey/config', () => {
|
|||||||
it('should return updated app config', async () => {
|
it('should return updated app config', async () => {
|
||||||
const appConfig = {
|
const appConfig = {
|
||||||
key: 'gitlab',
|
key: 'gitlab',
|
||||||
customConnectionAllowed: true,
|
useOnlyPredefinedAuthClients: true,
|
||||||
shared: true,
|
|
||||||
disabled: false,
|
disabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
await createAppConfig(appConfig);
|
await createAppConfig(appConfig);
|
||||||
|
|
||||||
const newAppConfigValues = {
|
const newAppConfigValues = {
|
||||||
shared: false,
|
|
||||||
disabled: true,
|
disabled: true,
|
||||||
customConnectionAllowed: false,
|
useOnlyPredefinedAuthClients: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await request(app)
|
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 () => {
|
it('should return not found response for unexisting app config', async () => {
|
||||||
const appConfig = {
|
const appConfig = {
|
||||||
shared: false,
|
|
||||||
disabled: true,
|
disabled: true,
|
||||||
customConnectionAllowed: false,
|
useOnlyPredefinedAuthClients: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
await request(app)
|
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 () => {
|
it('should return HTTP 422 for invalid app config data', async () => {
|
||||||
const appConfig = {
|
const appConfig = {
|
||||||
key: 'gitlab',
|
key: 'gitlab',
|
||||||
customConnectionAllowed: true,
|
useOnlyPredefinedAuthClients: true,
|
||||||
shared: true,
|
|
||||||
disabled: false,
|
disabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { renderObject } from '../../../../../helpers/renderer.js';
|
||||||
|
import OAuthClient from '../../../../../models/oauth-client.js';
|
||||||
|
|
||||||
|
export default async (request, response) => {
|
||||||
|
const oauthClient = await OAuthClient.query()
|
||||||
|
.findById(request.params.oauthClientId)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
await oauthClient.$query().patchAndFetch(oauthClientParams(request));
|
||||||
|
|
||||||
|
renderObject(response, oauthClient);
|
||||||
|
};
|
||||||
|
|
||||||
|
const oauthClientParams = (request) => {
|
||||||
|
const { active, name, formattedAuthDefaults } = request.body;
|
||||||
|
|
||||||
|
return {
|
||||||
|
active,
|
||||||
|
name,
|
||||||
|
formattedAuthDefaults,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -6,12 +6,12 @@ import app from '../../../../../app.js';
|
|||||||
import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js';
|
import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js';
|
||||||
import { createUser } from '../../../../../../test/factories/user.js';
|
import { createUser } from '../../../../../../test/factories/user.js';
|
||||||
import { createRole } from '../../../../../../test/factories/role.js';
|
import { createRole } from '../../../../../../test/factories/role.js';
|
||||||
import updateAppAuthClientMock from '../../../../../../test/mocks/rest/api/v1/admin/apps/update-auth-client.js';
|
import updateOAuthClientMock from '../../../../../../test/mocks/rest/api/v1/admin/apps/update-oauth-client.js';
|
||||||
import { createAppConfig } from '../../../../../../test/factories/app-config.js';
|
import { createAppConfig } from '../../../../../../test/factories/app-config.js';
|
||||||
import { createAppAuthClient } from '../../../../../../test/factories/app-auth-client.js';
|
import { createOAuthClient } from '../../../../../../test/factories/oauth-client.js';
|
||||||
import * as license from '../../../../../helpers/license.ee.js';
|
import * as license from '../../../../../helpers/license.ee.js';
|
||||||
|
|
||||||
describe('PATCH /api/v1/admin/apps/:appKey/auth-clients', () => {
|
describe('PATCH /api/v1/admin/apps/:appKey/oauth-clients', () => {
|
||||||
let currentUser, adminRole, token;
|
let currentUser, adminRole, token;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -27,8 +27,8 @@ describe('PATCH /api/v1/admin/apps/:appKey/auth-clients', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return updated entity for valid app auth client', async () => {
|
it('should return updated entity for valid oauth client', async () => {
|
||||||
const appAuthClient = {
|
const oauthClient = {
|
||||||
active: true,
|
active: true,
|
||||||
appKey: 'gitlab',
|
appKey: 'gitlab',
|
||||||
formattedAuthDefaults: {
|
formattedAuthDefaults: {
|
||||||
@@ -39,33 +39,33 @@ describe('PATCH /api/v1/admin/apps/:appKey/auth-clients', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const existingAppAuthClient = await createAppAuthClient({
|
const existingOAuthClient = await createOAuthClient({
|
||||||
appKey: 'gitlab',
|
appKey: 'gitlab',
|
||||||
name: 'First auth client',
|
name: 'First auth client',
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
.patch(
|
.patch(
|
||||||
`/api/v1/admin/apps/gitlab/auth-clients/${existingAppAuthClient.id}`
|
`/api/v1/admin/apps/gitlab/oauth-clients/${existingOAuthClient.id}`
|
||||||
)
|
)
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.send(appAuthClient)
|
.send(oauthClient)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const expectedPayload = updateAppAuthClientMock({
|
const expectedPayload = updateOAuthClientMock({
|
||||||
...existingAppAuthClient,
|
...existingOAuthClient,
|
||||||
...appAuthClient,
|
...oauthClient,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(response.body).toMatchObject(expectedPayload);
|
expect(response.body).toMatchObject(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return not found response for not existing app auth client', async () => {
|
it('should return not found response for not existing oauth client', async () => {
|
||||||
const notExistingAppAuthClientId = Crypto.randomUUID();
|
const notExistingOAuthClientId = Crypto.randomUUID();
|
||||||
|
|
||||||
await request(app)
|
await request(app)
|
||||||
.patch(
|
.patch(
|
||||||
`/api/v1/admin/apps/gitlab/auth-clients/${notExistingAppAuthClientId}`
|
`/api/v1/admin/apps/gitlab/oauth-clients/${notExistingOAuthClientId}`
|
||||||
)
|
)
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(404);
|
.expect(404);
|
||||||
@@ -73,27 +73,27 @@ describe('PATCH /api/v1/admin/apps/:appKey/auth-clients', () => {
|
|||||||
|
|
||||||
it('should return bad request response for invalid UUID', async () => {
|
it('should return bad request response for invalid UUID', async () => {
|
||||||
await request(app)
|
await request(app)
|
||||||
.patch('/api/v1/admin/apps/gitlab/auth-clients/invalidAuthClientUUID')
|
.patch('/api/v1/admin/apps/gitlab/oauth-clients/invalidAuthClientUUID')
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(400);
|
.expect(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return HTTP 422 for invalid payload', async () => {
|
it('should return HTTP 422 for invalid payload', async () => {
|
||||||
const appAuthClient = {
|
const oauthClient = {
|
||||||
formattedAuthDefaults: 'invalid input',
|
formattedAuthDefaults: 'invalid input',
|
||||||
};
|
};
|
||||||
|
|
||||||
const existingAppAuthClient = await createAppAuthClient({
|
const existingOAuthClient = await createOAuthClient({
|
||||||
appKey: 'gitlab',
|
appKey: 'gitlab',
|
||||||
name: 'First auth client',
|
name: 'First auth client',
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
.patch(
|
.patch(
|
||||||
`/api/v1/admin/apps/gitlab/auth-clients/${existingAppAuthClient.id}`
|
`/api/v1/admin/apps/gitlab/oauth-clients/${existingOAuthClient.id}`
|
||||||
)
|
)
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.send(appAuthClient)
|
.send(oauthClient)
|
||||||
.expect(422);
|
.expect(422);
|
||||||
|
|
||||||
expect(response.body.meta.type).toBe('ModelValidation');
|
expect(response.body.meta.type).toBe('ModelValidation');
|
||||||
@@ -9,18 +9,18 @@ export default async (request, response) => {
|
|||||||
.$query()
|
.$query()
|
||||||
.withGraphFetched({
|
.withGraphFetched({
|
||||||
appConfig: true,
|
appConfig: true,
|
||||||
appAuthClient: true,
|
oauthClient: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
renderObject(response, connectionWithAppConfigAndAuthClient, { status: 201 });
|
renderObject(response, connectionWithAppConfigAndAuthClient, { status: 201 });
|
||||||
};
|
};
|
||||||
|
|
||||||
const connectionParams = (request) => {
|
const connectionParams = (request) => {
|
||||||
const { appAuthClientId, formattedData } = request.body;
|
const { oauthClientId, formattedData } = request.body;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key: request.params.appKey,
|
key: request.params.appKey,
|
||||||
appAuthClientId,
|
oauthClientId,
|
||||||
formattedData,
|
formattedData,
|
||||||
verified: false,
|
verified: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import request from 'supertest';
|
|||||||
import app from '../../../../app.js';
|
import app from '../../../../app.js';
|
||||||
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js';
|
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js';
|
||||||
import { createAppConfig } from '../../../../../test/factories/app-config.js';
|
import { createAppConfig } from '../../../../../test/factories/app-config.js';
|
||||||
import { createAppAuthClient } from '../../../../../test/factories/app-auth-client.js';
|
import { createOAuthClient } from '../../../../../test/factories/oauth-client.js';
|
||||||
import { createUser } from '../../../../../test/factories/user.js';
|
import { createUser } from '../../../../../test/factories/user.js';
|
||||||
import { createPermission } from '../../../../../test/factories/permission.js';
|
import { createPermission } from '../../../../../test/factories/permission.js';
|
||||||
import { createRole } from '../../../../../test/factories/role.js';
|
import { createRole } from '../../../../../test/factories/role.js';
|
||||||
@@ -155,7 +155,7 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
|
|||||||
await createAppConfig({
|
await createAppConfig({
|
||||||
key: 'gitlab',
|
key: 'gitlab',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
customConnectionAllowed: true,
|
useOnlyPredefinedAuthClients: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -218,7 +218,7 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
|
|||||||
await createAppConfig({
|
await createAppConfig({
|
||||||
key: 'gitlab',
|
key: 'gitlab',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
customConnectionAllowed: false,
|
useOnlyPredefinedAuthClients: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -266,17 +266,17 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with auth clients enabled', async () => {
|
describe('with auth client enabled', async () => {
|
||||||
let appAuthClient;
|
let oauthClient;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await createAppConfig({
|
await createAppConfig({
|
||||||
key: 'gitlab',
|
key: 'gitlab',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
shared: true,
|
useOnlyPredefinedAuthClients: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
appAuthClient = await createAppAuthClient({
|
oauthClient = await createOAuthClient({
|
||||||
appKey: 'gitlab',
|
appKey: 'gitlab',
|
||||||
active: true,
|
active: true,
|
||||||
formattedAuthDefaults: {
|
formattedAuthDefaults: {
|
||||||
@@ -290,7 +290,7 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
|
|||||||
|
|
||||||
it('should return created connection', async () => {
|
it('should return created connection', async () => {
|
||||||
const connectionData = {
|
const connectionData = {
|
||||||
appAuthClientId: appAuthClient.id,
|
oauthClientId: oauthClient.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
@@ -310,19 +310,6 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
|
|||||||
expect(response.body).toStrictEqual(expectedPayload);
|
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 () => {
|
it('should return not found response for invalid app key', async () => {
|
||||||
await request(app)
|
await request(app)
|
||||||
.post('/api/v1/apps/invalid-app-key/connections')
|
.post('/api/v1/apps/invalid-app-key/connections')
|
||||||
@@ -349,31 +336,33 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('with auth clients disabled', async () => {
|
|
||||||
let appAuthClient;
|
describe('with auth client disabled', async () => {
|
||||||
|
let oauthClient;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await createAppConfig({
|
await createAppConfig({
|
||||||
key: 'gitlab',
|
key: 'gitlab',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
shared: false,
|
useOnlyPredefinedAuthClients: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
appAuthClient = await createAppAuthClient({
|
oauthClient = await createOAuthClient({
|
||||||
appKey: 'gitlab',
|
appKey: 'gitlab',
|
||||||
|
active: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return with not authorized response', async () => {
|
it('should return with not authorized response', async () => {
|
||||||
const connectionData = {
|
const connectionData = {
|
||||||
appAuthClientId: appAuthClient.id,
|
oauthClientId: oauthClient.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
await request(app)
|
await request(app)
|
||||||
.post('/api/v1/apps/gitlab/connections')
|
.post('/api/v1/apps/gitlab/connections')
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.send(connectionData)
|
.send(connectionData)
|
||||||
.expect(403);
|
.expect(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return not found response for invalid app key', async () => {
|
it('should return not found response for invalid app key', async () => {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ describe('GET /api/v1/apps/:appKey/actions/:actionKey/substeps', () => {
|
|||||||
exampleApp = await App.findOneByKey('github');
|
exampleApp = await App.findOneByKey('github');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the app auth info', async () => {
|
it('should return the action substeps info', async () => {
|
||||||
const actions = await App.findActionsByKey('github');
|
const actions = await App.findActionsByKey('github');
|
||||||
const exampleAction = actions.find(
|
const exampleAction = actions.find(
|
||||||
(action) => action.key === 'createIssue'
|
(action) => action.key === 'createIssue'
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { renderObject } from '../../../../helpers/renderer.js';
|
|
||||||
import AppAuthClient from '../../../../models/app-auth-client.js';
|
|
||||||
|
|
||||||
export default async (request, response) => {
|
|
||||||
const appAuthClient = await AppAuthClient.query()
|
|
||||||
.findById(request.params.appAuthClientId)
|
|
||||||
.where({ app_key: request.params.appKey, active: true })
|
|
||||||
.throwIfNotFound();
|
|
||||||
|
|
||||||
renderObject(response, appAuthClient);
|
|
||||||
};
|
|
||||||
@@ -4,7 +4,7 @@ import AppConfig from '../../../../models/app-config.js';
|
|||||||
export default async (request, response) => {
|
export default async (request, response) => {
|
||||||
const appConfig = await AppConfig.query()
|
const appConfig = await AppConfig.query()
|
||||||
.withGraphFetched({
|
.withGraphFetched({
|
||||||
appAuthClients: true,
|
oauthClients: true,
|
||||||
})
|
})
|
||||||
.findOne({
|
.findOne({
|
||||||
key: request.params.appKey,
|
key: request.params.appKey,
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ describe('GET /api/v1/apps/:appKey/config', () => {
|
|||||||
|
|
||||||
appConfig = await createAppConfig({
|
appConfig = await createAppConfig({
|
||||||
key: 'deepl',
|
key: 'deepl',
|
||||||
customConnectionAllowed: true,
|
useOnlyPredefinedAuthClients: false,
|
||||||
shared: true,
|
|
||||||
disabled: false,
|
disabled: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export default async (request, response) => {
|
|||||||
.select('connections.*')
|
.select('connections.*')
|
||||||
.withGraphFetched({
|
.withGraphFetched({
|
||||||
appConfig: true,
|
appConfig: true,
|
||||||
appAuthClient: true,
|
oauthClient: true,
|
||||||
})
|
})
|
||||||
.fullOuterJoinRelated('steps')
|
.fullOuterJoinRelated('steps')
|
||||||
.where({
|
.where({
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { renderObject } from '../../../../helpers/renderer.js';
|
||||||
|
import OAuthClient from '../../../../models/oauth-client.js';
|
||||||
|
|
||||||
|
export default async (request, response) => {
|
||||||
|
const oauthClient = await OAuthClient.query()
|
||||||
|
.findById(request.params.oauthClientId)
|
||||||
|
.where({ app_key: request.params.appKey, active: true })
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
renderObject(response, oauthClient);
|
||||||
|
};
|
||||||
@@ -4,46 +4,46 @@ import Crypto from 'crypto';
|
|||||||
import app from '../../../../app.js';
|
import app from '../../../../app.js';
|
||||||
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js';
|
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js';
|
||||||
import { createUser } from '../../../../../test/factories/user.js';
|
import { createUser } from '../../../../../test/factories/user.js';
|
||||||
import getAppAuthClientMock from '../../../../../test/mocks/rest/api/v1/apps/get-auth-client.js';
|
import getOAuthClientMock from '../../../../../test/mocks/rest/api/v1/apps/get-oauth-client.js';
|
||||||
import { createAppAuthClient } from '../../../../../test/factories/app-auth-client.js';
|
import { createOAuthClient } from '../../../../../test/factories/oauth-client.js';
|
||||||
import * as license from '../../../../helpers/license.ee.js';
|
import * as license from '../../../../helpers/license.ee.js';
|
||||||
|
|
||||||
describe('GET /api/v1/apps/:appKey/auth-clients/:appAuthClientId', () => {
|
describe('GET /api/v1/apps/:appKey/oauth-clients/:oauthClientId', () => {
|
||||||
let currentUser, currentAppAuthClient, token;
|
let currentUser, currentOAuthClient, token;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
|
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
|
||||||
|
|
||||||
currentUser = await createUser();
|
currentUser = await createUser();
|
||||||
currentAppAuthClient = await createAppAuthClient({
|
currentOAuthClient = await createOAuthClient({
|
||||||
appKey: 'deepl',
|
appKey: 'deepl',
|
||||||
});
|
});
|
||||||
|
|
||||||
token = await createAuthTokenByUserId(currentUser.id);
|
token = await createAuthTokenByUserId(currentUser.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return specified app auth client', async () => {
|
it('should return specified oauth client', async () => {
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
.get(`/api/v1/apps/deepl/auth-clients/${currentAppAuthClient.id}`)
|
.get(`/api/v1/apps/deepl/oauth-clients/${currentOAuthClient.id}`)
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const expectedPayload = getAppAuthClientMock(currentAppAuthClient);
|
const expectedPayload = getOAuthClientMock(currentOAuthClient);
|
||||||
expect(response.body).toStrictEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return not found response for not existing app auth client ID', async () => {
|
it('should return not found response for not existing oauth client ID', async () => {
|
||||||
const notExistingAppAuthClientUUID = Crypto.randomUUID();
|
const notExistingOAuthClientUUID = Crypto.randomUUID();
|
||||||
|
|
||||||
await request(app)
|
await request(app)
|
||||||
.get(`/api/v1/apps/deepl/auth-clients/${notExistingAppAuthClientUUID}`)
|
.get(`/api/v1/apps/deepl/oauth-clients/${notExistingOAuthClientUUID}`)
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(404);
|
.expect(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return bad request response for invalid UUID', async () => {
|
it('should return bad request response for invalid UUID', async () => {
|
||||||
await request(app)
|
await request(app)
|
||||||
.get('/api/v1/apps/deepl/auth-clients/invalidAppAuthClientUUID')
|
.get('/api/v1/apps/deepl/oauth-clients/invalidOAuthClientUUID')
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(400);
|
.expect(400);
|
||||||
});
|
});
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { renderObject } from '../../../../helpers/renderer.js';
|
import { renderObject } from '../../../../helpers/renderer.js';
|
||||||
import AppAuthClient from '../../../../models/app-auth-client.js';
|
import OAuthClient from '../../../../models/oauth-client.js';
|
||||||
|
|
||||||
export default async (request, response) => {
|
export default async (request, response) => {
|
||||||
const appAuthClients = await AppAuthClient.query()
|
const oauthClients = await OAuthClient.query()
|
||||||
.where({ app_key: request.params.appKey, active: true })
|
.where({ app_key: request.params.appKey, active: true })
|
||||||
.orderBy('created_at', 'desc');
|
.orderBy('created_at', 'desc');
|
||||||
|
|
||||||
renderObject(response, appAuthClients);
|
renderObject(response, oauthClients);
|
||||||
};
|
};
|
||||||
@@ -3,11 +3,11 @@ import request from 'supertest';
|
|||||||
import app from '../../../../app.js';
|
import app from '../../../../app.js';
|
||||||
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js';
|
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js';
|
||||||
import { createUser } from '../../../../../test/factories/user.js';
|
import { createUser } from '../../../../../test/factories/user.js';
|
||||||
import getAuthClientsMock from '../../../../../test/mocks/rest/api/v1/apps/get-auth-clients.js';
|
import getOAuthClientsMock from '../../../../../test/mocks/rest/api/v1/apps/get-oauth-clients.js';
|
||||||
import { createAppAuthClient } from '../../../../../test/factories/app-auth-client.js';
|
import { createOAuthClient } from '../../../../../test/factories/oauth-client.js';
|
||||||
import * as license from '../../../../helpers/license.ee.js';
|
import * as license from '../../../../helpers/license.ee.js';
|
||||||
|
|
||||||
describe('GET /api/v1/apps/:appKey/auth-clients', () => {
|
describe('GET /api/v1/apps/:appKey/oauth-clients', () => {
|
||||||
let currentUser, token;
|
let currentUser, token;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -18,23 +18,23 @@ describe('GET /api/v1/apps/:appKey/auth-clients', () => {
|
|||||||
token = await createAuthTokenByUserId(currentUser.id);
|
token = await createAuthTokenByUserId(currentUser.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return specified app auth client info', async () => {
|
it('should return specified oauth client info', async () => {
|
||||||
const appAuthClientOne = await createAppAuthClient({
|
const oauthClientOne = await createOAuthClient({
|
||||||
appKey: 'deepl',
|
appKey: 'deepl',
|
||||||
});
|
});
|
||||||
|
|
||||||
const appAuthClientTwo = await createAppAuthClient({
|
const oauthClientTwo = await createOAuthClient({
|
||||||
appKey: 'deepl',
|
appKey: 'deepl',
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
.get('/api/v1/apps/deepl/auth-clients')
|
.get('/api/v1/apps/deepl/oauth-clients')
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const expectedPayload = getAuthClientsMock([
|
const expectedPayload = getOAuthClientsMock([
|
||||||
appAuthClientTwo,
|
oauthClientTwo,
|
||||||
appAuthClientOne,
|
oauthClientOne,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(response.body).toStrictEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
@@ -15,7 +15,7 @@ describe('GET /api/v1/apps/:appKey/triggers/:triggerKey/substeps', () => {
|
|||||||
exampleApp = await App.findOneByKey('github');
|
exampleApp = await App.findOneByKey('github');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the app auth info', async () => {
|
it('should return the trigger substeps info', async () => {
|
||||||
const triggers = await App.findTriggersByKey('github');
|
const triggers = await App.findTriggersByKey('github');
|
||||||
const exampleTrigger = triggers.find(
|
const exampleTrigger = triggers.find(
|
||||||
(trigger) => trigger.key === 'newIssues'
|
(trigger) => trigger.key === 'newIssues'
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ describe('POST /api/v1/connections/:connectionId/reset', () => {
|
|||||||
|
|
||||||
const expectedPayload = resetConnectionMock({
|
const expectedPayload = resetConnectionMock({
|
||||||
...refetchedCurrentUserConnection,
|
...refetchedCurrentUserConnection,
|
||||||
reconnectable: refetchedCurrentUserConnection.reconnectable,
|
|
||||||
formattedData: {
|
formattedData: {
|
||||||
screenName: 'Connection name',
|
screenName: 'Connection name',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,6 +14,6 @@ export default async (request, response) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const connectionParams = (request) => {
|
const connectionParams = (request) => {
|
||||||
const { formattedData, appAuthClientId } = request.body;
|
const { formattedData, oauthClientId } = request.body;
|
||||||
return { formattedData, appAuthClientId };
|
return { formattedData, oauthClientId };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -55,10 +55,9 @@ describe('PATCH /api/v1/connections/:connectionId', () => {
|
|||||||
|
|
||||||
const refetchedCurrentUserConnection = await currentUserConnection.$query();
|
const refetchedCurrentUserConnection = await currentUserConnection.$query();
|
||||||
|
|
||||||
const expectedPayload = updateConnectionMock({
|
const expectedPayload = updateConnectionMock(
|
||||||
...refetchedCurrentUserConnection,
|
refetchedCurrentUserConnection
|
||||||
reconnectable: refetchedCurrentUserConnection.reconnectable,
|
);
|
||||||
});
|
|
||||||
|
|
||||||
expect(response.body).toStrictEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|||||||
11
packages/backend/src/controllers/api/v1/flows/export-flow.js
Normal file
11
packages/backend/src/controllers/api/v1/flows/export-flow.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { renderObject } from '../../../../helpers/renderer.js';
|
||||||
|
|
||||||
|
export default async (request, response) => {
|
||||||
|
const flow = await request.currentUser.authorizedFlows
|
||||||
|
.findById(request.params.flowId)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
const exportedFlow = await flow.export();
|
||||||
|
|
||||||
|
return renderObject(response, exportedFlow, { status: 201 });
|
||||||
|
};
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import request from 'supertest';
|
||||||
|
import Crypto from 'crypto';
|
||||||
|
import app from '../../../../app.js';
|
||||||
|
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js';
|
||||||
|
import { createUser } from '../../../../../test/factories/user.js';
|
||||||
|
import { createFlow } from '../../../../../test/factories/flow.js';
|
||||||
|
import { createStep } from '../../../../../test/factories/step.js';
|
||||||
|
import { createPermission } from '../../../../../test/factories/permission.js';
|
||||||
|
import exportFlowMock from '../../../../../test/mocks/rest/api/v1/flows/export-flow.js';
|
||||||
|
|
||||||
|
describe('POST /api/v1/flows/:flowId/export', () => {
|
||||||
|
let currentUser, currentUserRole, token;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
currentUser = await createUser();
|
||||||
|
currentUserRole = await currentUser.$relatedQuery('role');
|
||||||
|
|
||||||
|
token = await createAuthTokenByUserId(currentUser.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should export the flow data of the current user', async () => {
|
||||||
|
const currentUserFlow = await createFlow({ userId: currentUser.id });
|
||||||
|
|
||||||
|
const triggerStep = await createStep({
|
||||||
|
flowId: currentUserFlow.id,
|
||||||
|
type: 'trigger',
|
||||||
|
appKey: 'webhook',
|
||||||
|
key: 'catchRawWebhook',
|
||||||
|
name: 'Catch raw webhook',
|
||||||
|
parameters: {
|
||||||
|
workSynchronously: true,
|
||||||
|
},
|
||||||
|
position: 1,
|
||||||
|
webhookPath: `/webhooks/flows/${currentUserFlow.id}/sync`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const actionStep = await createStep({
|
||||||
|
flowId: currentUserFlow.id,
|
||||||
|
type: 'action',
|
||||||
|
appKey: 'formatter',
|
||||||
|
key: 'text',
|
||||||
|
name: 'Text',
|
||||||
|
parameters: {
|
||||||
|
input: `hello {{step.${triggerStep.id}.query.sample}} deneme`,
|
||||||
|
transform: 'capitalize',
|
||||||
|
},
|
||||||
|
position: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
await createPermission({
|
||||||
|
action: 'read',
|
||||||
|
subject: 'Flow',
|
||||||
|
roleId: currentUserRole.id,
|
||||||
|
conditions: ['isCreator'],
|
||||||
|
});
|
||||||
|
|
||||||
|
await createPermission({
|
||||||
|
action: 'update',
|
||||||
|
subject: 'Flow',
|
||||||
|
roleId: currentUserRole.id,
|
||||||
|
conditions: ['isCreator'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.post(`/api/v1/flows/${currentUserFlow.id}/export`)
|
||||||
|
.set('Authorization', token)
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
const expectedPayload = await exportFlowMock(currentUserFlow, [
|
||||||
|
triggerStep,
|
||||||
|
actionStep,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should export the flow data of another user', async () => {
|
||||||
|
const anotherUser = await createUser();
|
||||||
|
const anotherUserFlow = await createFlow({ userId: anotherUser.id });
|
||||||
|
|
||||||
|
const triggerStep = await createStep({
|
||||||
|
flowId: anotherUserFlow.id,
|
||||||
|
type: 'trigger',
|
||||||
|
appKey: 'webhook',
|
||||||
|
key: 'catchRawWebhook',
|
||||||
|
name: 'Catch raw webhook',
|
||||||
|
parameters: {
|
||||||
|
workSynchronously: true,
|
||||||
|
},
|
||||||
|
position: 1,
|
||||||
|
webhookPath: `/webhooks/flows/${anotherUserFlow.id}/sync`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const actionStep = await createStep({
|
||||||
|
flowId: anotherUserFlow.id,
|
||||||
|
type: 'action',
|
||||||
|
appKey: 'formatter',
|
||||||
|
key: 'text',
|
||||||
|
name: 'Text',
|
||||||
|
parameters: {
|
||||||
|
input: `hello {{step.${triggerStep.id}.query.sample}} deneme`,
|
||||||
|
transform: 'capitalize',
|
||||||
|
},
|
||||||
|
position: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
await createPermission({
|
||||||
|
action: 'read',
|
||||||
|
subject: 'Flow',
|
||||||
|
roleId: currentUserRole.id,
|
||||||
|
conditions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
await createPermission({
|
||||||
|
action: 'update',
|
||||||
|
subject: 'Flow',
|
||||||
|
roleId: currentUserRole.id,
|
||||||
|
conditions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.post(`/api/v1/flows/${anotherUserFlow.id}/export`)
|
||||||
|
.set('Authorization', token)
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
const expectedPayload = await exportFlowMock(anotherUserFlow, [
|
||||||
|
triggerStep,
|
||||||
|
actionStep,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return not found response for not existing flow UUID', async () => {
|
||||||
|
await createPermission({
|
||||||
|
action: 'read',
|
||||||
|
subject: 'Flow',
|
||||||
|
roleId: currentUserRole.id,
|
||||||
|
conditions: ['isCreator'],
|
||||||
|
});
|
||||||
|
|
||||||
|
await createPermission({
|
||||||
|
action: 'update',
|
||||||
|
subject: 'Flow',
|
||||||
|
roleId: currentUserRole.id,
|
||||||
|
conditions: ['isCreator'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const notExistingFlowUUID = Crypto.randomUUID();
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.post(`/api/v1/flows/${notExistingFlowUUID}/export`)
|
||||||
|
.set('Authorization', token)
|
||||||
|
.expect(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return not found response for unauthorized flow', async () => {
|
||||||
|
const anotherUser = await createUser();
|
||||||
|
const anotherUserFlow = await createFlow({ userId: anotherUser.id });
|
||||||
|
|
||||||
|
await createPermission({
|
||||||
|
action: 'read',
|
||||||
|
subject: 'Flow',
|
||||||
|
roleId: currentUserRole.id,
|
||||||
|
conditions: ['isCreator'],
|
||||||
|
});
|
||||||
|
|
||||||
|
await createPermission({
|
||||||
|
action: 'update',
|
||||||
|
subject: 'Flow',
|
||||||
|
roleId: currentUserRole.id,
|
||||||
|
conditions: ['isCreator'],
|
||||||
|
});
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.post(`/api/v1/flows/${anotherUserFlow.id}/export`)
|
||||||
|
.set('Authorization', token)
|
||||||
|
.expect(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return bad request response for invalid UUID', async () => {
|
||||||
|
await createPermission({
|
||||||
|
action: 'read',
|
||||||
|
subject: 'Flow',
|
||||||
|
roleId: currentUserRole.id,
|
||||||
|
conditions: ['isCreator'],
|
||||||
|
});
|
||||||
|
|
||||||
|
await createPermission({
|
||||||
|
action: 'update',
|
||||||
|
subject: 'Flow',
|
||||||
|
roleId: currentUserRole.id,
|
||||||
|
conditions: ['isCreator'],
|
||||||
|
});
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.post('/api/v1/flows/invalidFlowUUID/export')
|
||||||
|
.set('Authorization', token)
|
||||||
|
.expect(400);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,12 +11,13 @@ export default async (request, response) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const stepParams = (request) => {
|
const stepParams = (request) => {
|
||||||
const { connectionId, appKey, key, parameters } = request.body;
|
const { connectionId, appKey, key, name, parameters } = request.body;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
connectionId,
|
connectionId,
|
||||||
appKey,
|
appKey,
|
||||||
key,
|
key,
|
||||||
|
name,
|
||||||
parameters,
|
parameters,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ describe('PATCH /api/v1/steps/:stepId', () => {
|
|||||||
connectionId: currentUserConnection.id,
|
connectionId: currentUserConnection.id,
|
||||||
appKey: 'deepl',
|
appKey: 'deepl',
|
||||||
key: 'translateText',
|
key: 'translateText',
|
||||||
|
name: 'Translate text',
|
||||||
});
|
});
|
||||||
|
|
||||||
await createPermission({
|
await createPermission({
|
||||||
@@ -58,6 +59,7 @@ describe('PATCH /api/v1/steps/:stepId', () => {
|
|||||||
parameters: {
|
parameters: {
|
||||||
text: 'Hello world!',
|
text: 'Hello world!',
|
||||||
targetLanguage: 'de',
|
targetLanguage: 'de',
|
||||||
|
name: 'Translate text - Updated step name',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ import { renderObject } from '../../../../helpers/renderer.js';
|
|||||||
export default async (request, response) => {
|
export default async (request, response) => {
|
||||||
const apps = await request.currentUser.getApps(request.query.name);
|
const apps = await request.currentUser.getApps(request.query.name);
|
||||||
|
|
||||||
renderObject(response, apps, { serializer: 'App' });
|
renderObject(response, apps, { serializer: 'UserApp' });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
import path from 'node:path';
|
|
||||||
|
|
||||||
import Connection from '../../models/connection.js';
|
|
||||||
import logger from '../../helpers/logger.js';
|
|
||||||
import handler from '../../helpers/webhook-handler.js';
|
|
||||||
|
|
||||||
export default async (request, response) => {
|
|
||||||
const computedRequestPayload = {
|
|
||||||
headers: request.headers,
|
|
||||||
body: request.body,
|
|
||||||
query: request.query,
|
|
||||||
params: request.params,
|
|
||||||
};
|
|
||||||
logger.debug(`Handling incoming webhook request at ${request.originalUrl}.`);
|
|
||||||
logger.debug(JSON.stringify(computedRequestPayload, null, 2));
|
|
||||||
|
|
||||||
const { connectionId } = request.params;
|
|
||||||
|
|
||||||
const connection = await Connection.query()
|
|
||||||
.findById(connectionId)
|
|
||||||
.throwIfNotFound();
|
|
||||||
|
|
||||||
if (!(await connection.verifyWebhook(request))) {
|
|
||||||
return response.sendStatus(401);
|
|
||||||
}
|
|
||||||
|
|
||||||
const triggerSteps = await connection
|
|
||||||
.$relatedQuery('triggerSteps')
|
|
||||||
.where('webhook_path', path.join(request.baseUrl, request.path));
|
|
||||||
|
|
||||||
if (triggerSteps.length === 0) return response.sendStatus(404);
|
|
||||||
|
|
||||||
for (const triggerStep of triggerSteps) {
|
|
||||||
await handler(triggerStep.flowId, request, response);
|
|
||||||
}
|
|
||||||
|
|
||||||
response.sendStatus(204);
|
|
||||||
};
|
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
export async function up(knex) {
|
||||||
|
await knex.schema.renameTable('app_auth_clients', 'oauth_clients');
|
||||||
|
|
||||||
|
await knex.schema.raw(
|
||||||
|
'ALTER INDEX app_auth_clients_pkey RENAME TO oauth_clients_pkey'
|
||||||
|
);
|
||||||
|
|
||||||
|
await knex.schema.raw(
|
||||||
|
'ALTER INDEX app_auth_clients_name_unique RENAME TO oauth_clients_name_unique'
|
||||||
|
);
|
||||||
|
|
||||||
|
return await knex.schema.alterTable('connections', (table) => {
|
||||||
|
table.renameColumn('app_auth_client_id', 'oauth_client_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex) {
|
||||||
|
await knex.schema.renameTable('oauth_clients', 'app_auth_clients');
|
||||||
|
|
||||||
|
await knex.schema.raw(
|
||||||
|
'ALTER INDEX oauth_clients_pkey RENAME TO app_auth_clients_pkey'
|
||||||
|
);
|
||||||
|
|
||||||
|
await knex.schema.raw(
|
||||||
|
'ALTER INDEX oauth_clients_name_unique RENAME TO app_auth_clients_name_unique'
|
||||||
|
);
|
||||||
|
|
||||||
|
return await knex.schema.alterTable('connections', (table) => {
|
||||||
|
table.renameColumn('oauth_client_id', 'app_auth_client_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import toLower from 'lodash/toLower.js';
|
||||||
|
import startCase from 'lodash/startCase.js';
|
||||||
|
import upperFirst from 'lodash/upperFirst.js';
|
||||||
|
|
||||||
|
export async function up(knex) {
|
||||||
|
await knex.schema.table('steps', function (table) {
|
||||||
|
table.string('name');
|
||||||
|
});
|
||||||
|
|
||||||
|
const rows = await knex('steps').select('id', 'key');
|
||||||
|
|
||||||
|
const updates = rows.map((row) => {
|
||||||
|
if (!row.key) return;
|
||||||
|
|
||||||
|
const humanizedKey = upperFirst(toLower(startCase(row.key)));
|
||||||
|
return knex('steps').where({ id: row.id }).update({ name: humanizedKey });
|
||||||
|
});
|
||||||
|
|
||||||
|
return await Promise.all(updates);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex) {
|
||||||
|
return knex.schema.table('steps', function (table) {
|
||||||
|
table.dropColumn('name');
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -88,8 +88,8 @@ const sharedAuthenticationStepsWithAuthUrl = [
|
|||||||
value: '{key}',
|
value: '{key}',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'appAuthClientId',
|
name: 'oauthClientId',
|
||||||
value: '{appAuthClientId}',
|
value: '{oauthClientId}',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -113,6 +113,10 @@ const authorizationList = {
|
|||||||
action: 'create',
|
action: 'create',
|
||||||
subject: 'Flow',
|
subject: 'Flow',
|
||||||
},
|
},
|
||||||
|
'POST /api/v1/flows/:flowId/export': {
|
||||||
|
action: 'update',
|
||||||
|
subject: 'Flow',
|
||||||
|
},
|
||||||
'POST /api/v1/flows/:flowId/steps': {
|
'POST /api/v1/flows/:flowId/steps': {
|
||||||
action: 'update',
|
action: 'update',
|
||||||
subject: 'Flow',
|
subject: 'Flow',
|
||||||
|
|||||||
45
packages/backend/src/helpers/export-flow.js
Normal file
45
packages/backend/src/helpers/export-flow.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import Crypto from 'crypto';
|
||||||
|
|
||||||
|
const exportFlow = async (flow) => {
|
||||||
|
const steps = await flow.$relatedQuery('steps');
|
||||||
|
|
||||||
|
const newFlowId = Crypto.randomUUID();
|
||||||
|
const stepIdMap = Object.fromEntries(
|
||||||
|
steps.map((step) => [step.id, Crypto.randomUUID()])
|
||||||
|
);
|
||||||
|
|
||||||
|
const exportedFlow = {
|
||||||
|
id: newFlowId,
|
||||||
|
name: flow.name,
|
||||||
|
steps: steps.map((step) => ({
|
||||||
|
id: stepIdMap[step.id],
|
||||||
|
key: step.key,
|
||||||
|
name: step.name,
|
||||||
|
appKey: step.appKey,
|
||||||
|
type: step.type,
|
||||||
|
parameters: updateParameters(step.parameters, stepIdMap),
|
||||||
|
position: step.position,
|
||||||
|
webhookPath: step.webhookPath?.replace(flow.id, newFlowId),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
return exportedFlow;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateParameters = (parameters, stepIdMap) => {
|
||||||
|
if (!parameters) return parameters;
|
||||||
|
|
||||||
|
const stringifiedParameters = JSON.stringify(parameters);
|
||||||
|
let updatedParameters = stringifiedParameters;
|
||||||
|
|
||||||
|
Object.entries(stepIdMap).forEach(([oldStepId, newStepId]) => {
|
||||||
|
updatedParameters = updatedParameters.replace(
|
||||||
|
`{{step.${oldStepId}.`,
|
||||||
|
`{{step.${newStepId}.`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return JSON.parse(updatedParameters);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default exportFlow;
|
||||||
37
packages/backend/src/jobs/delete-user.ee.js
Normal file
37
packages/backend/src/jobs/delete-user.ee.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import appConfig from '../config/app.js';
|
||||||
|
import User from '../models/user.js';
|
||||||
|
import ExecutionStep from '../models/execution-step.js';
|
||||||
|
|
||||||
|
export const deleteUserJob = async (job) => {
|
||||||
|
const { id } = job.data;
|
||||||
|
|
||||||
|
const user = await User.query()
|
||||||
|
.withSoftDeleted()
|
||||||
|
.findById(id)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
const executionIds = (
|
||||||
|
await user
|
||||||
|
.$relatedQuery('executions')
|
||||||
|
.withSoftDeleted()
|
||||||
|
.select('executions.id')
|
||||||
|
).map((execution) => execution.id);
|
||||||
|
|
||||||
|
await ExecutionStep.query()
|
||||||
|
.withSoftDeleted()
|
||||||
|
.whereIn('execution_id', executionIds)
|
||||||
|
.hardDelete();
|
||||||
|
await user.$relatedQuery('executions').withSoftDeleted().hardDelete();
|
||||||
|
await user.$relatedQuery('steps').withSoftDeleted().hardDelete();
|
||||||
|
await user.$relatedQuery('flows').withSoftDeleted().hardDelete();
|
||||||
|
await user.$relatedQuery('connections').withSoftDeleted().hardDelete();
|
||||||
|
await user.$relatedQuery('identities').withSoftDeleted().hardDelete();
|
||||||
|
|
||||||
|
if (appConfig.isCloud) {
|
||||||
|
await user.$relatedQuery('subscriptions').withSoftDeleted().hardDelete();
|
||||||
|
await user.$relatedQuery('usageData').withSoftDeleted().hardDelete();
|
||||||
|
}
|
||||||
|
|
||||||
|
await user.$relatedQuery('accessTokens').withSoftDeleted().hardDelete();
|
||||||
|
await user.$query().withSoftDeleted().hardDelete();
|
||||||
|
};
|
||||||
46
packages/backend/src/jobs/execute-action.js
Normal file
46
packages/backend/src/jobs/execute-action.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import Step from '../models/step.js';
|
||||||
|
import actionQueue from '../queues/action.js';
|
||||||
|
import { processAction } from '../services/action.js';
|
||||||
|
import {
|
||||||
|
REMOVE_AFTER_30_DAYS_OR_150_JOBS,
|
||||||
|
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
|
||||||
|
} from '../helpers/remove-job-configuration.js';
|
||||||
|
import delayAsMilliseconds from '../helpers/delay-as-milliseconds.js';
|
||||||
|
|
||||||
|
const DEFAULT_DELAY_DURATION = 0;
|
||||||
|
|
||||||
|
export const executeActionJob = async (job) => {
|
||||||
|
const { stepId, flowId, executionId, computedParameters, executionStep } =
|
||||||
|
await processAction(job.data);
|
||||||
|
|
||||||
|
if (executionStep.isFailed) return;
|
||||||
|
|
||||||
|
const step = await Step.query().findById(stepId).throwIfNotFound();
|
||||||
|
const nextStep = await step.getNextStep();
|
||||||
|
|
||||||
|
if (!nextStep) return;
|
||||||
|
|
||||||
|
const jobName = `${executionId}-${nextStep.id}`;
|
||||||
|
|
||||||
|
const jobPayload = {
|
||||||
|
flowId,
|
||||||
|
executionId,
|
||||||
|
stepId: nextStep.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
const jobOptions = {
|
||||||
|
removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS,
|
||||||
|
removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS,
|
||||||
|
delay: DEFAULT_DELAY_DURATION,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (step.appKey === 'delay') {
|
||||||
|
jobOptions.delay = delayAsMilliseconds(step.key, computedParameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step.appKey === 'filter' && !executionStep.dataOut) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await actionQueue.add(jobName, jobPayload, jobOptions);
|
||||||
|
};
|
||||||
54
packages/backend/src/jobs/execute-flow.js
Normal file
54
packages/backend/src/jobs/execute-flow.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import triggerQueue from '../queues/trigger.js';
|
||||||
|
import { processFlow } from '../services/flow.js';
|
||||||
|
import Flow from '../models/flow.js';
|
||||||
|
import {
|
||||||
|
REMOVE_AFTER_30_DAYS_OR_150_JOBS,
|
||||||
|
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
|
||||||
|
} from '../helpers/remove-job-configuration.js';
|
||||||
|
|
||||||
|
export const executeFlowJob = async (job) => {
|
||||||
|
const { flowId } = job.data;
|
||||||
|
|
||||||
|
const flow = await Flow.query().findById(flowId).throwIfNotFound();
|
||||||
|
const user = await flow.$relatedQuery('user');
|
||||||
|
const allowedToRunFlows = await user.isAllowedToRunFlows();
|
||||||
|
|
||||||
|
if (!allowedToRunFlows) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const triggerStep = await flow.getTriggerStep();
|
||||||
|
|
||||||
|
const { data, error } = await processFlow({ flowId });
|
||||||
|
|
||||||
|
const reversedData = data.reverse();
|
||||||
|
|
||||||
|
const jobOptions = {
|
||||||
|
removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS,
|
||||||
|
removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const triggerItem of reversedData) {
|
||||||
|
const jobName = `${triggerStep.id}-${triggerItem.meta.internalId}`;
|
||||||
|
|
||||||
|
const jobPayload = {
|
||||||
|
flowId,
|
||||||
|
stepId: triggerStep.id,
|
||||||
|
triggerItem,
|
||||||
|
};
|
||||||
|
|
||||||
|
await triggerQueue.add(jobName, jobPayload, jobOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
const jobName = `${triggerStep.id}-error`;
|
||||||
|
|
||||||
|
const jobPayload = {
|
||||||
|
flowId,
|
||||||
|
stepId: triggerStep.id,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
|
||||||
|
await triggerQueue.add(jobName, jobPayload, jobOptions);
|
||||||
|
}
|
||||||
|
};
|
||||||
32
packages/backend/src/jobs/execute-trigger.js
Normal file
32
packages/backend/src/jobs/execute-trigger.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import actionQueue from '../queues/action.js';
|
||||||
|
import Step from '../models/step.js';
|
||||||
|
import { processTrigger } from '../services/trigger.js';
|
||||||
|
import {
|
||||||
|
REMOVE_AFTER_30_DAYS_OR_150_JOBS,
|
||||||
|
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
|
||||||
|
} from '../helpers/remove-job-configuration.js';
|
||||||
|
|
||||||
|
export const executeTriggerJob = async (job) => {
|
||||||
|
const { flowId, executionId, stepId, executionStep } = await processTrigger(
|
||||||
|
job.data
|
||||||
|
);
|
||||||
|
|
||||||
|
if (executionStep.isFailed) return;
|
||||||
|
|
||||||
|
const step = await Step.query().findById(stepId).throwIfNotFound();
|
||||||
|
const nextStep = await step.getNextStep();
|
||||||
|
const jobName = `${executionId}-${nextStep.id}`;
|
||||||
|
|
||||||
|
const jobPayload = {
|
||||||
|
flowId,
|
||||||
|
executionId,
|
||||||
|
stepId: nextStep.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
const jobOptions = {
|
||||||
|
removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS,
|
||||||
|
removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS,
|
||||||
|
};
|
||||||
|
|
||||||
|
await actionQueue.add(jobName, jobPayload, jobOptions);
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import Subscription from '../models/subscription.ee.js';
|
||||||
|
|
||||||
|
export const removeCancelledSubscriptionsJob = async () => {
|
||||||
|
await Subscription.query()
|
||||||
|
.delete()
|
||||||
|
.where({
|
||||||
|
status: 'deleted',
|
||||||
|
})
|
||||||
|
.andWhere(
|
||||||
|
'cancellation_effective_date',
|
||||||
|
'<=',
|
||||||
|
DateTime.now().startOf('day').toISODate()
|
||||||
|
);
|
||||||
|
};
|
||||||
31
packages/backend/src/jobs/send-email.js
Normal file
31
packages/backend/src/jobs/send-email.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import logger from '../helpers/logger.js';
|
||||||
|
import mailer from '../helpers/mailer.ee.js';
|
||||||
|
import compileEmail from '../helpers/compile-email.ee.js';
|
||||||
|
import appConfig from '../config/app.js';
|
||||||
|
|
||||||
|
export const sendEmailJob = async (job) => {
|
||||||
|
const { email, subject, template, params } = job.data;
|
||||||
|
|
||||||
|
if (isCloudSandbox() && !isAutomatischEmail(email)) {
|
||||||
|
logger.info(
|
||||||
|
'Only Automatisch emails are allowed for non-production environments!'
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await mailer.sendMail({
|
||||||
|
to: email,
|
||||||
|
from: appConfig.fromEmail,
|
||||||
|
subject: subject,
|
||||||
|
html: compileEmail(template, params),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isCloudSandbox = () => {
|
||||||
|
return appConfig.isCloud && !appConfig.isProd;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isAutomatischEmail = (email) => {
|
||||||
|
return email.endsWith('@automatisch.io');
|
||||||
|
};
|
||||||
@@ -3,17 +3,9 @@
|
|||||||
exports[`AppConfig model > jsonSchema should have correct validations 1`] = `
|
exports[`AppConfig model > jsonSchema should have correct validations 1`] = `
|
||||||
{
|
{
|
||||||
"properties": {
|
"properties": {
|
||||||
"connectionAllowed": {
|
|
||||||
"default": false,
|
|
||||||
"type": "boolean",
|
|
||||||
},
|
|
||||||
"createdAt": {
|
"createdAt": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
},
|
},
|
||||||
"customConnectionAllowed": {
|
|
||||||
"default": false,
|
|
||||||
"type": "boolean",
|
|
||||||
},
|
|
||||||
"disabled": {
|
"disabled": {
|
||||||
"default": false,
|
"default": false,
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
@@ -25,13 +17,13 @@ exports[`AppConfig model > jsonSchema should have correct validations 1`] = `
|
|||||||
"key": {
|
"key": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
},
|
},
|
||||||
"shared": {
|
|
||||||
"default": false,
|
|
||||||
"type": "boolean",
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
"updatedAt": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
},
|
},
|
||||||
|
"useOnlyPredefinedAuthClients": {
|
||||||
|
"default": false,
|
||||||
|
"type": "boolean",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"key",
|
"key",
|
||||||
|
|||||||
@@ -3,10 +3,6 @@
|
|||||||
exports[`Connection model > jsonSchema should have correct validations 1`] = `
|
exports[`Connection model > jsonSchema should have correct validations 1`] = `
|
||||||
{
|
{
|
||||||
"properties": {
|
"properties": {
|
||||||
"appAuthClientId": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"createdAt": {
|
"createdAt": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
},
|
},
|
||||||
@@ -31,6 +27,10 @@ exports[`Connection model > jsonSchema should have correct validations 1`] = `
|
|||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"type": "string",
|
"type": "string",
|
||||||
},
|
},
|
||||||
|
"oauthClientId": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
"updatedAt": {
|
"updatedAt": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`AppAuthClient model > jsonSchema should have correct validations 1`] = `
|
exports[`OAuthClient model > jsonSchema should have correct validations 1`] = `
|
||||||
{
|
{
|
||||||
"properties": {
|
"properties": {
|
||||||
"active": {
|
"active": {
|
||||||
@@ -38,6 +38,14 @@ exports[`Step model > jsonSchema should have correct validations 1`] = `
|
|||||||
"null",
|
"null",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
"name": {
|
||||||
|
"maxLength": 255,
|
||||||
|
"minLength": 1,
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null",
|
||||||
|
],
|
||||||
|
},
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,284 +0,0 @@
|
|||||||
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', () => {
|
|
||||||
expect(AppAuthClient.tableName).toBe('app_auth_clients');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('jsonSchema should have correct validations', () => {
|
|
||||||
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(
|
|
||||||
AppAuthClient.prototype,
|
|
||||||
'eligibleForEncryption'
|
|
||||||
).mockReturnValue(false);
|
|
||||||
|
|
||||||
const appAuthClient = new AppAuthClient();
|
|
||||||
|
|
||||||
expect(appAuthClient.encryptData()).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should encrypt formattedAuthDefaults and set it to authDefaults', async () => {
|
|
||||||
vi.spyOn(
|
|
||||||
AppAuthClient.prototype,
|
|
||||||
'eligibleForEncryption'
|
|
||||||
).mockReturnValue(true);
|
|
||||||
|
|
||||||
const formattedAuthDefaults = {
|
|
||||||
key: 'value',
|
|
||||||
};
|
|
||||||
|
|
||||||
const appAuthClient = new AppAuthClient();
|
|
||||||
appAuthClient.formattedAuthDefaults = formattedAuthDefaults;
|
|
||||||
appAuthClient.encryptData();
|
|
||||||
|
|
||||||
const expectedDecryptedValue = JSON.parse(
|
|
||||||
AES.decrypt(
|
|
||||||
appAuthClient.authDefaults,
|
|
||||||
appConfig.encryptionKey
|
|
||||||
).toString(enc)
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(formattedAuthDefaults).toStrictEqual(expectedDecryptedValue);
|
|
||||||
expect(appAuthClient.authDefaults).not.toStrictEqual(
|
|
||||||
formattedAuthDefaults
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should encrypt formattedAuthDefaults and remove formattedAuthDefaults', async () => {
|
|
||||||
vi.spyOn(
|
|
||||||
AppAuthClient.prototype,
|
|
||||||
'eligibleForEncryption'
|
|
||||||
).mockReturnValue(true);
|
|
||||||
|
|
||||||
const formattedAuthDefaults = {
|
|
||||||
key: 'value',
|
|
||||||
};
|
|
||||||
|
|
||||||
const appAuthClient = new AppAuthClient();
|
|
||||||
appAuthClient.formattedAuthDefaults = formattedAuthDefaults;
|
|
||||||
appAuthClient.encryptData();
|
|
||||||
|
|
||||||
expect(appAuthClient.formattedAuthDefaults).not.toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('decryptData', () => {
|
|
||||||
it('should return undefined if eligibleForDecryption is not true', () => {
|
|
||||||
vi.spyOn(
|
|
||||||
AppAuthClient.prototype,
|
|
||||||
'eligibleForDecryption'
|
|
||||||
).mockReturnValue(false);
|
|
||||||
|
|
||||||
const appAuthClient = new AppAuthClient();
|
|
||||||
|
|
||||||
expect(appAuthClient.decryptData()).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should decrypt authDefaults and set it to formattedAuthDefaults', async () => {
|
|
||||||
vi.spyOn(
|
|
||||||
AppAuthClient.prototype,
|
|
||||||
'eligibleForDecryption'
|
|
||||||
).mockReturnValue(true);
|
|
||||||
|
|
||||||
const formattedAuthDefaults = {
|
|
||||||
key: 'value',
|
|
||||||
};
|
|
||||||
|
|
||||||
const authDefaults = AES.encrypt(
|
|
||||||
JSON.stringify(formattedAuthDefaults),
|
|
||||||
appConfig.encryptionKey
|
|
||||||
).toString();
|
|
||||||
|
|
||||||
const appAuthClient = new AppAuthClient();
|
|
||||||
appAuthClient.authDefaults = authDefaults;
|
|
||||||
appAuthClient.decryptData();
|
|
||||||
|
|
||||||
expect(appAuthClient.formattedAuthDefaults).toStrictEqual(
|
|
||||||
formattedAuthDefaults
|
|
||||||
);
|
|
||||||
expect(appAuthClient.authDefaults).not.toStrictEqual(
|
|
||||||
formattedAuthDefaults
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('eligibleForEncryption', () => {
|
|
||||||
it('should return true when formattedAuthDefaults property exists', async () => {
|
|
||||||
const appAuthClient = await createAppAuthClient();
|
|
||||||
|
|
||||||
expect(appAuthClient.eligibleForEncryption()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return false when formattedAuthDefaults property doesn't exist", async () => {
|
|
||||||
const appAuthClient = await createAppAuthClient();
|
|
||||||
|
|
||||||
delete appAuthClient.formattedAuthDefaults;
|
|
||||||
|
|
||||||
expect(appAuthClient.eligibleForEncryption()).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('eligibleForDecryption', () => {
|
|
||||||
it('should return true when authDefaults property exists', async () => {
|
|
||||||
const appAuthClient = await createAppAuthClient();
|
|
||||||
|
|
||||||
expect(appAuthClient.eligibleForDecryption()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return false when authDefaults property doesn't exist", async () => {
|
|
||||||
const appAuthClient = await createAppAuthClient();
|
|
||||||
|
|
||||||
delete appAuthClient.authDefaults;
|
|
||||||
|
|
||||||
expect(appAuthClient.eligibleForDecryption()).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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,
|
|
||||||
'encryptData'
|
|
||||||
);
|
|
||||||
|
|
||||||
await createAppAuthClient();
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
const appAuthClientBeforeUpdateSpy = vi.spyOn(
|
|
||||||
AppAuthClient.prototype,
|
|
||||||
'encryptData'
|
|
||||||
);
|
|
||||||
|
|
||||||
await appAuthClient.$query().patchAndFetch({ name: 'sample' });
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
const appAuthClientAfterFindSpy = vi.spyOn(
|
|
||||||
AppAuthClient.prototype,
|
|
||||||
'decryptData'
|
|
||||||
);
|
|
||||||
|
|
||||||
await appAuthClient.$query();
|
|
||||||
|
|
||||||
expect(appAuthClientAfterFindSpy).toHaveBeenCalledOnce();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import App from './app.js';
|
import App from './app.js';
|
||||||
import AppAuthClient from './app-auth-client.js';
|
import OAuthClient from './oauth-client.js';
|
||||||
import Base from './base.js';
|
import Base from './base.js';
|
||||||
|
|
||||||
class AppConfig extends Base {
|
class AppConfig extends Base {
|
||||||
@@ -16,9 +16,7 @@ class AppConfig extends Base {
|
|||||||
properties: {
|
properties: {
|
||||||
id: { type: 'string', format: 'uuid' },
|
id: { type: 'string', format: 'uuid' },
|
||||||
key: { type: 'string' },
|
key: { type: 'string' },
|
||||||
connectionAllowed: { type: 'boolean', default: false },
|
useOnlyPredefinedAuthClients: { type: 'boolean', default: false },
|
||||||
customConnectionAllowed: { type: 'boolean', default: false },
|
|
||||||
shared: { type: 'boolean', default: false },
|
|
||||||
disabled: { type: 'boolean', default: false },
|
disabled: { type: 'boolean', default: false },
|
||||||
createdAt: { type: 'string' },
|
createdAt: { type: 'string' },
|
||||||
updatedAt: { type: 'string' },
|
updatedAt: { type: 'string' },
|
||||||
@@ -26,12 +24,12 @@ class AppConfig extends Base {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static relationMappings = () => ({
|
static relationMappings = () => ({
|
||||||
appAuthClients: {
|
oauthClients: {
|
||||||
relation: Base.HasManyRelation,
|
relation: Base.HasManyRelation,
|
||||||
modelClass: AppAuthClient,
|
modelClass: OAuthClient,
|
||||||
join: {
|
join: {
|
||||||
from: 'app_configs.key',
|
from: 'app_configs.key',
|
||||||
to: 'app_auth_clients.app_key',
|
to: 'oauth_clients.app_key',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -41,39 +39,6 @@ class AppConfig extends Base {
|
|||||||
|
|
||||||
return await App.findOneByKey(this.key);
|
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;
|
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 Base from './base.js';
|
||||||
import AppConfig from './app-config.js';
|
import AppConfig from './app-config.js';
|
||||||
import App from './app.js';
|
import App from './app.js';
|
||||||
import AppAuthClient from './app-auth-client.js';
|
import OAuthClient from './oauth-client.js';
|
||||||
import { createAppConfig } from '../../test/factories/app-config.js';
|
|
||||||
import { createAppAuthClient } from '../../test/factories/app-auth-client.js';
|
|
||||||
|
|
||||||
describe('AppConfig model', () => {
|
describe('AppConfig model', () => {
|
||||||
it('tableName should return correct name', () => {
|
it('tableName should return correct name', () => {
|
||||||
@@ -24,12 +22,12 @@ describe('AppConfig model', () => {
|
|||||||
const relationMappings = AppConfig.relationMappings();
|
const relationMappings = AppConfig.relationMappings();
|
||||||
|
|
||||||
const expectedRelations = {
|
const expectedRelations = {
|
||||||
appAuthClients: {
|
oauthClients: {
|
||||||
relation: Base.HasManyRelation,
|
relation: Base.HasManyRelation,
|
||||||
modelClass: AppAuthClient,
|
modelClass: OAuthClient,
|
||||||
join: {
|
join: {
|
||||||
from: 'app_configs.key',
|
from: 'app_configs.key',
|
||||||
to: 'app_auth_clients.app_key',
|
to: 'oauth_clients.app_key',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -55,126 +53,4 @@ describe('AppConfig model', () => {
|
|||||||
expect(app).toStrictEqual(expectedApp);
|
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();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import AES from 'crypto-js/aes.js';
|
|||||||
import enc from 'crypto-js/enc-utf8.js';
|
import enc from 'crypto-js/enc-utf8.js';
|
||||||
import App from './app.js';
|
import App from './app.js';
|
||||||
import AppConfig from './app-config.js';
|
import AppConfig from './app-config.js';
|
||||||
import AppAuthClient from './app-auth-client.js';
|
import OAuthClient from './oauth-client.js';
|
||||||
import Base from './base.js';
|
import Base from './base.js';
|
||||||
import User from './user.js';
|
import User from './user.js';
|
||||||
import Step from './step.js';
|
import Step from './step.js';
|
||||||
@@ -24,7 +24,7 @@ class Connection extends Base {
|
|||||||
data: { type: 'string' },
|
data: { type: 'string' },
|
||||||
formattedData: { type: 'object' },
|
formattedData: { type: 'object' },
|
||||||
userId: { type: 'string', format: 'uuid' },
|
userId: { type: 'string', format: 'uuid' },
|
||||||
appAuthClientId: { type: 'string', format: 'uuid' },
|
oauthClientId: { type: 'string', format: 'uuid' },
|
||||||
verified: { type: 'boolean', default: false },
|
verified: { type: 'boolean', default: false },
|
||||||
draft: { type: 'boolean' },
|
draft: { type: 'boolean' },
|
||||||
deletedAt: { type: 'string' },
|
deletedAt: { type: 'string' },
|
||||||
@@ -33,10 +33,6 @@ class Connection extends Base {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
static get virtualAttributes() {
|
|
||||||
return ['reconnectable'];
|
|
||||||
}
|
|
||||||
|
|
||||||
static relationMappings = () => ({
|
static relationMappings = () => ({
|
||||||
user: {
|
user: {
|
||||||
relation: Base.BelongsToOneRelation,
|
relation: Base.BelongsToOneRelation,
|
||||||
@@ -73,28 +69,16 @@ class Connection extends Base {
|
|||||||
to: 'app_configs.key',
|
to: 'app_configs.key',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
appAuthClient: {
|
oauthClient: {
|
||||||
relation: Base.BelongsToOneRelation,
|
relation: Base.BelongsToOneRelation,
|
||||||
modelClass: AppAuthClient,
|
modelClass: OAuthClient,
|
||||||
join: {
|
join: {
|
||||||
from: 'connections.app_auth_client_id',
|
from: 'connections.oauth_client_id',
|
||||||
to: 'app_auth_clients.id',
|
to: 'oauth_clients.id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
get reconnectable() {
|
|
||||||
if (this.appAuthClientId) {
|
|
||||||
return this.appAuthClient.active;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.appConfig) {
|
|
||||||
return !this.appConfig.disabled && this.appConfig.customConnectionAllowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
encryptData() {
|
encryptData() {
|
||||||
if (!this.eligibleForEncryption()) return;
|
if (!this.eligibleForEncryption()) return;
|
||||||
|
|
||||||
@@ -144,22 +128,16 @@ class Connection extends Base {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!appConfig.customConnectionAllowed && this.formattedData) {
|
if (appConfig.useOnlyPredefinedAuthClients && this.formattedData) {
|
||||||
throw new NotAuthorizedError(
|
throw new NotAuthorizedError(
|
||||||
`New custom connections have been disabled for ${app.name}!`
|
`New custom connections have been disabled for ${app.name}!`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!appConfig.shared && this.appAuthClientId) {
|
if (!this.formattedData) {
|
||||||
throw new NotAuthorizedError(
|
|
||||||
'The connection with the given app auth client is not allowed!'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appConfig.shared && !this.formattedData) {
|
|
||||||
const authClient = await appConfig
|
const authClient = await appConfig
|
||||||
.$relatedQuery('appAuthClients')
|
.$relatedQuery('oauthClients')
|
||||||
.findById(this.appAuthClientId)
|
.findById(this.oauthClientId)
|
||||||
.where({ active: true })
|
.where({ active: true })
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
@@ -237,13 +215,13 @@ class Connection extends Base {
|
|||||||
return updatedConnection;
|
return updatedConnection;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateFormattedData({ formattedData, appAuthClientId }) {
|
async updateFormattedData({ formattedData, oauthClientId }) {
|
||||||
if (appAuthClientId) {
|
if (oauthClientId) {
|
||||||
const appAuthClient = await AppAuthClient.query()
|
const oauthClient = await OAuthClient.query()
|
||||||
.findById(appAuthClientId)
|
.findById(oauthClientId)
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
formattedData = appAuthClient.formattedAuthDefaults;
|
formattedData = oauthClient.formattedAuthDefaults;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.$query().patchAndFetch({
|
return await this.$query().patchAndFetch({
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { describe, it, expect, vi } from 'vitest';
|
|||||||
import AES from 'crypto-js/aes.js';
|
import AES from 'crypto-js/aes.js';
|
||||||
import enc from 'crypto-js/enc-utf8.js';
|
import enc from 'crypto-js/enc-utf8.js';
|
||||||
import appConfig from '../config/app.js';
|
import appConfig from '../config/app.js';
|
||||||
import AppAuthClient from './app-auth-client.js';
|
import OAuthClient from './oauth-client.js';
|
||||||
import App from './app.js';
|
import App from './app.js';
|
||||||
import AppConfig from './app-config.js';
|
import AppConfig from './app-config.js';
|
||||||
import Base from './base.js';
|
import Base from './base.js';
|
||||||
@@ -12,7 +12,7 @@ import User from './user.js';
|
|||||||
import Telemetry from '../helpers/telemetry/index.js';
|
import Telemetry from '../helpers/telemetry/index.js';
|
||||||
import { createConnection } from '../../test/factories/connection.js';
|
import { createConnection } from '../../test/factories/connection.js';
|
||||||
import { createAppConfig } from '../../test/factories/app-config.js';
|
import { createAppConfig } from '../../test/factories/app-config.js';
|
||||||
import { createAppAuthClient } from '../../test/factories/app-auth-client.js';
|
import { createOAuthClient } from '../../test/factories/oauth-client.js';
|
||||||
|
|
||||||
describe('Connection model', () => {
|
describe('Connection model', () => {
|
||||||
it('tableName should return correct name', () => {
|
it('tableName should return correct name', () => {
|
||||||
@@ -23,14 +23,6 @@ describe('Connection model', () => {
|
|||||||
expect(Connection.jsonSchema).toMatchSnapshot();
|
expect(Connection.jsonSchema).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('virtualAttributes should return correct attributes', () => {
|
|
||||||
const virtualAttributes = Connection.virtualAttributes;
|
|
||||||
|
|
||||||
const expectedAttributes = ['reconnectable'];
|
|
||||||
|
|
||||||
expect(virtualAttributes).toStrictEqual(expectedAttributes);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('relationMappings', () => {
|
describe('relationMappings', () => {
|
||||||
it('should return correct associations', () => {
|
it('should return correct associations', () => {
|
||||||
const relationMappings = Connection.relationMappings();
|
const relationMappings = Connection.relationMappings();
|
||||||
@@ -69,12 +61,12 @@ describe('Connection model', () => {
|
|||||||
to: 'app_configs.key',
|
to: 'app_configs.key',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
appAuthClient: {
|
oauthClient: {
|
||||||
relation: Base.BelongsToOneRelation,
|
relation: Base.BelongsToOneRelation,
|
||||||
modelClass: AppAuthClient,
|
modelClass: OAuthClient,
|
||||||
join: {
|
join: {
|
||||||
from: 'connections.app_auth_client_id',
|
from: 'connections.oauth_client_id',
|
||||||
to: 'app_auth_clients.id',
|
to: 'oauth_clients.id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -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', () => {
|
describe('encryptData', () => {
|
||||||
it('should return undefined if eligibleForEncryption is not true', async () => {
|
it('should return undefined if eligibleForEncryption is not true', async () => {
|
||||||
vi.spyOn(Connection.prototype, 'eligibleForEncryption').mockReturnValue(
|
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 () => {
|
it('should throw an error when app config does not allow custom connection with formatted data', async () => {
|
||||||
vi.spyOn(Connection.prototype, 'getApp').mockResolvedValue({
|
vi.spyOn(Connection.prototype, 'getApp').mockResolvedValue({
|
||||||
name: 'gitlab',
|
name: 'gitlab',
|
||||||
@@ -373,7 +294,7 @@ describe('Connection model', () => {
|
|||||||
|
|
||||||
vi.spyOn(Connection.prototype, 'getAppConfig').mockResolvedValue({
|
vi.spyOn(Connection.prototype, 'getAppConfig').mockResolvedValue({
|
||||||
disabled: false,
|
disabled: false,
|
||||||
customConnectionAllowed: false,
|
useOnlyPredefinedAuthClients: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const connection = new Connection();
|
const connection = new Connection();
|
||||||
@@ -386,35 +307,13 @@ describe('Connection model', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error when app config is not shared with app auth client', async () => {
|
it('should apply oauth client auth defaults when creating with shared oauth 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({
|
await createAppConfig({
|
||||||
key: 'gitlab',
|
key: 'gitlab',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
customConnectionAllowed: true,
|
|
||||||
shared: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const appAuthClient = await createAppAuthClient({
|
const oauthClient = await createOAuthClient({
|
||||||
appKey: 'gitlab',
|
appKey: 'gitlab',
|
||||||
active: true,
|
active: true,
|
||||||
formattedAuthDefaults: {
|
formattedAuthDefaults: {
|
||||||
@@ -424,7 +323,7 @@ describe('Connection model', () => {
|
|||||||
|
|
||||||
const connection = await createConnection({
|
const connection = await createConnection({
|
||||||
key: 'gitlab',
|
key: 'gitlab',
|
||||||
appAuthClientId: appAuthClient.id,
|
oauthClientId: oauthClient.id,
|
||||||
formattedData: null,
|
formattedData: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -660,22 +559,22 @@ describe('Connection model', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('updateFormattedData', () => {
|
describe('updateFormattedData', () => {
|
||||||
it('should extend connection data with app auth client auth defaults', async () => {
|
it('should extend connection data with oauth client auth defaults', async () => {
|
||||||
const appAuthClient = await createAppAuthClient({
|
const oauthClient = await createOAuthClient({
|
||||||
formattedAuthDefaults: {
|
formattedAuthDefaults: {
|
||||||
clientId: 'sample-id',
|
clientId: 'sample-id',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const connection = await createConnection({
|
const connection = await createConnection({
|
||||||
appAuthClientId: appAuthClient.id,
|
oauthClientId: oauthClient.id,
|
||||||
formattedData: {
|
formattedData: {
|
||||||
token: 'sample-token',
|
token: 'sample-token',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const updatedConnection = await connection.updateFormattedData({
|
const updatedConnection = await connection.updateFormattedData({
|
||||||
appAuthClientId: appAuthClient.id,
|
oauthClientId: oauthClient.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(updatedConnection.formattedData).toStrictEqual({
|
expect(updatedConnection.formattedData).toStrictEqual({
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import ExecutionStep from './execution-step.js';
|
|||||||
import globalVariable from '../helpers/global-variable.js';
|
import globalVariable from '../helpers/global-variable.js';
|
||||||
import logger from '../helpers/logger.js';
|
import logger from '../helpers/logger.js';
|
||||||
import Telemetry from '../helpers/telemetry/index.js';
|
import Telemetry from '../helpers/telemetry/index.js';
|
||||||
|
import exportFlow from '../helpers/export-flow.js';
|
||||||
import flowQueue from '../queues/flow.js';
|
import flowQueue from '../queues/flow.js';
|
||||||
import {
|
import {
|
||||||
REMOVE_AFTER_30_DAYS_OR_150_JOBS,
|
REMOVE_AFTER_30_DAYS_OR_150_JOBS,
|
||||||
@@ -426,6 +427,10 @@ class Flow extends Base {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async export() {
|
||||||
|
return await exportFlow(this);
|
||||||
|
}
|
||||||
|
|
||||||
async $beforeUpdate(opt, queryContext) {
|
async $beforeUpdate(opt, queryContext) {
|
||||||
await super.$beforeUpdate(opt, queryContext);
|
await super.$beforeUpdate(opt, queryContext);
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { createFlow } from '../../test/factories/flow.js';
|
|||||||
import { createStep } from '../../test/factories/step.js';
|
import { createStep } from '../../test/factories/step.js';
|
||||||
import { createExecution } from '../../test/factories/execution.js';
|
import { createExecution } from '../../test/factories/execution.js';
|
||||||
import { createExecutionStep } from '../../test/factories/execution-step.js';
|
import { createExecutionStep } from '../../test/factories/execution-step.js';
|
||||||
|
import * as exportFlow from '../helpers/export-flow.js';
|
||||||
|
|
||||||
describe('Flow model', () => {
|
describe('Flow model', () => {
|
||||||
it('tableName should return correct name', () => {
|
it('tableName should return correct name', () => {
|
||||||
@@ -506,6 +507,22 @@ describe('Flow model', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('export', () => {
|
||||||
|
it('should return exportedFlow', async () => {
|
||||||
|
const flow = await createFlow();
|
||||||
|
|
||||||
|
const exportedFlowAsString = {
|
||||||
|
name: 'My Flow Name',
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.spyOn(exportFlow, 'default').mockReturnValue(exportedFlowAsString);
|
||||||
|
|
||||||
|
expect(await flow.export()).toStrictEqual({
|
||||||
|
name: 'My Flow Name',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('throwIfHavingLessThanTwoSteps', () => {
|
describe('throwIfHavingLessThanTwoSteps', () => {
|
||||||
it('should throw validation error with less than two steps', async () => {
|
it('should throw validation error with less than two steps', async () => {
|
||||||
const flow = await createFlow();
|
const flow = await createFlow();
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import appConfig from '../config/app.js';
|
|||||||
import Base from './base.js';
|
import Base from './base.js';
|
||||||
import AppConfig from './app-config.js';
|
import AppConfig from './app-config.js';
|
||||||
|
|
||||||
class AppAuthClient extends Base {
|
class OAuthClient extends Base {
|
||||||
static tableName = 'app_auth_clients';
|
static tableName = 'oauth_clients';
|
||||||
|
|
||||||
static jsonSchema = {
|
static jsonSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
@@ -27,7 +27,7 @@ class AppAuthClient extends Base {
|
|||||||
relation: Base.BelongsToOneRelation,
|
relation: Base.BelongsToOneRelation,
|
||||||
modelClass: AppConfig,
|
modelClass: AppConfig,
|
||||||
join: {
|
join: {
|
||||||
from: 'app_auth_clients.app_key',
|
from: 'oauth_clients.app_key',
|
||||||
to: 'app_configs.key',
|
to: 'app_configs.key',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -60,39 +60,26 @@ class AppAuthClient extends Base {
|
|||||||
return this.authDefaults ? true : false;
|
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
|
// TODO: Make another abstraction like beforeSave instead of using
|
||||||
// beforeInsert and beforeUpdate separately for the same operation.
|
// beforeInsert and beforeUpdate separately for the same operation.
|
||||||
async $beforeInsert(queryContext) {
|
async $beforeInsert(queryContext) {
|
||||||
await super.$beforeInsert(queryContext);
|
await super.$beforeInsert(queryContext);
|
||||||
|
|
||||||
this.encryptData();
|
this.encryptData();
|
||||||
}
|
}
|
||||||
|
|
||||||
async $afterInsert(queryContext) {
|
async $afterInsert(queryContext) {
|
||||||
await super.$afterInsert(queryContext);
|
await super.$afterInsert(queryContext);
|
||||||
|
|
||||||
await this.triggerAppConfigUpdate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async $beforeUpdate(opt, queryContext) {
|
async $beforeUpdate(opt, queryContext) {
|
||||||
await super.$beforeUpdate(opt, queryContext);
|
await super.$beforeUpdate(opt, queryContext);
|
||||||
|
|
||||||
this.encryptData();
|
this.encryptData();
|
||||||
}
|
}
|
||||||
|
|
||||||
async $afterUpdate(opt, queryContext) {
|
async $afterUpdate(opt, queryContext) {
|
||||||
await super.$afterUpdate(opt, queryContext);
|
await super.$afterUpdate(opt, queryContext);
|
||||||
|
|
||||||
await this.triggerAppConfigUpdate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async $afterFind() {
|
async $afterFind() {
|
||||||
@@ -100,4 +87,4 @@ class AppAuthClient extends Base {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AppAuthClient;
|
export default OAuthClient;
|
||||||
192
packages/backend/src/models/oauth-client.test.js
Normal file
192
packages/backend/src/models/oauth-client.test.js
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
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 OAuthClient from './oauth-client.js';
|
||||||
|
import Base from './base.js';
|
||||||
|
import appConfig from '../config/app.js';
|
||||||
|
import { createOAuthClient } from '../../test/factories/oauth-client.js';
|
||||||
|
|
||||||
|
describe('OAuthClient model', () => {
|
||||||
|
it('tableName should return correct name', () => {
|
||||||
|
expect(OAuthClient.tableName).toBe('oauth_clients');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('jsonSchema should have correct validations', () => {
|
||||||
|
expect(OAuthClient.jsonSchema).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('relationMappings should return correct associations', () => {
|
||||||
|
const relationMappings = OAuthClient.relationMappings();
|
||||||
|
|
||||||
|
const expectedRelations = {
|
||||||
|
appConfig: {
|
||||||
|
relation: Base.BelongsToOneRelation,
|
||||||
|
modelClass: AppConfig,
|
||||||
|
join: {
|
||||||
|
from: 'oauth_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(OAuthClient.prototype, 'eligibleForEncryption').mockReturnValue(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
const oauthClient = new OAuthClient();
|
||||||
|
|
||||||
|
expect(oauthClient.encryptData()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should encrypt formattedAuthDefaults and set it to authDefaults', async () => {
|
||||||
|
vi.spyOn(OAuthClient.prototype, 'eligibleForEncryption').mockReturnValue(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
const formattedAuthDefaults = {
|
||||||
|
key: 'value',
|
||||||
|
};
|
||||||
|
|
||||||
|
const oauthClient = new OAuthClient();
|
||||||
|
oauthClient.formattedAuthDefaults = formattedAuthDefaults;
|
||||||
|
oauthClient.encryptData();
|
||||||
|
|
||||||
|
const expectedDecryptedValue = JSON.parse(
|
||||||
|
AES.decrypt(oauthClient.authDefaults, appConfig.encryptionKey).toString(
|
||||||
|
enc
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(formattedAuthDefaults).toStrictEqual(expectedDecryptedValue);
|
||||||
|
expect(oauthClient.authDefaults).not.toStrictEqual(formattedAuthDefaults);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should encrypt formattedAuthDefaults and remove formattedAuthDefaults', async () => {
|
||||||
|
vi.spyOn(OAuthClient.prototype, 'eligibleForEncryption').mockReturnValue(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
const formattedAuthDefaults = {
|
||||||
|
key: 'value',
|
||||||
|
};
|
||||||
|
|
||||||
|
const oauthClient = new OAuthClient();
|
||||||
|
oauthClient.formattedAuthDefaults = formattedAuthDefaults;
|
||||||
|
oauthClient.encryptData();
|
||||||
|
|
||||||
|
expect(oauthClient.formattedAuthDefaults).not.toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('decryptData', () => {
|
||||||
|
it('should return undefined if eligibleForDecryption is not true', () => {
|
||||||
|
vi.spyOn(OAuthClient.prototype, 'eligibleForDecryption').mockReturnValue(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
const oauthClient = new OAuthClient();
|
||||||
|
|
||||||
|
expect(oauthClient.decryptData()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should decrypt authDefaults and set it to formattedAuthDefaults', async () => {
|
||||||
|
vi.spyOn(OAuthClient.prototype, 'eligibleForDecryption').mockReturnValue(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
const formattedAuthDefaults = {
|
||||||
|
key: 'value',
|
||||||
|
};
|
||||||
|
|
||||||
|
const authDefaults = AES.encrypt(
|
||||||
|
JSON.stringify(formattedAuthDefaults),
|
||||||
|
appConfig.encryptionKey
|
||||||
|
).toString();
|
||||||
|
|
||||||
|
const oauthClient = new OAuthClient();
|
||||||
|
oauthClient.authDefaults = authDefaults;
|
||||||
|
oauthClient.decryptData();
|
||||||
|
|
||||||
|
expect(oauthClient.formattedAuthDefaults).toStrictEqual(
|
||||||
|
formattedAuthDefaults
|
||||||
|
);
|
||||||
|
expect(oauthClient.authDefaults).not.toStrictEqual(formattedAuthDefaults);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('eligibleForEncryption', () => {
|
||||||
|
it('should return true when formattedAuthDefaults property exists', async () => {
|
||||||
|
const oauthClient = await createOAuthClient();
|
||||||
|
|
||||||
|
expect(oauthClient.eligibleForEncryption()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false when formattedAuthDefaults property doesn't exist", async () => {
|
||||||
|
const oauthClient = await createOAuthClient();
|
||||||
|
|
||||||
|
delete oauthClient.formattedAuthDefaults;
|
||||||
|
|
||||||
|
expect(oauthClient.eligibleForEncryption()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('eligibleForDecryption', () => {
|
||||||
|
it('should return true when authDefaults property exists', async () => {
|
||||||
|
const oauthClient = await createOAuthClient();
|
||||||
|
|
||||||
|
expect(oauthClient.eligibleForDecryption()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false when authDefaults property doesn't exist", async () => {
|
||||||
|
const oauthClient = await createOAuthClient();
|
||||||
|
|
||||||
|
delete oauthClient.authDefaults;
|
||||||
|
|
||||||
|
expect(oauthClient.eligibleForDecryption()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('$beforeInsert should call OAuthClient.encryptData', async () => {
|
||||||
|
const oauthClientBeforeInsertSpy = vi.spyOn(
|
||||||
|
OAuthClient.prototype,
|
||||||
|
'encryptData'
|
||||||
|
);
|
||||||
|
|
||||||
|
await createOAuthClient();
|
||||||
|
|
||||||
|
expect(oauthClientBeforeInsertSpy).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('$beforeUpdate should call OAuthClient.encryptData', async () => {
|
||||||
|
const oauthClient = await createOAuthClient();
|
||||||
|
|
||||||
|
const oauthClientBeforeUpdateSpy = vi.spyOn(
|
||||||
|
OAuthClient.prototype,
|
||||||
|
'encryptData'
|
||||||
|
);
|
||||||
|
|
||||||
|
await oauthClient.$query().patchAndFetch({ name: 'sample' });
|
||||||
|
|
||||||
|
expect(oauthClientBeforeUpdateSpy).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('$afterFind should call OAuthClient.decryptData', async () => {
|
||||||
|
const oauthClient = await createOAuthClient();
|
||||||
|
|
||||||
|
const oauthClientAfterFindSpy = vi.spyOn(
|
||||||
|
OAuthClient.prototype,
|
||||||
|
'decryptData'
|
||||||
|
);
|
||||||
|
|
||||||
|
await oauthClient.$query();
|
||||||
|
|
||||||
|
expect(oauthClientAfterFindSpy).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { URL } from 'node:url';
|
import { URL } from 'node:url';
|
||||||
import get from 'lodash.get';
|
|
||||||
import Base from './base.js';
|
import Base from './base.js';
|
||||||
import App from './app.js';
|
import App from './app.js';
|
||||||
import Flow from './flow.js';
|
import Flow from './flow.js';
|
||||||
@@ -22,6 +21,7 @@ class Step extends Base {
|
|||||||
id: { type: 'string', format: 'uuid' },
|
id: { type: 'string', format: 'uuid' },
|
||||||
flowId: { type: 'string', format: 'uuid' },
|
flowId: { type: 'string', format: 'uuid' },
|
||||||
key: { type: ['string', 'null'] },
|
key: { type: ['string', 'null'] },
|
||||||
|
name: { type: ['string', 'null'], minLength: 1, maxLength: 255 },
|
||||||
appKey: { type: ['string', 'null'], minLength: 1, maxLength: 255 },
|
appKey: { type: ['string', 'null'], minLength: 1, maxLength: 255 },
|
||||||
type: { type: 'string', enum: ['action', 'trigger'] },
|
type: { type: 'string', enum: ['action', 'trigger'] },
|
||||||
connectionId: { type: ['string', 'null'], format: 'uuid' },
|
connectionId: { type: ['string', 'null'], format: 'uuid' },
|
||||||
@@ -108,25 +108,10 @@ class Step extends Base {
|
|||||||
|
|
||||||
if (!triggerCommand) return null;
|
if (!triggerCommand) return null;
|
||||||
|
|
||||||
const { useSingletonWebhook, singletonWebhookRefValueParameter, type } =
|
const isWebhook = triggerCommand.type === 'webhook';
|
||||||
triggerCommand;
|
|
||||||
|
|
||||||
const isWebhook = type === 'webhook';
|
|
||||||
|
|
||||||
if (!isWebhook) return null;
|
if (!isWebhook) return null;
|
||||||
|
|
||||||
if (singletonWebhookRefValueParameter) {
|
|
||||||
const parameterValue = get(
|
|
||||||
this.parameters,
|
|
||||||
singletonWebhookRefValueParameter
|
|
||||||
);
|
|
||||||
return `/webhooks/connections/${this.connectionId}/${parameterValue}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (useSingletonWebhook) {
|
|
||||||
return `/webhooks/connections/${this.connectionId}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.parameters.workSynchronously) {
|
if (this.parameters.workSynchronously) {
|
||||||
return `/webhooks/flows/${this.flowId}/sync`;
|
return `/webhooks/flows/${this.flowId}/sync`;
|
||||||
}
|
}
|
||||||
@@ -314,7 +299,13 @@ class Step extends Base {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateFor(user, newStepData) {
|
async updateFor(user, newStepData) {
|
||||||
const { appKey = this.appKey, connectionId, key, parameters } = newStepData;
|
const {
|
||||||
|
appKey = this.appKey,
|
||||||
|
name,
|
||||||
|
connectionId,
|
||||||
|
key,
|
||||||
|
parameters,
|
||||||
|
} = newStepData;
|
||||||
|
|
||||||
if (connectionId && appKey) {
|
if (connectionId && appKey) {
|
||||||
await user.authorizedConnections
|
await user.authorizedConnections
|
||||||
@@ -335,6 +326,7 @@ class Step extends Base {
|
|||||||
|
|
||||||
const updatedStep = await this.$query().patchAndFetch({
|
const updatedStep = await this.$query().patchAndFetch({
|
||||||
key,
|
key,
|
||||||
|
name,
|
||||||
appKey,
|
appKey,
|
||||||
connectionId: connectionId,
|
connectionId: connectionId,
|
||||||
parameters: parameters,
|
parameters: parameters,
|
||||||
|
|||||||
@@ -376,7 +376,10 @@ describe('User model', () => {
|
|||||||
const anotherUserConnection = await createConnection();
|
const anotherUserConnection = await createConnection();
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await userWithRoleAndPermissions.authorizedConnections
|
await userWithRoleAndPermissions.authorizedConnections.orderBy(
|
||||||
|
'created_at',
|
||||||
|
'asc'
|
||||||
|
)
|
||||||
).toStrictEqual([userConnection, anotherUserConnection]);
|
).toStrictEqual([userConnection, anotherUserConnection]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,4 @@
|
|||||||
import process from 'process';
|
import { generateQueue } from './queue.js';
|
||||||
import { Queue } from 'bullmq';
|
|
||||||
import redisConfig from '../config/redis.js';
|
|
||||||
import logger from '../helpers/logger.js';
|
|
||||||
|
|
||||||
const CONNECTION_REFUSED = 'ECONNREFUSED';
|
|
||||||
|
|
||||||
const redisConnection = {
|
|
||||||
connection: redisConfig,
|
|
||||||
};
|
|
||||||
|
|
||||||
const actionQueue = new Queue('action', redisConnection);
|
|
||||||
|
|
||||||
actionQueue.on('error', (error) => {
|
|
||||||
if (error.code === CONNECTION_REFUSED) {
|
|
||||||
logger.error(
|
|
||||||
'Make sure you have installed Redis and it is running.',
|
|
||||||
error
|
|
||||||
);
|
|
||||||
|
|
||||||
process.exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.error('Error happened in action queue!', error);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
const actionQueue = generateQueue('action');
|
||||||
export default actionQueue;
|
export default actionQueue;
|
||||||
|
|||||||
@@ -1,27 +1,4 @@
|
|||||||
import process from 'process';
|
import { generateQueue } from './queue.js';
|
||||||
import { Queue } from 'bullmq';
|
|
||||||
import redisConfig from '../config/redis.js';
|
|
||||||
import logger from '../helpers/logger.js';
|
|
||||||
|
|
||||||
const CONNECTION_REFUSED = 'ECONNREFUSED';
|
|
||||||
|
|
||||||
const redisConnection = {
|
|
||||||
connection: redisConfig,
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteUserQueue = new Queue('delete-user', redisConnection);
|
|
||||||
|
|
||||||
deleteUserQueue.on('error', (error) => {
|
|
||||||
if (error.code === CONNECTION_REFUSED) {
|
|
||||||
logger.error(
|
|
||||||
'Make sure you have installed Redis and it is running.',
|
|
||||||
error
|
|
||||||
);
|
|
||||||
|
|
||||||
process.exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.error('Error happened in delete user queue!', error);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
const deleteUserQueue = generateQueue('delete-user');
|
||||||
export default deleteUserQueue;
|
export default deleteUserQueue;
|
||||||
|
|||||||
@@ -1,27 +1,4 @@
|
|||||||
import process from 'process';
|
import { generateQueue } from './queue.js';
|
||||||
import { Queue } from 'bullmq';
|
|
||||||
import redisConfig from '../config/redis.js';
|
|
||||||
import logger from '../helpers/logger.js';
|
|
||||||
|
|
||||||
const CONNECTION_REFUSED = 'ECONNREFUSED';
|
|
||||||
|
|
||||||
const redisConnection = {
|
|
||||||
connection: redisConfig,
|
|
||||||
};
|
|
||||||
|
|
||||||
const emailQueue = new Queue('email', redisConnection);
|
|
||||||
|
|
||||||
emailQueue.on('error', (error) => {
|
|
||||||
if (error.code === CONNECTION_REFUSED) {
|
|
||||||
logger.error(
|
|
||||||
'Make sure you have installed Redis and it is running.',
|
|
||||||
error
|
|
||||||
);
|
|
||||||
|
|
||||||
process.exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.error('Error happened in email queue!', error);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
const emailQueue = generateQueue('email');
|
||||||
export default emailQueue;
|
export default emailQueue;
|
||||||
|
|||||||
@@ -1,27 +1,4 @@
|
|||||||
import process from 'process';
|
import { generateQueue } from './queue.js';
|
||||||
import { Queue } from 'bullmq';
|
|
||||||
import redisConfig from '../config/redis.js';
|
|
||||||
import logger from '../helpers/logger.js';
|
|
||||||
|
|
||||||
const CONNECTION_REFUSED = 'ECONNREFUSED';
|
|
||||||
|
|
||||||
const redisConnection = {
|
|
||||||
connection: redisConfig,
|
|
||||||
};
|
|
||||||
|
|
||||||
const flowQueue = new Queue('flow', redisConnection);
|
|
||||||
|
|
||||||
flowQueue.on('error', (error) => {
|
|
||||||
if (error.code === CONNECTION_REFUSED) {
|
|
||||||
logger.error(
|
|
||||||
'Make sure you have installed Redis and it is running.',
|
|
||||||
error
|
|
||||||
);
|
|
||||||
|
|
||||||
process.exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.error('Error happened in flow queue!', error);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
const flowQueue = generateQueue('flow');
|
||||||
export default flowQueue;
|
export default flowQueue;
|
||||||
|
|||||||
44
packages/backend/src/queues/queue.js
Normal file
44
packages/backend/src/queues/queue.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import process from 'process';
|
||||||
|
import { Queue } from 'bullmq';
|
||||||
|
import redisConfig from '../config/redis.js';
|
||||||
|
import logger from '../helpers/logger.js';
|
||||||
|
|
||||||
|
const CONNECTION_REFUSED = 'ECONNREFUSED';
|
||||||
|
|
||||||
|
const redisConnection = {
|
||||||
|
connection: redisConfig,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateQueue = (queueName, options) => {
|
||||||
|
const queue = new Queue(queueName, redisConnection);
|
||||||
|
|
||||||
|
queue.on('error', (error) => queueOnError(error, queueName));
|
||||||
|
|
||||||
|
if (options?.runDaily) addScheduler(queueName, queue);
|
||||||
|
|
||||||
|
return queue;
|
||||||
|
};
|
||||||
|
|
||||||
|
const queueOnError = (error, queueName) => {
|
||||||
|
if (error.code === CONNECTION_REFUSED) {
|
||||||
|
const errorMessage =
|
||||||
|
'Make sure you have installed Redis and it is running.';
|
||||||
|
|
||||||
|
logger.error(errorMessage, error);
|
||||||
|
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(`Error happened in ${queueName} queue!`, error);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addScheduler = (queueName, queue) => {
|
||||||
|
const everydayAtOneOclock = '0 1 * * *';
|
||||||
|
|
||||||
|
queue.add(queueName, null, {
|
||||||
|
jobId: queueName,
|
||||||
|
repeat: {
|
||||||
|
pattern: everydayAtOneOclock,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,40 +1,8 @@
|
|||||||
import process from 'process';
|
import { generateQueue } from './queue.js';
|
||||||
import { Queue } from 'bullmq';
|
|
||||||
import redisConfig from '../config/redis.js';
|
|
||||||
import logger from '../helpers/logger.js';
|
|
||||||
|
|
||||||
const CONNECTION_REFUSED = 'ECONNREFUSED';
|
const removeCancelledSubscriptionsQueue = generateQueue(
|
||||||
|
|
||||||
const redisConnection = {
|
|
||||||
connection: redisConfig,
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeCancelledSubscriptionsQueue = new Queue(
|
|
||||||
'remove-cancelled-subscriptions',
|
'remove-cancelled-subscriptions',
|
||||||
redisConnection
|
{ runDaily: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
removeCancelledSubscriptionsQueue.on('error', (error) => {
|
|
||||||
if (error.code === CONNECTION_REFUSED) {
|
|
||||||
logger.error(
|
|
||||||
'Make sure you have installed Redis and it is running.',
|
|
||||||
error
|
|
||||||
);
|
|
||||||
|
|
||||||
process.exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.error(
|
|
||||||
'Error happened in remove cancelled subscriptions queue!',
|
|
||||||
error
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
removeCancelledSubscriptionsQueue.add('remove-cancelled-subscriptions', null, {
|
|
||||||
jobId: 'remove-cancelled-subscriptions',
|
|
||||||
repeat: {
|
|
||||||
pattern: '0 1 * * *',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default removeCancelledSubscriptionsQueue;
|
export default removeCancelledSubscriptionsQueue;
|
||||||
|
|||||||
@@ -1,27 +1,4 @@
|
|||||||
import process from 'process';
|
import { generateQueue } from './queue.js';
|
||||||
import { Queue } from 'bullmq';
|
|
||||||
import redisConfig from '../config/redis.js';
|
|
||||||
import logger from '../helpers/logger.js';
|
|
||||||
|
|
||||||
const CONNECTION_REFUSED = 'ECONNREFUSED';
|
|
||||||
|
|
||||||
const redisConnection = {
|
|
||||||
connection: redisConfig,
|
|
||||||
};
|
|
||||||
|
|
||||||
const triggerQueue = new Queue('trigger', redisConnection);
|
|
||||||
|
|
||||||
triggerQueue.on('error', (error) => {
|
|
||||||
if (error.code === CONNECTION_REFUSED) {
|
|
||||||
logger.error(
|
|
||||||
'Make sure you have installed Redis and it is running.',
|
|
||||||
error
|
|
||||||
);
|
|
||||||
|
|
||||||
process.exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.error('Error happened in trigger queue!', error);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
const triggerQueue = generateQueue('trigger');
|
||||||
export default triggerQueue;
|
export default triggerQueue;
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import { authorizeAdmin } from '../../../../helpers/authorization.js';
|
|||||||
import { checkIsEnterprise } from '../../../../helpers/check-is-enterprise.js';
|
import { checkIsEnterprise } from '../../../../helpers/check-is-enterprise.js';
|
||||||
import createConfigAction from '../../../../controllers/api/v1/admin/apps/create-config.ee.js';
|
import createConfigAction from '../../../../controllers/api/v1/admin/apps/create-config.ee.js';
|
||||||
import updateConfigAction from '../../../../controllers/api/v1/admin/apps/update-config.ee.js';
|
import updateConfigAction from '../../../../controllers/api/v1/admin/apps/update-config.ee.js';
|
||||||
import getAuthClientsAction from '../../../../controllers/api/v1/admin/apps/get-auth-clients.ee.js';
|
import getOAuthClientsAction from '../../../../controllers/api/v1/admin/apps/get-oauth-clients.ee.js';
|
||||||
import getAuthClientAction from '../../../../controllers/api/v1/admin/apps/get-auth-client.ee.js';
|
import getOAuthClientAction from '../../../../controllers/api/v1/admin/apps/get-oauth-client.ee.js';
|
||||||
import createAuthClientAction from '../../../../controllers/api/v1/admin/apps/create-auth-client.ee.js';
|
import createOAuthClientAction from '../../../../controllers/api/v1/admin/apps/create-oauth-client.ee.js';
|
||||||
import updateAuthClientAction from '../../../../controllers/api/v1/admin/apps/update-auth-client.ee.js';
|
import updateOAuthClientAction from '../../../../controllers/api/v1/admin/apps/update-oauth-client.ee.js';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -28,35 +28,35 @@ router.patch(
|
|||||||
);
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/:appKey/auth-clients',
|
'/:appKey/oauth-clients',
|
||||||
authenticateUser,
|
authenticateUser,
|
||||||
authorizeAdmin,
|
authorizeAdmin,
|
||||||
checkIsEnterprise,
|
checkIsEnterprise,
|
||||||
getAuthClientsAction
|
getOAuthClientsAction
|
||||||
);
|
);
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/:appKey/auth-clients',
|
'/:appKey/oauth-clients',
|
||||||
authenticateUser,
|
authenticateUser,
|
||||||
authorizeAdmin,
|
authorizeAdmin,
|
||||||
checkIsEnterprise,
|
checkIsEnterprise,
|
||||||
createAuthClientAction
|
createOAuthClientAction
|
||||||
);
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/:appKey/auth-clients/:appAuthClientId',
|
'/:appKey/oauth-clients/:oauthClientId',
|
||||||
authenticateUser,
|
authenticateUser,
|
||||||
authorizeAdmin,
|
authorizeAdmin,
|
||||||
checkIsEnterprise,
|
checkIsEnterprise,
|
||||||
getAuthClientAction
|
getOAuthClientAction
|
||||||
);
|
);
|
||||||
|
|
||||||
router.patch(
|
router.patch(
|
||||||
'/:appKey/auth-clients/:appAuthClientId',
|
'/:appKey/oauth-clients/:oauthClientId',
|
||||||
authenticateUser,
|
authenticateUser,
|
||||||
authorizeAdmin,
|
authorizeAdmin,
|
||||||
checkIsEnterprise,
|
checkIsEnterprise,
|
||||||
updateAuthClientAction
|
updateOAuthClientAction
|
||||||
);
|
);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import getAppsAction from '../../../controllers/api/v1/apps/get-apps.js';
|
|||||||
import getAuthAction from '../../../controllers/api/v1/apps/get-auth.js';
|
import getAuthAction from '../../../controllers/api/v1/apps/get-auth.js';
|
||||||
import getConnectionsAction from '../../../controllers/api/v1/apps/get-connections.js';
|
import getConnectionsAction from '../../../controllers/api/v1/apps/get-connections.js';
|
||||||
import getConfigAction from '../../../controllers/api/v1/apps/get-config.ee.js';
|
import getConfigAction from '../../../controllers/api/v1/apps/get-config.ee.js';
|
||||||
import getAuthClientsAction from '../../../controllers/api/v1/apps/get-auth-clients.ee.js';
|
import getOAuthClientsAction from '../../../controllers/api/v1/apps/get-oauth-clients.ee.js';
|
||||||
import getAuthClientAction from '../../../controllers/api/v1/apps/get-auth-client.ee.js';
|
import getOAuthClientAction from '../../../controllers/api/v1/apps/get-oauth-client.ee.js';
|
||||||
import getTriggersAction from '../../../controllers/api/v1/apps/get-triggers.js';
|
import getTriggersAction from '../../../controllers/api/v1/apps/get-triggers.js';
|
||||||
import getTriggerSubstepsAction from '../../../controllers/api/v1/apps/get-trigger-substeps.js';
|
import getTriggerSubstepsAction from '../../../controllers/api/v1/apps/get-trigger-substeps.js';
|
||||||
import getActionsAction from '../../../controllers/api/v1/apps/get-actions.js';
|
import getActionsAction from '../../../controllers/api/v1/apps/get-actions.js';
|
||||||
@@ -44,17 +44,17 @@ router.get(
|
|||||||
);
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/:appKey/auth-clients',
|
'/:appKey/oauth-clients',
|
||||||
authenticateUser,
|
authenticateUser,
|
||||||
checkIsEnterprise,
|
checkIsEnterprise,
|
||||||
getAuthClientsAction
|
getOAuthClientsAction
|
||||||
);
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/:appKey/auth-clients/:appAuthClientId',
|
'/:appKey/oauth-clients/:oauthClientId',
|
||||||
authenticateUser,
|
authenticateUser,
|
||||||
checkIsEnterprise,
|
checkIsEnterprise,
|
||||||
getAuthClientAction
|
getOAuthClientAction
|
||||||
);
|
);
|
||||||
|
|
||||||
router.get('/:appKey/triggers', authenticateUser, getTriggersAction);
|
router.get('/:appKey/triggers', authenticateUser, getTriggersAction);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import createFlowAction from '../../../controllers/api/v1/flows/create-flow.js';
|
|||||||
import createStepAction from '../../../controllers/api/v1/flows/create-step.js';
|
import createStepAction from '../../../controllers/api/v1/flows/create-step.js';
|
||||||
import deleteFlowAction from '../../../controllers/api/v1/flows/delete-flow.js';
|
import deleteFlowAction from '../../../controllers/api/v1/flows/delete-flow.js';
|
||||||
import duplicateFlowAction from '../../../controllers/api/v1/flows/duplicate-flow.js';
|
import duplicateFlowAction from '../../../controllers/api/v1/flows/duplicate-flow.js';
|
||||||
|
import exportFlowAction from '../../../controllers/api/v1/flows/export-flow.js';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -17,6 +18,13 @@ router.get('/:flowId', authenticateUser, authorizeUser, getFlowAction);
|
|||||||
router.post('/', authenticateUser, authorizeUser, createFlowAction);
|
router.post('/', authenticateUser, authorizeUser, createFlowAction);
|
||||||
router.patch('/:flowId', authenticateUser, authorizeUser, updateFlowAction);
|
router.patch('/:flowId', authenticateUser, authorizeUser, updateFlowAction);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/:flowId/export',
|
||||||
|
authenticateUser,
|
||||||
|
authorizeUser,
|
||||||
|
exportFlowAction
|
||||||
|
);
|
||||||
|
|
||||||
router.patch(
|
router.patch(
|
||||||
'/:flowId/status',
|
'/:flowId/status',
|
||||||
authenticateUser,
|
authenticateUser,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import multer from 'multer';
|
|||||||
import appConfig from '../config/app.js';
|
import appConfig from '../config/app.js';
|
||||||
import webhookHandlerByFlowId from '../controllers/webhooks/handler-by-flow-id.js';
|
import webhookHandlerByFlowId from '../controllers/webhooks/handler-by-flow-id.js';
|
||||||
import webhookHandlerSyncByFlowId from '../controllers/webhooks/handler-sync-by-flow-id.js';
|
import webhookHandlerSyncByFlowId from '../controllers/webhooks/handler-sync-by-flow-id.js';
|
||||||
import webhookHandlerByConnectionIdAndRefValue from '../controllers/webhooks/handler-by-connection-id-and-ref-value.js';
|
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
const upload = multer();
|
const upload = multer();
|
||||||
@@ -39,14 +38,6 @@ function createRouteHandler(path, handler) {
|
|||||||
.post(wrappedHandler);
|
.post(wrappedHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
createRouteHandler(
|
|
||||||
'/connections/:connectionId/:refValue',
|
|
||||||
webhookHandlerByConnectionIdAndRefValue
|
|
||||||
);
|
|
||||||
createRouteHandler(
|
|
||||||
'/connections/:connectionId',
|
|
||||||
webhookHandlerByConnectionIdAndRefValue
|
|
||||||
);
|
|
||||||
createRouteHandler('/flows/:flowId/sync', webhookHandlerSyncByFlowId);
|
createRouteHandler('/flows/:flowId/sync', webhookHandlerSyncByFlowId);
|
||||||
createRouteHandler('/flows/:flowId', webhookHandlerByFlowId);
|
createRouteHandler('/flows/:flowId', webhookHandlerByFlowId);
|
||||||
createRouteHandler('/:flowId', webhookHandlerByFlowId);
|
createRouteHandler('/:flowId', webhookHandlerByFlowId);
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
const appAuthClientSerializer = (appAuthClient) => {
|
|
||||||
return {
|
|
||||||
id: appAuthClient.id,
|
|
||||||
appConfigId: appAuthClient.appConfigId,
|
|
||||||
name: appAuthClient.name,
|
|
||||||
active: appAuthClient.active,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default appAuthClientSerializer;
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { describe, it, expect, beforeEach } from 'vitest';
|
|
||||||
import { createAppAuthClient } from '../../test/factories/app-auth-client';
|
|
||||||
import appAuthClientSerializer from './app-auth-client';
|
|
||||||
|
|
||||||
describe('appAuthClient serializer', () => {
|
|
||||||
let appAuthClient;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
appAuthClient = await createAppAuthClient();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return app auth client data', async () => {
|
|
||||||
const expectedPayload = {
|
|
||||||
id: appAuthClient.id,
|
|
||||||
appConfigId: appAuthClient.appConfigId,
|
|
||||||
name: appAuthClient.name,
|
|
||||||
active: appAuthClient.active,
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(appAuthClientSerializer(appAuthClient)).toStrictEqual(
|
|
||||||
expectedPayload
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
const appConfigSerializer = (appConfig) => {
|
const appConfigSerializer = (appConfig) => {
|
||||||
return {
|
return {
|
||||||
key: appConfig.key,
|
key: appConfig.key,
|
||||||
customConnectionAllowed: appConfig.customConnectionAllowed,
|
useOnlyPredefinedAuthClients: appConfig.useOnlyPredefinedAuthClients,
|
||||||
shared: appConfig.shared,
|
|
||||||
disabled: appConfig.disabled,
|
disabled: appConfig.disabled,
|
||||||
connectionAllowed: appConfig.connectionAllowed,
|
|
||||||
createdAt: appConfig.createdAt.getTime(),
|
createdAt: appConfig.createdAt.getTime(),
|
||||||
updatedAt: appConfig.updatedAt.getTime(),
|
updatedAt: appConfig.updatedAt.getTime(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,10 +12,8 @@ describe('appConfig serializer', () => {
|
|||||||
it('should return app config data', async () => {
|
it('should return app config data', async () => {
|
||||||
const expectedPayload = {
|
const expectedPayload = {
|
||||||
key: appConfig.key,
|
key: appConfig.key,
|
||||||
customConnectionAllowed: appConfig.customConnectionAllowed,
|
useOnlyPredefinedAuthClients: appConfig.useOnlyPredefinedAuthClients,
|
||||||
shared: appConfig.shared,
|
|
||||||
disabled: appConfig.disabled,
|
disabled: appConfig.disabled,
|
||||||
connectionAllowed: appConfig.connectionAllowed,
|
|
||||||
createdAt: appConfig.createdAt.getTime(),
|
createdAt: appConfig.createdAt.getTime(),
|
||||||
updatedAt: appConfig.updatedAt.getTime(),
|
updatedAt: appConfig.updatedAt.getTime(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const appSerializer = (app) => {
|
|||||||
primaryColor: app.primaryColor,
|
primaryColor: app.primaryColor,
|
||||||
authDocUrl: app.authDocUrl,
|
authDocUrl: app.authDocUrl,
|
||||||
supportsConnections: app.supportsConnections,
|
supportsConnections: app.supportsConnections,
|
||||||
|
supportsOauthClients: app?.auth?.generateAuthUrl ? true : false,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (app.connectionCount) {
|
if (app.connectionCount) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ describe('appSerializer', () => {
|
|||||||
iconUrl: app.iconUrl,
|
iconUrl: app.iconUrl,
|
||||||
authDocUrl: app.authDocUrl,
|
authDocUrl: app.authDocUrl,
|
||||||
supportsConnections: app.supportsConnections,
|
supportsConnections: app.supportsConnections,
|
||||||
|
supportsOauthClients: app.auth.generateAuthUrl ? true : false,
|
||||||
primaryColor: app.primaryColor,
|
primaryColor: app.primaryColor,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ const authSerializer = (auth) => {
|
|||||||
return {
|
return {
|
||||||
fields: auth.fields,
|
fields: auth.fields,
|
||||||
authenticationSteps: auth.authenticationSteps,
|
authenticationSteps: auth.authenticationSteps,
|
||||||
|
sharedAuthenticationSteps: auth.sharedAuthenticationSteps,
|
||||||
reconnectionSteps: auth.reconnectionSteps,
|
reconnectionSteps: auth.reconnectionSteps,
|
||||||
|
sharedReconnectionSteps: auth.sharedReconnectionSteps,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ describe('authSerializer', () => {
|
|||||||
fields: auth.fields,
|
fields: auth.fields,
|
||||||
authenticationSteps: auth.authenticationSteps,
|
authenticationSteps: auth.authenticationSteps,
|
||||||
reconnectionSteps: auth.reconnectionSteps,
|
reconnectionSteps: auth.reconnectionSteps,
|
||||||
|
sharedAuthenticationSteps: auth.sharedAuthenticationSteps,
|
||||||
|
sharedReconnectionSteps: auth.sharedReconnectionSteps,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(authSerializer(auth)).toStrictEqual(expectedPayload);
|
expect(authSerializer(auth)).toStrictEqual(expectedPayload);
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ const connectionSerializer = (connection) => {
|
|||||||
return {
|
return {
|
||||||
id: connection.id,
|
id: connection.id,
|
||||||
key: connection.key,
|
key: connection.key,
|
||||||
reconnectable: connection.reconnectable,
|
oauthClientId: connection.oauthClientId,
|
||||||
appAuthClientId: connection.appAuthClientId,
|
|
||||||
formattedData: {
|
formattedData: {
|
||||||
screenName: connection.formattedData.screenName,
|
screenName: connection.formattedData.screenName,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ describe('connectionSerializer', () => {
|
|||||||
const expectedPayload = {
|
const expectedPayload = {
|
||||||
id: connection.id,
|
id: connection.id,
|
||||||
key: connection.key,
|
key: connection.key,
|
||||||
reconnectable: connection.reconnectable,
|
oauthClientId: connection.oauthClientId,
|
||||||
appAuthClientId: connection.appAuthClientId,
|
|
||||||
formattedData: {
|
formattedData: {
|
||||||
screenName: connection.formattedData.screenName,
|
screenName: connection.formattedData.screenName,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ import permissionSerializer from './permission.js';
|
|||||||
import adminSamlAuthProviderSerializer from './admin-saml-auth-provider.ee.js';
|
import adminSamlAuthProviderSerializer from './admin-saml-auth-provider.ee.js';
|
||||||
import samlAuthProviderSerializer from './saml-auth-provider.ee.js';
|
import samlAuthProviderSerializer from './saml-auth-provider.ee.js';
|
||||||
import samlAuthProviderRoleMappingSerializer from './role-mapping.ee.js';
|
import samlAuthProviderRoleMappingSerializer from './role-mapping.ee.js';
|
||||||
import appAuthClientSerializer from './app-auth-client.js';
|
import oauthClientSerializer from './oauth-client.js';
|
||||||
import appConfigSerializer from './app-config.js';
|
import appConfigSerializer from './app-config.js';
|
||||||
import flowSerializer from './flow.js';
|
import flowSerializer from './flow.js';
|
||||||
import stepSerializer from './step.js';
|
import stepSerializer from './step.js';
|
||||||
import connectionSerializer from './connection.js';
|
import connectionSerializer from './connection.js';
|
||||||
import appSerializer from './app.js';
|
import appSerializer from './app.js';
|
||||||
|
import userAppSerializer from './user-app.js';
|
||||||
import authSerializer from './auth.js';
|
import authSerializer from './auth.js';
|
||||||
import triggerSerializer from './trigger.js';
|
import triggerSerializer from './trigger.js';
|
||||||
import actionSerializer from './action.js';
|
import actionSerializer from './action.js';
|
||||||
@@ -27,12 +28,13 @@ const serializers = {
|
|||||||
AdminSamlAuthProvider: adminSamlAuthProviderSerializer,
|
AdminSamlAuthProvider: adminSamlAuthProviderSerializer,
|
||||||
SamlAuthProvider: samlAuthProviderSerializer,
|
SamlAuthProvider: samlAuthProviderSerializer,
|
||||||
RoleMapping: samlAuthProviderRoleMappingSerializer,
|
RoleMapping: samlAuthProviderRoleMappingSerializer,
|
||||||
AppAuthClient: appAuthClientSerializer,
|
OAuthClient: oauthClientSerializer,
|
||||||
AppConfig: appConfigSerializer,
|
AppConfig: appConfigSerializer,
|
||||||
Flow: flowSerializer,
|
Flow: flowSerializer,
|
||||||
Step: stepSerializer,
|
Step: stepSerializer,
|
||||||
Connection: connectionSerializer,
|
Connection: connectionSerializer,
|
||||||
App: appSerializer,
|
App: appSerializer,
|
||||||
|
UserApp: userAppSerializer,
|
||||||
Auth: authSerializer,
|
Auth: authSerializer,
|
||||||
Trigger: triggerSerializer,
|
Trigger: triggerSerializer,
|
||||||
Action: actionSerializer,
|
Action: actionSerializer,
|
||||||
|
|||||||
10
packages/backend/src/serializers/oauth-client.js
Normal file
10
packages/backend/src/serializers/oauth-client.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
const oauthClientSerializer = (oauthClient) => {
|
||||||
|
return {
|
||||||
|
id: oauthClient.id,
|
||||||
|
appConfigId: oauthClient.appConfigId,
|
||||||
|
name: oauthClient.name,
|
||||||
|
active: oauthClient.active,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default oauthClientSerializer;
|
||||||
22
packages/backend/src/serializers/oauth-client.test.js
Normal file
22
packages/backend/src/serializers/oauth-client.test.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import { createOAuthClient } from '../../test/factories/oauth-client';
|
||||||
|
import oauthClientSerializer from './oauth-client';
|
||||||
|
|
||||||
|
describe('oauthClient serializer', () => {
|
||||||
|
let oauthClient;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
oauthClient = await createOAuthClient();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return oauth client data', async () => {
|
||||||
|
const expectedPayload = {
|
||||||
|
id: oauthClient.id,
|
||||||
|
appConfigId: oauthClient.appConfigId,
|
||||||
|
name: oauthClient.name,
|
||||||
|
active: oauthClient.active,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(oauthClientSerializer(oauthClient)).toStrictEqual(expectedPayload);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -5,6 +5,7 @@ const stepSerializer = (step) => {
|
|||||||
id: step.id,
|
id: step.id,
|
||||||
type: step.type,
|
type: step.type,
|
||||||
key: step.key,
|
key: step.key,
|
||||||
|
name: step.name,
|
||||||
appKey: step.appKey,
|
appKey: step.appKey,
|
||||||
iconUrl: step.iconUrl,
|
iconUrl: step.iconUrl,
|
||||||
webhookUrl: step.webhookUrl,
|
webhookUrl: step.webhookUrl,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ describe('stepSerializer', () => {
|
|||||||
id: step.id,
|
id: step.id,
|
||||||
type: step.type,
|
type: step.type,
|
||||||
key: step.key,
|
key: step.key,
|
||||||
|
name: step.name,
|
||||||
appKey: step.appKey,
|
appKey: step.appKey,
|
||||||
iconUrl: step.iconUrl,
|
iconUrl: step.iconUrl,
|
||||||
webhookUrl: step.webhookUrl,
|
webhookUrl: step.webhookUrl,
|
||||||
|
|||||||
22
packages/backend/src/serializers/user-app.js
Normal file
22
packages/backend/src/serializers/user-app.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
const userAppSerializer = (userApp) => {
|
||||||
|
let appData = {
|
||||||
|
key: userApp.key,
|
||||||
|
name: userApp.name,
|
||||||
|
iconUrl: userApp.iconUrl,
|
||||||
|
primaryColor: userApp.primaryColor,
|
||||||
|
authDocUrl: userApp.authDocUrl,
|
||||||
|
supportsConnections: userApp.supportsConnections,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (userApp.connectionCount) {
|
||||||
|
appData.connectionCount = userApp.connectionCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userApp.flowCount) {
|
||||||
|
appData.flowCount = userApp.flowCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return appData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default userAppSerializer;
|
||||||
@@ -1,76 +1,6 @@
|
|||||||
import { Worker } from 'bullmq';
|
import { generateWorker } from './worker.js';
|
||||||
|
import { executeActionJob } from '../jobs/execute-action.js';
|
||||||
|
|
||||||
import * as Sentry from '../helpers/sentry.ee.js';
|
const actionWorker = generateWorker('action', executeActionJob);
|
||||||
import redisConfig from '../config/redis.js';
|
|
||||||
import logger from '../helpers/logger.js';
|
|
||||||
import Step from '../models/step.js';
|
|
||||||
import actionQueue from '../queues/action.js';
|
|
||||||
import { processAction } from '../services/action.js';
|
|
||||||
import {
|
|
||||||
REMOVE_AFTER_30_DAYS_OR_150_JOBS,
|
|
||||||
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
|
|
||||||
} from '../helpers/remove-job-configuration.js';
|
|
||||||
import delayAsMilliseconds from '../helpers/delay-as-milliseconds.js';
|
|
||||||
|
|
||||||
const DEFAULT_DELAY_DURATION = 0;
|
|
||||||
|
|
||||||
const actionWorker = new Worker(
|
|
||||||
'action',
|
|
||||||
async (job) => {
|
|
||||||
const { stepId, flowId, executionId, computedParameters, executionStep } =
|
|
||||||
await processAction(job.data);
|
|
||||||
|
|
||||||
if (executionStep.isFailed) return;
|
|
||||||
|
|
||||||
const step = await Step.query().findById(stepId).throwIfNotFound();
|
|
||||||
const nextStep = await step.getNextStep();
|
|
||||||
|
|
||||||
if (!nextStep) return;
|
|
||||||
|
|
||||||
const jobName = `${executionId}-${nextStep.id}`;
|
|
||||||
|
|
||||||
const jobPayload = {
|
|
||||||
flowId,
|
|
||||||
executionId,
|
|
||||||
stepId: nextStep.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
const jobOptions = {
|
|
||||||
removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS,
|
|
||||||
removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS,
|
|
||||||
delay: DEFAULT_DELAY_DURATION,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (step.appKey === 'delay') {
|
|
||||||
jobOptions.delay = delayAsMilliseconds(step.key, computedParameters);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (step.appKey === 'filter' && !executionStep.dataOut) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await actionQueue.add(jobName, jobPayload, jobOptions);
|
|
||||||
},
|
|
||||||
{ connection: redisConfig }
|
|
||||||
);
|
|
||||||
|
|
||||||
actionWorker.on('completed', (job) => {
|
|
||||||
logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`);
|
|
||||||
});
|
|
||||||
|
|
||||||
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}
|
|
||||||
`;
|
|
||||||
|
|
||||||
logger.error(errorMessage);
|
|
||||||
|
|
||||||
Sentry.captureException(err, {
|
|
||||||
extra: {
|
|
||||||
jobId: job.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export default actionWorker;
|
export default actionWorker;
|
||||||
|
|||||||
@@ -1,69 +1,6 @@
|
|||||||
import { Worker } from 'bullmq';
|
import { generateWorker } from './worker.js';
|
||||||
|
import { deleteUserJob } from '../jobs/delete-user.ee.js';
|
||||||
|
|
||||||
import * as Sentry from '../helpers/sentry.ee.js';
|
const deleteUserWorker = generateWorker('delete-user', deleteUserJob);
|
||||||
import redisConfig from '../config/redis.js';
|
|
||||||
import logger from '../helpers/logger.js';
|
|
||||||
import appConfig from '../config/app.js';
|
|
||||||
import User from '../models/user.js';
|
|
||||||
import ExecutionStep from '../models/execution-step.js';
|
|
||||||
|
|
||||||
const deleteUserWorker = new Worker(
|
|
||||||
'delete-user',
|
|
||||||
async (job) => {
|
|
||||||
const { id } = job.data;
|
|
||||||
|
|
||||||
const user = await User.query()
|
|
||||||
.withSoftDeleted()
|
|
||||||
.findById(id)
|
|
||||||
.throwIfNotFound();
|
|
||||||
|
|
||||||
const executionIds = (
|
|
||||||
await user
|
|
||||||
.$relatedQuery('executions')
|
|
||||||
.withSoftDeleted()
|
|
||||||
.select('executions.id')
|
|
||||||
).map((execution) => execution.id);
|
|
||||||
|
|
||||||
await ExecutionStep.query()
|
|
||||||
.withSoftDeleted()
|
|
||||||
.whereIn('execution_id', executionIds)
|
|
||||||
.hardDelete();
|
|
||||||
await user.$relatedQuery('executions').withSoftDeleted().hardDelete();
|
|
||||||
await user.$relatedQuery('steps').withSoftDeleted().hardDelete();
|
|
||||||
await user.$relatedQuery('flows').withSoftDeleted().hardDelete();
|
|
||||||
await user.$relatedQuery('connections').withSoftDeleted().hardDelete();
|
|
||||||
await user.$relatedQuery('identities').withSoftDeleted().hardDelete();
|
|
||||||
|
|
||||||
if (appConfig.isCloud) {
|
|
||||||
await user.$relatedQuery('subscriptions').withSoftDeleted().hardDelete();
|
|
||||||
await user.$relatedQuery('usageData').withSoftDeleted().hardDelete();
|
|
||||||
}
|
|
||||||
|
|
||||||
await user.$relatedQuery('accessTokens').withSoftDeleted().hardDelete();
|
|
||||||
await user.$query().withSoftDeleted().hardDelete();
|
|
||||||
},
|
|
||||||
{ connection: redisConfig }
|
|
||||||
);
|
|
||||||
|
|
||||||
deleteUserWorker.on('completed', (job) => {
|
|
||||||
logger.info(
|
|
||||||
`JOB ID: ${job.id} - The user with the ID of '${job.data.id}' has been deleted!`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
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}
|
|
||||||
`;
|
|
||||||
|
|
||||||
logger.error(errorMessage);
|
|
||||||
|
|
||||||
Sentry.captureException(err, {
|
|
||||||
extra: {
|
|
||||||
jobId: job.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export default deleteUserWorker;
|
export default deleteUserWorker;
|
||||||
|
|||||||
@@ -1,62 +1,6 @@
|
|||||||
import { Worker } from 'bullmq';
|
import { generateWorker } from './worker.js';
|
||||||
|
import { sendEmailJob } from '../jobs/send-email.js';
|
||||||
|
|
||||||
import * as Sentry from '../helpers/sentry.ee.js';
|
const emailWorker = generateWorker('email', sendEmailJob);
|
||||||
import redisConfig from '../config/redis.js';
|
|
||||||
import logger from '../helpers/logger.js';
|
|
||||||
import mailer from '../helpers/mailer.ee.js';
|
|
||||||
import compileEmail from '../helpers/compile-email.ee.js';
|
|
||||||
import appConfig from '../config/app.js';
|
|
||||||
|
|
||||||
const isCloudSandbox = () => {
|
|
||||||
return appConfig.isCloud && !appConfig.isProd;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isAutomatischEmail = (email) => {
|
|
||||||
return email.endsWith('@automatisch.io');
|
|
||||||
};
|
|
||||||
|
|
||||||
const emailWorker = new Worker(
|
|
||||||
'email',
|
|
||||||
async (job) => {
|
|
||||||
const { email, subject, template, params } = job.data;
|
|
||||||
|
|
||||||
if (isCloudSandbox() && !isAutomatischEmail(email)) {
|
|
||||||
logger.info(
|
|
||||||
'Only Automatisch emails are allowed for non-production environments!'
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await mailer.sendMail({
|
|
||||||
to: email,
|
|
||||||
from: appConfig.fromEmail,
|
|
||||||
subject: subject,
|
|
||||||
html: compileEmail(template, params),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ connection: redisConfig }
|
|
||||||
);
|
|
||||||
|
|
||||||
emailWorker.on('completed', (job) => {
|
|
||||||
logger.info(
|
|
||||||
`JOB ID: ${job.id} - ${job.data.subject} email sent to ${job.data.email}!`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
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}
|
|
||||||
`;
|
|
||||||
|
|
||||||
logger.error(errorMessage);
|
|
||||||
|
|
||||||
Sentry.captureException(err, {
|
|
||||||
extra: {
|
|
||||||
jobId: job.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export default emailWorker;
|
export default emailWorker;
|
||||||
|
|||||||
@@ -1,97 +1,6 @@
|
|||||||
import { Worker } from 'bullmq';
|
import { generateWorker } from './worker.js';
|
||||||
|
import { executeFlowJob } from '../jobs/execute-flow.js';
|
||||||
|
|
||||||
import * as Sentry from '../helpers/sentry.ee.js';
|
const flowWorker = generateWorker('flow', executeFlowJob);
|
||||||
import redisConfig from '../config/redis.js';
|
|
||||||
import logger from '../helpers/logger.js';
|
|
||||||
import flowQueue from '../queues/flow.js';
|
|
||||||
import triggerQueue from '../queues/trigger.js';
|
|
||||||
import { processFlow } from '../services/flow.js';
|
|
||||||
import Flow from '../models/flow.js';
|
|
||||||
import {
|
|
||||||
REMOVE_AFTER_30_DAYS_OR_150_JOBS,
|
|
||||||
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
|
|
||||||
} from '../helpers/remove-job-configuration.js';
|
|
||||||
|
|
||||||
const flowWorker = new Worker(
|
|
||||||
'flow',
|
|
||||||
async (job) => {
|
|
||||||
const { flowId } = job.data;
|
|
||||||
|
|
||||||
const flow = await Flow.query().findById(flowId).throwIfNotFound();
|
|
||||||
const user = await flow.$relatedQuery('user');
|
|
||||||
const allowedToRunFlows = await user.isAllowedToRunFlows();
|
|
||||||
|
|
||||||
if (!allowedToRunFlows) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const triggerStep = await flow.getTriggerStep();
|
|
||||||
|
|
||||||
const { data, error } = await processFlow({ flowId });
|
|
||||||
|
|
||||||
const reversedData = data.reverse();
|
|
||||||
|
|
||||||
const jobOptions = {
|
|
||||||
removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS,
|
|
||||||
removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS,
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const triggerItem of reversedData) {
|
|
||||||
const jobName = `${triggerStep.id}-${triggerItem.meta.internalId}`;
|
|
||||||
|
|
||||||
const jobPayload = {
|
|
||||||
flowId,
|
|
||||||
stepId: triggerStep.id,
|
|
||||||
triggerItem,
|
|
||||||
};
|
|
||||||
|
|
||||||
await triggerQueue.add(jobName, jobPayload, jobOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
const jobName = `${triggerStep.id}-error`;
|
|
||||||
|
|
||||||
const jobPayload = {
|
|
||||||
flowId,
|
|
||||||
stepId: triggerStep.id,
|
|
||||||
error,
|
|
||||||
};
|
|
||||||
|
|
||||||
await triggerQueue.add(jobName, jobPayload, jobOptions);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ connection: redisConfig }
|
|
||||||
);
|
|
||||||
|
|
||||||
flowWorker.on('completed', (job) => {
|
|
||||||
logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`);
|
|
||||||
});
|
|
||||||
|
|
||||||
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}
|
|
||||||
`;
|
|
||||||
|
|
||||||
logger.error(errorMessage);
|
|
||||||
|
|
||||||
const flow = await Flow.query().findById(job.data.flowId);
|
|
||||||
|
|
||||||
if (!flow) {
|
|
||||||
await flowQueue.removeRepeatableByKey(job.repeatJobKey);
|
|
||||||
|
|
||||||
const flowNotFoundErrorMessage = `
|
|
||||||
JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has been deleted from Redis because flow was not found!
|
|
||||||
`;
|
|
||||||
|
|
||||||
logger.error(flowNotFoundErrorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
Sentry.captureException(err, {
|
|
||||||
extra: {
|
|
||||||
jobId: job.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export default flowWorker;
|
export default flowWorker;
|
||||||
|
|||||||
@@ -1,44 +1,9 @@
|
|||||||
import { Worker } from 'bullmq';
|
import { generateWorker } from './worker.js';
|
||||||
import { DateTime } from 'luxon';
|
import { removeCancelledSubscriptionsJob } from '../jobs/remove-cancelled-subscriptions.ee.js';
|
||||||
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';
|
|
||||||
|
|
||||||
const removeCancelledSubscriptionsWorker = new Worker(
|
const removeCancelledSubscriptionsWorker = generateWorker(
|
||||||
'remove-cancelled-subscriptions',
|
'remove-cancelled-subscriptions',
|
||||||
async () => {
|
removeCancelledSubscriptionsJob
|
||||||
await Subscription.query()
|
|
||||||
.delete()
|
|
||||||
.where({
|
|
||||||
status: 'deleted',
|
|
||||||
})
|
|
||||||
.andWhere(
|
|
||||||
'cancellation_effective_date',
|
|
||||||
'<=',
|
|
||||||
DateTime.now().startOf('day').toISODate()
|
|
||||||
);
|
|
||||||
},
|
|
||||||
{ connection: redisConfig }
|
|
||||||
);
|
);
|
||||||
|
|
||||||
removeCancelledSubscriptionsWorker.on('completed', (job) => {
|
|
||||||
logger.info(
|
|
||||||
`JOB ID: ${job.id} - The cancelled subscriptions have been removed!`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
removeCancelledSubscriptionsWorker.on('failed', (job, err) => {
|
|
||||||
const errorMessage = `
|
|
||||||
JOB ID: ${job.id} - ERROR: The cancelled subscriptions can not be removed! ${err.message}
|
|
||||||
\n ${err.stack}
|
|
||||||
`;
|
|
||||||
logger.error(errorMessage);
|
|
||||||
Sentry.captureException(err, {
|
|
||||||
extra: {
|
|
||||||
jobId: job.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export default removeCancelledSubscriptionsWorker;
|
export default removeCancelledSubscriptionsWorker;
|
||||||
|
|||||||
@@ -1,62 +1,6 @@
|
|||||||
import { Worker } from 'bullmq';
|
import { generateWorker } from './worker.js';
|
||||||
|
import { executeTriggerJob } from '../jobs/execute-trigger.js';
|
||||||
|
|
||||||
import * as Sentry from '../helpers/sentry.ee.js';
|
const triggerWorker = generateWorker('flow', executeTriggerJob);
|
||||||
import redisConfig from '../config/redis.js';
|
|
||||||
import logger from '../helpers/logger.js';
|
|
||||||
import actionQueue from '../queues/action.js';
|
|
||||||
import Step from '../models/step.js';
|
|
||||||
import { processTrigger } from '../services/trigger.js';
|
|
||||||
import {
|
|
||||||
REMOVE_AFTER_30_DAYS_OR_150_JOBS,
|
|
||||||
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
|
|
||||||
} from '../helpers/remove-job-configuration.js';
|
|
||||||
|
|
||||||
const triggerWorker = new Worker(
|
|
||||||
'trigger',
|
|
||||||
async (job) => {
|
|
||||||
const { flowId, executionId, stepId, executionStep } = await processTrigger(
|
|
||||||
job.data
|
|
||||||
);
|
|
||||||
|
|
||||||
if (executionStep.isFailed) return;
|
|
||||||
|
|
||||||
const step = await Step.query().findById(stepId).throwIfNotFound();
|
|
||||||
const nextStep = await step.getNextStep();
|
|
||||||
const jobName = `${executionId}-${nextStep.id}`;
|
|
||||||
|
|
||||||
const jobPayload = {
|
|
||||||
flowId,
|
|
||||||
executionId,
|
|
||||||
stepId: nextStep.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
const jobOptions = {
|
|
||||||
removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS,
|
|
||||||
removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS,
|
|
||||||
};
|
|
||||||
|
|
||||||
await actionQueue.add(jobName, jobPayload, jobOptions);
|
|
||||||
},
|
|
||||||
{ connection: redisConfig }
|
|
||||||
);
|
|
||||||
|
|
||||||
triggerWorker.on('completed', (job) => {
|
|
||||||
logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`);
|
|
||||||
});
|
|
||||||
|
|
||||||
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}
|
|
||||||
`;
|
|
||||||
|
|
||||||
logger.error(errorMessage);
|
|
||||||
|
|
||||||
Sentry.captureException(err, {
|
|
||||||
extra: {
|
|
||||||
jobId: job.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export default triggerWorker;
|
export default triggerWorker;
|
||||||
|
|||||||
28
packages/backend/src/workers/worker.js
Normal file
28
packages/backend/src/workers/worker.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { Worker } from 'bullmq';
|
||||||
|
|
||||||
|
import * as Sentry from '../helpers/sentry.ee.js';
|
||||||
|
import redisConfig from '../config/redis.js';
|
||||||
|
import logger from '../helpers/logger.js';
|
||||||
|
|
||||||
|
export const generateWorker = (workerName, job) => {
|
||||||
|
const worker = new Worker(workerName, job, { connection: redisConfig });
|
||||||
|
|
||||||
|
worker.on('completed', (job) => {
|
||||||
|
logger.info(`JOB ID: ${job.id} - has been successfully completed!`);
|
||||||
|
});
|
||||||
|
|
||||||
|
worker.on('failed', (job, err) => {
|
||||||
|
logger.error(`
|
||||||
|
JOB ID: ${job.id} - has failed to be completed! ${err.message}
|
||||||
|
\n ${err.stack}
|
||||||
|
`);
|
||||||
|
|
||||||
|
Sentry.captureException(err, {
|
||||||
|
extra: {
|
||||||
|
jobId: job.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return worker;
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import AppAuthClient from '../../src/models/app-auth-client';
|
import OAuthClient from '../../src/models/oauth-client';
|
||||||
|
|
||||||
const formattedAuthDefaults = {
|
const formattedAuthDefaults = {
|
||||||
oAuthRedirectUrl: faker.internet.url(),
|
oAuthRedirectUrl: faker.internet.url(),
|
||||||
@@ -8,14 +8,14 @@ const formattedAuthDefaults = {
|
|||||||
clientSecret: faker.string.uuid(),
|
clientSecret: faker.string.uuid(),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createAppAuthClient = async (params = {}) => {
|
export const createOAuthClient = async (params = {}) => {
|
||||||
params.name = params?.name || faker.person.fullName();
|
params.name = params?.name || faker.person.fullName();
|
||||||
params.appKey = params?.appKey || 'deepl';
|
params.appKey = params?.appKey || 'deepl';
|
||||||
params.active = params?.active ?? true;
|
params.active = params?.active ?? true;
|
||||||
params.formattedAuthDefaults =
|
params.formattedAuthDefaults =
|
||||||
params?.formattedAuthDefaults || formattedAuthDefaults;
|
params?.formattedAuthDefaults || formattedAuthDefaults;
|
||||||
|
|
||||||
const appAuthClient = await AppAuthClient.query().insertAndFetch(params);
|
const oauthClient = await OAuthClient.query().insertAndFetch(params);
|
||||||
|
|
||||||
return appAuthClient;
|
return oauthClient;
|
||||||
};
|
};
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
const createAppAuthClientMock = (appAuthClient) => {
|
|
||||||
return {
|
|
||||||
data: {
|
|
||||||
name: appAuthClient.name,
|
|
||||||
active: appAuthClient.active,
|
|
||||||
},
|
|
||||||
meta: {
|
|
||||||
count: 1,
|
|
||||||
currentPage: null,
|
|
||||||
isArray: false,
|
|
||||||
totalPages: null,
|
|
||||||
type: 'AppAuthClient',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default createAppAuthClientMock;
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user