@appsemble/cli 0.27.11 → 0.27.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/commands/serve.js +20 -1
- package/lib/app.d.ts +3 -1
- package/lib/app.js +178 -0
- package/lib/app.test.js +274 -1
- package/package.json +6 -6
- package/server/options/getAppVariables.d.ts +3 -0
- package/server/options/getAppVariables.js +4 -0
- package/server/options/index.d.ts +1 -0
- package/server/options/index.js +1 -0
- package/server/options/options.js +2 -1
- package/templates/apps/empty/app-definition.yaml +2 -2
- package/templates/blocks/mini-jsx/package.json +1 -1
- package/templates/blocks/preact/package.json +2 -2
- package/templates/blocks/vanilla/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
#  Appsemble CLI
|
|
2
2
|
|
|
3
3
|
> Manage apps and blocks from the command line.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@appsemble/cli)
|
|
6
|
-
[](https://gitlab.com/appsemble/appsemble/-/releases/0.27.12)
|
|
7
7
|
[](https://prettier.io)
|
|
8
8
|
|
|
9
9
|
## Table of Contents
|
|
@@ -323,5 +323,5 @@ appsemble run-cronjobs --interval 30
|
|
|
323
323
|
|
|
324
324
|
## License
|
|
325
325
|
|
|
326
|
-
[LGPL-3.0-only](https://gitlab.com/appsemble/appsemble/-/blob/0.27.
|
|
326
|
+
[LGPL-3.0-only](https://gitlab.com/appsemble/appsemble/-/blob/0.27.12/LICENSE.md) ©
|
|
327
327
|
[Appsemble](https://appsemble.com)
|
package/commands/serve.js
CHANGED
|
@@ -9,7 +9,7 @@ import axios from 'axios';
|
|
|
9
9
|
import csvToJson from 'csvtojson';
|
|
10
10
|
import FormData from 'form-data';
|
|
11
11
|
import globalCacheDir from 'global-cache-dir';
|
|
12
|
-
import { traverseAppDirectory } from '../lib/app.js';
|
|
12
|
+
import { parseValues, traverseAppDirectory } from '../lib/app.js';
|
|
13
13
|
import { buildBlock } from '../lib/block.js';
|
|
14
14
|
import { getProjectBuildConfig, getProjectWebpackConfig } from '../lib/config.js';
|
|
15
15
|
import { createApiServer, createStaticServer } from '../lib/createServers.js';
|
|
@@ -207,6 +207,24 @@ export async function handler(argv) {
|
|
|
207
207
|
const { name } = parse(stat.name);
|
|
208
208
|
await Resource.bulkCreate(resources, name, true);
|
|
209
209
|
}, { allowMissing: true });
|
|
210
|
+
const appVariables = [];
|
|
211
|
+
const configPath = join(appPath, 'config');
|
|
212
|
+
if (existsSync(configPath)) {
|
|
213
|
+
const variablesPath = join(configPath, 'variables.json');
|
|
214
|
+
if (existsSync(variablesPath)) {
|
|
215
|
+
const [readAppVariables] = (await readData(variablesPath));
|
|
216
|
+
if (readAppVariables.length) {
|
|
217
|
+
for (const appVariable of readAppVariables) {
|
|
218
|
+
const { name, value } = appVariable;
|
|
219
|
+
const [parsedValues, missingValues] = parseValues('variable', name, [{ value }]);
|
|
220
|
+
if (missingValues) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
appVariables.push({ name, ...parsedValues });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
210
228
|
const stubbedApp = {
|
|
211
229
|
...appsembleApp,
|
|
212
230
|
id: appId,
|
|
@@ -221,6 +239,7 @@ export async function handler(argv) {
|
|
|
221
239
|
appsembleApp: stubbedApp,
|
|
222
240
|
appBlocks,
|
|
223
241
|
appMessages,
|
|
242
|
+
appVariables,
|
|
224
243
|
...(appSecurity
|
|
225
244
|
? {
|
|
226
245
|
appMembers,
|
package/lib/app.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/// <reference types="node" resolution-mode="require"/>
|
|
2
2
|
/// <reference types="node" resolution-mode="require"/>
|
|
3
3
|
import { type ReadStream } from 'node:fs';
|
|
4
|
-
import { type App, type AppLock, type AppsembleContext, type AppsembleRC, type AppVisibility } from '@appsemble/types';
|
|
4
|
+
import { type App, type AppLock, type AppsembleContext, type AppsembleRC, type AppVisibility, type ValueFromDefinition, type ValueFromProcess } from '@appsemble/types';
|
|
5
5
|
import FormData from 'form-data';
|
|
6
6
|
/**
|
|
7
7
|
* Traverses an app directory and appends the files it finds to the given FormData object.
|
|
@@ -33,6 +33,8 @@ export declare function exportAppAsZip(appId: number, assets: boolean, resources
|
|
|
33
33
|
export declare function uploadMessages(path: string, appId: number, remote: string, force: boolean): Promise<void>;
|
|
34
34
|
export declare function publishSeedResources(path: string, app: App, remote: string): Promise<void>;
|
|
35
35
|
export declare function publishSeedAssets(path: string, app: App, remote: string, assetsClonable: boolean): Promise<void>;
|
|
36
|
+
export declare function parseValues(type: string, name: string, valuesToParse: Record<string, ValueFromDefinition>[]): [Record<string, ValueFromProcess>, boolean];
|
|
37
|
+
export declare function publishAppConfig(path: string, app: App, remote: string): Promise<void>;
|
|
36
38
|
/**
|
|
37
39
|
* @param path The path to the app directory.
|
|
38
40
|
* @param languages A list of languages for which translations should be added in addition to the
|
package/lib/app.js
CHANGED
|
@@ -318,6 +318,182 @@ export async function publishSeedAssets(path, app, remote, assetsClonable) {
|
|
|
318
318
|
logger.warn(`Missing assets directory in ${path}. Skipping...`);
|
|
319
319
|
}
|
|
320
320
|
}
|
|
321
|
+
function parseValueFromDefinition(value) {
|
|
322
|
+
let parsed = value;
|
|
323
|
+
if (typeof parsed === 'string' && parsed.includes('{{') && parsed.includes('}}')) {
|
|
324
|
+
parsed = parsed.replaceAll(/{{\s*([^\s{}]+)\s*}}/g, (match, variable) => process.env[variable.trim()] || match);
|
|
325
|
+
}
|
|
326
|
+
return parsed;
|
|
327
|
+
}
|
|
328
|
+
export function parseValues(type, name, valuesToParse) {
|
|
329
|
+
const parsedValues = {};
|
|
330
|
+
let missingValues = false;
|
|
331
|
+
for (const valueToParse of valuesToParse) {
|
|
332
|
+
const [key, value] = Object.entries(valueToParse)[0];
|
|
333
|
+
parsedValues[key] = parseValueFromDefinition(value);
|
|
334
|
+
if (!parsedValues[key]) {
|
|
335
|
+
logger.error(`Missing ${key} value for ${type} ${name}`);
|
|
336
|
+
missingValues = true;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return [parsedValues, missingValues];
|
|
340
|
+
}
|
|
341
|
+
export async function publishAppConfig(path, app, remote) {
|
|
342
|
+
const configPath = join(path, 'config');
|
|
343
|
+
logger.info(`Publishing app config from ${configPath}`);
|
|
344
|
+
if (existsSync(configPath)) {
|
|
345
|
+
try {
|
|
346
|
+
logger.info(`Deleting existing variables from app ${app.id}`);
|
|
347
|
+
await axios.delete(`/api/apps/${app.id}/variables`, { baseURL: remote });
|
|
348
|
+
const appVariablesPath = join(configPath, 'variables.json');
|
|
349
|
+
if (existsSync(appVariablesPath)) {
|
|
350
|
+
const [appVariables] = (await readData(appVariablesPath));
|
|
351
|
+
if (appVariables.length) {
|
|
352
|
+
logger.info(`Publishing ${appVariables.length} app variable(s)`);
|
|
353
|
+
for (const appVariable of appVariables) {
|
|
354
|
+
const { name, value } = appVariable;
|
|
355
|
+
const [parsedValues, missingValues] = parseValues('variable', name, [{ value }]);
|
|
356
|
+
if (missingValues) {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
await axios.post(`/api/apps/${app.id}/variables`, { name, ...parsedValues }, { baseURL: remote });
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
logger.error('Something went wrong when creating app variables:');
|
|
366
|
+
logger.error(error);
|
|
367
|
+
}
|
|
368
|
+
try {
|
|
369
|
+
logger.info(`Deleting existing service secrets from app ${app.id}`);
|
|
370
|
+
await axios.delete(`/api/apps/${app.id}/secrets/service`, { baseURL: remote });
|
|
371
|
+
const serviceSecretsPath = join(configPath, 'secrets', 'service.json');
|
|
372
|
+
if (existsSync(serviceSecretsPath)) {
|
|
373
|
+
const [serviceSecrets] = (await readData(serviceSecretsPath));
|
|
374
|
+
if (serviceSecrets.length) {
|
|
375
|
+
logger.info(`Publishing ${serviceSecrets.length} app service secret(s)`);
|
|
376
|
+
for (const serviceSecret of serviceSecrets) {
|
|
377
|
+
const { identifier, name, secret, urlPatterns, ...rest } = serviceSecret;
|
|
378
|
+
const [parsedValues, missingValues] = parseValues('service secret', name, [
|
|
379
|
+
{ secret },
|
|
380
|
+
{ identifier },
|
|
381
|
+
{ urlPatterns },
|
|
382
|
+
]);
|
|
383
|
+
if (missingValues) {
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
await axios.post(`/api/apps/${app.id}/secrets/service`, { name, ...parsedValues, ...rest }, { baseURL: remote });
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
logger.error('Something went wrong when creating app service secrets:');
|
|
393
|
+
logger.error(error);
|
|
394
|
+
}
|
|
395
|
+
try {
|
|
396
|
+
logger.info(`Deleting existing saml secrets from app ${app.id}`);
|
|
397
|
+
await axios.delete(`/api/apps/${app.id}/secrets/saml`, { baseURL: remote });
|
|
398
|
+
const samlSecretsPath = join(configPath, 'secrets', 'saml.json');
|
|
399
|
+
if (existsSync(samlSecretsPath)) {
|
|
400
|
+
const [samlSecrets] = (await readData(samlSecretsPath));
|
|
401
|
+
if (samlSecrets.length) {
|
|
402
|
+
logger.info(`Publishing ${samlSecrets.length} app saml secret(s)`);
|
|
403
|
+
for (const samlSecret of samlSecrets) {
|
|
404
|
+
const { entityId, idpCertificate, name, ssoUrl, ...rest } = samlSecret;
|
|
405
|
+
const [parsedValues, missingValues] = parseValues('saml secret', name, [
|
|
406
|
+
{ entityId },
|
|
407
|
+
{ idpCertificate },
|
|
408
|
+
{ ssoUrl },
|
|
409
|
+
]);
|
|
410
|
+
if (missingValues) {
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
await axios.post(`/api/apps/${app.id}/secrets/saml`, { name, ...parsedValues, ...rest }, { baseURL: remote });
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
logger.error('Something went wrong when creating app saml secrets:');
|
|
420
|
+
logger.error(error);
|
|
421
|
+
}
|
|
422
|
+
try {
|
|
423
|
+
logger.info(`Deleting existing oauth2 secrets from app ${app.id}`);
|
|
424
|
+
await axios.delete(`/api/apps/${app.id}/secrets/oauth2`, { baseURL: remote });
|
|
425
|
+
const oauth2SecretsPath = join(configPath, 'secrets', 'oauth2.json');
|
|
426
|
+
if (existsSync(oauth2SecretsPath)) {
|
|
427
|
+
const [oauth2Secrets] = (await readData(oauth2SecretsPath));
|
|
428
|
+
if (oauth2Secrets.length) {
|
|
429
|
+
logger.info(`Publishing ${oauth2Secrets.length} app oauth2 secrets`);
|
|
430
|
+
for (const oauth2Secret of oauth2Secrets) {
|
|
431
|
+
const { authorizationUrl, clientId, clientSecret, name, tokenUrl, userInfoUrl, ...rest } = oauth2Secret;
|
|
432
|
+
const [parsedValues, missingValues] = parseValues('oauth2 secret', name, [
|
|
433
|
+
{ authorizationUrl },
|
|
434
|
+
{ tokenUrl },
|
|
435
|
+
{ userInfoUrl },
|
|
436
|
+
{ clientId },
|
|
437
|
+
{ clientSecret },
|
|
438
|
+
]);
|
|
439
|
+
if (missingValues) {
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
await axios.post(`/api/apps/${app.id}/secrets/oauth2`, { name, ...parsedValues, ...rest }, { baseURL: remote });
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
logger.error('Something went wrong when creating app oauth2 secrets:');
|
|
449
|
+
logger.error(error);
|
|
450
|
+
}
|
|
451
|
+
try {
|
|
452
|
+
const sslSecretPath = join(configPath, 'secrets', 'ssl.json');
|
|
453
|
+
if (existsSync(sslSecretPath)) {
|
|
454
|
+
const [sslSecret] = (await readData(sslSecretPath));
|
|
455
|
+
if (sslSecret) {
|
|
456
|
+
logger.info('Publishing app ssl secret');
|
|
457
|
+
const { certificate, key } = sslSecret;
|
|
458
|
+
const [parsedValues, missingValues] = parseValues('ssl secret', '', [
|
|
459
|
+
{ certificate },
|
|
460
|
+
{ key },
|
|
461
|
+
]);
|
|
462
|
+
if (missingValues) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
await axios.put(`/api/apps/${app.id}/secrets/ssl`, { ...parsedValues }, { baseURL: remote });
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
logger.error('Something went wrong when creating app ssl secret:');
|
|
471
|
+
logger.error(error);
|
|
472
|
+
}
|
|
473
|
+
try {
|
|
474
|
+
const scimSecretPath = join(configPath, 'secrets', 'scim.json');
|
|
475
|
+
if (existsSync(scimSecretPath)) {
|
|
476
|
+
const [scimSecret] = (await readData(scimSecretPath));
|
|
477
|
+
if (scimSecret) {
|
|
478
|
+
logger.info('Publishing app scim secret');
|
|
479
|
+
const { enabled, token } = scimSecret;
|
|
480
|
+
const [parsedValues, missingValues] = parseValues('scim secret', '', [{ token }]);
|
|
481
|
+
if (missingValues) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
await axios.patch(`/api/apps/${app.id}/secrets/scim`, { enabled, ...parsedValues }, { baseURL: remote });
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
catch (error) {
|
|
489
|
+
logger.error('Something went wrong when creating app scim secret:');
|
|
490
|
+
logger.error(error);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
logger.warn(`Missing config directory in ${path}. Skipping...`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
321
497
|
/**
|
|
322
498
|
* @param path The path to the app directory.
|
|
323
499
|
* @param languages A list of languages for which translations should be added in addition to the
|
|
@@ -616,6 +792,7 @@ export async function publishApp({ clientCredentials, context, dryRun, modifyCon
|
|
|
616
792
|
// After uploading the app, upload block styles, messages if they are available
|
|
617
793
|
await traverseBlockThemes(appVariantPath, data.id, remote, false);
|
|
618
794
|
await uploadMessages(appVariantPath, data.id, remote, false);
|
|
795
|
+
await publishAppConfig(appVariantPath, data, remote);
|
|
619
796
|
// After uploading the app, publish seed resources and assets if they are available
|
|
620
797
|
if (assets) {
|
|
621
798
|
await publishSeedAssets(appVariantPath, data, remote, assetsClonable);
|
|
@@ -776,6 +953,7 @@ export async function updateApp({ clientCredentials, context, force, path, ...op
|
|
|
776
953
|
// After uploading the app, upload block styles and messages if they are available
|
|
777
954
|
await traverseBlockThemes(appVariantPath, data.id, remote, force);
|
|
778
955
|
await uploadMessages(appVariantPath, data.id, remote, force);
|
|
956
|
+
await publishAppConfig(appVariantPath, data, remote);
|
|
779
957
|
// After updating the app, publish seed resources and assets if they are available
|
|
780
958
|
if (assets && (data.locked !== 'fullLock' || force)) {
|
|
781
959
|
await publishSeedAssets(appVariantPath, data, remote, assetsClonable);
|
package/lib/app.test.js
CHANGED
|
@@ -6,7 +6,7 @@ import { hash } from 'bcrypt';
|
|
|
6
6
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
7
|
import { patchApp, publishApp, updateApp } from './app.js';
|
|
8
8
|
import { initAxios } from './initAxios.js';
|
|
9
|
-
const { App, AppBlockStyle, AppCollection, AppCollectionApp, AppMessages, AppScreenshot, Asset, BlockVersion, Organization, OrganizationMember, Resource, } = models;
|
|
9
|
+
const { App, AppBlockStyle, AppCollection, AppCollectionApp, AppMessages, AppOAuth2Secret, AppSamlSecret, AppScreenshot, AppServiceSecret, AppVariable, Asset, BlockVersion, Organization, OrganizationMember, Resource, } = models;
|
|
10
10
|
const argv = { host: 'http://localhost', secret: 'test', aesSecret: 'testSecret' };
|
|
11
11
|
let user;
|
|
12
12
|
let organization;
|
|
@@ -633,6 +633,142 @@ describe('publishApp', () => {
|
|
|
633
633
|
await readFixture('apps/test/variants/tux/assets/small-tux.png'),
|
|
634
634
|
]);
|
|
635
635
|
});
|
|
636
|
+
it('should publish app variables and secrets', async () => {
|
|
637
|
+
vi.useRealTimers();
|
|
638
|
+
const clientCredentials = await authorizeCLI('apps:write resources:write');
|
|
639
|
+
process.env.TEST_VARIABLE_1 = 'test-variable-1';
|
|
640
|
+
process.env.TEST_VARIABLE_2 = 'test-variable-2';
|
|
641
|
+
process.env.TEST_SECRET = 'test-secret';
|
|
642
|
+
process.env.TEST_SERVICE_URL_PATTERNS = 'http://localhost:1234';
|
|
643
|
+
process.env.TEST_SERVICE_IDENTIFIER = 'Authorization';
|
|
644
|
+
process.env.TEST_SERVICE_SECRET = 'secret';
|
|
645
|
+
process.env.TEST_SAML_IDP_CERTIFICATE = 'certificate';
|
|
646
|
+
process.env.TEST_SAML_ENTITY_ID = 'http://localhost:1234';
|
|
647
|
+
process.env.TEST_SAML_SSO_URL = 'http://localhost:1234';
|
|
648
|
+
process.env.TEST_OAUTH2_AUTHORIZATION_URL = 'http://localhost:1234';
|
|
649
|
+
process.env.TEST_OAUTH2_TOKEN_URL = 'http://localhost:1234';
|
|
650
|
+
process.env.TEST_OAUTH2_USER_INFO_URL = 'http://localhost:1234';
|
|
651
|
+
process.env.TEST_OAUTH2_CLIENT_ID = 'id';
|
|
652
|
+
process.env.TEST_OAUTH2_CLIENT_SECRET = 'secret';
|
|
653
|
+
process.env.TEST_SCIM_TOKEN = 'token';
|
|
654
|
+
process.env.TEST_SSL_KEY = 'key';
|
|
655
|
+
process.env.TEST_SSL_CERTIFICATE = 'certificate';
|
|
656
|
+
await publishApp({
|
|
657
|
+
path: resolveFixture('apps/test'),
|
|
658
|
+
organization: organization.id,
|
|
659
|
+
remote: testApp.defaults.baseURL,
|
|
660
|
+
clientCredentials,
|
|
661
|
+
visibility: 'unlisted',
|
|
662
|
+
iconBackground: '#ffffff',
|
|
663
|
+
});
|
|
664
|
+
vi.useFakeTimers();
|
|
665
|
+
const app = await App.findOne({
|
|
666
|
+
attributes: ['scimEnabled', 'scimToken', 'sslKey', 'sslCertificate'],
|
|
667
|
+
where: {
|
|
668
|
+
id: 1,
|
|
669
|
+
},
|
|
670
|
+
});
|
|
671
|
+
expect({
|
|
672
|
+
scimEnabled: app.scimEnabled,
|
|
673
|
+
scimToken: app.scimToken,
|
|
674
|
+
sslKey: app.sslKey,
|
|
675
|
+
sslCertificate: app.sslCertificate,
|
|
676
|
+
}).toStrictEqual({
|
|
677
|
+
scimEnabled: true,
|
|
678
|
+
scimToken: expect.any(Buffer),
|
|
679
|
+
sslKey: 'key',
|
|
680
|
+
sslCertificate: 'certificate',
|
|
681
|
+
});
|
|
682
|
+
const appVariables = await AppVariable.findAll({
|
|
683
|
+
attributes: ['name', 'value'],
|
|
684
|
+
where: {
|
|
685
|
+
AppId: 1,
|
|
686
|
+
},
|
|
687
|
+
});
|
|
688
|
+
expect(appVariables[0].toJSON()).toStrictEqual({
|
|
689
|
+
name: 'test-variable-inline',
|
|
690
|
+
value: 'inline',
|
|
691
|
+
});
|
|
692
|
+
expect(appVariables[1].toJSON()).toStrictEqual({
|
|
693
|
+
name: 'test-variable',
|
|
694
|
+
value: 'test-variable-1.test-variable-2',
|
|
695
|
+
});
|
|
696
|
+
const appServiceSecret = await AppServiceSecret.findOne({
|
|
697
|
+
attributes: ['name', 'authenticationMethod', 'urlPatterns', 'identifier', 'secret'],
|
|
698
|
+
where: {
|
|
699
|
+
AppId: 1,
|
|
700
|
+
},
|
|
701
|
+
});
|
|
702
|
+
expect(appServiceSecret.toJSON()).toStrictEqual({
|
|
703
|
+
name: 'test-service-secret',
|
|
704
|
+
authenticationMethod: 'custom-header',
|
|
705
|
+
urlPatterns: 'http://localhost:1234',
|
|
706
|
+
identifier: 'Authorization',
|
|
707
|
+
secret: expect.any(Buffer),
|
|
708
|
+
});
|
|
709
|
+
const appSamlSecret = await AppSamlSecret.findOne({
|
|
710
|
+
attributes: [
|
|
711
|
+
'name',
|
|
712
|
+
'icon',
|
|
713
|
+
'nameAttribute',
|
|
714
|
+
'emailAttribute',
|
|
715
|
+
'idpCertificate',
|
|
716
|
+
'entityId',
|
|
717
|
+
'ssoUrl',
|
|
718
|
+
],
|
|
719
|
+
where: {
|
|
720
|
+
AppId: 1,
|
|
721
|
+
},
|
|
722
|
+
});
|
|
723
|
+
expect(appSamlSecret.toJSON()).toStrictEqual({
|
|
724
|
+
emailAttribute: 'email',
|
|
725
|
+
entityId: 'http://localhost:1234',
|
|
726
|
+
icon: 'redhat',
|
|
727
|
+
id: 1,
|
|
728
|
+
idpCertificate: 'certificate',
|
|
729
|
+
name: 'test-saml-secret',
|
|
730
|
+
nameAttribute: 'name',
|
|
731
|
+
ssoUrl: 'http://localhost:1234',
|
|
732
|
+
spCertificate: expect.any(String),
|
|
733
|
+
});
|
|
734
|
+
const appOauth2Secret = await AppOAuth2Secret.findOne({
|
|
735
|
+
attributes: [
|
|
736
|
+
'name',
|
|
737
|
+
'icon',
|
|
738
|
+
'scope',
|
|
739
|
+
'remapper',
|
|
740
|
+
'authorizationUrl',
|
|
741
|
+
'tokenUrl',
|
|
742
|
+
'userInfoUrl',
|
|
743
|
+
'clientId',
|
|
744
|
+
'clientSecret',
|
|
745
|
+
],
|
|
746
|
+
where: {
|
|
747
|
+
AppId: 1,
|
|
748
|
+
},
|
|
749
|
+
});
|
|
750
|
+
expect({
|
|
751
|
+
authorizationUrl: appOauth2Secret.authorizationUrl,
|
|
752
|
+
clientId: appOauth2Secret.clientId,
|
|
753
|
+
clientSecret: appOauth2Secret.clientSecret,
|
|
754
|
+
icon: appOauth2Secret.icon,
|
|
755
|
+
name: appOauth2Secret.name,
|
|
756
|
+
remapper: appOauth2Secret.remapper,
|
|
757
|
+
scope: appOauth2Secret.scope,
|
|
758
|
+
tokenUrl: appOauth2Secret.tokenUrl,
|
|
759
|
+
userInfoUrl: appOauth2Secret.userInfoUrl,
|
|
760
|
+
}).toStrictEqual({
|
|
761
|
+
authorizationUrl: 'http://localhost:1234',
|
|
762
|
+
clientId: 'id',
|
|
763
|
+
clientSecret: 'secret',
|
|
764
|
+
icon: 'redhat',
|
|
765
|
+
name: 'test-oauth2-secret',
|
|
766
|
+
remapper: [{ prop: 'email' }],
|
|
767
|
+
scope: 'email openid profile',
|
|
768
|
+
tokenUrl: 'http://localhost:1234',
|
|
769
|
+
userInfoUrl: 'http://localhost:1234',
|
|
770
|
+
});
|
|
771
|
+
});
|
|
636
772
|
});
|
|
637
773
|
describe('updateApp', () => {
|
|
638
774
|
let app;
|
|
@@ -1226,6 +1362,143 @@ describe('updateApp', () => {
|
|
|
1226
1362
|
await readFixture('apps/test/variants/tux/assets/small-tux.png'),
|
|
1227
1363
|
]);
|
|
1228
1364
|
});
|
|
1365
|
+
it('should update app variables and secrets', async () => {
|
|
1366
|
+
vi.useRealTimers();
|
|
1367
|
+
const clientCredentials = await authorizeCLI('apps:write resources:write');
|
|
1368
|
+
process.env.TEST_VARIABLE_1 = 'test-variable-1';
|
|
1369
|
+
process.env.TEST_VARIABLE_2 = 'test-variable-2';
|
|
1370
|
+
process.env.TEST_SECRET = 'test-secret';
|
|
1371
|
+
process.env.TEST_SERVICE_URL_PATTERNS = 'http://localhost:1234';
|
|
1372
|
+
process.env.TEST_SERVICE_IDENTIFIER = 'Authorization';
|
|
1373
|
+
process.env.TEST_SERVICE_SECRET = 'secret';
|
|
1374
|
+
process.env.TEST_SAML_IDP_CERTIFICATE = 'certificate';
|
|
1375
|
+
process.env.TEST_SAML_ENTITY_ID = 'http://localhost:1234';
|
|
1376
|
+
process.env.TEST_SAML_SSO_URL = 'http://localhost:1234';
|
|
1377
|
+
process.env.TEST_OAUTH2_AUTHORIZATION_URL = 'http://localhost:1234';
|
|
1378
|
+
process.env.TEST_OAUTH2_TOKEN_URL = 'http://localhost:1234';
|
|
1379
|
+
process.env.TEST_OAUTH2_USER_INFO_URL = 'http://localhost:1234';
|
|
1380
|
+
process.env.TEST_OAUTH2_CLIENT_ID = 'id';
|
|
1381
|
+
process.env.TEST_OAUTH2_CLIENT_SECRET = 'secret';
|
|
1382
|
+
process.env.TEST_SCIM_TOKEN = 'token';
|
|
1383
|
+
process.env.TEST_SSL_KEY = 'key';
|
|
1384
|
+
process.env.TEST_SSL_CERTIFICATE = 'certificate';
|
|
1385
|
+
await updateApp({
|
|
1386
|
+
id: 1,
|
|
1387
|
+
path: resolveFixture('apps/test'),
|
|
1388
|
+
remote: testApp.defaults.baseURL,
|
|
1389
|
+
clientCredentials,
|
|
1390
|
+
visibility: 'unlisted',
|
|
1391
|
+
iconBackground: '#ffffff',
|
|
1392
|
+
force: true,
|
|
1393
|
+
});
|
|
1394
|
+
vi.useFakeTimers();
|
|
1395
|
+
const updatedApp = await App.findOne({
|
|
1396
|
+
attributes: ['scimEnabled', 'scimToken', 'sslKey', 'sslCertificate'],
|
|
1397
|
+
where: {
|
|
1398
|
+
id: 1,
|
|
1399
|
+
},
|
|
1400
|
+
});
|
|
1401
|
+
expect({
|
|
1402
|
+
scimEnabled: updatedApp.scimEnabled,
|
|
1403
|
+
scimToken: updatedApp.scimToken,
|
|
1404
|
+
sslKey: updatedApp.sslKey,
|
|
1405
|
+
sslCertificate: updatedApp.sslCertificate,
|
|
1406
|
+
}).toStrictEqual({
|
|
1407
|
+
scimEnabled: true,
|
|
1408
|
+
scimToken: expect.any(Buffer),
|
|
1409
|
+
sslKey: 'key',
|
|
1410
|
+
sslCertificate: 'certificate',
|
|
1411
|
+
});
|
|
1412
|
+
const appVariables = await AppVariable.findAll({
|
|
1413
|
+
attributes: ['name', 'value'],
|
|
1414
|
+
where: {
|
|
1415
|
+
AppId: 1,
|
|
1416
|
+
},
|
|
1417
|
+
});
|
|
1418
|
+
expect(appVariables[0].toJSON()).toStrictEqual({
|
|
1419
|
+
name: 'test-variable-inline',
|
|
1420
|
+
value: 'inline',
|
|
1421
|
+
});
|
|
1422
|
+
expect(appVariables[1].toJSON()).toStrictEqual({
|
|
1423
|
+
name: 'test-variable',
|
|
1424
|
+
value: 'test-variable-1.test-variable-2',
|
|
1425
|
+
});
|
|
1426
|
+
const appServiceSecret = await AppServiceSecret.findOne({
|
|
1427
|
+
attributes: ['name', 'authenticationMethod', 'urlPatterns', 'identifier', 'secret'],
|
|
1428
|
+
where: {
|
|
1429
|
+
AppId: 1,
|
|
1430
|
+
},
|
|
1431
|
+
});
|
|
1432
|
+
expect(appServiceSecret.toJSON()).toStrictEqual({
|
|
1433
|
+
name: 'test-service-secret',
|
|
1434
|
+
authenticationMethod: 'custom-header',
|
|
1435
|
+
urlPatterns: 'http://localhost:1234',
|
|
1436
|
+
identifier: 'Authorization',
|
|
1437
|
+
secret: expect.any(Buffer),
|
|
1438
|
+
});
|
|
1439
|
+
const appSamlSecret = await AppSamlSecret.findOne({
|
|
1440
|
+
attributes: [
|
|
1441
|
+
'name',
|
|
1442
|
+
'icon',
|
|
1443
|
+
'nameAttribute',
|
|
1444
|
+
'emailAttribute',
|
|
1445
|
+
'idpCertificate',
|
|
1446
|
+
'entityId',
|
|
1447
|
+
'ssoUrl',
|
|
1448
|
+
],
|
|
1449
|
+
where: {
|
|
1450
|
+
AppId: 1,
|
|
1451
|
+
},
|
|
1452
|
+
});
|
|
1453
|
+
expect(appSamlSecret.toJSON()).toStrictEqual({
|
|
1454
|
+
emailAttribute: 'email',
|
|
1455
|
+
entityId: 'http://localhost:1234',
|
|
1456
|
+
icon: 'redhat',
|
|
1457
|
+
id: 1,
|
|
1458
|
+
idpCertificate: 'certificate',
|
|
1459
|
+
name: 'test-saml-secret',
|
|
1460
|
+
nameAttribute: 'name',
|
|
1461
|
+
ssoUrl: 'http://localhost:1234',
|
|
1462
|
+
spCertificate: expect.any(String),
|
|
1463
|
+
});
|
|
1464
|
+
const appOauth2Secret = await AppOAuth2Secret.findOne({
|
|
1465
|
+
attributes: [
|
|
1466
|
+
'name',
|
|
1467
|
+
'icon',
|
|
1468
|
+
'scope',
|
|
1469
|
+
'remapper',
|
|
1470
|
+
'authorizationUrl',
|
|
1471
|
+
'tokenUrl',
|
|
1472
|
+
'userInfoUrl',
|
|
1473
|
+
'clientId',
|
|
1474
|
+
'clientSecret',
|
|
1475
|
+
],
|
|
1476
|
+
where: {
|
|
1477
|
+
AppId: 1,
|
|
1478
|
+
},
|
|
1479
|
+
});
|
|
1480
|
+
expect({
|
|
1481
|
+
authorizationUrl: appOauth2Secret.authorizationUrl,
|
|
1482
|
+
clientId: appOauth2Secret.clientId,
|
|
1483
|
+
clientSecret: appOauth2Secret.clientSecret,
|
|
1484
|
+
icon: appOauth2Secret.icon,
|
|
1485
|
+
name: appOauth2Secret.name,
|
|
1486
|
+
remapper: appOauth2Secret.remapper,
|
|
1487
|
+
scope: appOauth2Secret.scope,
|
|
1488
|
+
tokenUrl: appOauth2Secret.tokenUrl,
|
|
1489
|
+
userInfoUrl: appOauth2Secret.userInfoUrl,
|
|
1490
|
+
}).toStrictEqual({
|
|
1491
|
+
authorizationUrl: 'http://localhost:1234',
|
|
1492
|
+
clientId: 'id',
|
|
1493
|
+
clientSecret: 'secret',
|
|
1494
|
+
icon: 'redhat',
|
|
1495
|
+
name: 'test-oauth2-secret',
|
|
1496
|
+
remapper: [{ prop: 'email' }],
|
|
1497
|
+
scope: 'email openid profile',
|
|
1498
|
+
tokenUrl: 'http://localhost:1234',
|
|
1499
|
+
userInfoUrl: 'http://localhost:1234',
|
|
1500
|
+
});
|
|
1501
|
+
});
|
|
1229
1502
|
});
|
|
1230
1503
|
describe('patchApp', () => {
|
|
1231
1504
|
it('should apply patches to app', async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appsemble/cli",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.12",
|
|
4
4
|
"description": "The CLI for developing with Appsemble apps and blocks",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"app",
|
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
"test": "vitest"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@appsemble/node-utils": "0.27.
|
|
46
|
-
"@appsemble/types": "0.27.
|
|
47
|
-
"@appsemble/utils": "0.27.
|
|
45
|
+
"@appsemble/node-utils": "0.27.12",
|
|
46
|
+
"@appsemble/types": "0.27.12",
|
|
47
|
+
"@appsemble/utils": "0.27.12",
|
|
48
48
|
"@fortawesome/fontawesome-common-types": "^6.0.0",
|
|
49
49
|
"@koa/cors": "^4.0.0",
|
|
50
50
|
"@odata/parser": "^0.2.0",
|
|
@@ -87,8 +87,8 @@
|
|
|
87
87
|
"yargs": "^17.0.0"
|
|
88
88
|
},
|
|
89
89
|
"devDependencies": {
|
|
90
|
-
"@appsemble/types": "0.27.
|
|
91
|
-
"@appsemble/server": "0.27.
|
|
90
|
+
"@appsemble/types": "0.27.12",
|
|
91
|
+
"@appsemble/server": "0.27.12",
|
|
92
92
|
"bcrypt": "^5.0.0",
|
|
93
93
|
"axios-test-instance": "^7.0.0",
|
|
94
94
|
"@types/concat-stream": "^2.0.0",
|
package/server/options/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { applyAppServiceSecrets, checkRole, createAppAsset, createAppResourcesWithAssets, createSettings, createTheme, deleteAppAsset, deleteAppResource, email, getApp, getAppAssets, getAppBlockStyles, getAppDetails, getAppIcon, getAppMembers, getAppMessages, getAppReadmes, getAppResource, getAppResources, getAppScreenshots, getAppStyles, getAppTeams, getAppUrl, getAppUserInfo, getBlockAsset, getBlockMessages, getBlocksAssetsPaths, getCsp, getDbUpdated, getHost, getTheme, parseQuery, reloadUser, sendNotifications, updateAppResource, verifyResourceActionPermission, } from './index.js';
|
|
1
|
+
import { applyAppServiceSecrets, checkRole, createAppAsset, createAppResourcesWithAssets, createSettings, createTheme, deleteAppAsset, deleteAppResource, email, getApp, getAppAssets, getAppBlockStyles, getAppDetails, getAppIcon, getAppMembers, getAppMessages, getAppReadmes, getAppResource, getAppResources, getAppScreenshots, getAppStyles, getAppTeams, getAppUrl, getAppUserInfo, getAppVariables, getBlockAsset, getBlockMessages, getBlocksAssetsPaths, getCsp, getDbUpdated, getHost, getTheme, parseQuery, reloadUser, sendNotifications, updateAppResource, verifyResourceActionPermission, } from './index.js';
|
|
2
2
|
export const options = {
|
|
3
3
|
applyAppServiceSecrets,
|
|
4
4
|
checkRole,
|
|
@@ -24,6 +24,7 @@ export const options = {
|
|
|
24
24
|
getAppTeams,
|
|
25
25
|
getAppUrl,
|
|
26
26
|
getAppUserInfo,
|
|
27
|
+
getAppVariables,
|
|
27
28
|
getBlockAsset,
|
|
28
29
|
getBlockMessages,
|
|
29
30
|
getBlocksAssetsPaths,
|
|
@@ -6,7 +6,7 @@ pages:
|
|
|
6
6
|
- name: Example Page A
|
|
7
7
|
blocks:
|
|
8
8
|
- type: action-button
|
|
9
|
-
version: 0.27.
|
|
9
|
+
version: 0.27.12
|
|
10
10
|
parameters:
|
|
11
11
|
icon: arrow-right
|
|
12
12
|
actions:
|
|
@@ -17,7 +17,7 @@ pages:
|
|
|
17
17
|
- name: Example Page B
|
|
18
18
|
blocks:
|
|
19
19
|
- type: action-button
|
|
20
|
-
version: 0.27.
|
|
20
|
+
version: 0.27.12
|
|
21
21
|
parameters:
|
|
22
22
|
icon: arrow-left
|
|
23
23
|
actions:
|