Compare commits

...

31 Commits

Author SHA1 Message Date
Faruk AYDIN
62b656add1 feat: Move canCustomConnect logic to static db field for AppConfig 2024-03-28 00:58:20 +01:00
Faruk AYDIN
40636119fb feat: Move canConnect logic into the database column for AppConfig 2024-03-28 00:49:20 +01:00
Faruk AYDIN
beb3b2cf45 chore: Correct the folder of get auth client mock 2024-03-27 14:04:16 +01:00
Faruk AYDIN
c2375ed3d4 chore: Remove old app auth client routers 2024-03-27 14:01:20 +01:00
Faruk AYDIN
4942cf8dae feat: Implement new admin get app auth client API endpoint 2024-03-27 14:00:16 +01:00
Faruk AYDIN
8f444eafa7 chore: Remove old admin app auth clients API endpoint 2024-03-27 13:44:01 +01:00
Faruk AYDIN
2484a0e631 feat: Implement new admin get auth clients API endpoint 2024-03-27 13:42:41 +01:00
Faruk AYDIN
d3747ad050 fix: Typo for the get auth clients test file 2024-03-27 13:42:18 +01:00
Faruk AYDIN
bb68a75636 feat: Implement new get app auth clients API endpoint 2024-03-26 23:58:35 +01:00
Faruk AYDIN
98131d633e refactor: Remove redundant appConfigId from get auth clients mock 2024-03-26 21:27:38 +01:00
Faruk AYDIN
e8193e0e17 feat: Make appKey column of app auth clients not nullable 2024-03-26 21:23:22 +01:00
Faruk AYDIN
74b7dd8f34 chore: Remove old app auth clients API endpoint 2024-03-26 21:19:44 +01:00
Faruk AYDIN
4f500e2d04 feat: Implement new get auth clients api endpoint 2024-03-26 21:17:58 +01:00
Faruk AYDIN
b53ddca8ce feat: Remove app config relation from app auth clients 2024-03-26 21:00:26 +01:00
Faruk AYDIN
70f30034ab feat: Remove app auth clients relation from app configs 2024-03-26 20:59:33 +01:00
Faruk AYDIN
fcd83909f7 feat: Remove app config id from app auth clients 2024-03-26 20:58:51 +01:00
Faruk AYDIN
eadb472af9 feat: Migrate app config id to app key 2024-03-26 20:56:50 +01:00
Faruk AYDIN
600316577e feat: Add appKey to app auth clients 2024-03-26 20:49:53 +01:00
Ali BARIN
9aa48c20e4 Merge pull request #1764 from automatisch/AUT-872
refactor: rewrite useDynamicFields with RQ
2024-03-26 16:58:43 +01:00
Rıdvan Akca
1b5d3beeca refactor: rewrite useDynamicFields with RQ 2024-03-26 18:50:36 +03:00
Ömer Faruk Aydın
00115d313e Merge pull request #1769 from automatisch/rest-logger
refactor: Use additional logger line only for graphQL
2024-03-26 16:25:12 +01:00
Faruk AYDIN
190f1a205f refactor: Use additional logger line only for graphQL 2024-03-26 15:39:02 +01:00
Ali BARIN
8ab6f0c3fe Merge pull request #1767 from automatisch/fix-trial-badge
fix: show trial status badge if user has trial
2024-03-26 13:52:40 +01:00
Rıdvan Akca
13eea263c0 fix: show trial status badge if user has trial 2024-03-26 15:45:26 +03:00
Ömer Faruk Aydın
b52b40962e Merge pull request #1768 from automatisch/rest-create-access-token
feat: Implement create access token API endpoint
2024-03-26 13:31:18 +01:00
Faruk AYDIN
7d1fa2e40c feat: Implement create access token API endpoint 2024-03-26 13:14:33 +01:00
Faruk AYDIN
93b2098829 refactor: Extract token generation logic to User model 2024-03-26 13:14:10 +01:00
Faruk AYDIN
a2acdc6b12 feat: Add draft version of renderError to renderer helper 2024-03-26 13:13:37 +01:00
Ali BARIN
38b2c1e30f Merge pull request #1765 from automatisch/refactor-get-app-config
refactor: Move app config endpoint to apps namespace
2024-03-26 12:38:42 +01:00
Rıdvan Akca
e07f579f3c refactor: update endpoint in useAppConfig 2024-03-25 19:12:45 +03:00
Faruk AYDIN
df3297b6ca refactor: Move app config endpoint to apps namespace 2024-03-25 17:01:16 +01:00
44 changed files with 448 additions and 304 deletions

View File

@@ -0,0 +1,13 @@
import User from '../../../../models/user.js';
import { renderObject, renderError } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const { email, password } = request.body;
const token = await User.authenticate(email, password);
if (token) {
return renderObject(response, { token });
}
renderError(response, [{ general: ['Incorrect email or password.'] }]);
};

View File

@@ -0,0 +1,39 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../../../app.js';
import { createUser } from '../../../../../test/factories/user';
describe('POST /api/v1/access-tokens', () => {
beforeEach(async () => {
await createUser({
email: 'user@automatisch.io',
password: 'password',
});
});
it('should return the token data with correct credentials', async () => {
const response = await request(app)
.post('/api/v1/access-tokens')
.send({
email: 'user@automatisch.io',
password: 'password',
})
.expect(200);
expect(response.body.data.token.length).toBeGreaterThan(0);
});
it('should return error with incorrect credentials', async () => {
const response = await request(app)
.post('/api/v1/access-tokens')
.send({
email: 'incorrect@email.com',
password: 'incorrectpassword',
})
.expect(422);
expect(response.body.errors.general).toEqual([
'Incorrect email or password.',
]);
});
});

View File

@@ -1,52 +0,0 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import Crypto from 'crypto';
import app from '../../../../../app.js';
import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js';
import { createUser } from '../../../../../../test/factories/user.js';
import getAdminAppAuthClientMock from '../../../../../../test/mocks/rest/api/v1/admin/app-auth-clients/get-app-auth-client.js';
import { createAppAuthClient } from '../../../../../../test/factories/app-auth-client.js';
import { createRole } from '../../../../../../test/factories/role.js';
import * as license from '../../../../../helpers/license.ee.js';
describe('GET /api/v1/admin/app-auth-clients/:appAuthClientId', () => {
let currentUser, currentUserRole, currentAppAuthClient, token;
describe('with valid license key', () => {
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
currentUserRole = await createRole({ key: 'admin' });
currentUser = await createUser({ roleId: currentUserRole.id });
currentAppAuthClient = await createAppAuthClient();
token = createAuthTokenByUserId(currentUser.id);
});
it('should return specified app auth client info', async () => {
const response = await request(app)
.get(`/api/v1/admin/app-auth-clients/${currentAppAuthClient.id}`)
.set('Authorization', token)
.expect(200);
const expectedPayload = getAdminAppAuthClientMock(currentAppAuthClient);
expect(response.body).toEqual(expectedPayload);
});
it('should return not found response for not existing app auth client UUID', async () => {
const notExistingAppAuthClientUUID = Crypto.randomUUID();
await request(app)
.get(`/api/v1/admin/app-auth-clients/${notExistingAppAuthClientUUID}`)
.set('Authorization', token)
.expect(404);
});
it('should return bad request response for invalid UUID', async () => {
await request(app)
.get('/api/v1/admin/app-auth-clients/invalidAppAuthClientUUID')
.set('Authorization', token)
.expect(400);
});
});
});

View File

@@ -1,41 +0,0 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
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 getAdminAppAuthClientsMock from '../../../../../../test/mocks/rest/api/v1/admin/app-auth-clients/get-app-auth-clients.js';
import { createAppAuthClient } from '../../../../../../test/factories/app-auth-client.js';
import { createRole } from '../../../../../../test/factories/role.js';
import * as license from '../../../../../helpers/license.ee.js';
describe('GET /api/v1/admin/app-auth-clients', () => {
let currentUser, currentUserRole, token;
describe('with valid license key', () => {
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
currentUserRole = await createRole({ key: 'admin' });
currentUser = await createUser({ roleId: currentUserRole.id });
token = createAuthTokenByUserId(currentUser.id);
});
it('should return app auth clients', async () => {
const appAuthClientOne = await createAppAuthClient();
const appAuthClientTwo = await createAppAuthClient();
const response = await request(app)
.get('/api/v1/admin/app-auth-clients')
.set('Authorization', token)
.expect(200);
const expectedPayload = getAdminAppAuthClientsMock([
appAuthClientTwo,
appAuthClientOne,
]);
expect(response.body).toEqual(expectedPayload);
});
});
});

View File

@@ -4,6 +4,7 @@ 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);

View File

@@ -0,0 +1,55 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import Crypto from 'crypto';
import app from '../../../../../app.js';
import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js';
import { createUser } from '../../../../../../test/factories/user.js';
import { 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 * as license from '../../../../../helpers/license.ee.js';
describe('GET /api/v1/admin/apps/:appKey/auth-clients/:appAuthClientId', () => {
let currentUser, adminRole, currentAppAuthClient, token;
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
adminRole = await createRole({ key: 'admin' });
currentUser = await createUser({ roleId: adminRole.id });
currentAppAuthClient = await createAppAuthClient({
appKey: 'deepl',
});
token = createAuthTokenByUserId(currentUser.id);
});
it('should return specified app auth client', async () => {
const response = await request(app)
.get(`/api/v1/admin/apps/deepl/auth-clients/${currentAppAuthClient.id}`)
.set('Authorization', token)
.expect(200);
const expectedPayload = getAppAuthClientMock(currentAppAuthClient);
expect(response.body).toEqual(expectedPayload);
});
it('should return not found response for not existing app auth client ID', async () => {
const notExistingAppAuthClientUUID = Crypto.randomUUID();
await request(app)
.get(
`/api/v1/admin/apps/deepl/auth-clients/${notExistingAppAuthClientUUID}`
)
.set('Authorization', token)
.expect(404);
});
it('should return bad request response for invalid UUID', async () => {
await request(app)
.get('/api/v1/admin/apps/deepl/auth-clients/invalidAppAuthClientUUID')
.set('Authorization', token)
.expect(400);
});
});

View File

@@ -2,10 +2,9 @@ import { renderObject } from '../../../../../helpers/renderer.js';
import AppAuthClient from '../../../../../models/app-auth-client.js';
export default async (request, response) => {
const appAuthClients = await AppAuthClient.query().orderBy(
'created_at',
'desc'
);
const appAuthClients = await AppAuthClient.query()
.where({ app_key: request.params.appKey })
.orderBy('created_at', 'desc');
renderObject(response, appAuthClients);
};

View File

@@ -0,0 +1,44 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
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 { 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 * as license from '../../../../../helpers/license.ee.js';
describe('GET /api/v1/admin/apps/:appKey/auth-clients', () => {
let currentUser, adminRole, token;
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
adminRole = await createRole({ key: 'admin' });
currentUser = await createUser({ roleId: adminRole.id });
token = createAuthTokenByUserId(currentUser.id);
});
it('should return specified app auth client info', async () => {
const appAuthClientOne = await createAppAuthClient({
appKey: 'deepl',
});
const appAuthClientTwo = await createAppAuthClient({
appKey: 'deepl',
});
const response = await request(app)
.get('/api/v1/admin/apps/deepl/auth-clients')
.set('Authorization', token)
.expect(200);
const expectedPayload = getAuthClientsMock([
appAuthClientTwo,
appAuthClientOne,
]);
expect(response.body).toEqual(expectedPayload);
});
});

View File

@@ -4,7 +4,7 @@ import AppAuthClient from '../../../../models/app-auth-client.js';
export default async (request, response) => {
const appAuthClient = await AppAuthClient.query()
.findById(request.params.appAuthClientId)
.where({ active: true })
.where({ app_key: request.params.appKey, active: true })
.throwIfNotFound();
renderObject(response, appAuthClient);

View File

@@ -4,25 +4,27 @@ 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/app-auth-clients/get-app-auth-client.js';
import getAppAuthClientMock from '../../../../../test/mocks/rest/api/v1/apps/get-auth-client.js';
import { createAppAuthClient } from '../../../../../test/factories/app-auth-client.js';
import * as license from '../../../../helpers/license.ee.js';
describe('GET /api/v1/app-auth-clients/:id', () => {
describe('GET /api/v1/apps/:appKey/auth-clients/:appAuthClientId', () => {
let currentUser, currentAppAuthClient, token;
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
currentUser = await createUser();
currentAppAuthClient = await createAppAuthClient();
currentAppAuthClient = await createAppAuthClient({
appKey: 'deepl',
});
token = createAuthTokenByUserId(currentUser.id);
});
it('should return specified app auth client info', async () => {
it('should return specified app auth client', async () => {
const response = await request(app)
.get(`/api/v1/app-auth-clients/${currentAppAuthClient.id}`)
.get(`/api/v1/apps/deepl/auth-clients/${currentAppAuthClient.id}`)
.set('Authorization', token)
.expect(200);
@@ -34,14 +36,14 @@ describe('GET /api/v1/app-auth-clients/:id', () => {
const notExistingAppAuthClientUUID = Crypto.randomUUID();
await request(app)
.get(`/api/v1/app-auth-clients/${notExistingAppAuthClientUUID}`)
.get(`/api/v1/apps/deepl/auth-clients/${notExistingAppAuthClientUUID}`)
.set('Authorization', token)
.expect(404);
});
it('should return bad request response for invalid UUID', async () => {
await request(app)
.get('/api/v1/app-auth-clients/invalidAppAuthClientUUID')
.get('/api/v1/apps/deepl/auth-clients/invalidAppAuthClientUUID')
.set('Authorization', token)
.expect(400);
});

View File

@@ -3,7 +3,7 @@ import AppAuthClient from '../../../../models/app-auth-client.js';
export default async (request, response) => {
const appAuthClients = await AppAuthClient.query()
.where({ active: true })
.where({ app_key: request.params.appKey, active: true })
.orderBy('created_at', 'desc');
renderObject(response, appAuthClients);

View File

@@ -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 getAppAuthClientsMock from '../../../../../test/mocks/rest/api/v1/app-auth-clients/get-app-auth-clients.js';
import getAuthClientsMock from '../../../../../test/mocks/rest/api/v1/apps/get-auth-clients.js';
import { createAppAuthClient } from '../../../../../test/factories/app-auth-client.js';
import * as license from '../../../../helpers/license.ee.js';
describe('GET /api/v1/app-auth-clients', () => {
describe('GET /api/v1/apps/:appKey/auth-clients', () => {
let currentUser, token;
beforeEach(async () => {
@@ -19,15 +19,20 @@ describe('GET /api/v1/app-auth-clients', () => {
});
it('should return specified app auth client info', async () => {
const appAuthClientOne = await createAppAuthClient();
const appAuthClientTwo = await createAppAuthClient();
const appAuthClientOne = await createAppAuthClient({
appKey: 'deepl',
});
const appAuthClientTwo = await createAppAuthClient({
appKey: 'deepl',
});
const response = await request(app)
.get('/api/v1/app-auth-clients')
.get('/api/v1/apps/deepl/auth-clients')
.set('Authorization', token)
.expect(200);
const expectedPayload = getAppAuthClientsMock([
const expectedPayload = getAuthClientsMock([
appAuthClientTwo,
appAuthClientOne,
]);

View File

@@ -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 getAppConfigMock from '../../../../../test/mocks/rest/api/v1/app-configs/get-app-config.js';
import getAppConfigMock from '../../../../../test/mocks/rest/api/v1/apps/get-config.js';
import { createAppConfig } from '../../../../../test/factories/app-config.js';
import * as license from '../../../../helpers/license.ee.js';
describe('GET /api/v1/app-configs/:appKey', () => {
describe('GET /api/v1/apps/:appKey/config', () => {
let currentUser, appConfig, token;
beforeEach(async () => {
@@ -27,7 +27,7 @@ describe('GET /api/v1/app-configs/:appKey', () => {
it('should return specified app config info', async () => {
const response = await request(app)
.get(`/api/v1/app-configs/${appConfig.key}`)
.get(`/api/v1/apps/${appConfig.key}/config`)
.set('Authorization', token)
.expect(200);
@@ -37,7 +37,7 @@ describe('GET /api/v1/app-configs/:appKey', () => {
it('should return not found response for not existing app key', async () => {
await request(app)
.get('/api/v1/app-configs/not-existing-app-key')
.get('/api/v1/apps/not-existing-app-key/config')
.set('Authorization', token)
.expect(404);
});

View File

@@ -0,0 +1,11 @@
export async function up(knex) {
await knex.schema.table('app_auth_clients', (table) => {
table.string('app_key');
});
}
export async function down(knex) {
await knex.schema.table('app_auth_clients', (table) => {
table.dropColumn('app_key');
});
}

View File

@@ -0,0 +1,17 @@
export async function up(knex) {
const appAuthClients = await knex('app_auth_clients').select('*');
for (const appAuthClient of appAuthClients) {
const appConfig = await knex('app_configs')
.where('id', appAuthClient.app_config_id)
.first();
await knex('app_auth_clients')
.where('id', appAuthClient.id)
.update({ app_key: appConfig.key });
}
}
export async function down() {
// void
}

View File

@@ -0,0 +1,15 @@
export async function up(knex) {
await knex.schema.table('app_auth_clients', (table) => {
table.dropColumn('app_config_id');
});
}
export async function down(knex) {
await knex.schema.table('app_auth_clients', (table) => {
table
.uuid('app_config_id')
.notNullable()
.references('id')
.inTable('app_configs');
});
}

View File

@@ -0,0 +1,11 @@
export async function up(knex) {
await knex.schema.table('app_auth_clients', (table) => {
table.string('app_key').notNullable().alter();
});
}
export async function down(knex) {
await knex.schema.table('app_auth_clients', (table) => {
table.string('app_key').nullable().alter();
});
}

View File

@@ -0,0 +1,11 @@
export async function up(knex) {
await knex.schema.table('app_configs', (table) => {
table.boolean('can_connect').defaultTo(false);
});
}
export async function down(knex) {
await knex.schema.table('app_configs', (table) => {
table.dropColumn('can_connect');
});
}

View File

@@ -0,0 +1,11 @@
export async function up(knex) {
await knex.schema.table('app_configs', (table) => {
table.boolean('can_custom_connect').defaultTo(false);
});
}
export async function down(knex) {
await knex.schema.table('app_configs', (table) => {
table.dropColumn('can_custom_connect');
});
}

View File

@@ -1,40 +0,0 @@
import App from '../../models/app.js';
import Step from '../../models/step.js';
import globalVariable from '../../helpers/global-variable.js';
const getDynamicFields = async (_parent, params, context) => {
const conditions = context.currentUser.can('update', 'Flow');
const userSteps = context.currentUser.$relatedQuery('steps');
const allSteps = Step.query();
const stepBaseQuery = conditions.isCreator ? userSteps : allSteps;
const step = await stepBaseQuery
.clone()
.withGraphFetched({
connection: true,
flow: true,
})
.findById(params.stepId);
if (!step) return null;
const connection = step.connection;
if (!step.appKey) return null;
const app = await App.findOneByKey(step.appKey);
const $ = await globalVariable({ connection, app, flow: step.flow, step });
const command = app.dynamicFields.find((data) => data.key === params.key);
for (const parameterKey in params.parameters) {
const parameterValue = params.parameters[parameterKey];
$.step.parameters[parameterKey] = parameterValue;
}
const additionalFields = (await command.run($)) || [];
return additionalFields;
};
export default getDynamicFields;

View File

@@ -4,7 +4,6 @@ import getAppAuthClients from './queries/get-app-auth-clients.ee.js';
import getBillingAndUsage from './queries/get-billing-and-usage.ee.js';
import getConnectedApps from './queries/get-connected-apps.js';
import getDynamicData from './queries/get-dynamic-data.js';
import getDynamicFields from './queries/get-dynamic-fields.js';
import getFlow from './queries/get-flow.js';
import getStepWithTestExecutions from './queries/get-step-with-test-executions.js';
import testConnection from './queries/test-connection.js';
@@ -16,7 +15,6 @@ const queryResolvers = {
getBillingAndUsage,
getConnectedApps,
getDynamicData,
getDynamicFields,
getFlow,
getStepWithTestExecutions,
testConnection,

View File

@@ -11,11 +11,6 @@ type Query {
key: String!
parameters: JSONObject
): JSONObject
getDynamicFields(
stepId: String!
key: String!
parameters: JSONObject
): [SubstepArgument]
getBillingAndUsage: GetBillingAndUsage
}

View File

@@ -9,7 +9,7 @@ const stream = {
const registerGraphQLToken = () => {
morgan.token('graphql-query', (req) => {
if (req.body.query) {
return `GraphQL ${req.body.query}`;
return `\n GraphQL ${req.body.query}`;
}
});
};
@@ -17,7 +17,7 @@ const registerGraphQLToken = () => {
registerGraphQLToken();
const morganMiddleware = morgan(
':method :url :status :res[content-length] - :response-time ms\n:graphql-query',
':method :url :status :res[content-length] - :response-time ms :graphql-query',
{ stream }
);

View File

@@ -44,4 +44,22 @@ const renderObject = (response, object, options) => {
return response.json(computedPayload);
};
export { renderObject };
const renderError = (response, errors, status, type) => {
const errorStatus = status || 422;
const errorType = type || 'ValidationError';
const payload = {
errors: errors.reduce((acc, error) => {
const key = Object.keys(error)[0];
acc[key] = error[key];
return acc;
}, {}),
meta: {
type: errorType,
},
};
return response.status(errorStatus).send(payload);
};
export { renderObject, renderError };

View File

@@ -9,11 +9,11 @@ class AppAuthClient extends Base {
static jsonSchema = {
type: 'object',
required: ['name', 'appConfigId', 'formattedAuthDefaults'],
required: ['name', 'appKey', 'formattedAuthDefaults'],
properties: {
id: { type: 'string', format: 'uuid' },
appConfigId: { type: 'string', format: 'uuid' },
appKey: { type: 'string' },
active: { type: 'boolean' },
authDefaults: { type: ['string', 'null'] },
formattedAuthDefaults: { type: 'object' },
@@ -22,17 +22,6 @@ class AppAuthClient extends Base {
},
};
static relationMappings = () => ({
appConfig: {
relation: Base.BelongsToOneRelation,
modelClass: AppConfig,
join: {
from: 'app_auth_clients.app_config_id',
to: 'app_configs.id',
},
},
});
encryptData() {
if (!this.eligibleForEncryption()) return;
@@ -71,6 +60,21 @@ class AppAuthClient extends Base {
this.encryptData();
}
async assignCanConnectForAppConfig() {
const appConfig = await AppConfig.query().findOne({ key: this.appKey });
await appConfig?.$query()?.patch({});
}
async $afterInsert(queryContext) {
await super.$afterInsert(queryContext);
await this.assignCanConnectForAppConfig();
}
async $afterUpdate(opt, queryContext) {
await super.$afterUpdate(opt, queryContext);
await this.assignCanConnectForAppConfig();
}
async $afterFind() {
this.decryptData();
}

View File

@@ -1,6 +1,6 @@
import App from './app.js';
import Base from './base.js';
import AppAuthClient from './app-auth-client.js';
import Base from './base.js';
class AppConfig extends Base {
static tableName = 'app_configs';
@@ -15,45 +15,56 @@ class AppConfig extends Base {
allowCustomConnection: { type: 'boolean', default: false },
shared: { type: 'boolean', default: false },
disabled: { type: 'boolean', default: false },
canConnect: { type: 'boolean', default: false },
canCustomConnect: { type: 'boolean', default: false },
},
};
static get virtualAttributes() {
return ['canConnect', 'canCustomConnect'];
}
static relationMappings = () => ({
appAuthClients: {
relation: Base.HasManyRelation,
modelClass: AppAuthClient,
join: {
from: 'app_configs.id',
to: 'app_auth_clients.app_config_id',
},
},
});
get canCustomConnect() {
return !this.disabled && this.allowCustomConnection;
}
get canConnect() {
const hasSomeActiveAppAuthClients = !!this.appAuthClients?.some(
(appAuthClient) => appAuthClient.active
);
const shared = this.shared;
const active = this.disabled === false;
const conditions = [hasSomeActiveAppAuthClients, shared, active];
return conditions.every(Boolean);
}
async getApp() {
if (!this.key) return null;
return await App.findOneByKey(this.key);
}
async hasActiveAppAuthClients() {
const appAuthClients = await AppAuthClient.query().where({
appKey: this.key,
});
const hasSomeActiveAppAuthClients = !!appAuthClients?.some(
(appAuthClient) => appAuthClient.active
);
return hasSomeActiveAppAuthClients;
}
async assignCanConnect() {
const shared = this.shared;
const active = this.disabled === false;
const hasSomeActiveAppAuthClients = await this.hasActiveAppAuthClients();
const conditions = [hasSomeActiveAppAuthClients, shared, active];
const canConnect = conditions.every(Boolean);
this.canConnect = canConnect;
}
async assignCanCustomConnect() {
const canCustomConnect = !this.disabled && this.allowCustomConnection;
this.canCustomConnect = canCustomConnect;
}
async $beforeInsert(queryContext) {
await super.$beforeInsert(queryContext);
await this.assignCanConnect();
await this.assignCanCustomConnect();
}
async $beforeUpdate(opt, queryContext) {
await super.$beforeUpdate(opt, queryContext);
await this.assignCanConnect();
await this.assignCanCustomConnect();
}
}
export default AppConfig;

View File

@@ -5,6 +5,7 @@ import crypto from 'node:crypto';
import appConfig from '../config/app.js';
import { hasValidLicense } from '../helpers/license.ee.js';
import userAbility from '../helpers/user-ability.js';
import createAuthTokenByUserId from '../helpers/create-auth-token-by-user-id.js';
import Base from './base.js';
import Connection from './connection.js';
import Execution from './execution.js';
@@ -161,6 +162,17 @@ class User extends Base {
: Execution.query();
}
static async authenticate(email, password) {
const user = await User.query().findOne({
email: email?.toLowerCase() || null,
});
if (user && (await user.login(password))) {
const token = createAuthTokenByUserId(user.id);
return token;
}
}
login(password) {
return bcrypt.compare(password, this.password);
}

View File

@@ -0,0 +1,9 @@
import { Router } from 'express';
import asyncHandler from 'express-async-handler';
import createAccessTokenAction from '../../../controllers/api/v1/access-tokens/create-access-token.js';
const router = Router();
router.post('/', asyncHandler(createAccessTokenAction));
export default router;

View File

@@ -3,25 +3,25 @@ import asyncHandler from 'express-async-handler';
import { authenticateUser } from '../../../../helpers/authentication.js';
import { authorizeAdmin } from '../../../../helpers/authorization.js';
import { checkIsEnterprise } from '../../../../helpers/check-is-enterprise.js';
import getAdminAppAuthClientsAction from '../../../../controllers/api/v1/admin/app-auth-clients/get-app-auth-clients.ee.js';
import getAdminAppAuthClientAction from '../../../../controllers/api/v1/admin/app-auth-clients/get-app-auth-client.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';
const router = Router();
router.get(
'/',
'/:appKey/auth-clients',
authenticateUser,
authorizeAdmin,
checkIsEnterprise,
asyncHandler(getAdminAppAuthClientsAction)
asyncHandler(getAuthClientsAction)
);
router.get(
'/:appAuthClientId',
'/:appKey/auth-clients/:appAuthClientId',
authenticateUser,
authorizeAdmin,
checkIsEnterprise,
asyncHandler(getAdminAppAuthClientAction)
asyncHandler(getAuthClientAction)
);
export default router;

View File

@@ -1,24 +0,0 @@
import { Router } from 'express';
import asyncHandler from 'express-async-handler';
import { authenticateUser } from '../../../helpers/authentication.js';
import { checkIsEnterprise } from '../../../helpers/check-is-enterprise.js';
import getAppAuthClientAction from '../../../controllers/api/v1/app-auth-clients/get-app-auth-client.js';
import getAppAuthClientsAction from '../../../controllers/api/v1/app-auth-clients/get-app-auth-clients.js';
const router = Router();
router.get(
'/',
authenticateUser,
checkIsEnterprise,
asyncHandler(getAppAuthClientsAction)
);
router.get(
'/:appAuthClientId',
authenticateUser,
checkIsEnterprise,
asyncHandler(getAppAuthClientAction)
);
export default router;

View File

@@ -1,16 +0,0 @@
import { Router } from 'express';
import asyncHandler from 'express-async-handler';
import { authenticateUser } from '../../../helpers/authentication.js';
import { checkIsEnterprise } from '../../../helpers/check-is-enterprise.js';
import getAppConfigAction from '../../../controllers/api/v1/app-configs/get-app-config.ee.js';
const router = Router();
router.get(
'/:appKey',
authenticateUser,
checkIsEnterprise,
asyncHandler(getAppConfigAction)
);
export default router;

View File

@@ -2,9 +2,13 @@ import { Router } from 'express';
import asyncHandler from 'express-async-handler';
import { authenticateUser } from '../../../helpers/authentication.js';
import { authorizeUser } from '../../../helpers/authorization.js';
import { checkIsEnterprise } from '../../../helpers/check-is-enterprise.js';
import getAppAction from '../../../controllers/api/v1/apps/get-app.js';
import getAppsAction from '../../../controllers/api/v1/apps/get-apps.js';
import getAuthAction from '../../../controllers/api/v1/apps/get-auth.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 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';
@@ -17,6 +21,27 @@ router.get('/', authenticateUser, asyncHandler(getAppsAction));
router.get('/:appKey', authenticateUser, asyncHandler(getAppAction));
router.get('/:appKey/auth', authenticateUser, asyncHandler(getAuthAction));
router.get(
'/:appKey/config',
authenticateUser,
checkIsEnterprise,
asyncHandler(getConfigAction)
);
router.get(
'/:appKey/auth-clients',
authenticateUser,
checkIsEnterprise,
asyncHandler(getAuthClientsAction)
);
router.get(
'/:appKey/auth-clients/:appAuthClientId',
authenticateUser,
checkIsEnterprise,
asyncHandler(getAuthClientAction)
);
router.get(
'/:appKey/triggers',
authenticateUser,

View File

@@ -4,21 +4,20 @@ import webhooksRouter from './webhooks.js';
import paddleRouter from './paddle.ee.js';
import healthcheckRouter from './healthcheck.js';
import automatischRouter from './api/v1/automatisch.js';
import accessTokensRouter from './api/v1/access-tokens.js';
import usersRouter from './api/v1/users.js';
import paymentRouter from './api/v1/payment.ee.js';
import appAuthClientsRouter from './api/v1/app-auth-clients.js';
import appConfigsRouter from './api/v1/app-configs.ee.js';
import flowsRouter from './api/v1/flows.js';
import stepsRouter from './api/v1/steps.js';
import appsRouter from './api/v1/apps.js';
import connectionsRouter from './api/v1/connections.js';
import executionsRouter from './api/v1/executions.js';
import samlAuthProvidersRouter from './api/v1/saml-auth-providers.ee.js';
import adminAppsRouter from './api/v1/admin/apps.ee.js';
import adminSamlAuthProvidersRouter from './api/v1/admin/saml-auth-providers.ee.js';
import rolesRouter from './api/v1/admin/roles.ee.js';
import permissionsRouter from './api/v1/admin/permissions.ee.js';
import adminUsersRouter from './api/v1/admin/users.ee.js';
import adminAppAuthClientsRouter from './api/v1/admin/app-auth-clients.ee.js';
const router = Router();
@@ -27,20 +26,19 @@ router.use('/webhooks', webhooksRouter);
router.use('/paddle', paddleRouter);
router.use('/healthcheck', healthcheckRouter);
router.use('/api/v1/automatisch', automatischRouter);
router.use('/api/v1/access-tokens', accessTokensRouter);
router.use('/api/v1/users', usersRouter);
router.use('/api/v1/payment', paymentRouter);
router.use('/api/v1/app-auth-clients', appAuthClientsRouter);
router.use('/api/v1/app-configs', appConfigsRouter);
router.use('/api/v1/flows', flowsRouter);
router.use('/api/v1/steps', stepsRouter);
router.use('/api/v1/apps', appsRouter);
router.use('/api/v1/connections', connectionsRouter);
router.use('/api/v1/flows', flowsRouter);
router.use('/api/v1/steps', stepsRouter);
router.use('/api/v1/executions', executionsRouter);
router.use('/api/v1/saml-auth-providers', samlAuthProvidersRouter);
router.use('/api/v1/admin/saml-auth-providers', adminSamlAuthProvidersRouter);
router.use('/api/v1/admin/apps', adminAppsRouter);
router.use('/api/v1/admin/users', adminUsersRouter);
router.use('/api/v1/admin/roles', rolesRouter);
router.use('/api/v1/admin/permissions', permissionsRouter);
router.use('/api/v1/admin/users', adminUsersRouter);
router.use('/api/v1/admin/app-auth-clients', adminAppAuthClientsRouter);
router.use('/api/v1/admin/saml-auth-providers', adminSamlAuthProvidersRouter);
export default router;

View File

@@ -1,5 +1,4 @@
import { faker } from '@faker-js/faker';
import { createAppConfig } from './app-config.js';
import AppAuthClient from '../../src/models/app-auth-client';
const formattedAuthDefaults = {
@@ -12,7 +11,7 @@ const formattedAuthDefaults = {
export const createAppAuthClient = async (params = {}) => {
params.name = params?.name || faker.person.fullName();
params.id = params?.id || faker.string.uuid();
params.appConfigId = params?.appConfigId || (await createAppConfig()).id;
params.appKey = params?.appKey || 'deepl';
params.active = params?.active ?? true;
params.formattedAuthDefaults =
params?.formattedAuthDefaults || formattedAuthDefaults;

View File

@@ -1,7 +1,6 @@
const getAdminAppAuthClientsMock = (appAuthClients) => {
return {
data: appAuthClients.map((appAuthClient) => ({
appConfigId: appAuthClient.appConfigId,
name: appAuthClient.name,
id: appAuthClient.id,
active: appAuthClient.active,

View File

@@ -1,9 +1,9 @@
const getAdminAppAuthClientMock = (appAuthClient) => {
const getAppAuthClientMock = (appAuthClient) => {
return {
data: {
appConfigId: appAuthClient.appConfigId,
name: appAuthClient.name,
id: appAuthClient.id,
appConfigId: appAuthClient.appConfigId,
active: appAuthClient.active,
},
meta: {
@@ -16,4 +16,4 @@ const getAdminAppAuthClientMock = (appAuthClient) => {
};
};
export default getAdminAppAuthClientMock;
export default getAppAuthClientMock;

View File

@@ -1,7 +1,6 @@
const getAppAuthClientsMock = (appAuthClients) => {
return {
data: appAuthClients.map((appAuthClient) => ({
appConfigId: appAuthClient.appConfigId,
name: appAuthClient.name,
id: appAuthClient.id,
active: appAuthClient.active,

View File

@@ -1,6 +1,7 @@
import * as React from 'react';
import MuiTextField from '@mui/material/TextField';
import CircularProgress from '@mui/material/CircularProgress';
import useDynamicFields from 'hooks/useDynamicFields';
import useDynamicData from 'hooks/useDynamicData';
import PowerInput from 'components/PowerInput';
@@ -8,8 +9,10 @@ import TextField from 'components/TextField';
import ControlledAutocomplete from 'components/ControlledAutocomplete';
import ControlledCustomAutocomplete from 'components/ControlledCustomAutocomplete';
import DynamicField from 'components/DynamicField';
const optionGenerator = (options) =>
options?.map(({ name, value }) => ({ label: name, value: value }));
export default function InputCreator(props) {
const {
onChange,
@@ -31,9 +34,12 @@ export default function InputCreator(props) {
type,
} = schema;
const { data, loading } = useDynamicData(stepId, schema);
const { data: additionalFields, loading: additionalFieldsLoading } =
const { data: additionalFieldsData, isLoading: isDynamicFieldsLoading } =
useDynamicFields(stepId, schema);
const additionalFields = additionalFieldsData?.data;
const computedName = namePrefix ? `${namePrefix}.${name}` : name;
if (type === 'dynamic') {
return (
<DynamicField
@@ -50,8 +56,10 @@ export default function InputCreator(props) {
/>
);
}
if (type === 'dropdown') {
const preparedOptions = schema.options || optionGenerator(data);
return (
<React.Fragment>
{!schema.variables && (
@@ -63,7 +71,9 @@ export default function InputCreator(props) {
disablePortal
disableClearable={required}
options={preparedOptions}
renderInput={(params) => <MuiTextField {...params} label={label} required={required}/>}
renderInput={(params) => (
<MuiTextField {...params} label={label} required={required} />
)}
defaultValue={value}
description={description}
loading={loading}
@@ -93,7 +103,7 @@ export default function InputCreator(props) {
/>
)}
{additionalFieldsLoading && !additionalFields?.length && (
{isDynamicFieldsLoading && !additionalFields?.length && (
<div>
<CircularProgress sx={{ display: 'block', margin: '20px auto' }} />
</div>
@@ -113,6 +123,7 @@ export default function InputCreator(props) {
</React.Fragment>
);
}
if (type === 'string') {
if (schema.variables) {
return (
@@ -127,7 +138,7 @@ export default function InputCreator(props) {
shouldUnregister={shouldUnregister}
/>
{additionalFieldsLoading && !additionalFields?.length && (
{isDynamicFieldsLoading && !additionalFields?.length && (
<div>
<CircularProgress
sx={{ display: 'block', margin: '20px auto' }}
@@ -149,6 +160,7 @@ export default function InputCreator(props) {
</React.Fragment>
);
}
return (
<React.Fragment>
<TextField
@@ -168,7 +180,7 @@ export default function InputCreator(props) {
shouldUnregister={shouldUnregister}
/>
{additionalFieldsLoading && !additionalFields?.length && (
{isDynamicFieldsLoading && !additionalFields?.length && (
<div>
<CircularProgress sx={{ display: 'block', margin: '20px auto' }} />
</div>

View File

@@ -5,7 +5,7 @@ export default function useAppConfig(appKey) {
const query = useQuery({
queryKey: ['appConfig', appKey],
queryFn: async ({ signal }) => {
const { data } = await api.get(`/v1/app-configs/${appKey}`, {
const { data } = await api.get(`/v1/apps/${appKey}/config`, {
signal,
});

View File

@@ -1,27 +1,35 @@
import * as React from 'react';
import { useLazyQuery } from '@apollo/client';
import { useFormContext } from 'react-hook-form';
import set from 'lodash/set';
import isEqual from 'lodash/isEqual';
import { GET_DYNAMIC_FIELDS } from 'graphql/queries/get-dynamic-fields';
import { useQuery } from '@tanstack/react-query';
import api from 'helpers/api';
const variableRegExp = /({.*?})/;
// TODO: extract this function to a separate file
function computeArguments(args, getValues) {
const initialValue = {};
return args.reduce((result, { name, value }) => {
const isVariable = variableRegExp.test(value);
if (isVariable) {
const sanitizedFieldPath = value.replace(/{|}/g, '');
const computedValue = getValues(sanitizedFieldPath);
if (computedValue === undefined || computedValue === '')
throw new Error(`The ${sanitizedFieldPath} field is required.`);
set(result, name, computedValue);
return result;
}
set(result, name, value);
return result;
}, initialValue);
}
/**
* Fetch the dynamic fields for the given step.
* This hook must be within a react-hook-form context.
@@ -31,10 +39,9 @@ function computeArguments(args, getValues) {
*/
function useDynamicFields(stepId, schema) {
const lastComputedVariables = React.useRef({});
const [getDynamicFields, { called, data, loading }] =
useLazyQuery(GET_DYNAMIC_FIELDS);
const { getValues } = useFormContext();
const formValues = getValues();
/**
* Return `null` when even a field is missing value.
*
@@ -58,31 +65,28 @@ function useDynamicFields(stepId, schema) {
return null;
}
}
return null;
/**
* `formValues` is to trigger recomputation when form is updated.
* `getValues` is for convenience as it supports paths for fields like `getValues('foo.bar.baz')`.
*/
}, [schema, formValues, getValues]);
React.useEffect(() => {
if (
schema.type === 'dropdown' &&
stepId &&
schema.additionalFields &&
computedVariables
) {
getDynamicFields({
variables: {
stepId,
...computedVariables,
},
const query = useQuery({
queryKey: ['dynamicFields', stepId, computedVariables],
queryFn: async () => {
const { data } = await api.post(`/v1/steps/${stepId}/dynamic-fields`, {
dynamicFieldsKey: computedVariables.key,
parameters: computedVariables.parameters,
});
}
}, [getDynamicFields, stepId, schema, computedVariables]);
return {
called,
data: data?.getDynamicFields,
loading,
};
return data;
},
enabled: !!stepId && !!computedVariables,
});
return query;
}
export default useDynamicFields;

View File

@@ -79,7 +79,7 @@ export default function useUserTrial() {
[checkoutCompleted, hasTrial, setIsPolling],
);
if (isUserTrialLoading || !userTrial) return null;
if (isUserTrialLoading || !hasTrial) return null;
const expireAt = DateTime.fromISO(userTrial?.expireAt).startOf('day');