Merge branch 'main' into AUT-1372
This commit is contained in:
2
.github/workflows/backend.yml
vendored
2
.github/workflows/backend.yml
vendored
@@ -47,5 +47,5 @@ jobs:
|
||||
run: cp .env-example.test .env.test
|
||||
working-directory: packages/backend
|
||||
- name: Run tests
|
||||
run: yarn test
|
||||
run: yarn test:coverage
|
||||
working-directory: packages/backend
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"pretest": "APP_ENV=test node ./test/setup/prepare-test-env.js",
|
||||
"test": "APP_ENV=test vitest run",
|
||||
"test:watch": "APP_ENV=test vitest watch",
|
||||
"test:coverage": "yarn test --coverage",
|
||||
"lint": "eslint .",
|
||||
"db:create": "node ./bin/database/create.js",
|
||||
"db:seed:user": "node ./bin/database/seed-user.js",
|
||||
@@ -97,10 +98,11 @@
|
||||
"url": "https://github.com/automatisch/automatisch/issues"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitest/coverage-v8": "^2.1.5",
|
||||
"node-gyp": "^10.1.0",
|
||||
"nodemon": "^2.0.13",
|
||||
"supertest": "^6.3.3",
|
||||
"vitest": "^1.1.3"
|
||||
"vitest": "^2.1.5"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -10,12 +10,11 @@ export default async (request, response) => {
|
||||
};
|
||||
|
||||
const appConfigParams = (request) => {
|
||||
const { customConnectionAllowed, shared, disabled } = request.body;
|
||||
const { useOnlyPredefinedAuthClients, disabled } = request.body;
|
||||
|
||||
return {
|
||||
key: request.params.appKey,
|
||||
customConnectionAllowed,
|
||||
shared,
|
||||
useOnlyPredefinedAuthClients,
|
||||
disabled,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -23,8 +23,7 @@ describe('POST /api/v1/admin/apps/:appKey/config', () => {
|
||||
|
||||
it('should return created app config', async () => {
|
||||
const appConfig = {
|
||||
customConnectionAllowed: true,
|
||||
shared: true,
|
||||
useOnlyPredefinedAuthClients: false,
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
@@ -38,14 +37,14 @@ describe('POST /api/v1/admin/apps/:appKey/config', () => {
|
||||
...appConfig,
|
||||
key: 'gitlab',
|
||||
});
|
||||
|
||||
expect(response.body).toMatchObject(expectedPayload);
|
||||
});
|
||||
|
||||
it('should return HTTP 422 for already existing app config', async () => {
|
||||
const appConfig = {
|
||||
key: 'gitlab',
|
||||
customConnectionAllowed: true,
|
||||
shared: true,
|
||||
useOnlyPredefinedAuthClients: false,
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
|
||||
@@ -6,14 +6,14 @@ export default async (request, response) => {
|
||||
.findOne({ key: request.params.appKey })
|
||||
.throwIfNotFound();
|
||||
|
||||
const appAuthClient = await appConfig
|
||||
.$relatedQuery('appAuthClients')
|
||||
.insert(appAuthClientParams(request));
|
||||
const oauthClient = await appConfig
|
||||
.$relatedQuery('oauthClients')
|
||||
.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;
|
||||
|
||||
return {
|
||||
@@ -5,11 +5,11 @@ import app from '../../../../../app.js';
|
||||
import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js';
|
||||
import { createUser } from '../../../../../../test/factories/user.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 * 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;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -26,7 +26,7 @@ describe('POST /api/v1/admin/apps/:appKey/auth-clients', () => {
|
||||
key: 'gitlab',
|
||||
});
|
||||
|
||||
const appAuthClient = {
|
||||
const oauthClient = {
|
||||
active: true,
|
||||
appKey: 'gitlab',
|
||||
name: 'First auth client',
|
||||
@@ -39,17 +39,17 @@ describe('POST /api/v1/admin/apps/:appKey/auth-clients', () => {
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/v1/admin/apps/gitlab/auth-clients')
|
||||
.post('/api/v1/admin/apps/gitlab/oauth-clients')
|
||||
.set('Authorization', token)
|
||||
.send(appAuthClient)
|
||||
.send(oauthClient)
|
||||
.expect(201);
|
||||
|
||||
const expectedPayload = createAppAuthClientMock(appAuthClient);
|
||||
const expectedPayload = createOAuthClientMock(oauthClient);
|
||||
expect(response.body).toMatchObject(expectedPayload);
|
||||
});
|
||||
|
||||
it('should return not found response for not existing app config', async () => {
|
||||
const appAuthClient = {
|
||||
const oauthClient = {
|
||||
active: true,
|
||||
appKey: 'gitlab',
|
||||
name: 'First auth client',
|
||||
@@ -62,9 +62,9 @@ describe('POST /api/v1/admin/apps/:appKey/auth-clients', () => {
|
||||
};
|
||||
|
||||
await request(app)
|
||||
.post('/api/v1/admin/apps/gitlab/auth-clients')
|
||||
.post('/api/v1/admin/apps/gitlab/oauth-clients')
|
||||
.set('Authorization', token)
|
||||
.send(appAuthClient)
|
||||
.send(oauthClient)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
@@ -73,14 +73,14 @@ describe('POST /api/v1/admin/apps/:appKey/auth-clients', () => {
|
||||
key: 'gitlab',
|
||||
});
|
||||
|
||||
const appAuthClient = {
|
||||
const oauthClient = {
|
||||
appKey: 'gitlab',
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/v1/admin/apps/gitlab/auth-clients')
|
||||
.post('/api/v1/admin/apps/gitlab/oauth-clients')
|
||||
.set('Authorization', token)
|
||||
.send(appAuthClient)
|
||||
.send(oauthClient)
|
||||
.expect(422);
|
||||
|
||||
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 { createUser } from '../../../../../../test/factories/user.js';
|
||||
import { createRole } from '../../../../../../test/factories/role.js';
|
||||
import getAppAuthClientMock from '../../../../../../test/mocks/rest/api/v1/admin/apps/get-auth-client.js';
|
||||
import { createAppAuthClient } from '../../../../../../test/factories/app-auth-client.js';
|
||||
import getOAuthClientMock from '../../../../../../test/mocks/rest/api/v1/admin/apps/get-oauth-client.js';
|
||||
import { createOAuthClient } from '../../../../../../test/factories/oauth-client.js';
|
||||
import * as license from '../../../../../helpers/license.ee.js';
|
||||
|
||||
describe('GET /api/v1/admin/apps/:appKey/auth-clients/:appAuthClientId', () => {
|
||||
let currentUser, adminRole, currentAppAuthClient, token;
|
||||
describe('GET /api/v1/admin/apps/:appKey/oauth-clients/:oauthClientId', () => {
|
||||
let currentUser, adminRole, currentOAuthClient, token;
|
||||
|
||||
beforeEach(async () => {
|
||||
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' });
|
||||
currentUser = await createUser({ roleId: adminRole.id });
|
||||
|
||||
currentAppAuthClient = await createAppAuthClient({
|
||||
currentOAuthClient = await createOAuthClient({
|
||||
appKey: 'deepl',
|
||||
});
|
||||
|
||||
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)
|
||||
.get(`/api/v1/admin/apps/deepl/auth-clients/${currentAppAuthClient.id}`)
|
||||
.get(`/api/v1/admin/apps/deepl/oauth-clients/${currentOAuthClient.id}`)
|
||||
.set('Authorization', token)
|
||||
.expect(200);
|
||||
|
||||
const expectedPayload = getAppAuthClientMock(currentAppAuthClient);
|
||||
const expectedPayload = getOAuthClientMock(currentOAuthClient);
|
||||
expect(response.body).toStrictEqual(expectedPayload);
|
||||
});
|
||||
|
||||
it('should return not found response for not existing app auth client ID', async () => {
|
||||
const notExistingAppAuthClientUUID = Crypto.randomUUID();
|
||||
it('should return not found response for not existing oauth client ID', async () => {
|
||||
const notExistingOAuthClientUUID = Crypto.randomUUID();
|
||||
|
||||
await request(app)
|
||||
.get(
|
||||
`/api/v1/admin/apps/deepl/auth-clients/${notExistingAppAuthClientUUID}`
|
||||
`/api/v1/admin/apps/deepl/oauth-clients/${notExistingOAuthClientUUID}`
|
||||
)
|
||||
.set('Authorization', token)
|
||||
.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 () => {
|
||||
await request(app)
|
||||
.get('/api/v1/admin/apps/deepl/auth-clients/invalidAppAuthClientUUID')
|
||||
.get('/api/v1/admin/apps/deepl/oauth-clients/invalidOAuthClientUUID')
|
||||
.set('Authorization', token)
|
||||
.expect(400);
|
||||
});
|
||||
@@ -1,10 +1,10 @@
|
||||
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) => {
|
||||
const appAuthClients = await AppAuthClient.query()
|
||||
const oauthClients = await OAuthClient.query()
|
||||
.where({ app_key: request.params.appKey })
|
||||
.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 { createUser } from '../../../../../../test/factories/user.js';
|
||||
import { createRole } from '../../../../../../test/factories/role.js';
|
||||
import getAuthClientsMock from '../../../../../../test/mocks/rest/api/v1/admin/apps/get-auth-clients.js';
|
||||
import { createAppAuthClient } from '../../../../../../test/factories/app-auth-client.js';
|
||||
import getAdminOAuthClientsMock from '../../../../../../test/mocks/rest/api/v1/admin/apps/get-oauth-clients.js';
|
||||
import { createOAuthClient } from '../../../../../../test/factories/oauth-client.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;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -20,23 +20,23 @@ describe('GET /api/v1/admin/apps/:appKey/auth-clients', () => {
|
||||
token = await createAuthTokenByUserId(currentUser.id);
|
||||
});
|
||||
|
||||
it('should return specified app auth client info', async () => {
|
||||
const appAuthClientOne = await createAppAuthClient({
|
||||
it('should return specified oauth client info', async () => {
|
||||
const oauthClientOne = await createOAuthClient({
|
||||
appKey: 'deepl',
|
||||
});
|
||||
|
||||
const appAuthClientTwo = await createAppAuthClient({
|
||||
const oauthClientTwo = await createOAuthClient({
|
||||
appKey: 'deepl',
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/v1/admin/apps/deepl/auth-clients')
|
||||
.get('/api/v1/admin/apps/deepl/oauth-clients')
|
||||
.set('Authorization', token)
|
||||
.expect(200);
|
||||
|
||||
const expectedPayload = getAuthClientsMock([
|
||||
appAuthClientTwo,
|
||||
appAuthClientOne,
|
||||
const expectedPayload = getAdminOAuthClientsMock([
|
||||
oauthClientTwo,
|
||||
oauthClientOne,
|
||||
]);
|
||||
|
||||
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 { customConnectionAllowed, shared, disabled } = request.body;
|
||||
const { useOnlyPredefinedAuthClients, disabled } = request.body;
|
||||
|
||||
return {
|
||||
customConnectionAllowed,
|
||||
shared,
|
||||
useOnlyPredefinedAuthClients,
|
||||
disabled,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -24,17 +24,15 @@ describe('PATCH /api/v1/admin/apps/:appKey/config', () => {
|
||||
it('should return updated app config', async () => {
|
||||
const appConfig = {
|
||||
key: 'gitlab',
|
||||
customConnectionAllowed: true,
|
||||
shared: true,
|
||||
useOnlyPredefinedAuthClients: true,
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
await createAppConfig(appConfig);
|
||||
|
||||
const newAppConfigValues = {
|
||||
shared: false,
|
||||
disabled: true,
|
||||
customConnectionAllowed: false,
|
||||
useOnlyPredefinedAuthClients: false,
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
@@ -53,9 +51,8 @@ describe('PATCH /api/v1/admin/apps/:appKey/config', () => {
|
||||
|
||||
it('should return not found response for unexisting app config', async () => {
|
||||
const appConfig = {
|
||||
shared: false,
|
||||
disabled: true,
|
||||
customConnectionAllowed: false,
|
||||
useOnlyPredefinedAuthClients: false,
|
||||
};
|
||||
|
||||
await request(app)
|
||||
@@ -68,8 +65,7 @@ describe('PATCH /api/v1/admin/apps/:appKey/config', () => {
|
||||
it('should return HTTP 422 for invalid app config data', async () => {
|
||||
const appConfig = {
|
||||
key: 'gitlab',
|
||||
customConnectionAllowed: true,
|
||||
shared: true,
|
||||
useOnlyPredefinedAuthClients: true,
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
|
||||
@@ -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 { createUser } from '../../../../../../test/factories/user.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 { createAppAuthClient } from '../../../../../../test/factories/app-auth-client.js';
|
||||
import { createOAuthClient } from '../../../../../../test/factories/oauth-client.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;
|
||||
|
||||
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 () => {
|
||||
const appAuthClient = {
|
||||
it('should return updated entity for valid oauth client', async () => {
|
||||
const oauthClient = {
|
||||
active: true,
|
||||
appKey: 'gitlab',
|
||||
formattedAuthDefaults: {
|
||||
@@ -39,33 +39,33 @@ describe('PATCH /api/v1/admin/apps/:appKey/auth-clients', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const existingAppAuthClient = await createAppAuthClient({
|
||||
const existingOAuthClient = await createOAuthClient({
|
||||
appKey: 'gitlab',
|
||||
name: 'First auth client',
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.patch(
|
||||
`/api/v1/admin/apps/gitlab/auth-clients/${existingAppAuthClient.id}`
|
||||
`/api/v1/admin/apps/gitlab/oauth-clients/${existingOAuthClient.id}`
|
||||
)
|
||||
.set('Authorization', token)
|
||||
.send(appAuthClient)
|
||||
.send(oauthClient)
|
||||
.expect(200);
|
||||
|
||||
const expectedPayload = updateAppAuthClientMock({
|
||||
...existingAppAuthClient,
|
||||
...appAuthClient,
|
||||
const expectedPayload = updateOAuthClientMock({
|
||||
...existingOAuthClient,
|
||||
...oauthClient,
|
||||
});
|
||||
|
||||
expect(response.body).toMatchObject(expectedPayload);
|
||||
});
|
||||
|
||||
it('should return not found response for not existing app auth client', async () => {
|
||||
const notExistingAppAuthClientId = Crypto.randomUUID();
|
||||
it('should return not found response for not existing oauth client', async () => {
|
||||
const notExistingOAuthClientId = Crypto.randomUUID();
|
||||
|
||||
await request(app)
|
||||
.patch(
|
||||
`/api/v1/admin/apps/gitlab/auth-clients/${notExistingAppAuthClientId}`
|
||||
`/api/v1/admin/apps/gitlab/oauth-clients/${notExistingOAuthClientId}`
|
||||
)
|
||||
.set('Authorization', token)
|
||||
.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 () => {
|
||||
await request(app)
|
||||
.patch('/api/v1/admin/apps/gitlab/auth-clients/invalidAuthClientUUID')
|
||||
.patch('/api/v1/admin/apps/gitlab/oauth-clients/invalidAuthClientUUID')
|
||||
.set('Authorization', token)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('should return HTTP 422 for invalid payload', async () => {
|
||||
const appAuthClient = {
|
||||
const oauthClient = {
|
||||
formattedAuthDefaults: 'invalid input',
|
||||
};
|
||||
|
||||
const existingAppAuthClient = await createAppAuthClient({
|
||||
const existingOAuthClient = await createOAuthClient({
|
||||
appKey: 'gitlab',
|
||||
name: 'First auth client',
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.patch(
|
||||
`/api/v1/admin/apps/gitlab/auth-clients/${existingAppAuthClient.id}`
|
||||
`/api/v1/admin/apps/gitlab/oauth-clients/${existingOAuthClient.id}`
|
||||
)
|
||||
.set('Authorization', token)
|
||||
.send(appAuthClient)
|
||||
.send(oauthClient)
|
||||
.expect(422);
|
||||
|
||||
expect(response.body.meta.type).toBe('ModelValidation');
|
||||
@@ -7,7 +7,7 @@ export default async (request, response) => {
|
||||
.throwIfNotFound();
|
||||
|
||||
const roleMappings = await samlAuthProvider
|
||||
.$relatedQuery('samlAuthProvidersRoleMappings')
|
||||
.$relatedQuery('roleMappings')
|
||||
.orderBy('remote_role_name', 'asc');
|
||||
|
||||
renderObject(response, roleMappings);
|
||||
|
||||
@@ -8,15 +8,14 @@ export default async (request, response) => {
|
||||
.findById(samlAuthProviderId)
|
||||
.throwIfNotFound();
|
||||
|
||||
const samlAuthProvidersRoleMappings =
|
||||
await samlAuthProvider.updateRoleMappings(
|
||||
samlAuthProvidersRoleMappingsParams(request)
|
||||
);
|
||||
const roleMappings = await samlAuthProvider.updateRoleMappings(
|
||||
roleMappingsParams(request)
|
||||
);
|
||||
|
||||
renderObject(response, samlAuthProvidersRoleMappings);
|
||||
renderObject(response, roleMappings);
|
||||
};
|
||||
|
||||
const samlAuthProvidersRoleMappingsParams = (request) => {
|
||||
const roleMappingsParams = (request) => {
|
||||
const roleMappings = request.body;
|
||||
|
||||
return roleMappings.map(({ roleId, remoteRoleName }) => ({
|
||||
|
||||
@@ -6,7 +6,7 @@ import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by
|
||||
import { createRole } from '../../../../../../test/factories/role.js';
|
||||
import { createUser } from '../../../../../../test/factories/user.js';
|
||||
import { createSamlAuthProvider } from '../../../../../../test/factories/saml-auth-provider.ee.js';
|
||||
import { createSamlAuthProvidersRoleMapping } from '../../../../../../test/factories/saml-auth-providers-role-mapping.js';
|
||||
import { createRoleMapping } from '../../../../../../test/factories/role-mapping.js';
|
||||
import createRoleMappingsMock from '../../../../../../test/mocks/rest/api/v1/admin/saml-auth-providers/update-role-mappings.ee.js';
|
||||
import * as license from '../../../../../helpers/license.ee.js';
|
||||
|
||||
@@ -21,12 +21,12 @@ describe('PATCH /api/v1/admin/saml-auth-providers/:samlAuthProviderId/role-mappi
|
||||
|
||||
samlAuthProvider = await createSamlAuthProvider();
|
||||
|
||||
await createSamlAuthProvidersRoleMapping({
|
||||
await createRoleMapping({
|
||||
samlAuthProviderId: samlAuthProvider.id,
|
||||
remoteRoleName: 'Viewer',
|
||||
});
|
||||
|
||||
await createSamlAuthProvidersRoleMapping({
|
||||
await createRoleMapping({
|
||||
samlAuthProviderId: samlAuthProvider.id,
|
||||
remoteRoleName: 'Editor',
|
||||
});
|
||||
@@ -64,7 +64,7 @@ describe('PATCH /api/v1/admin/saml-auth-providers/:samlAuthProviderId/role-mappi
|
||||
|
||||
it('should delete role mappings when given empty role mappings', async () => {
|
||||
const existingRoleMappings = await samlAuthProvider.$relatedQuery(
|
||||
'samlAuthProvidersRoleMappings'
|
||||
'roleMappings'
|
||||
);
|
||||
|
||||
expect(existingRoleMappings.length).toBe(2);
|
||||
@@ -149,34 +149,4 @@ describe('PATCH /api/v1/admin/saml-auth-providers/:samlAuthProviderId/role-mappi
|
||||
.send(roleMappings)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it('should not delete existing role mapping when error thrown', async () => {
|
||||
const roleMappings = [
|
||||
{
|
||||
roleId: userRole.id,
|
||||
remoteRoleName: {
|
||||
invalid: 'data',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const roleMappingsBeforeRequest = await samlAuthProvider.$relatedQuery(
|
||||
'samlAuthProvidersRoleMappings'
|
||||
);
|
||||
|
||||
await request(app)
|
||||
.patch(
|
||||
`/api/v1/admin/saml-auth-providers/${samlAuthProvider.id}/role-mappings`
|
||||
)
|
||||
.set('Authorization', token)
|
||||
.send(roleMappings)
|
||||
.expect(422);
|
||||
|
||||
const roleMappingsAfterRequest = await samlAuthProvider.$relatedQuery(
|
||||
'samlAuthProvidersRoleMappings'
|
||||
);
|
||||
|
||||
expect(roleMappingsBeforeRequest).toStrictEqual(roleMappingsAfterRequest);
|
||||
expect(roleMappingsAfterRequest.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,18 +9,18 @@ export default async (request, response) => {
|
||||
.$query()
|
||||
.withGraphFetched({
|
||||
appConfig: true,
|
||||
appAuthClient: true,
|
||||
oauthClient: true,
|
||||
});
|
||||
|
||||
renderObject(response, connectionWithAppConfigAndAuthClient, { status: 201 });
|
||||
};
|
||||
|
||||
const connectionParams = (request) => {
|
||||
const { appAuthClientId, formattedData } = request.body;
|
||||
const { oauthClientId, formattedData } = request.body;
|
||||
|
||||
return {
|
||||
key: request.params.appKey,
|
||||
appAuthClientId,
|
||||
oauthClientId,
|
||||
formattedData,
|
||||
verified: false,
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import request from 'supertest';
|
||||
import app from '../../../../app.js';
|
||||
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.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 { createPermission } from '../../../../../test/factories/permission.js';
|
||||
import { createRole } from '../../../../../test/factories/role.js';
|
||||
@@ -155,7 +155,7 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
|
||||
await createAppConfig({
|
||||
key: 'gitlab',
|
||||
disabled: false,
|
||||
customConnectionAllowed: true,
|
||||
useOnlyPredefinedAuthClients: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -218,7 +218,7 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
|
||||
await createAppConfig({
|
||||
key: 'gitlab',
|
||||
disabled: false,
|
||||
customConnectionAllowed: false,
|
||||
useOnlyPredefinedAuthClients: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -266,17 +266,17 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('with auth clients enabled', async () => {
|
||||
let appAuthClient;
|
||||
describe('with auth client enabled', async () => {
|
||||
let oauthClient;
|
||||
|
||||
beforeEach(async () => {
|
||||
await createAppConfig({
|
||||
key: 'gitlab',
|
||||
disabled: false,
|
||||
shared: true,
|
||||
useOnlyPredefinedAuthClients: false,
|
||||
});
|
||||
|
||||
appAuthClient = await createAppAuthClient({
|
||||
oauthClient = await createOAuthClient({
|
||||
appKey: 'gitlab',
|
||||
active: true,
|
||||
formattedAuthDefaults: {
|
||||
@@ -290,7 +290,7 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
|
||||
|
||||
it('should return created connection', async () => {
|
||||
const connectionData = {
|
||||
appAuthClientId: appAuthClient.id,
|
||||
oauthClientId: oauthClient.id,
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
@@ -310,19 +310,6 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
|
||||
expect(response.body).toStrictEqual(expectedPayload);
|
||||
});
|
||||
|
||||
it('should return not authorized response for appAuthClientId and formattedData together', async () => {
|
||||
const connectionData = {
|
||||
appAuthClientId: appAuthClient.id,
|
||||
formattedData: {},
|
||||
};
|
||||
|
||||
await request(app)
|
||||
.post('/api/v1/apps/gitlab/connections')
|
||||
.set('Authorization', token)
|
||||
.send(connectionData)
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
it('should return not found response for invalid app key', async () => {
|
||||
await request(app)
|
||||
.post('/api/v1/apps/invalid-app-key/connections')
|
||||
@@ -349,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 () => {
|
||||
await createAppConfig({
|
||||
key: 'gitlab',
|
||||
disabled: false,
|
||||
shared: false,
|
||||
useOnlyPredefinedAuthClients: false,
|
||||
});
|
||||
|
||||
appAuthClient = await createAppAuthClient({
|
||||
oauthClient = await createOAuthClient({
|
||||
appKey: 'gitlab',
|
||||
active: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return with not authorized response', async () => {
|
||||
const connectionData = {
|
||||
appAuthClientId: appAuthClient.id,
|
||||
oauthClientId: oauthClient.id,
|
||||
};
|
||||
|
||||
await request(app)
|
||||
.post('/api/v1/apps/gitlab/connections')
|
||||
.set('Authorization', token)
|
||||
.send(connectionData)
|
||||
.expect(403);
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it('should return not found response for invalid app key', async () => {
|
||||
|
||||
@@ -15,7 +15,7 @@ describe('GET /api/v1/apps/:appKey/actions/:actionKey/substeps', () => {
|
||||
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 exampleAction = actions.find(
|
||||
(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) => {
|
||||
const appConfig = await AppConfig.query()
|
||||
.withGraphFetched({
|
||||
appAuthClients: true,
|
||||
oauthClients: true,
|
||||
})
|
||||
.findOne({
|
||||
key: request.params.appKey,
|
||||
|
||||
@@ -17,8 +17,7 @@ describe('GET /api/v1/apps/:appKey/config', () => {
|
||||
|
||||
appConfig = await createAppConfig({
|
||||
key: 'deepl',
|
||||
customConnectionAllowed: true,
|
||||
shared: true,
|
||||
useOnlyPredefinedAuthClients: false,
|
||||
disabled: false,
|
||||
});
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ export default async (request, response) => {
|
||||
.select('connections.*')
|
||||
.withGraphFetched({
|
||||
appConfig: true,
|
||||
appAuthClient: true,
|
||||
oauthClient: true,
|
||||
})
|
||||
.fullOuterJoinRelated('steps')
|
||||
.where({
|
||||
|
||||
@@ -87,14 +87,14 @@ describe('GET /api/v1/apps/:appKey/connections', () => {
|
||||
|
||||
it('should return not found response for invalid connection UUID', async () => {
|
||||
await createPermission({
|
||||
action: 'update',
|
||||
action: 'read',
|
||||
subject: 'Connection',
|
||||
roleId: currentUserRole.id,
|
||||
conditions: ['isCreator'],
|
||||
});
|
||||
|
||||
await request(app)
|
||||
.get('/api/v1/connections/invalid-connection-id/connections')
|
||||
.get('/api/v1/apps/invalid-connection-id/connections')
|
||||
.set('Authorization', token)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
@@ -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 createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js';
|
||||
import { createUser } from '../../../../../test/factories/user.js';
|
||||
import getAppAuthClientMock from '../../../../../test/mocks/rest/api/v1/apps/get-auth-client.js';
|
||||
import { createAppAuthClient } from '../../../../../test/factories/app-auth-client.js';
|
||||
import getOAuthClientMock from '../../../../../test/mocks/rest/api/v1/apps/get-oauth-client.js';
|
||||
import { createOAuthClient } from '../../../../../test/factories/oauth-client.js';
|
||||
import * as license from '../../../../helpers/license.ee.js';
|
||||
|
||||
describe('GET /api/v1/apps/:appKey/auth-clients/:appAuthClientId', () => {
|
||||
let currentUser, currentAppAuthClient, token;
|
||||
describe('GET /api/v1/apps/:appKey/oauth-clients/:oauthClientId', () => {
|
||||
let currentUser, currentOAuthClient, token;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
|
||||
|
||||
currentUser = await createUser();
|
||||
currentAppAuthClient = await createAppAuthClient({
|
||||
currentOAuthClient = await createOAuthClient({
|
||||
appKey: 'deepl',
|
||||
});
|
||||
|
||||
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)
|
||||
.get(`/api/v1/apps/deepl/auth-clients/${currentAppAuthClient.id}`)
|
||||
.get(`/api/v1/apps/deepl/oauth-clients/${currentOAuthClient.id}`)
|
||||
.set('Authorization', token)
|
||||
.expect(200);
|
||||
|
||||
const expectedPayload = getAppAuthClientMock(currentAppAuthClient);
|
||||
const expectedPayload = getOAuthClientMock(currentOAuthClient);
|
||||
expect(response.body).toStrictEqual(expectedPayload);
|
||||
});
|
||||
|
||||
it('should return not found response for not existing app auth client ID', async () => {
|
||||
const notExistingAppAuthClientUUID = Crypto.randomUUID();
|
||||
it('should return not found response for not existing oauth client ID', async () => {
|
||||
const notExistingOAuthClientUUID = Crypto.randomUUID();
|
||||
|
||||
await request(app)
|
||||
.get(`/api/v1/apps/deepl/auth-clients/${notExistingAppAuthClientUUID}`)
|
||||
.get(`/api/v1/apps/deepl/oauth-clients/${notExistingOAuthClientUUID}`)
|
||||
.set('Authorization', token)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it('should return bad request response for invalid UUID', async () => {
|
||||
await request(app)
|
||||
.get('/api/v1/apps/deepl/auth-clients/invalidAppAuthClientUUID')
|
||||
.get('/api/v1/apps/deepl/oauth-clients/invalidOAuthClientUUID')
|
||||
.set('Authorization', token)
|
||||
.expect(400);
|
||||
});
|
||||
@@ -1,10 +1,10 @@
|
||||
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) => {
|
||||
const appAuthClients = await AppAuthClient.query()
|
||||
const oauthClients = await OAuthClient.query()
|
||||
.where({ app_key: request.params.appKey, active: true })
|
||||
.orderBy('created_at', 'desc');
|
||||
|
||||
renderObject(response, appAuthClients);
|
||||
renderObject(response, oauthClients);
|
||||
};
|
||||
@@ -3,11 +3,11 @@ import request from 'supertest';
|
||||
import app from '../../../../app.js';
|
||||
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js';
|
||||
import { createUser } from '../../../../../test/factories/user.js';
|
||||
import getAuthClientsMock from '../../../../../test/mocks/rest/api/v1/apps/get-auth-clients.js';
|
||||
import { createAppAuthClient } from '../../../../../test/factories/app-auth-client.js';
|
||||
import getOAuthClientsMock from '../../../../../test/mocks/rest/api/v1/apps/get-oauth-clients.js';
|
||||
import { createOAuthClient } from '../../../../../test/factories/oauth-client.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;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -18,23 +18,23 @@ describe('GET /api/v1/apps/:appKey/auth-clients', () => {
|
||||
token = await createAuthTokenByUserId(currentUser.id);
|
||||
});
|
||||
|
||||
it('should return specified app auth client info', async () => {
|
||||
const appAuthClientOne = await createAppAuthClient({
|
||||
it('should return specified oauth client info', async () => {
|
||||
const oauthClientOne = await createOAuthClient({
|
||||
appKey: 'deepl',
|
||||
});
|
||||
|
||||
const appAuthClientTwo = await createAppAuthClient({
|
||||
const oauthClientTwo = await createOAuthClient({
|
||||
appKey: 'deepl',
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/v1/apps/deepl/auth-clients')
|
||||
.get('/api/v1/apps/deepl/oauth-clients')
|
||||
.set('Authorization', token)
|
||||
.expect(200);
|
||||
|
||||
const expectedPayload = getAuthClientsMock([
|
||||
appAuthClientTwo,
|
||||
appAuthClientOne,
|
||||
const expectedPayload = getOAuthClientsMock([
|
||||
oauthClientTwo,
|
||||
oauthClientOne,
|
||||
]);
|
||||
|
||||
expect(response.body).toStrictEqual(expectedPayload);
|
||||
@@ -15,7 +15,7 @@ describe('GET /api/v1/apps/:appKey/triggers/:triggerKey/substeps', () => {
|
||||
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 exampleTrigger = triggers.find(
|
||||
(trigger) => trigger.key === 'newIssues'
|
||||
|
||||
@@ -47,7 +47,6 @@ describe('POST /api/v1/connections/:connectionId/reset', () => {
|
||||
|
||||
const expectedPayload = resetConnectionMock({
|
||||
...refetchedCurrentUserConnection,
|
||||
reconnectable: refetchedCurrentUserConnection.reconnectable,
|
||||
formattedData: {
|
||||
screenName: 'Connection name',
|
||||
},
|
||||
|
||||
@@ -14,6 +14,6 @@ export default async (request, response) => {
|
||||
};
|
||||
|
||||
const connectionParams = (request) => {
|
||||
const { formattedData, appAuthClientId } = request.body;
|
||||
return { formattedData, appAuthClientId };
|
||||
const { formattedData, oauthClientId } = request.body;
|
||||
return { formattedData, oauthClientId };
|
||||
};
|
||||
|
||||
@@ -55,10 +55,9 @@ describe('PATCH /api/v1/connections/:connectionId', () => {
|
||||
|
||||
const refetchedCurrentUserConnection = await currentUserConnection.$query();
|
||||
|
||||
const expectedPayload = updateConnectionMock({
|
||||
...refetchedCurrentUserConnection,
|
||||
reconnectable: refetchedCurrentUserConnection.reconnectable,
|
||||
});
|
||||
const expectedPayload = updateConnectionMock(
|
||||
refetchedCurrentUserConnection
|
||||
);
|
||||
|
||||
expect(response.body).toStrictEqual(expectedPayload);
|
||||
});
|
||||
|
||||
@@ -193,7 +193,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
|
||||
const notExistingStepUUID = Crypto.randomUUID();
|
||||
|
||||
await request(app)
|
||||
.get(`/api/v1/steps/${notExistingStepUUID}/dynamic-data`)
|
||||
.post(`/api/v1/steps/${notExistingStepUUID}/dynamic-data`)
|
||||
.set('Authorization', token)
|
||||
.expect(404);
|
||||
});
|
||||
@@ -216,7 +216,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
|
||||
const step = await createStep({ appKey: null });
|
||||
|
||||
await request(app)
|
||||
.get(`/api/v1/steps/${step.id}/dynamic-data`)
|
||||
.post(`/api/v1/steps/${step.id}/dynamic-data`)
|
||||
.set('Authorization', token)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
@@ -118,7 +118,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-fields', () => {
|
||||
const notExistingStepUUID = Crypto.randomUUID();
|
||||
|
||||
await request(app)
|
||||
.get(`/api/v1/steps/${notExistingStepUUID}/dynamic-fields`)
|
||||
.post(`/api/v1/steps/${notExistingStepUUID}/dynamic-fields`)
|
||||
.set('Authorization', token)
|
||||
.expect(404);
|
||||
});
|
||||
@@ -138,10 +138,11 @@ describe('POST /api/v1/steps/:stepId/dynamic-fields', () => {
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
const step = await createStep({ appKey: null });
|
||||
const step = await createStep();
|
||||
await step.$query().patch({ appKey: null });
|
||||
|
||||
await request(app)
|
||||
.get(`/api/v1/steps/${step.id}/dynamic-fields`)
|
||||
.post(`/api/v1/steps/${step.id}/dynamic-fields`)
|
||||
.set('Authorization', token)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
@@ -3,5 +3,5 @@ import { renderObject } from '../../../../helpers/renderer.js';
|
||||
export default async (request, response) => {
|
||||
const apps = await request.currentUser.getApps(request.query.name);
|
||||
|
||||
renderObject(response, apps, { serializer: 'App' });
|
||||
renderObject(response, apps, { serializer: 'UserApp' });
|
||||
};
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
export async function up(knex) {
|
||||
await knex.schema.createTable('role_mappings', (table) => {
|
||||
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
||||
table
|
||||
.uuid('saml_auth_provider_id')
|
||||
.references('id')
|
||||
.inTable('saml_auth_providers');
|
||||
table.uuid('role_id').references('id').inTable('roles');
|
||||
table.string('remote_role_name').notNullable();
|
||||
|
||||
table.unique(['saml_auth_provider_id', 'remote_role_name']);
|
||||
|
||||
table.timestamps(true, true);
|
||||
});
|
||||
|
||||
const existingRoleMappings = await knex('saml_auth_providers_role_mappings');
|
||||
|
||||
if (existingRoleMappings.length) {
|
||||
await knex('role_mappings').insert(existingRoleMappings);
|
||||
}
|
||||
|
||||
return await knex.schema.dropTable('saml_auth_providers_role_mappings');
|
||||
}
|
||||
|
||||
export async function down(knex) {
|
||||
await knex.schema.createTable(
|
||||
'saml_auth_providers_role_mappings',
|
||||
(table) => {
|
||||
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
||||
table
|
||||
.uuid('saml_auth_provider_id')
|
||||
.references('id')
|
||||
.inTable('saml_auth_providers');
|
||||
table.uuid('role_id').references('id').inTable('roles');
|
||||
table.string('remote_role_name').notNullable();
|
||||
|
||||
table.unique(['saml_auth_provider_id', 'remote_role_name']);
|
||||
|
||||
table.timestamps(true, true);
|
||||
}
|
||||
);
|
||||
|
||||
const existingRoleMappings = await knex('role_mappings');
|
||||
|
||||
if (existingRoleMappings.length) {
|
||||
await knex('saml_auth_providers_role_mappings').insert(
|
||||
existingRoleMappings
|
||||
);
|
||||
}
|
||||
|
||||
return await knex.schema.dropTable('role_mappings');
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export async function up(knex) {
|
||||
return await knex.schema.alterTable('app_configs', (table) => {
|
||||
table.boolean('use_only_predefined_auth_clients').defaultTo(false);
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex) {
|
||||
return await knex.schema.alterTable('app_configs', (table) => {
|
||||
table.dropColumn('use_only_predefined_auth_clients');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export async function up(knex) {
|
||||
return await knex.schema.alterTable('app_configs', (table) => {
|
||||
table.dropColumn('shared');
|
||||
table.dropColumn('connection_allowed');
|
||||
table.dropColumn('custom_connection_allowed');
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex) {
|
||||
return await knex.schema.alterTable('app_configs', (table) => {
|
||||
table.boolean('shared').defaultTo(false);
|
||||
table.boolean('connection_allowed').defaultTo(false);
|
||||
table.boolean('custom_connection_allowed').defaultTo(false);
|
||||
});
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
@@ -88,8 +88,8 @@ const sharedAuthenticationStepsWithAuthUrl = [
|
||||
value: '{key}',
|
||||
},
|
||||
{
|
||||
name: 'appAuthClientId',
|
||||
value: '{appAuthClientId}',
|
||||
name: 'oauthClientId',
|
||||
value: '{oauthClientId}',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -30,7 +30,7 @@ const findOrCreateUserBySamlIdentity = async (
|
||||
: [mappedUser.role];
|
||||
|
||||
const samlAuthProviderRoleMapping = await samlAuthProvider
|
||||
.$relatedQuery('samlAuthProvidersRoleMappings')
|
||||
.$relatedQuery('roleMappings')
|
||||
.whereIn('remote_role_name', mappedRoles)
|
||||
.limit(1)
|
||||
.first();
|
||||
|
||||
46
packages/backend/src/helpers/user-ability.test.js
Normal file
46
packages/backend/src/helpers/user-ability.test.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import userAbility from './user-ability.js';
|
||||
|
||||
describe('userAbility', () => {
|
||||
it('should return PureAbility instantiated with user permissions', () => {
|
||||
const user = {
|
||||
permissions: [
|
||||
{
|
||||
subject: 'Flow',
|
||||
action: 'read',
|
||||
conditions: ['isCreator'],
|
||||
},
|
||||
],
|
||||
role: {
|
||||
name: 'User',
|
||||
},
|
||||
};
|
||||
|
||||
const ability = userAbility(user);
|
||||
|
||||
expect(ability.rules).toStrictEqual(user.permissions);
|
||||
});
|
||||
|
||||
it('should return permission-less PureAbility for user with no role', () => {
|
||||
const user = {
|
||||
permissions: [
|
||||
{
|
||||
subject: 'Flow',
|
||||
action: 'read',
|
||||
conditions: ['isCreator'],
|
||||
},
|
||||
],
|
||||
role: null,
|
||||
};
|
||||
const ability = userAbility(user);
|
||||
|
||||
expect(ability.rules).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it('should return permission-less PureAbility for user with no permissions', () => {
|
||||
const user = { permissions: null, role: { name: 'User' } };
|
||||
const ability = userAbility(user);
|
||||
|
||||
expect(ability.rules).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
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`] = `
|
||||
{
|
||||
"properties": {
|
||||
"connectionAllowed": {
|
||||
"default": false,
|
||||
"type": "boolean",
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
},
|
||||
"customConnectionAllowed": {
|
||||
"default": false,
|
||||
"type": "boolean",
|
||||
},
|
||||
"disabled": {
|
||||
"default": false,
|
||||
"type": "boolean",
|
||||
@@ -25,13 +17,13 @@ exports[`AppConfig model > jsonSchema should have correct validations 1`] = `
|
||||
"key": {
|
||||
"type": "string",
|
||||
},
|
||||
"shared": {
|
||||
"default": false,
|
||||
"type": "boolean",
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string",
|
||||
},
|
||||
"useOnlyPredefinedAuthClients": {
|
||||
"default": false,
|
||||
"type": "boolean",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"key",
|
||||
|
||||
@@ -3,10 +3,6 @@
|
||||
exports[`Connection model > jsonSchema should have correct validations 1`] = `
|
||||
{
|
||||
"properties": {
|
||||
"appAuthClientId": {
|
||||
"format": "uuid",
|
||||
"type": "string",
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
},
|
||||
@@ -31,6 +27,10 @@ exports[`Connection model > jsonSchema should have correct validations 1`] = `
|
||||
"minLength": 1,
|
||||
"type": "string",
|
||||
},
|
||||
"oauthClientId": {
|
||||
"format": "uuid",
|
||||
"type": "string",
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string",
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// 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": {
|
||||
"active": {
|
||||
@@ -0,0 +1,30 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`RoleMapping model > jsonSchema should have the correct schema 1`] = `
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"format": "uuid",
|
||||
"type": "string",
|
||||
},
|
||||
"remoteRoleName": {
|
||||
"minLength": 1,
|
||||
"type": "string",
|
||||
},
|
||||
"roleId": {
|
||||
"format": "uuid",
|
||||
"type": "string",
|
||||
},
|
||||
"samlAuthProviderId": {
|
||||
"format": "uuid",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"samlAuthProviderId",
|
||||
"roleId",
|
||||
"remoteRoleName",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
@@ -1,6 +1,6 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`SamlAuthProvidersRoleMapping model > jsonSchema should have the correct schema 1`] = `
|
||||
exports[`RoleMapping model > jsonSchema should have the correct schema 1`] = `
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
@@ -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 AppAuthClient from './app-auth-client.js';
|
||||
import OAuthClient from './oauth-client.js';
|
||||
import Base from './base.js';
|
||||
|
||||
class AppConfig extends Base {
|
||||
@@ -16,9 +16,7 @@ class AppConfig extends Base {
|
||||
properties: {
|
||||
id: { type: 'string', format: 'uuid' },
|
||||
key: { type: 'string' },
|
||||
connectionAllowed: { type: 'boolean', default: false },
|
||||
customConnectionAllowed: { type: 'boolean', default: false },
|
||||
shared: { type: 'boolean', default: false },
|
||||
useOnlyPredefinedAuthClients: { type: 'boolean', default: false },
|
||||
disabled: { type: 'boolean', default: false },
|
||||
createdAt: { type: 'string' },
|
||||
updatedAt: { type: 'string' },
|
||||
@@ -26,12 +24,12 @@ class AppConfig extends Base {
|
||||
};
|
||||
|
||||
static relationMappings = () => ({
|
||||
appAuthClients: {
|
||||
oauthClients: {
|
||||
relation: Base.HasManyRelation,
|
||||
modelClass: AppAuthClient,
|
||||
modelClass: OAuthClient,
|
||||
join: {
|
||||
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);
|
||||
}
|
||||
|
||||
async computeAndAssignConnectionAllowedProperty() {
|
||||
this.connectionAllowed = await this.computeConnectionAllowedProperty();
|
||||
}
|
||||
|
||||
async computeConnectionAllowedProperty() {
|
||||
const appAuthClients = await this.$relatedQuery('appAuthClients');
|
||||
|
||||
const hasSomeActiveAppAuthClients =
|
||||
appAuthClients?.some((appAuthClient) => appAuthClient.active) || false;
|
||||
|
||||
const conditions = [
|
||||
hasSomeActiveAppAuthClients,
|
||||
this.shared,
|
||||
!this.disabled,
|
||||
];
|
||||
|
||||
const connectionAllowed = conditions.every(Boolean);
|
||||
|
||||
return connectionAllowed;
|
||||
}
|
||||
|
||||
async $beforeInsert(queryContext) {
|
||||
await super.$beforeInsert(queryContext);
|
||||
|
||||
await this.computeAndAssignConnectionAllowedProperty();
|
||||
}
|
||||
|
||||
async $beforeUpdate(opt, queryContext) {
|
||||
await super.$beforeUpdate(opt, queryContext);
|
||||
|
||||
await this.computeAndAssignConnectionAllowedProperty();
|
||||
}
|
||||
}
|
||||
|
||||
export default AppConfig;
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { vi, describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import Base from './base.js';
|
||||
import AppConfig from './app-config.js';
|
||||
import App from './app.js';
|
||||
import AppAuthClient from './app-auth-client.js';
|
||||
import { createAppConfig } from '../../test/factories/app-config.js';
|
||||
import { createAppAuthClient } from '../../test/factories/app-auth-client.js';
|
||||
import OAuthClient from './oauth-client.js';
|
||||
|
||||
describe('AppConfig model', () => {
|
||||
it('tableName should return correct name', () => {
|
||||
@@ -24,12 +22,12 @@ describe('AppConfig model', () => {
|
||||
const relationMappings = AppConfig.relationMappings();
|
||||
|
||||
const expectedRelations = {
|
||||
appAuthClients: {
|
||||
oauthClients: {
|
||||
relation: Base.HasManyRelation,
|
||||
modelClass: AppAuthClient,
|
||||
modelClass: OAuthClient,
|
||||
join: {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
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 App from './app.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 User from './user.js';
|
||||
import Step from './step.js';
|
||||
@@ -24,7 +24,7 @@ class Connection extends Base {
|
||||
data: { type: 'string' },
|
||||
formattedData: { type: 'object' },
|
||||
userId: { type: 'string', format: 'uuid' },
|
||||
appAuthClientId: { type: 'string', format: 'uuid' },
|
||||
oauthClientId: { type: 'string', format: 'uuid' },
|
||||
verified: { type: 'boolean', default: false },
|
||||
draft: { type: 'boolean' },
|
||||
deletedAt: { type: 'string' },
|
||||
@@ -33,10 +33,6 @@ class Connection extends Base {
|
||||
},
|
||||
};
|
||||
|
||||
static get virtualAttributes() {
|
||||
return ['reconnectable'];
|
||||
}
|
||||
|
||||
static relationMappings = () => ({
|
||||
user: {
|
||||
relation: Base.BelongsToOneRelation,
|
||||
@@ -73,28 +69,16 @@ class Connection extends Base {
|
||||
to: 'app_configs.key',
|
||||
},
|
||||
},
|
||||
appAuthClient: {
|
||||
oauthClient: {
|
||||
relation: Base.BelongsToOneRelation,
|
||||
modelClass: AppAuthClient,
|
||||
modelClass: OAuthClient,
|
||||
join: {
|
||||
from: 'connections.app_auth_client_id',
|
||||
to: 'app_auth_clients.id',
|
||||
from: 'connections.oauth_client_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() {
|
||||
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(
|
||||
`New custom connections have been disabled for ${app.name}!`
|
||||
);
|
||||
}
|
||||
|
||||
if (!appConfig.shared && this.appAuthClientId) {
|
||||
throw new NotAuthorizedError(
|
||||
'The connection with the given app auth client is not allowed!'
|
||||
);
|
||||
}
|
||||
|
||||
if (appConfig.shared && !this.formattedData) {
|
||||
if (!this.formattedData) {
|
||||
const authClient = await appConfig
|
||||
.$relatedQuery('appAuthClients')
|
||||
.findById(this.appAuthClientId)
|
||||
.$relatedQuery('oauthClients')
|
||||
.findById(this.oauthClientId)
|
||||
.where({ active: true })
|
||||
.throwIfNotFound();
|
||||
|
||||
@@ -237,13 +215,13 @@ class Connection extends Base {
|
||||
return updatedConnection;
|
||||
}
|
||||
|
||||
async updateFormattedData({ formattedData, appAuthClientId }) {
|
||||
if (appAuthClientId) {
|
||||
const appAuthClient = await AppAuthClient.query()
|
||||
.findById(appAuthClientId)
|
||||
async updateFormattedData({ formattedData, oauthClientId }) {
|
||||
if (oauthClientId) {
|
||||
const oauthClient = await OAuthClient.query()
|
||||
.findById(oauthClientId)
|
||||
.throwIfNotFound();
|
||||
|
||||
formattedData = appAuthClient.formattedAuthDefaults;
|
||||
formattedData = oauthClient.formattedAuthDefaults;
|
||||
}
|
||||
|
||||
return await this.$query().patchAndFetch({
|
||||
|
||||
@@ -2,7 +2,7 @@ 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 '../config/app.js';
|
||||
import AppAuthClient from './app-auth-client.js';
|
||||
import OAuthClient from './oauth-client.js';
|
||||
import App from './app.js';
|
||||
import AppConfig from './app-config.js';
|
||||
import Base from './base.js';
|
||||
@@ -12,7 +12,7 @@ import User from './user.js';
|
||||
import Telemetry from '../helpers/telemetry/index.js';
|
||||
import { createConnection } from '../../test/factories/connection.js';
|
||||
import { createAppConfig } from '../../test/factories/app-config.js';
|
||||
import { createAppAuthClient } from '../../test/factories/app-auth-client.js';
|
||||
import { createOAuthClient } from '../../test/factories/oauth-client.js';
|
||||
|
||||
describe('Connection model', () => {
|
||||
it('tableName should return correct name', () => {
|
||||
@@ -23,14 +23,6 @@ describe('Connection model', () => {
|
||||
expect(Connection.jsonSchema).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('virtualAttributes should return correct attributes', () => {
|
||||
const virtualAttributes = Connection.virtualAttributes;
|
||||
|
||||
const expectedAttributes = ['reconnectable'];
|
||||
|
||||
expect(virtualAttributes).toStrictEqual(expectedAttributes);
|
||||
});
|
||||
|
||||
describe('relationMappings', () => {
|
||||
it('should return correct associations', () => {
|
||||
const relationMappings = Connection.relationMappings();
|
||||
@@ -69,12 +61,12 @@ describe('Connection model', () => {
|
||||
to: 'app_configs.key',
|
||||
},
|
||||
},
|
||||
appAuthClient: {
|
||||
oauthClient: {
|
||||
relation: Base.BelongsToOneRelation,
|
||||
modelClass: AppAuthClient,
|
||||
modelClass: OAuthClient,
|
||||
join: {
|
||||
from: 'connections.app_auth_client_id',
|
||||
to: 'app_auth_clients.id',
|
||||
from: 'connections.oauth_client_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', () => {
|
||||
it('should return undefined if eligibleForEncryption is not true', async () => {
|
||||
vi.spyOn(Connection.prototype, 'eligibleForEncryption').mockReturnValue(
|
||||
@@ -366,6 +286,7 @@ describe('Connection model', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// TODO: update test case name
|
||||
it('should throw an error when app config does not allow custom connection with formatted data', async () => {
|
||||
vi.spyOn(Connection.prototype, 'getApp').mockResolvedValue({
|
||||
name: 'gitlab',
|
||||
@@ -373,7 +294,7 @@ describe('Connection model', () => {
|
||||
|
||||
vi.spyOn(Connection.prototype, 'getAppConfig').mockResolvedValue({
|
||||
disabled: false,
|
||||
customConnectionAllowed: false,
|
||||
useOnlyPredefinedAuthClients: true,
|
||||
});
|
||||
|
||||
const connection = new Connection();
|
||||
@@ -386,35 +307,13 @@ describe('Connection model', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error when app config is not shared with app auth client', async () => {
|
||||
vi.spyOn(Connection.prototype, 'getApp').mockResolvedValue({
|
||||
name: 'gitlab',
|
||||
});
|
||||
|
||||
vi.spyOn(Connection.prototype, 'getAppConfig').mockResolvedValue({
|
||||
disabled: false,
|
||||
shared: false,
|
||||
});
|
||||
|
||||
const connection = new Connection();
|
||||
connection.appAuthClientId = 'sample-id';
|
||||
|
||||
await expect(() =>
|
||||
connection.checkEligibilityForCreation()
|
||||
).rejects.toThrow(
|
||||
'The connection with the given app auth client is not allowed!'
|
||||
);
|
||||
});
|
||||
|
||||
it('should apply app auth client auth defaults when creating with shared app auth client', async () => {
|
||||
it('should apply oauth client auth defaults when creating with shared oauth client', async () => {
|
||||
await createAppConfig({
|
||||
key: 'gitlab',
|
||||
disabled: false,
|
||||
customConnectionAllowed: true,
|
||||
shared: true,
|
||||
});
|
||||
|
||||
const appAuthClient = await createAppAuthClient({
|
||||
const oauthClient = await createOAuthClient({
|
||||
appKey: 'gitlab',
|
||||
active: true,
|
||||
formattedAuthDefaults: {
|
||||
@@ -424,7 +323,7 @@ describe('Connection model', () => {
|
||||
|
||||
const connection = await createConnection({
|
||||
key: 'gitlab',
|
||||
appAuthClientId: appAuthClient.id,
|
||||
oauthClientId: oauthClient.id,
|
||||
formattedData: null,
|
||||
});
|
||||
|
||||
@@ -660,22 +559,22 @@ describe('Connection model', () => {
|
||||
});
|
||||
|
||||
describe('updateFormattedData', () => {
|
||||
it('should extend connection data with app auth client auth defaults', async () => {
|
||||
const appAuthClient = await createAppAuthClient({
|
||||
it('should extend connection data with oauth client auth defaults', async () => {
|
||||
const oauthClient = await createOAuthClient({
|
||||
formattedAuthDefaults: {
|
||||
clientId: 'sample-id',
|
||||
},
|
||||
});
|
||||
|
||||
const connection = await createConnection({
|
||||
appAuthClientId: appAuthClient.id,
|
||||
oauthClientId: oauthClient.id,
|
||||
formattedData: {
|
||||
token: 'sample-token',
|
||||
},
|
||||
});
|
||||
|
||||
const updatedConnection = await connection.updateFormattedData({
|
||||
appAuthClientId: appAuthClient.id,
|
||||
oauthClientId: oauthClient.id,
|
||||
});
|
||||
|
||||
expect(updatedConnection.formattedData).toStrictEqual({
|
||||
|
||||
@@ -4,8 +4,8 @@ import appConfig from '../config/app.js';
|
||||
import Base from './base.js';
|
||||
import AppConfig from './app-config.js';
|
||||
|
||||
class AppAuthClient extends Base {
|
||||
static tableName = 'app_auth_clients';
|
||||
class OAuthClient extends Base {
|
||||
static tableName = 'oauth_clients';
|
||||
|
||||
static jsonSchema = {
|
||||
type: 'object',
|
||||
@@ -27,7 +27,7 @@ class AppAuthClient extends Base {
|
||||
relation: Base.BelongsToOneRelation,
|
||||
modelClass: AppConfig,
|
||||
join: {
|
||||
from: 'app_auth_clients.app_key',
|
||||
from: 'oauth_clients.app_key',
|
||||
to: 'app_configs.key',
|
||||
},
|
||||
},
|
||||
@@ -60,39 +60,26 @@ class AppAuthClient extends Base {
|
||||
return this.authDefaults ? true : false;
|
||||
}
|
||||
|
||||
async triggerAppConfigUpdate() {
|
||||
const appConfig = await this.$relatedQuery('appConfig');
|
||||
|
||||
// This is a workaround to update connection allowed column for AppConfig
|
||||
await appConfig?.$query().patch({
|
||||
key: appConfig.key,
|
||||
shared: appConfig.shared,
|
||||
disabled: appConfig.disabled,
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Make another abstraction like beforeSave instead of using
|
||||
// beforeInsert and beforeUpdate separately for the same operation.
|
||||
async $beforeInsert(queryContext) {
|
||||
await super.$beforeInsert(queryContext);
|
||||
|
||||
this.encryptData();
|
||||
}
|
||||
|
||||
async $afterInsert(queryContext) {
|
||||
await super.$afterInsert(queryContext);
|
||||
|
||||
await this.triggerAppConfigUpdate();
|
||||
}
|
||||
|
||||
async $beforeUpdate(opt, queryContext) {
|
||||
await super.$beforeUpdate(opt, queryContext);
|
||||
|
||||
this.encryptData();
|
||||
}
|
||||
|
||||
async $afterUpdate(opt, queryContext) {
|
||||
await super.$afterUpdate(opt, queryContext);
|
||||
|
||||
await this.triggerAppConfigUpdate();
|
||||
}
|
||||
|
||||
async $afterFind() {
|
||||
@@ -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,8 +1,8 @@
|
||||
import Base from './base.js';
|
||||
import SamlAuthProvider from './saml-auth-provider.ee.js';
|
||||
|
||||
class SamlAuthProvidersRoleMapping extends Base {
|
||||
static tableName = 'saml_auth_providers_role_mappings';
|
||||
class RoleMapping extends Base {
|
||||
static tableName = 'role_mappings';
|
||||
|
||||
static jsonSchema = {
|
||||
type: 'object',
|
||||
@@ -21,11 +21,11 @@ class SamlAuthProvidersRoleMapping extends Base {
|
||||
relation: Base.BelongsToOneRelation,
|
||||
modelClass: SamlAuthProvider,
|
||||
join: {
|
||||
from: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
|
||||
from: 'role_mappings.saml_auth_provider_id',
|
||||
to: 'saml_auth_providers.id',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default SamlAuthProvidersRoleMapping;
|
||||
export default RoleMapping;
|
||||
@@ -1,28 +1,26 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import SamlAuthProvidersRoleMapping from '../models/saml-auth-providers-role-mapping.ee';
|
||||
import RoleMapping from './role-mapping.ee';
|
||||
import SamlAuthProvider from './saml-auth-provider.ee';
|
||||
import Base from './base';
|
||||
|
||||
describe('SamlAuthProvidersRoleMapping model', () => {
|
||||
describe('RoleMapping model', () => {
|
||||
it('tableName should return correct name', () => {
|
||||
expect(SamlAuthProvidersRoleMapping.tableName).toBe(
|
||||
'saml_auth_providers_role_mappings'
|
||||
);
|
||||
expect(RoleMapping.tableName).toBe('role_mappings');
|
||||
});
|
||||
|
||||
it('jsonSchema should have the correct schema', () => {
|
||||
expect(SamlAuthProvidersRoleMapping.jsonSchema).toMatchSnapshot();
|
||||
expect(RoleMapping.jsonSchema).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('relationMappings should return correct associations', () => {
|
||||
const relationMappings = SamlAuthProvidersRoleMapping.relationMappings();
|
||||
const relationMappings = RoleMapping.relationMappings();
|
||||
|
||||
const expectedRelations = {
|
||||
samlAuthProvider: {
|
||||
relation: Base.BelongsToOneRelation,
|
||||
modelClass: SamlAuthProvider,
|
||||
join: {
|
||||
from: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
|
||||
from: 'role_mappings.saml_auth_provider_id',
|
||||
to: 'saml_auth_providers.id',
|
||||
},
|
||||
},
|
||||
@@ -5,7 +5,7 @@ import appConfig from '../config/app.js';
|
||||
import axios from '../helpers/axios-with-proxy.js';
|
||||
import Base from './base.js';
|
||||
import Identity from './identity.ee.js';
|
||||
import SamlAuthProvidersRoleMapping from './saml-auth-providers-role-mapping.ee.js';
|
||||
import RoleMapping from './role-mapping.ee.js';
|
||||
|
||||
class SamlAuthProvider extends Base {
|
||||
static tableName = 'saml_auth_providers';
|
||||
@@ -53,12 +53,12 @@ class SamlAuthProvider extends Base {
|
||||
to: 'saml_auth_providers.id',
|
||||
},
|
||||
},
|
||||
samlAuthProvidersRoleMappings: {
|
||||
roleMappings: {
|
||||
relation: Base.HasManyRelation,
|
||||
modelClass: SamlAuthProvidersRoleMapping,
|
||||
modelClass: RoleMapping,
|
||||
join: {
|
||||
from: 'saml_auth_providers.id',
|
||||
to: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
|
||||
to: 'role_mappings.saml_auth_provider_id',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -133,27 +133,22 @@ class SamlAuthProvider extends Base {
|
||||
}
|
||||
|
||||
async updateRoleMappings(roleMappings) {
|
||||
return await SamlAuthProvider.transaction(async (trx) => {
|
||||
await this.$relatedQuery('samlAuthProvidersRoleMappings', trx).delete();
|
||||
await this.$relatedQuery('roleMappings').delete();
|
||||
|
||||
if (isEmpty(roleMappings)) {
|
||||
return [];
|
||||
}
|
||||
if (isEmpty(roleMappings)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const samlAuthProvidersRoleMappingsData = roleMappings.map(
|
||||
(samlAuthProvidersRoleMapping) => ({
|
||||
...samlAuthProvidersRoleMapping,
|
||||
samlAuthProviderId: this.id,
|
||||
})
|
||||
);
|
||||
const roleMappingsData = roleMappings.map((roleMapping) => ({
|
||||
...roleMapping,
|
||||
samlAuthProviderId: this.id,
|
||||
}));
|
||||
|
||||
const samlAuthProvidersRoleMappings =
|
||||
await SamlAuthProvidersRoleMapping.query(trx).insertAndFetch(
|
||||
samlAuthProvidersRoleMappingsData
|
||||
);
|
||||
const newRoleMappings = await RoleMapping.query().insertAndFetch(
|
||||
roleMappingsData
|
||||
);
|
||||
|
||||
return samlAuthProvidersRoleMappings;
|
||||
});
|
||||
return newRoleMappings;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { vi, describe, it, expect } from 'vitest';
|
||||
import { vi, beforeEach, describe, it, expect } from 'vitest';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import SamlAuthProvider from '../models/saml-auth-provider.ee';
|
||||
import SamlAuthProvidersRoleMapping from '../models/saml-auth-providers-role-mapping.ee';
|
||||
import RoleMapping from '../models/role-mapping.ee';
|
||||
import axios from '../helpers/axios-with-proxy.js';
|
||||
import Identity from './identity.ee';
|
||||
import Base from './base';
|
||||
import appConfig from '../config/app';
|
||||
import { createSamlAuthProvider } from '../../test/factories/saml-auth-provider.ee.js';
|
||||
import { createRoleMapping } from '../../test/factories/role-mapping.js';
|
||||
import { createRole } from '../../test/factories/role.js';
|
||||
|
||||
describe('SamlAuthProvider model', () => {
|
||||
it('tableName should return correct name', () => {
|
||||
@@ -26,12 +31,12 @@ describe('SamlAuthProvider model', () => {
|
||||
to: 'saml_auth_providers.id',
|
||||
},
|
||||
},
|
||||
samlAuthProvidersRoleMappings: {
|
||||
roleMappings: {
|
||||
relation: Base.HasManyRelation,
|
||||
modelClass: SamlAuthProvidersRoleMapping,
|
||||
modelClass: RoleMapping,
|
||||
join: {
|
||||
from: 'saml_auth_providers.id',
|
||||
to: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
|
||||
to: 'role_mappings.saml_auth_provider_id',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -81,4 +86,146 @@ describe('SamlAuthProvider model', () => {
|
||||
'https://example.com/saml/logout'
|
||||
);
|
||||
});
|
||||
|
||||
it('config should return the correct configuration object', () => {
|
||||
const samlAuthProvider = new SamlAuthProvider();
|
||||
|
||||
samlAuthProvider.certificate = 'sample-certificate';
|
||||
samlAuthProvider.signatureAlgorithm = 'sha256';
|
||||
samlAuthProvider.entryPoint = 'https://example.com/saml';
|
||||
samlAuthProvider.issuer = 'sample-issuer';
|
||||
|
||||
vi.spyOn(appConfig, 'baseUrl', 'get').mockReturnValue(
|
||||
'https://automatisch.io'
|
||||
);
|
||||
|
||||
const expectedConfig = {
|
||||
callbackUrl: 'https://automatisch.io/login/saml/sample-issuer/callback',
|
||||
cert: 'sample-certificate',
|
||||
entryPoint: 'https://example.com/saml',
|
||||
issuer: 'sample-issuer',
|
||||
signatureAlgorithm: 'sha256',
|
||||
logoutUrl: 'https://example.com/saml',
|
||||
};
|
||||
|
||||
expect(samlAuthProvider.config).toStrictEqual(expectedConfig);
|
||||
});
|
||||
|
||||
it('generateLogoutRequestBody should return a correctly encoded SAML logout request', () => {
|
||||
vi.mock('uuid', () => ({
|
||||
v4: vi.fn(),
|
||||
}));
|
||||
|
||||
const samlAuthProvider = new SamlAuthProvider();
|
||||
|
||||
samlAuthProvider.entryPoint = 'https://example.com/saml';
|
||||
samlAuthProvider.issuer = 'sample-issuer';
|
||||
|
||||
const mockUuid = '123e4567-e89b-12d3-a456-426614174000';
|
||||
uuidv4.mockReturnValue(mockUuid);
|
||||
|
||||
const sessionId = 'test-session-id';
|
||||
|
||||
const logoutRequest = samlAuthProvider.generateLogoutRequestBody(sessionId);
|
||||
|
||||
const expectedLogoutRequest = `
|
||||
<samlp:LogoutRequest
|
||||
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
|
||||
ID="${mockUuid}"
|
||||
Version="2.0"
|
||||
IssueInstant="${new Date().toISOString()}"
|
||||
Destination="https://example.com/saml">
|
||||
|
||||
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">sample-issuer</saml:Issuer>
|
||||
<samlp:SessionIndex>test-session-id</samlp:SessionIndex>
|
||||
</samlp:LogoutRequest>
|
||||
`;
|
||||
|
||||
const expectedEncodedRequest = Buffer.from(expectedLogoutRequest).toString(
|
||||
'base64'
|
||||
);
|
||||
|
||||
expect(logoutRequest).toBe(expectedEncodedRequest);
|
||||
});
|
||||
|
||||
it('terminateRemoteSession should send the correct POST request and return the response', async () => {
|
||||
vi.mock('../helpers/axios-with-proxy.js', () => ({
|
||||
default: {
|
||||
post: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const samlAuthProvider = new SamlAuthProvider();
|
||||
|
||||
samlAuthProvider.entryPoint = 'https://example.com/saml';
|
||||
samlAuthProvider.generateLogoutRequestBody = vi
|
||||
.fn()
|
||||
.mockReturnValue('mockEncodedLogoutRequest');
|
||||
|
||||
const sessionId = 'test-session-id';
|
||||
|
||||
const mockResponse = { data: 'Logout Successful' };
|
||||
axios.post.mockResolvedValue(mockResponse);
|
||||
|
||||
const response = await samlAuthProvider.terminateRemoteSession(sessionId);
|
||||
|
||||
expect(samlAuthProvider.generateLogoutRequestBody).toHaveBeenCalledWith(
|
||||
sessionId
|
||||
);
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
'https://example.com/saml',
|
||||
'SAMLRequest=mockEncodedLogoutRequest',
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(response).toBe(mockResponse);
|
||||
});
|
||||
|
||||
describe('updateRoleMappings', () => {
|
||||
let samlAuthProvider;
|
||||
|
||||
beforeEach(async () => {
|
||||
samlAuthProvider = await createSamlAuthProvider();
|
||||
});
|
||||
|
||||
it('should remove all existing role mappings', async () => {
|
||||
await createRoleMapping({
|
||||
samlAuthProviderId: samlAuthProvider.id,
|
||||
remoteRoleName: 'Admin',
|
||||
});
|
||||
|
||||
await createRoleMapping({
|
||||
samlAuthProviderId: samlAuthProvider.id,
|
||||
remoteRoleName: 'User',
|
||||
});
|
||||
|
||||
await samlAuthProvider.updateRoleMappings([]);
|
||||
|
||||
const roleMappings = await samlAuthProvider.$relatedQuery('roleMappings');
|
||||
expect(roleMappings).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it('should return the updated role mappings when new ones are provided', async () => {
|
||||
const adminRole = await createRole({ name: 'Admin' });
|
||||
const userRole = await createRole({ name: 'User' });
|
||||
|
||||
const newRoleMappings = [
|
||||
{ remoteRoleName: 'Admin', roleId: adminRole.id },
|
||||
{ remoteRoleName: 'User', roleId: userRole.id },
|
||||
];
|
||||
|
||||
const result = await samlAuthProvider.updateRoleMappings(newRoleMappings);
|
||||
|
||||
const refetchedRoleMappings = await samlAuthProvider.$relatedQuery(
|
||||
'roleMappings'
|
||||
);
|
||||
|
||||
expect(result).toStrictEqual(refetchedRoleMappings);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -212,6 +212,10 @@ class User extends Base {
|
||||
return `${appConfig.webAppUrl}/accept-invitation?token=${this.invitationToken}`;
|
||||
}
|
||||
|
||||
get ability() {
|
||||
return userAbility(this);
|
||||
}
|
||||
|
||||
static async authenticate(email, password) {
|
||||
const user = await User.query().findOne({
|
||||
email: email?.toLowerCase() || null,
|
||||
@@ -583,62 +587,6 @@ class User extends Base {
|
||||
return user;
|
||||
}
|
||||
|
||||
async $beforeInsert(queryContext) {
|
||||
await super.$beforeInsert(queryContext);
|
||||
|
||||
this.email = this.email.toLowerCase();
|
||||
await this.generateHash();
|
||||
|
||||
if (appConfig.isCloud) {
|
||||
this.startTrialPeriod();
|
||||
}
|
||||
}
|
||||
|
||||
async $beforeUpdate(opt, queryContext) {
|
||||
await super.$beforeUpdate(opt, queryContext);
|
||||
|
||||
if (this.email) {
|
||||
this.email = this.email.toLowerCase();
|
||||
}
|
||||
|
||||
await this.generateHash();
|
||||
}
|
||||
|
||||
async $afterInsert(queryContext) {
|
||||
await super.$afterInsert(queryContext);
|
||||
|
||||
if (appConfig.isCloud) {
|
||||
await this.$relatedQuery('usageData').insert({
|
||||
userId: this.id,
|
||||
consumedTaskCount: 0,
|
||||
nextResetAt: DateTime.now().plus({ days: 30 }).toISODate(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async $afterFind() {
|
||||
if (await hasValidLicense()) return this;
|
||||
|
||||
if (Array.isArray(this.permissions)) {
|
||||
this.permissions = this.permissions.filter((permission) => {
|
||||
const restrictedSubjects = [
|
||||
'App',
|
||||
'Role',
|
||||
'SamlAuthProvider',
|
||||
'Config',
|
||||
];
|
||||
|
||||
return !restrictedSubjects.includes(permission.subject);
|
||||
});
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
get ability() {
|
||||
return userAbility(this);
|
||||
}
|
||||
|
||||
can(action, subject) {
|
||||
const can = this.ability.can(action, subject);
|
||||
|
||||
@@ -654,12 +602,68 @@ class User extends Base {
|
||||
return conditionMap;
|
||||
}
|
||||
|
||||
cannot(action, subject) {
|
||||
const cannot = this.ability.cannot(action, subject);
|
||||
lowercaseEmail() {
|
||||
if (this.email) {
|
||||
this.email = this.email.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
if (cannot) throw new NotAuthorizedError();
|
||||
async createUsageData() {
|
||||
if (appConfig.isCloud) {
|
||||
return await this.$relatedQuery('usageData').insertAndFetch({
|
||||
userId: this.id,
|
||||
consumedTaskCount: 0,
|
||||
nextResetAt: DateTime.now().plus({ days: 30 }).toISODate(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return cannot;
|
||||
async omitEnterprisePermissionsWithoutValidLicense() {
|
||||
if (await hasValidLicense()) {
|
||||
return this;
|
||||
}
|
||||
|
||||
if (Array.isArray(this.permissions)) {
|
||||
this.permissions = this.permissions.filter((permission) => {
|
||||
const restrictedSubjects = [
|
||||
'App',
|
||||
'Role',
|
||||
'SamlAuthProvider',
|
||||
'Config',
|
||||
];
|
||||
|
||||
return !restrictedSubjects.includes(permission.subject);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async $beforeInsert(queryContext) {
|
||||
await super.$beforeInsert(queryContext);
|
||||
|
||||
this.lowercaseEmail();
|
||||
await this.generateHash();
|
||||
|
||||
if (appConfig.isCloud) {
|
||||
this.startTrialPeriod();
|
||||
}
|
||||
}
|
||||
|
||||
async $beforeUpdate(opt, queryContext) {
|
||||
await super.$beforeUpdate(opt, queryContext);
|
||||
|
||||
this.lowercaseEmail();
|
||||
|
||||
await this.generateHash();
|
||||
}
|
||||
|
||||
async $afterInsert(queryContext) {
|
||||
await super.$afterInsert(queryContext);
|
||||
|
||||
await this.createUsageData();
|
||||
}
|
||||
|
||||
async $afterFind() {
|
||||
await this.omitEnterprisePermissionsWithoutValidLicense();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { DateTime, Duration } from 'luxon';
|
||||
import appConfig from '../config/app.js';
|
||||
import * as licenseModule from '../helpers/license.ee.js';
|
||||
import Base from './base.js';
|
||||
import AccessToken from './access-token.js';
|
||||
import Config from './config.js';
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
REMOVE_AFTER_30_DAYS_OR_150_JOBS,
|
||||
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
|
||||
} from '../helpers/remove-job-configuration.js';
|
||||
import * as userAbilityModule from '../helpers/user-ability.js';
|
||||
import { createUser } from '../../test/factories/user.js';
|
||||
import { createConnection } from '../../test/factories/connection.js';
|
||||
import { createRole } from '../../test/factories/role.js';
|
||||
@@ -205,64 +207,6 @@ describe('User model', () => {
|
||||
expect(virtualAttributes).toStrictEqual(expectedAttributes);
|
||||
});
|
||||
|
||||
it('acceptInvitationUrl should return accept invitation page URL with invitation token', async () => {
|
||||
const user = new User();
|
||||
user.invitationToken = 'invitation-token';
|
||||
|
||||
vi.spyOn(appConfig, 'webAppUrl', 'get').mockReturnValue(
|
||||
'https://automatisch.io'
|
||||
);
|
||||
|
||||
expect(user.acceptInvitationUrl).toBe(
|
||||
'https://automatisch.io/accept-invitation?token=invitation-token'
|
||||
);
|
||||
});
|
||||
|
||||
describe('authenticate', () => {
|
||||
it('should create and return the token for correct email and password', async () => {
|
||||
const user = await createUser({
|
||||
email: 'test-user@automatisch.io',
|
||||
password: 'sample-password',
|
||||
});
|
||||
|
||||
const token = await User.authenticate(
|
||||
'test-user@automatisch.io',
|
||||
'sample-password'
|
||||
);
|
||||
|
||||
const persistedToken = await AccessToken.query().findOne({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
expect(token).toBe(persistedToken.token);
|
||||
});
|
||||
|
||||
it('should return undefined for existing email and incorrect password', async () => {
|
||||
await createUser({
|
||||
email: 'test-user@automatisch.io',
|
||||
password: 'sample-password',
|
||||
});
|
||||
|
||||
const token = await User.authenticate(
|
||||
'test-user@automatisch.io',
|
||||
'wrong-password'
|
||||
);
|
||||
|
||||
expect(token).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should return undefined for non-existing email', async () => {
|
||||
await createUser({
|
||||
email: 'test-user@automatisch.io',
|
||||
password: 'sample-password',
|
||||
});
|
||||
|
||||
const token = await User.authenticate('non-existing-user@automatisch.io');
|
||||
|
||||
expect(token).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('authorizedFlows', () => {
|
||||
it('should return user flows with isCreator condition', async () => {
|
||||
const userRole = await createRole({ name: 'User' });
|
||||
@@ -432,7 +376,10 @@ describe('User model', () => {
|
||||
const anotherUserConnection = await createConnection();
|
||||
|
||||
expect(
|
||||
await userWithRoleAndPermissions.authorizedConnections
|
||||
await userWithRoleAndPermissions.authorizedConnections.orderBy(
|
||||
'created_at',
|
||||
'asc'
|
||||
)
|
||||
).toStrictEqual([userConnection, anotherUserConnection]);
|
||||
});
|
||||
|
||||
@@ -505,6 +452,76 @@ describe('User model', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('acceptInvitationUrl should return accept invitation page URL with invitation token', async () => {
|
||||
const user = new User();
|
||||
user.invitationToken = 'invitation-token';
|
||||
|
||||
vi.spyOn(appConfig, 'webAppUrl', 'get').mockReturnValue(
|
||||
'https://automatisch.io'
|
||||
);
|
||||
|
||||
expect(user.acceptInvitationUrl).toBe(
|
||||
'https://automatisch.io/accept-invitation?token=invitation-token'
|
||||
);
|
||||
});
|
||||
|
||||
it('ability should return userAbility for the user', () => {
|
||||
const user = new User();
|
||||
user.fullName = 'Sample user';
|
||||
|
||||
const userAbilitySpy = vi
|
||||
.spyOn(userAbilityModule, 'default')
|
||||
.mockReturnValue('user-ability');
|
||||
|
||||
expect(user.ability).toStrictEqual('user-ability');
|
||||
expect(userAbilitySpy).toHaveBeenNthCalledWith(1, user);
|
||||
});
|
||||
|
||||
describe('authenticate', () => {
|
||||
it('should create and return the token for correct email and password', async () => {
|
||||
const user = await createUser({
|
||||
email: 'test-user@automatisch.io',
|
||||
password: 'sample-password',
|
||||
});
|
||||
|
||||
const token = await User.authenticate(
|
||||
'test-user@automatisch.io',
|
||||
'sample-password'
|
||||
);
|
||||
|
||||
const persistedToken = await AccessToken.query().findOne({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
expect(token).toBe(persistedToken.token);
|
||||
});
|
||||
|
||||
it('should return undefined for existing email and incorrect password', async () => {
|
||||
await createUser({
|
||||
email: 'test-user@automatisch.io',
|
||||
password: 'sample-password',
|
||||
});
|
||||
|
||||
const token = await User.authenticate(
|
||||
'test-user@automatisch.io',
|
||||
'wrong-password'
|
||||
);
|
||||
|
||||
expect(token).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should return undefined for non-existing email', async () => {
|
||||
await createUser({
|
||||
email: 'test-user@automatisch.io',
|
||||
password: 'sample-password',
|
||||
});
|
||||
|
||||
const token = await User.authenticate('non-existing-user@automatisch.io');
|
||||
|
||||
expect(token).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('login', () => {
|
||||
it('should return true when the given password matches with the user password', async () => {
|
||||
const user = await createUser({ password: 'sample-password' });
|
||||
@@ -982,21 +999,9 @@ describe('User model', () => {
|
||||
|
||||
const user = await createUser();
|
||||
|
||||
const presentDate = DateTime.fromObject(
|
||||
{ year: 2024, month: 11, day: 17, hour: 11, minute: 30 },
|
||||
{ zone: 'UTC+0' }
|
||||
);
|
||||
|
||||
vi.setSystemTime(presentDate);
|
||||
|
||||
await user.startTrialPeriod();
|
||||
|
||||
const futureDate = DateTime.fromObject(
|
||||
{ year: 2025, month: 1, day: 1 },
|
||||
{ zone: 'UTC+0' }
|
||||
);
|
||||
|
||||
vi.setSystemTime(futureDate);
|
||||
vi.setSystemTime(DateTime.now().plus({ month: 1 }));
|
||||
|
||||
const refetchedUser = await user.$query();
|
||||
|
||||
@@ -1104,7 +1109,9 @@ describe('User model', () => {
|
||||
|
||||
const user = await createUser();
|
||||
|
||||
expect(() => user.getPlanAndUsage()).rejects.toThrow('NotFoundError');
|
||||
await expect(() => user.getPlanAndUsage()).rejects.toThrow(
|
||||
'NotFoundError'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1175,7 +1182,7 @@ describe('User model', () => {
|
||||
});
|
||||
|
||||
it('should throw not found error when user role does not exist', async () => {
|
||||
expect(() =>
|
||||
await expect(() =>
|
||||
User.registerUser({
|
||||
fullName: 'Sample user',
|
||||
email: 'user@automatisch.io',
|
||||
@@ -1184,4 +1191,342 @@ describe('User model', () => {
|
||||
).rejects.toThrowError('NotFoundError');
|
||||
});
|
||||
});
|
||||
|
||||
describe('can', () => {
|
||||
it('should return conditions for the given action and subject of the user', async () => {
|
||||
const userRole = await createRole({ name: 'User' });
|
||||
|
||||
await createPermission({
|
||||
roleId: userRole.id,
|
||||
subject: 'Flow',
|
||||
action: 'read',
|
||||
conditions: ['isCreator'],
|
||||
});
|
||||
|
||||
await createPermission({
|
||||
roleId: userRole.id,
|
||||
subject: 'Connection',
|
||||
action: 'read',
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
const user = await createUser({ roleId: userRole.id });
|
||||
|
||||
const userWithRoleAndPermissions = await user
|
||||
.$query()
|
||||
.withGraphFetched({ role: true, permissions: true });
|
||||
|
||||
expect(userWithRoleAndPermissions.can('read', 'Flow')).toStrictEqual({
|
||||
isCreator: true,
|
||||
});
|
||||
|
||||
expect(
|
||||
userWithRoleAndPermissions.can('read', 'Connection')
|
||||
).toStrictEqual({});
|
||||
});
|
||||
|
||||
it('should return not authorized error when the user is not permitted for the given action and subject', async () => {
|
||||
const userRole = await createRole({ name: 'User' });
|
||||
const user = await createUser({ roleId: userRole.id });
|
||||
|
||||
const userWithRoleAndPermissions = await user
|
||||
.$query()
|
||||
.withGraphFetched({ role: true, permissions: true });
|
||||
|
||||
expect(() => userWithRoleAndPermissions.can('read', 'Flow')).toThrowError(
|
||||
'The user is not authorized!'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('lowercaseEmail should lowercase the user email', () => {
|
||||
const user = new User();
|
||||
user.email = 'USER@AUTOMATISCH.IO';
|
||||
|
||||
user.lowercaseEmail();
|
||||
|
||||
expect(user.email).toBe('user@automatisch.io');
|
||||
});
|
||||
|
||||
describe('createUsageData', () => {
|
||||
it('should create usage data if Automatisch is a cloud installation', async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true);
|
||||
|
||||
const user = await createUser({
|
||||
fullName: 'Sample user',
|
||||
email: 'user@automatisch.io',
|
||||
});
|
||||
|
||||
vi.setSystemTime(DateTime.now().plus({ month: 1 }));
|
||||
|
||||
const usageData = await user.createUsageData();
|
||||
const currentUsageData = await user.$relatedQuery('currentUsageData');
|
||||
|
||||
expect(usageData).toStrictEqual(currentUsageData);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should not create usage data if Automatisch is not a cloud installation', async () => {
|
||||
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false);
|
||||
|
||||
const user = await createUser({
|
||||
fullName: 'Sample user',
|
||||
email: 'user@automatisch.io',
|
||||
});
|
||||
|
||||
const usageData = await user.createUsageData();
|
||||
|
||||
expect(usageData).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('omitEnterprisePermissionsWithoutValidLicense', () => {
|
||||
it('should return user as-is with valid license', async () => {
|
||||
const userRole = await createRole({ name: 'User' });
|
||||
const user = await createUser({
|
||||
fullName: 'Sample user',
|
||||
email: 'user@automatisch.io',
|
||||
roleId: userRole.id,
|
||||
});
|
||||
|
||||
const readFlowPermission = await createPermission({
|
||||
roleId: userRole.id,
|
||||
subject: 'Flow',
|
||||
action: 'read',
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
await createPermission({
|
||||
roleId: userRole.id,
|
||||
subject: 'App',
|
||||
action: 'read',
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
await createPermission({
|
||||
roleId: userRole.id,
|
||||
subject: 'Role',
|
||||
action: 'read',
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
await createPermission({
|
||||
roleId: userRole.id,
|
||||
subject: 'Config',
|
||||
action: 'read',
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
await createPermission({
|
||||
roleId: userRole.id,
|
||||
subject: 'SamlAuthProvider',
|
||||
action: 'read',
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
const userWithRoleAndPermissions = await user
|
||||
.$query()
|
||||
.withGraphFetched({ role: true, permissions: true });
|
||||
|
||||
expect(userWithRoleAndPermissions.permissions).toStrictEqual([
|
||||
readFlowPermission,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should omit enterprise permissions without valid license', async () => {
|
||||
vi.spyOn(licenseModule, 'hasValidLicense').mockResolvedValue(false);
|
||||
|
||||
const userRole = await createRole({ name: 'User' });
|
||||
const user = await createUser({
|
||||
fullName: 'Sample user',
|
||||
email: 'user@automatisch.io',
|
||||
roleId: userRole.id,
|
||||
});
|
||||
|
||||
const readFlowPermission = await createPermission({
|
||||
roleId: userRole.id,
|
||||
subject: 'Flow',
|
||||
action: 'read',
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
await createPermission({
|
||||
roleId: userRole.id,
|
||||
subject: 'App',
|
||||
action: 'read',
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
await createPermission({
|
||||
roleId: userRole.id,
|
||||
subject: 'Role',
|
||||
action: 'read',
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
await createPermission({
|
||||
roleId: userRole.id,
|
||||
subject: 'Config',
|
||||
action: 'read',
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
await createPermission({
|
||||
roleId: userRole.id,
|
||||
subject: 'SamlAuthProvider',
|
||||
action: 'read',
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
const userWithRoleAndPermissions = await user
|
||||
.$query()
|
||||
.withGraphFetched({ role: true, permissions: true });
|
||||
|
||||
expect(userWithRoleAndPermissions.permissions).toStrictEqual([
|
||||
readFlowPermission,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('$beforeInsert', () => {
|
||||
it('should call super.$beforeInsert', async () => {
|
||||
const superBeforeInsertSpy = vi
|
||||
.spyOn(User.prototype, '$beforeInsert')
|
||||
.mockResolvedValue();
|
||||
|
||||
await createUser();
|
||||
|
||||
expect(superBeforeInsertSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should lowercase the user email', async () => {
|
||||
const user = await createUser({
|
||||
fullName: 'Sample user',
|
||||
email: 'USER@AUTOMATISCH.IO',
|
||||
});
|
||||
|
||||
expect(user.email).toBe('user@automatisch.io');
|
||||
});
|
||||
|
||||
it('should generate password hash', async () => {
|
||||
const user = await createUser({
|
||||
fullName: 'Sample user',
|
||||
email: 'user@automatisch.io',
|
||||
password: 'sample-password',
|
||||
});
|
||||
|
||||
expect(user.password).not.toBe('sample-password');
|
||||
expect(await user.login('sample-password')).toBe(true);
|
||||
});
|
||||
|
||||
it('should start trial period if Automatisch is a cloud installation', async () => {
|
||||
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true);
|
||||
|
||||
const startTrialPeriodSpy = vi.spyOn(User.prototype, 'startTrialPeriod');
|
||||
|
||||
await createUser({
|
||||
fullName: 'Sample user',
|
||||
email: 'user@automatisch.io',
|
||||
});
|
||||
|
||||
expect(startTrialPeriodSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should not start trial period if Automatisch is not a cloud installation', async () => {
|
||||
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false);
|
||||
|
||||
const startTrialPeriodSpy = vi.spyOn(User.prototype, 'startTrialPeriod');
|
||||
|
||||
await createUser({
|
||||
fullName: 'Sample user',
|
||||
email: 'user@automatisch.io',
|
||||
});
|
||||
|
||||
expect(startTrialPeriodSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('$beforeUpdate', () => {
|
||||
it('should call super.$beforeUpdate', async () => {
|
||||
const superBeforeUpdateSpy = vi
|
||||
.spyOn(User.prototype, '$beforeUpdate')
|
||||
.mockResolvedValue();
|
||||
|
||||
const user = await createUser({
|
||||
fullName: 'Sample user',
|
||||
email: 'user@automatisch.io',
|
||||
});
|
||||
|
||||
await user.$query().patch({ fullName: 'Updated user name' });
|
||||
|
||||
expect(superBeforeUpdateSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should lowercase the user email if given', async () => {
|
||||
const user = await createUser({
|
||||
fullName: 'Sample user',
|
||||
email: 'user@automatisch.io',
|
||||
});
|
||||
|
||||
await user.$query().patchAndFetch({ email: 'NEW_EMAIL@AUTOMATISCH.IO' });
|
||||
|
||||
expect(user.email).toBe('new_email@automatisch.io');
|
||||
});
|
||||
|
||||
it('should generate password hash', async () => {
|
||||
const user = await createUser({
|
||||
fullName: 'Sample user',
|
||||
email: 'user@automatisch.io',
|
||||
password: 'sample-password',
|
||||
});
|
||||
|
||||
await user.$query().patchAndFetch({ password: 'new-password' });
|
||||
|
||||
expect(user.password).not.toBe('new-password');
|
||||
expect(await user.login('new-password')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('$afterInsert', () => {
|
||||
it('should call super.$afterInsert', async () => {
|
||||
const superAfterInsertSpy = vi.spyOn(User.prototype, '$afterInsert');
|
||||
|
||||
await createUser({
|
||||
fullName: 'Sample user',
|
||||
email: 'user@automatisch.io',
|
||||
});
|
||||
|
||||
expect(superAfterInsertSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should call createUsageData', async () => {
|
||||
const createUsageDataSpy = vi.spyOn(User.prototype, 'createUsageData');
|
||||
|
||||
await createUser({
|
||||
fullName: 'Sample user',
|
||||
email: 'user@automatisch.io',
|
||||
});
|
||||
|
||||
expect(createUsageDataSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
it('$afterFind should invoke omitEnterprisePermissionsWithoutValidLicense method', async () => {
|
||||
const omitEnterprisePermissionsWithoutValidLicenseSpy = vi.spyOn(
|
||||
User.prototype,
|
||||
'omitEnterprisePermissionsWithoutValidLicense'
|
||||
);
|
||||
|
||||
await createUser({
|
||||
fullName: 'Sample user',
|
||||
email: 'user@automatisch.io',
|
||||
});
|
||||
|
||||
expect(
|
||||
omitEnterprisePermissionsWithoutValidLicenseSpy
|
||||
).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,31 +1,4 @@
|
||||
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,
|
||||
};
|
||||
|
||||
const actionQueue = new Queue('action', redisConnection);
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await actionQueue.close();
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
import { generateQueue } from './queue.js';
|
||||
|
||||
const actionQueue = generateQueue('action');
|
||||
export default actionQueue;
|
||||
|
||||
@@ -1,31 +1,4 @@
|
||||
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,
|
||||
};
|
||||
|
||||
const deleteUserQueue = new Queue('delete-user', redisConnection);
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await deleteUserQueue.close();
|
||||
});
|
||||
|
||||
deleteUserQueue.on('error', (error) => {
|
||||
if (error.code === CONNECTION_REFUSED) {
|
||||
logger.error(
|
||||
'Make sure you have installed Redis and it is running.',
|
||||
error
|
||||
);
|
||||
|
||||
process.exit();
|
||||
}
|
||||
|
||||
logger.error('Error happened in delete user queue!', error);
|
||||
});
|
||||
import { generateQueue } from './queue.js';
|
||||
|
||||
const deleteUserQueue = generateQueue('delete-user');
|
||||
export default deleteUserQueue;
|
||||
|
||||
@@ -1,31 +1,4 @@
|
||||
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,
|
||||
};
|
||||
|
||||
const emailQueue = new Queue('email', redisConnection);
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await emailQueue.close();
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
import { generateQueue } from './queue.js';
|
||||
|
||||
const emailQueue = generateQueue('email');
|
||||
export default emailQueue;
|
||||
|
||||
@@ -1,31 +1,4 @@
|
||||
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,
|
||||
};
|
||||
|
||||
const flowQueue = new Queue('flow', redisConnection);
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await flowQueue.close();
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
import { generateQueue } from './queue.js';
|
||||
|
||||
const flowQueue = generateQueue('flow');
|
||||
export default flowQueue;
|
||||
|
||||
21
packages/backend/src/queues/index.js
Normal file
21
packages/backend/src/queues/index.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import appConfig from '../config/app.js';
|
||||
import actionQueue from './action.js';
|
||||
import emailQueue from './email.js';
|
||||
import flowQueue from './flow.js';
|
||||
import triggerQueue from './trigger.js';
|
||||
import deleteUserQueue from './delete-user.ee.js';
|
||||
import removeCancelledSubscriptionsQueue from './remove-cancelled-subscriptions.ee.js';
|
||||
|
||||
const queues = [
|
||||
actionQueue,
|
||||
emailQueue,
|
||||
flowQueue,
|
||||
triggerQueue,
|
||||
deleteUserQueue,
|
||||
];
|
||||
|
||||
if (appConfig.isCloud) {
|
||||
queues.push(removeCancelledSubscriptionsQueue);
|
||||
}
|
||||
|
||||
export default queues;
|
||||
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,44 +1,8 @@
|
||||
import process from 'process';
|
||||
import { Queue } from 'bullmq';
|
||||
import redisConfig from '../config/redis.js';
|
||||
import logger from '../helpers/logger.js';
|
||||
import { generateQueue } from './queue.js';
|
||||
|
||||
const CONNECTION_REFUSED = 'ECONNREFUSED';
|
||||
|
||||
const redisConnection = {
|
||||
connection: redisConfig,
|
||||
};
|
||||
|
||||
const removeCancelledSubscriptionsQueue = new Queue(
|
||||
const removeCancelledSubscriptionsQueue = generateQueue(
|
||||
'remove-cancelled-subscriptions',
|
||||
redisConnection
|
||||
{ runDaily: true }
|
||||
);
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await removeCancelledSubscriptionsQueue.close();
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
@@ -1,31 +1,4 @@
|
||||
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,
|
||||
};
|
||||
|
||||
const triggerQueue = new Queue('trigger', redisConnection);
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await triggerQueue.close();
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
import { generateQueue } from './queue.js';
|
||||
|
||||
const triggerQueue = generateQueue('trigger');
|
||||
export default triggerQueue;
|
||||
|
||||
@@ -4,10 +4,10 @@ import { authorizeAdmin } from '../../../../helpers/authorization.js';
|
||||
import { checkIsEnterprise } from '../../../../helpers/check-is-enterprise.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 getAuthClientsAction from '../../../../controllers/api/v1/admin/apps/get-auth-clients.ee.js';
|
||||
import getAuthClientAction from '../../../../controllers/api/v1/admin/apps/get-auth-client.ee.js';
|
||||
import createAuthClientAction from '../../../../controllers/api/v1/admin/apps/create-auth-client.ee.js';
|
||||
import updateAuthClientAction from '../../../../controllers/api/v1/admin/apps/update-auth-client.ee.js';
|
||||
import getOAuthClientsAction from '../../../../controllers/api/v1/admin/apps/get-oauth-clients.ee.js';
|
||||
import getOAuthClientAction from '../../../../controllers/api/v1/admin/apps/get-oauth-client.ee.js';
|
||||
import createOAuthClientAction from '../../../../controllers/api/v1/admin/apps/create-oauth-client.ee.js';
|
||||
import updateOAuthClientAction from '../../../../controllers/api/v1/admin/apps/update-oauth-client.ee.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -28,35 +28,35 @@ router.patch(
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:appKey/auth-clients',
|
||||
'/:appKey/oauth-clients',
|
||||
authenticateUser,
|
||||
authorizeAdmin,
|
||||
checkIsEnterprise,
|
||||
getAuthClientsAction
|
||||
getOAuthClientsAction
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/:appKey/auth-clients',
|
||||
'/:appKey/oauth-clients',
|
||||
authenticateUser,
|
||||
authorizeAdmin,
|
||||
checkIsEnterprise,
|
||||
createAuthClientAction
|
||||
createOAuthClientAction
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:appKey/auth-clients/:appAuthClientId',
|
||||
'/:appKey/oauth-clients/:oauthClientId',
|
||||
authenticateUser,
|
||||
authorizeAdmin,
|
||||
checkIsEnterprise,
|
||||
getAuthClientAction
|
||||
getOAuthClientAction
|
||||
);
|
||||
|
||||
router.patch(
|
||||
'/:appKey/auth-clients/:appAuthClientId',
|
||||
'/:appKey/oauth-clients/:oauthClientId',
|
||||
authenticateUser,
|
||||
authorizeAdmin,
|
||||
checkIsEnterprise,
|
||||
updateAuthClientAction
|
||||
updateOAuthClientAction
|
||||
);
|
||||
|
||||
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 getConnectionsAction from '../../../controllers/api/v1/apps/get-connections.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 getAuthClientAction from '../../../controllers/api/v1/apps/get-auth-client.ee.js';
|
||||
import getOAuthClientsAction from '../../../controllers/api/v1/apps/get-oauth-clients.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 getTriggerSubstepsAction from '../../../controllers/api/v1/apps/get-trigger-substeps.js';
|
||||
import getActionsAction from '../../../controllers/api/v1/apps/get-actions.js';
|
||||
@@ -44,17 +44,17 @@ router.get(
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:appKey/auth-clients',
|
||||
'/:appKey/oauth-clients',
|
||||
authenticateUser,
|
||||
checkIsEnterprise,
|
||||
getAuthClientsAction
|
||||
getOAuthClientsAction
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:appKey/auth-clients/:appAuthClientId',
|
||||
'/:appKey/oauth-clients/:oauthClientId',
|
||||
authenticateUser,
|
||||
checkIsEnterprise,
|
||||
getAuthClientAction
|
||||
getOAuthClientAction
|
||||
);
|
||||
|
||||
router.get('/:appKey/triggers', authenticateUser, getTriggersAction);
|
||||
|
||||
@@ -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) => {
|
||||
return {
|
||||
key: appConfig.key,
|
||||
customConnectionAllowed: appConfig.customConnectionAllowed,
|
||||
shared: appConfig.shared,
|
||||
useOnlyPredefinedAuthClients: appConfig.useOnlyPredefinedAuthClients,
|
||||
disabled: appConfig.disabled,
|
||||
connectionAllowed: appConfig.connectionAllowed,
|
||||
createdAt: appConfig.createdAt.getTime(),
|
||||
updatedAt: appConfig.updatedAt.getTime(),
|
||||
};
|
||||
|
||||
@@ -12,10 +12,8 @@ describe('appConfig serializer', () => {
|
||||
it('should return app config data', async () => {
|
||||
const expectedPayload = {
|
||||
key: appConfig.key,
|
||||
customConnectionAllowed: appConfig.customConnectionAllowed,
|
||||
shared: appConfig.shared,
|
||||
useOnlyPredefinedAuthClients: appConfig.useOnlyPredefinedAuthClients,
|
||||
disabled: appConfig.disabled,
|
||||
connectionAllowed: appConfig.connectionAllowed,
|
||||
createdAt: appConfig.createdAt.getTime(),
|
||||
updatedAt: appConfig.updatedAt.getTime(),
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ const appSerializer = (app) => {
|
||||
primaryColor: app.primaryColor,
|
||||
authDocUrl: app.authDocUrl,
|
||||
supportsConnections: app.supportsConnections,
|
||||
supportsOauthClients: app?.auth?.generateAuthUrl ? true : false,
|
||||
};
|
||||
|
||||
if (app.connectionCount) {
|
||||
|
||||
@@ -12,6 +12,7 @@ describe('appSerializer', () => {
|
||||
iconUrl: app.iconUrl,
|
||||
authDocUrl: app.authDocUrl,
|
||||
supportsConnections: app.supportsConnections,
|
||||
supportsOauthClients: app.auth.generateAuthUrl ? true : false,
|
||||
primaryColor: app.primaryColor,
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@ const authSerializer = (auth) => {
|
||||
return {
|
||||
fields: auth.fields,
|
||||
authenticationSteps: auth.authenticationSteps,
|
||||
sharedAuthenticationSteps: auth.sharedAuthenticationSteps,
|
||||
reconnectionSteps: auth.reconnectionSteps,
|
||||
sharedReconnectionSteps: auth.sharedReconnectionSteps,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ describe('authSerializer', () => {
|
||||
fields: auth.fields,
|
||||
authenticationSteps: auth.authenticationSteps,
|
||||
reconnectionSteps: auth.reconnectionSteps,
|
||||
sharedAuthenticationSteps: auth.sharedAuthenticationSteps,
|
||||
sharedReconnectionSteps: auth.sharedReconnectionSteps,
|
||||
};
|
||||
|
||||
expect(authSerializer(auth)).toStrictEqual(expectedPayload);
|
||||
|
||||
@@ -2,8 +2,7 @@ const connectionSerializer = (connection) => {
|
||||
return {
|
||||
id: connection.id,
|
||||
key: connection.key,
|
||||
reconnectable: connection.reconnectable,
|
||||
appAuthClientId: connection.appAuthClientId,
|
||||
oauthClientId: connection.oauthClientId,
|
||||
formattedData: {
|
||||
screenName: connection.formattedData.screenName,
|
||||
},
|
||||
|
||||
@@ -13,8 +13,7 @@ describe('connectionSerializer', () => {
|
||||
const expectedPayload = {
|
||||
id: connection.id,
|
||||
key: connection.key,
|
||||
reconnectable: connection.reconnectable,
|
||||
appAuthClientId: connection.appAuthClientId,
|
||||
oauthClientId: connection.oauthClientId,
|
||||
formattedData: {
|
||||
screenName: connection.formattedData.screenName,
|
||||
},
|
||||
|
||||
@@ -4,12 +4,13 @@ import permissionSerializer from './permission.js';
|
||||
import adminSamlAuthProviderSerializer from './admin-saml-auth-provider.ee.js';
|
||||
import samlAuthProviderSerializer from './saml-auth-provider.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 flowSerializer from './flow.js';
|
||||
import stepSerializer from './step.js';
|
||||
import connectionSerializer from './connection.js';
|
||||
import appSerializer from './app.js';
|
||||
import userAppSerializer from './user-app.js';
|
||||
import authSerializer from './auth.js';
|
||||
import triggerSerializer from './trigger.js';
|
||||
import actionSerializer from './action.js';
|
||||
@@ -26,13 +27,14 @@ const serializers = {
|
||||
Permission: permissionSerializer,
|
||||
AdminSamlAuthProvider: adminSamlAuthProviderSerializer,
|
||||
SamlAuthProvider: samlAuthProviderSerializer,
|
||||
SamlAuthProvidersRoleMapping: samlAuthProviderRoleMappingSerializer,
|
||||
AppAuthClient: appAuthClientSerializer,
|
||||
RoleMapping: samlAuthProviderRoleMappingSerializer,
|
||||
OAuthClient: oauthClientSerializer,
|
||||
AppConfig: appConfigSerializer,
|
||||
Flow: flowSerializer,
|
||||
Step: stepSerializer,
|
||||
Connection: connectionSerializer,
|
||||
App: appSerializer,
|
||||
UserApp: userAppSerializer,
|
||||
Auth: authSerializer,
|
||||
Trigger: triggerSerializer,
|
||||
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);
|
||||
});
|
||||
});
|
||||
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,20 +1,22 @@
|
||||
import * as Sentry from './helpers/sentry.ee.js';
|
||||
import appConfig from './config/app.js';
|
||||
import process from 'node:process';
|
||||
|
||||
Sentry.init();
|
||||
|
||||
import './config/orm.js';
|
||||
import './helpers/check-worker-readiness.js';
|
||||
import './workers/flow.js';
|
||||
import './workers/trigger.js';
|
||||
import './workers/action.js';
|
||||
import './workers/email.js';
|
||||
import './workers/delete-user.ee.js';
|
||||
import queues from './queues/index.js';
|
||||
import workers from './workers/index.js';
|
||||
|
||||
if (appConfig.isCloud) {
|
||||
import('./workers/remove-cancelled-subscriptions.ee.js');
|
||||
import('./queues/remove-cancelled-subscriptions.ee.js');
|
||||
}
|
||||
process.on('SIGTERM', async () => {
|
||||
for (const queue of queues) {
|
||||
await queue.close();
|
||||
}
|
||||
|
||||
for (const worker of workers) {
|
||||
await worker.close();
|
||||
}
|
||||
});
|
||||
|
||||
import telemetry from './helpers/telemetry/index.js';
|
||||
|
||||
|
||||
@@ -1,79 +1,6 @@
|
||||
import { Worker } from 'bullmq';
|
||||
import process from 'node:process';
|
||||
import { generateWorker } from './worker.js';
|
||||
import { executeActionJob } from '../jobs/execute-action.js';
|
||||
|
||||
import * as Sentry from '../helpers/sentry.ee.js';
|
||||
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 actionWorker = generateWorker('action', executeActionJob);
|
||||
|
||||
const DEFAULT_DELAY_DURATION = 0;
|
||||
|
||||
export const worker = 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 }
|
||||
);
|
||||
|
||||
worker.on('completed', (job) => {
|
||||
logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`);
|
||||
});
|
||||
|
||||
worker.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,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await worker.close();
|
||||
});
|
||||
export default actionWorker;
|
||||
|
||||
@@ -1,72 +1,6 @@
|
||||
import { Worker } from 'bullmq';
|
||||
import process from 'node:process';
|
||||
import { generateWorker } from './worker.js';
|
||||
import { deleteUserJob } from '../jobs/delete-user.ee.js';
|
||||
|
||||
import * as Sentry from '../helpers/sentry.ee.js';
|
||||
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 = generateWorker('delete-user', deleteUserJob);
|
||||
|
||||
export const worker = 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 }
|
||||
);
|
||||
|
||||
worker.on('completed', (job) => {
|
||||
logger.info(
|
||||
`JOB ID: ${job.id} - The user with the ID of '${job.data.id}' has been deleted!`
|
||||
);
|
||||
});
|
||||
|
||||
worker.on('failed', (job, err) => {
|
||||
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,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await worker.close();
|
||||
});
|
||||
export default deleteUserWorker;
|
||||
|
||||
@@ -1,65 +1,6 @@
|
||||
import { Worker } from 'bullmq';
|
||||
import process from 'node:process';
|
||||
import { generateWorker } from './worker.js';
|
||||
import { sendEmailJob } from '../jobs/send-email.js';
|
||||
|
||||
import * as Sentry from '../helpers/sentry.ee.js';
|
||||
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 emailWorker = generateWorker('email', sendEmailJob);
|
||||
|
||||
const isCloudSandbox = () => {
|
||||
return appConfig.isCloud && !appConfig.isProd;
|
||||
};
|
||||
|
||||
const isAutomatischEmail = (email) => {
|
||||
return email.endsWith('@automatisch.io');
|
||||
};
|
||||
|
||||
export const worker = 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 }
|
||||
);
|
||||
|
||||
worker.on('completed', (job) => {
|
||||
logger.info(
|
||||
`JOB ID: ${job.id} - ${job.data.subject} email sent to ${job.data.email}!`
|
||||
);
|
||||
});
|
||||
|
||||
worker.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,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await worker.close();
|
||||
});
|
||||
export default emailWorker;
|
||||
|
||||
@@ -1,100 +1,6 @@
|
||||
import { Worker } from 'bullmq';
|
||||
import process from 'node:process';
|
||||
import { generateWorker } from './worker.js';
|
||||
import { executeFlowJob } from '../jobs/execute-flow.js';
|
||||
|
||||
import * as Sentry from '../helpers/sentry.ee.js';
|
||||
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 = generateWorker('flow', executeFlowJob);
|
||||
|
||||
export const worker = 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 }
|
||||
);
|
||||
|
||||
worker.on('completed', (job) => {
|
||||
logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`);
|
||||
});
|
||||
|
||||
worker.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,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await worker.close();
|
||||
});
|
||||
export default flowWorker;
|
||||
|
||||
21
packages/backend/src/workers/index.js
Normal file
21
packages/backend/src/workers/index.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import appConfig from '../config/app.js';
|
||||
import actionWorker from './action.js';
|
||||
import emailWorker from './email.js';
|
||||
import flowWorker from './flow.js';
|
||||
import triggerWorker from './trigger.js';
|
||||
import deleteUserWorker from './delete-user.ee.js';
|
||||
import removeCancelledSubscriptionsWorker from './remove-cancelled-subscriptions.ee.js';
|
||||
|
||||
const workers = [
|
||||
actionWorker,
|
||||
emailWorker,
|
||||
flowWorker,
|
||||
triggerWorker,
|
||||
deleteUserWorker,
|
||||
];
|
||||
|
||||
if (appConfig.isCloud) {
|
||||
workers.push(removeCancelledSubscriptionsWorker);
|
||||
}
|
||||
|
||||
export default workers;
|
||||
@@ -1,47 +1,9 @@
|
||||
import { Worker } from 'bullmq';
|
||||
import process from 'node:process';
|
||||
import { DateTime } from 'luxon';
|
||||
import * as Sentry from '../helpers/sentry.ee.js';
|
||||
import redisConfig from '../config/redis.js';
|
||||
import logger from '../helpers/logger.js';
|
||||
import Subscription from '../models/subscription.ee.js';
|
||||
import { generateWorker } from './worker.js';
|
||||
import { removeCancelledSubscriptionsJob } from '../jobs/remove-cancelled-subscriptions.ee.js';
|
||||
|
||||
export const worker = new Worker(
|
||||
const removeCancelledSubscriptionsWorker = generateWorker(
|
||||
'remove-cancelled-subscriptions',
|
||||
async () => {
|
||||
await Subscription.query()
|
||||
.delete()
|
||||
.where({
|
||||
status: 'deleted',
|
||||
})
|
||||
.andWhere(
|
||||
'cancellation_effective_date',
|
||||
'<=',
|
||||
DateTime.now().startOf('day').toISODate()
|
||||
);
|
||||
},
|
||||
{ connection: redisConfig }
|
||||
removeCancelledSubscriptionsJob
|
||||
);
|
||||
|
||||
worker.on('completed', (job) => {
|
||||
logger.info(
|
||||
`JOB ID: ${job.id} - The cancelled subscriptions have been removed!`
|
||||
);
|
||||
});
|
||||
|
||||
worker.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,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await worker.close();
|
||||
});
|
||||
export default removeCancelledSubscriptionsWorker;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user