Compare commits

..

30 Commits

Author SHA1 Message Date
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
53 changed files with 429 additions and 466 deletions

View File

@@ -31,7 +31,6 @@
"accounting": "^0.4.1",
"ajv-formats": "^2.1.1",
"axios": "1.6.0",
"basic-ftp": "^5.0.5",
"bcrypt": "^5.0.1",
"bullmq": "^3.0.0",
"cors": "^2.8.5",

View File

@@ -1 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 122.88 109.36" style="enable-background:new 0 0 122.88 109.36" xml:space="preserve"><g><path d="M14.69,16.44h3.56V8.63c0-1.09,0.88-1.97,1.97-1.97h3.5V1.97C23.72,0.88,24.6,0,25.69,0h31.92c1.09,0,1.97,0.88,1.97,1.97 v4.69h43.05c1.09,0,1.97,0.88,1.97,1.97v7.82h3.59c1.09,0,1.97,0.88,1.97,1.97c0,0.14-0.01,0.28-0.04,0.42l-6.88,50.59 c-0.22,1.65-0.95,3.18-2.05,4.29c-1.1,1.1-2.54,1.78-4.25,1.78H70.95v5.86h2.12c1.54,0,2.8,1.26,2.8,2.8v5.06h10.44 c1.25,0,2.32,0.83,2.67,1.97h26.1c3.73,0,6.14,2.07,7.2,4.71c0.4,1,0.6,2.08,0.6,3.14c0,1.06-0.2,2.14-0.6,3.14 c-1.06,2.64-3.47,4.71-7.2,4.71H89.09c-0.15,1.4-1.35,2.49-2.78,2.49H72.63H38.8c-1.44,0-2.63-1.1-2.78-2.49H7.8 c-3.73,0-6.14-2.07-7.2-4.71c-0.4-1-0.6-2.08-0.6-3.14c0-1.06,0.2-2.14,0.6-3.14c1.06-2.64,3.47-4.71,7.2-4.71h28.32 c0.36-1.14,1.42-1.97,2.67-1.97h8.74v-5.06c0-1.54,1.26-2.8,2.8-2.8h1.69v-5.86H24.38c-1.71,0-3.19-0.69-4.3-1.79 c-1.11-1.11-1.83-2.66-2.01-4.33l-5.33-50.74c-0.11-1.08,0.67-2.04,1.75-2.15C14.56,16.45,14.63,16.45,14.69,16.44L14.69,16.44 L14.69,16.44L14.69,16.44z M31.38,34.43h16.29v4.58H38v3.72h8.26v4.3H38v8.71h-6.61V34.43L31.38,34.43z M50.08,34.43H70.1v5.27 h-6.72v16.05h-6.59V39.7h-6.71V34.43L50.08,34.43z M73.41,34.43h10.95c2.38,0,4.17,0.57,5.35,1.7c1.19,1.14,1.78,2.75,1.78,4.84 c0,2.15-0.65,3.84-1.94,5.05c-1.3,1.21-3.27,1.82-5.93,1.82h-3.6v7.91h-6.61V34.43L73.41,34.43z M80.03,43.52h1.61 c1.27,0,2.16-0.22,2.68-0.66c0.51-0.44,0.77-1,0.77-1.69c0-0.67-0.22-1.24-0.67-1.7c-0.44-0.47-1.28-0.7-2.51-0.7h-1.88V43.52 L80.03,43.52z M56.38,81.34h10.21v-5.79H56.38V81.34L56.38,81.34z M89.11,94.88v8.27h25.97c1.97,0,3.22-1.04,3.76-2.37 c0.22-0.54,0.33-1.15,0.33-1.76s-0.11-1.22-0.33-1.76c-0.54-1.33-1.79-2.37-3.76-2.37H89.11L89.11,94.88z M36,103.15v-8.27H7.8 c-1.97,0-3.22,1.04-3.76,2.37c-0.22,0.54-0.33,1.15-0.33,1.76s0.11,1.22,0.33,1.76c0.54,1.33,1.79,2.37,3.76,2.37H36L36,103.15z M20.23,20.38h-3.35l5.1,48.57c0.08,0.78,0.39,1.47,0.87,1.95c0.4,0.4,0.92,0.64,1.53,0.64h72.54c0.59,0,1.1-0.24,1.48-0.62 c0.49-0.49,0.82-1.22,0.93-2.03l6.6-48.51L20.23,20.38L20.23,20.38L20.23,20.38z M22.19,10.6v5.83l78.46-0.83v-5H57.61 c-1.09,0-1.97-0.88-1.97-1.97V3.94H27.66v4.69c0,1.09-0.88,1.97-1.97,1.97L22.19,10.6L22.19,10.6L22.19,10.6z"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -1,91 +0,0 @@
import verifyCredentials from './verify-credentials.js';
import isStillVerified from './is-still-verified.js';
export default {
fields: [
{
key: 'screenName',
label: 'Screen Name',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description:
'Screen name of your connection to be used on Automatisch UI.',
clickToCopy: false,
},
{
key: 'host',
label: 'Host',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: 'The host information Automatisch will connect to.',
docUrl: 'https://automatisch.io/docs/ftp#host',
clickToCopy: false,
},
{
key: 'username',
label: 'Email/Username',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: 'Your FTP login credentials.',
docUrl: 'https://automatisch.io/docs/ftp#username',
clickToCopy: false,
},
{
key: 'password',
label: 'Password',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: null,
docUrl: 'https://automatisch.io/docs/ftp#password',
clickToCopy: false,
},
{
key: 'secure',
label: 'Secure',
type: 'dropdown',
required: false,
readOnly: false,
value: false,
placeholder: null,
description: null,
docUrl: 'https://automatisch.io/docs/ftp#secure',
clickToCopy: false,
options: [
{
label: 'Yes',
value: true,
},
{
label: 'No',
value: false,
},
],
},
{
key: 'port',
label: 'Port',
type: 'string',
required: false,
readOnly: false,
value: '21',
placeholder: null,
description: null,
docUrl: 'https://automatisch.io/docs/ftp#port',
clickToCopy: false,
},
],
verifyCredentials,
isStillVerified,
};

View File

@@ -1,8 +0,0 @@
import verifyCredentials from './verify-credentials.js';
const isStillVerified = async ($) => {
await verifyCredentials($);
return true;
};
export default isStillVerified;

View File

@@ -1,19 +0,0 @@
import { Client } from 'basic-ftp';
const verifyCredentials = async ($) => {
const client = new Client();
client.ftp.verbose = true;
await client.access({
host: $.auth.data.host,
user: $.auth.data.username,
password: $.auth.data.password,
secure: $.auth.data.secure,
});
await $.auth.set({
screenName: $.auth.data.screenName,
});
};
export default verifyCredentials;

View File

@@ -1,14 +0,0 @@
import defineApp from '../../helpers/define-app.js';
import auth from './auth/index.js';
export default defineApp({
name: 'FTP',
key: 'ftp',
iconUrl: '{BASE_URL}/apps/ftp/assets/favicon.svg',
authDocUrl: 'https://automatisch.io/docs/apps/ftp/connection',
supportsConnections: true,
baseUrl: '',
apiBaseUrl: '',
primaryColor: '000000',
auth,
});

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

@@ -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?.assignCanConnect();
}
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,48 @@ class AppConfig extends Base {
allowCustomConnection: { type: 'boolean', default: false },
shared: { type: 'boolean', default: false },
disabled: { type: 'boolean', default: false },
canConnect: { 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 $beforeInsert(queryContext) {
await super.$beforeInsert(queryContext);
await this.assignCanConnect();
}
async $beforeUpdate(opt, queryContext) {
await super.$beforeUpdate(opt, queryContext);
await this.assignCanConnect();
}
}
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

@@ -113,12 +113,6 @@ export default defineConfig({
{ text: 'Connection', link: '/apps/formatter/connection' },
],
},
{
text: 'FTP',
collapsible: true,
collapsed: true,
items: [{ text: 'Connection', link: '/apps/ftp/connection' }],
},
{
text: 'Ghost',
collapsible: true,

View File

@@ -1,16 +0,0 @@
# FTP
:::info
This page explains the steps you need to follow to set up the FTP connection in Automatisch. If any of the steps are outdated, please let us know!
:::
FTP, which stands for File Transfer Protocol, is a standard network protocol used for the transfer of computer files between a client and server on a computer network. t's commonly used to download files from a server or upload files to a server.. You need to provide the following information from Automatisch by using FTP connection.
1. Write any screen name to be displayed in Automatisch.
1. Fill host address field with the FTP host address.
1. Fill username field with the FTP username.
1. Fill password field with the FTP password.
1. Select the Secure field.
1. Fill port field with the FTP port. Default is 21.
1. Click **Submit** button on Automatisch.
1. Now, you can start using the FTP connection with Automatisch.

View File

@@ -1 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 122.88 109.36" style="enable-background:new 0 0 122.88 109.36" xml:space="preserve"><g><path d="M14.69,16.44h3.56V8.63c0-1.09,0.88-1.97,1.97-1.97h3.5V1.97C23.72,0.88,24.6,0,25.69,0h31.92c1.09,0,1.97,0.88,1.97,1.97 v4.69h43.05c1.09,0,1.97,0.88,1.97,1.97v7.82h3.59c1.09,0,1.97,0.88,1.97,1.97c0,0.14-0.01,0.28-0.04,0.42l-6.88,50.59 c-0.22,1.65-0.95,3.18-2.05,4.29c-1.1,1.1-2.54,1.78-4.25,1.78H70.95v5.86h2.12c1.54,0,2.8,1.26,2.8,2.8v5.06h10.44 c1.25,0,2.32,0.83,2.67,1.97h26.1c3.73,0,6.14,2.07,7.2,4.71c0.4,1,0.6,2.08,0.6,3.14c0,1.06-0.2,2.14-0.6,3.14 c-1.06,2.64-3.47,4.71-7.2,4.71H89.09c-0.15,1.4-1.35,2.49-2.78,2.49H72.63H38.8c-1.44,0-2.63-1.1-2.78-2.49H7.8 c-3.73,0-6.14-2.07-7.2-4.71c-0.4-1-0.6-2.08-0.6-3.14c0-1.06,0.2-2.14,0.6-3.14c1.06-2.64,3.47-4.71,7.2-4.71h28.32 c0.36-1.14,1.42-1.97,2.67-1.97h8.74v-5.06c0-1.54,1.26-2.8,2.8-2.8h1.69v-5.86H24.38c-1.71,0-3.19-0.69-4.3-1.79 c-1.11-1.11-1.83-2.66-2.01-4.33l-5.33-50.74c-0.11-1.08,0.67-2.04,1.75-2.15C14.56,16.45,14.63,16.45,14.69,16.44L14.69,16.44 L14.69,16.44L14.69,16.44z M31.38,34.43h16.29v4.58H38v3.72h8.26v4.3H38v8.71h-6.61V34.43L31.38,34.43z M50.08,34.43H70.1v5.27 h-6.72v16.05h-6.59V39.7h-6.71V34.43L50.08,34.43z M73.41,34.43h10.95c2.38,0,4.17,0.57,5.35,1.7c1.19,1.14,1.78,2.75,1.78,4.84 c0,2.15-0.65,3.84-1.94,5.05c-1.3,1.21-3.27,1.82-5.93,1.82h-3.6v7.91h-6.61V34.43L73.41,34.43z M80.03,43.52h1.61 c1.27,0,2.16-0.22,2.68-0.66c0.51-0.44,0.77-1,0.77-1.69c0-0.67-0.22-1.24-0.67-1.7c-0.44-0.47-1.28-0.7-2.51-0.7h-1.88V43.52 L80.03,43.52z M56.38,81.34h10.21v-5.79H56.38V81.34L56.38,81.34z M89.11,94.88v8.27h25.97c1.97,0,3.22-1.04,3.76-2.37 c0.22-0.54,0.33-1.15,0.33-1.76s-0.11-1.22-0.33-1.76c-0.54-1.33-1.79-2.37-3.76-2.37H89.11L89.11,94.88z M36,103.15v-8.27H7.8 c-1.97,0-3.22,1.04-3.76,2.37c-0.22,0.54-0.33,1.15-0.33,1.76s0.11,1.22,0.33,1.76c0.54,1.33,1.79,2.37,3.76,2.37H36L36,103.15z M20.23,20.38h-3.35l5.1,48.57c0.08,0.78,0.39,1.47,0.87,1.95c0.4,0.4,0.92,0.64,1.53,0.64h72.54c0.59,0,1.1-0.24,1.48-0.62 c0.49-0.49,0.82-1.22,0.93-2.03l6.6-48.51L20.23,20.38L20.23,20.38L20.23,20.38z M22.19,10.6v5.83l78.46-0.83v-5H57.61 c-1.09,0-1.97-0.88-1.97-1.97V3.94H27.66v4.69c0,1.09-0.88,1.97-1.97,1.97L22.19,10.6L22.19,10.6L22.19,10.6z"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

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');

View File

@@ -5471,11 +5471,6 @@ basic-auth@^2.0.1, basic-auth@~2.0.1:
dependencies:
safe-buffer "5.1.2"
basic-ftp@^5.0.5:
version "5.0.5"
resolved "https://registry.yarnpkg.com/basic-ftp/-/basic-ftp-5.0.5.tgz#14a474f5fffecca1f4f406f1c26b18f800225ac0"
integrity sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==
batch@0.6.1:
version "0.6.1"
resolved "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz"