mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-19 01:17:18 +02:00
Merge main
This commit is contained in:
commit
709c142a87
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "unleash-server",
|
"name": "unleash-server",
|
||||||
"description": "Unleash is an enterprise ready feature toggles service. It provides different strategies for handling feature toggles.",
|
"description": "Unleash is an enterprise ready feature toggles service. It provides different strategies for handling feature toggles.",
|
||||||
"version": "4.15.0-beta.2",
|
"version": "4.15.0-beta.4",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"unleash",
|
"unleash",
|
||||||
"feature toggle",
|
"feature toggle",
|
||||||
@ -127,6 +127,7 @@
|
|||||||
"ts-toolbelt": "^9.6.0",
|
"ts-toolbelt": "^9.6.0",
|
||||||
"type-is": "^1.6.18",
|
"type-is": "^1.6.18",
|
||||||
"unleash-client": "3.15.0",
|
"unleash-client": "3.15.0",
|
||||||
|
"unleash-frontend": "4.14.8",
|
||||||
"uuid": "^8.3.2"
|
"uuid": "^8.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -150,8 +151,8 @@
|
|||||||
"@types/supertest": "2.0.12",
|
"@types/supertest": "2.0.12",
|
||||||
"@types/type-is": "1.6.3",
|
"@types/type-is": "1.6.3",
|
||||||
"@types/uuid": "8.3.4",
|
"@types/uuid": "8.3.4",
|
||||||
"@typescript-eslint/eslint-plugin": "5.34.0",
|
"@typescript-eslint/eslint-plugin": "5.35.1",
|
||||||
"@typescript-eslint/parser": "5.34.0",
|
"@typescript-eslint/parser": "5.35.1",
|
||||||
"copyfiles": "2.4.1",
|
"copyfiles": "2.4.1",
|
||||||
"coveralls": "3.1.1",
|
"coveralls": "3.1.1",
|
||||||
"del-cli": "5.0.0",
|
"del-cli": "5.0.0",
|
||||||
@ -176,7 +177,7 @@
|
|||||||
"ts-jest": "27.1.5",
|
"ts-jest": "27.1.5",
|
||||||
"ts-node": "10.9.1",
|
"ts-node": "10.9.1",
|
||||||
"tsc-watch": "5.0.3",
|
"tsc-watch": "5.0.3",
|
||||||
"typescript": "4.7.4"
|
"typescript": "4.8.2"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"async": "^3.2.4",
|
"async": "^3.2.4",
|
||||||
|
@ -11,7 +11,7 @@ services:
|
|||||||
- 5432:5432
|
- 5432:5432
|
||||||
|
|
||||||
pgadmin:
|
pgadmin:
|
||||||
image: dpage/pgadmin4:6.12
|
image: dpage/pgadmin4:6.13
|
||||||
environment:
|
environment:
|
||||||
PGADMIN_DEFAULT_EMAIL: 'admin@admin.com'
|
PGADMIN_DEFAULT_EMAIL: 'admin@admin.com'
|
||||||
PGADMIN_DEFAULT_PASSWORD: 'admin'
|
PGADMIN_DEFAULT_PASSWORD: 'admin'
|
||||||
|
@ -62,10 +62,30 @@ Object {
|
|||||||
},
|
},
|
||||||
"eventHook": undefined,
|
"eventHook": undefined,
|
||||||
"experimental": Object {
|
"experimental": Object {
|
||||||
"batchMetrics": false,
|
"externalResolver": Object {
|
||||||
"embedProxy": false,
|
"isEnabled": [Function],
|
||||||
|
},
|
||||||
|
"flags": Object {
|
||||||
|
"ENABLE_DARK_MODE_SUPPORT": false,
|
||||||
|
"anonymiseEventLog": false,
|
||||||
|
"batchMetrics": false,
|
||||||
|
"embedProxy": false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"frontendApiOrigins": Array [],
|
"flagResolver": FlagResolver {
|
||||||
|
"experiments": Object {
|
||||||
|
"ENABLE_DARK_MODE_SUPPORT": false,
|
||||||
|
"anonymiseEventLog": false,
|
||||||
|
"batchMetrics": false,
|
||||||
|
"embedProxy": false,
|
||||||
|
},
|
||||||
|
"externalResolver": Object {
|
||||||
|
"isEnabled": [Function],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"frontendApiOrigins": Array [
|
||||||
|
"*",
|
||||||
|
],
|
||||||
"getLogger": [Function],
|
"getLogger": [Function],
|
||||||
"import": Object {
|
"import": Object {
|
||||||
"dropBeforeImport": false,
|
"dropBeforeImport": false,
|
||||||
@ -106,6 +126,7 @@ Object {
|
|||||||
"ui": Object {
|
"ui": Object {
|
||||||
"flags": Object {
|
"flags": Object {
|
||||||
"E": true,
|
"E": true,
|
||||||
|
"ENABLE_DARK_MODE_SUPPORT": false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"versionCheck": Object {
|
"versionCheck": Object {
|
||||||
|
@ -70,16 +70,13 @@ export default async function getApp(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
config.experimental.embedProxy &&
|
config.experimental.flags.embedProxy &&
|
||||||
config.frontendApiOrigins.length > 0
|
config.frontendApiOrigins.length > 0
|
||||||
) {
|
) {
|
||||||
// Support CORS preflight requests for the frontend endpoints.
|
// Support CORS preflight requests for the frontend endpoints.
|
||||||
// Preflight requests should not have Authorization headers,
|
// Preflight requests should not have Authorization headers,
|
||||||
// so this must be handled before the API token middleware.
|
// so this must be handled before the API token middleware.
|
||||||
app.options(
|
app.options('/api/frontend*', corsOriginMiddleware(services));
|
||||||
'/api/frontend*',
|
|
||||||
corsOriginMiddleware(config.frontendApiOrigins),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (config.authentication.type) {
|
switch (config.authentication.type) {
|
||||||
|
@ -403,3 +403,28 @@ test('Environment variables for client features caching takes priority over opti
|
|||||||
expect(config.clientFeatureCaching.enabled).toBe(true);
|
expect(config.clientFeatureCaching.enabled).toBe(true);
|
||||||
expect(config.clientFeatureCaching.maxAge).toBe(120);
|
expect(config.clientFeatureCaching.maxAge).toBe(120);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Environment variables for frontend CORS origins takes priority over options', async () => {
|
||||||
|
const create = (frontendApiOrigins?): string[] => {
|
||||||
|
return createConfig({
|
||||||
|
frontendApiOrigins,
|
||||||
|
}).frontendApiOrigins;
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(create()).toEqual(['*']);
|
||||||
|
expect(create([])).toEqual([]);
|
||||||
|
expect(create(['*'])).toEqual(['*']);
|
||||||
|
expect(create(['https://example.com'])).toEqual(['https://example.com']);
|
||||||
|
expect(() => create(['a'])).toThrow('Invalid origin: a');
|
||||||
|
|
||||||
|
process.env.UNLEASH_FRONTEND_API_ORIGINS = '';
|
||||||
|
expect(create()).toEqual([]);
|
||||||
|
process.env.UNLEASH_FRONTEND_API_ORIGINS = '*';
|
||||||
|
expect(create()).toEqual(['*']);
|
||||||
|
process.env.UNLEASH_FRONTEND_API_ORIGINS = 'https://example.com, *';
|
||||||
|
expect(create()).toEqual(['https://example.com', '*']);
|
||||||
|
process.env.UNLEASH_FRONTEND_API_ORIGINS = 'b';
|
||||||
|
expect(() => create(['a'])).toThrow('Invalid origin: b');
|
||||||
|
delete process.env.UNLEASH_FRONTEND_API_ORIGINS;
|
||||||
|
expect(create()).toEqual(['*']);
|
||||||
|
});
|
||||||
|
@ -34,11 +34,16 @@ import {
|
|||||||
parseEnvVarNumber,
|
parseEnvVarNumber,
|
||||||
parseEnvVarStrings,
|
parseEnvVarStrings,
|
||||||
} from './util/parseEnvVar';
|
} from './util/parseEnvVar';
|
||||||
import { IExperimentalOptions } from './experimental';
|
import {
|
||||||
|
defaultExperimentalOptions,
|
||||||
|
IExperimentalOptions,
|
||||||
|
} from './types/experimental';
|
||||||
import {
|
import {
|
||||||
DEFAULT_SEGMENT_VALUES_LIMIT,
|
DEFAULT_SEGMENT_VALUES_LIMIT,
|
||||||
DEFAULT_STRATEGY_SEGMENTS_LIMIT,
|
DEFAULT_STRATEGY_SEGMENTS_LIMIT,
|
||||||
} from './util/segments';
|
} from './util/segments';
|
||||||
|
import FlagResolver from './util/flag-resolver';
|
||||||
|
import { validateOrigins } from './util/validateOrigin';
|
||||||
|
|
||||||
const safeToUpper = (s: string) => (s ? s.toUpperCase() : s);
|
const safeToUpper = (s: string) => (s ? s.toUpperCase() : s);
|
||||||
|
|
||||||
@ -55,15 +60,12 @@ function mergeAll<T>(objects: Partial<T>[]): T {
|
|||||||
|
|
||||||
function loadExperimental(options: IUnleashOptions): IExperimentalOptions {
|
function loadExperimental(options: IUnleashOptions): IExperimentalOptions {
|
||||||
return {
|
return {
|
||||||
|
...defaultExperimentalOptions,
|
||||||
...options.experimental,
|
...options.experimental,
|
||||||
embedProxy: parseEnvVarBoolean(
|
flags: {
|
||||||
process.env.UNLEASH_EXPERIMENTAL_EMBED_PROXY,
|
...defaultExperimentalOptions.flags,
|
||||||
Boolean(options.experimental?.embedProxy),
|
...options.experimental?.flags,
|
||||||
),
|
},
|
||||||
batchMetrics: parseEnvVarBoolean(
|
|
||||||
process.env.UNLEASH_EXPERIMENTAL_BATCH_METRICS,
|
|
||||||
Boolean(options.experimental?.batchMetrics),
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,6 +104,7 @@ function loadUI(options: IUnleashOptions): IUIConfig {
|
|||||||
|
|
||||||
ui.flags = {
|
ui.flags = {
|
||||||
E: true,
|
E: true,
|
||||||
|
ENABLE_DARK_MODE_SUPPORT: false,
|
||||||
};
|
};
|
||||||
return mergeAll([uiO, ui]);
|
return mergeAll([uiO, ui]);
|
||||||
}
|
}
|
||||||
@ -309,6 +312,20 @@ const parseCspEnvironmentVariables = (): ICspDomainConfig => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const parseFrontendApiOrigins = (options: IUnleashOptions): string[] => {
|
||||||
|
const frontendApiOrigins = parseEnvVarStrings(
|
||||||
|
process.env.UNLEASH_FRONTEND_API_ORIGINS,
|
||||||
|
options.frontendApiOrigins || ['*'],
|
||||||
|
);
|
||||||
|
|
||||||
|
const error = validateOrigins(frontendApiOrigins);
|
||||||
|
if (error) {
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return frontendApiOrigins;
|
||||||
|
};
|
||||||
|
|
||||||
export function createConfig(options: IUnleashOptions): IUnleashConfig {
|
export function createConfig(options: IUnleashOptions): IUnleashConfig {
|
||||||
let extraDbOptions = {};
|
let extraDbOptions = {};
|
||||||
|
|
||||||
@ -375,6 +392,7 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const experimental = loadExperimental(options);
|
const experimental = loadExperimental(options);
|
||||||
|
const flagResolver = new FlagResolver(experimental);
|
||||||
|
|
||||||
const ui = loadUI(options);
|
const ui = loadUI(options);
|
||||||
|
|
||||||
@ -417,10 +435,6 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
|
|||||||
DEFAULT_STRATEGY_SEGMENTS_LIMIT,
|
DEFAULT_STRATEGY_SEGMENTS_LIMIT,
|
||||||
);
|
);
|
||||||
|
|
||||||
const frontendApiOrigins =
|
|
||||||
options.frontendApiOrigins ||
|
|
||||||
parseEnvVarStrings(process.env.UNLEASH_FRONTEND_API_ORIGINS, []);
|
|
||||||
|
|
||||||
const clientFeatureCaching = loadClientCachingOptions(options);
|
const clientFeatureCaching = loadClientCachingOptions(options);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -434,6 +448,7 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
|
|||||||
ui,
|
ui,
|
||||||
import: importSetting,
|
import: importSetting,
|
||||||
experimental,
|
experimental,
|
||||||
|
flagResolver,
|
||||||
email,
|
email,
|
||||||
secureHeaders,
|
secureHeaders,
|
||||||
enableOAS,
|
enableOAS,
|
||||||
@ -445,7 +460,7 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
|
|||||||
eventBus: new EventEmitter(),
|
eventBus: new EventEmitter(),
|
||||||
environmentEnableOverrides,
|
environmentEnableOverrides,
|
||||||
additionalCspAllowedDomains,
|
additionalCspAllowedDomains,
|
||||||
frontendApiOrigins,
|
frontendApiOrigins: parseFrontendApiOrigins(options),
|
||||||
inlineSegmentConstraints,
|
inlineSegmentConstraints,
|
||||||
segmentValuesLimit,
|
segmentValuesLimit,
|
||||||
strategySegmentsLimit,
|
strategySegmentsLimit,
|
||||||
|
@ -45,16 +45,8 @@ export class AccessStore implements IAccessStore {
|
|||||||
|
|
||||||
private db: Knex;
|
private db: Knex;
|
||||||
|
|
||||||
private enableUserGroupPermissions: boolean;
|
constructor(db: Knex, eventBus: EventEmitter, getLogger: Function) {
|
||||||
|
|
||||||
constructor(
|
|
||||||
db: Knex,
|
|
||||||
eventBus: EventEmitter,
|
|
||||||
getLogger: Function,
|
|
||||||
enableUserGroupPermissions: boolean,
|
|
||||||
) {
|
|
||||||
this.db = db;
|
this.db = db;
|
||||||
this.enableUserGroupPermissions = enableUserGroupPermissions;
|
|
||||||
this.logger = getLogger('access-store.ts');
|
this.logger = getLogger('access-store.ts');
|
||||||
this.timer = (action: string) =>
|
this.timer = (action: string) =>
|
||||||
metricsHelper.wrapTimer(eventBus, DB_TIME, {
|
metricsHelper.wrapTimer(eventBus, DB_TIME, {
|
||||||
@ -133,27 +125,21 @@ export class AccessStore implements IAccessStore {
|
|||||||
.join(`${T.PERMISSIONS} AS p`, 'p.id', 'rp.permission_id')
|
.join(`${T.PERMISSIONS} AS p`, 'p.id', 'rp.permission_id')
|
||||||
.where('ur.user_id', '=', userId);
|
.where('ur.user_id', '=', userId);
|
||||||
|
|
||||||
if (this.enableUserGroupPermissions) {
|
userPermissionQuery = userPermissionQuery.union((db) => {
|
||||||
userPermissionQuery = userPermissionQuery.union((db) => {
|
db.select(
|
||||||
db.select(
|
'project',
|
||||||
'project',
|
'permission',
|
||||||
'permission',
|
'environment',
|
||||||
'environment',
|
'p.type',
|
||||||
'p.type',
|
'gr.role_id',
|
||||||
'gr.role_id',
|
)
|
||||||
)
|
.from<IPermissionRow>(`${T.GROUP_USER} AS gu`)
|
||||||
.from<IPermissionRow>(`${T.GROUP_USER} AS gu`)
|
.join(`${T.GROUPS} AS g`, 'g.id', 'gu.group_id')
|
||||||
.join(`${T.GROUPS} AS g`, 'g.id', 'gu.group_id')
|
.join(`${T.GROUP_ROLE} AS gr`, 'gu.group_id', 'gr.group_id')
|
||||||
.join(`${T.GROUP_ROLE} AS gr`, 'gu.group_id', 'gr.group_id')
|
.join(`${T.ROLE_PERMISSION} AS rp`, 'rp.role_id', 'gr.role_id')
|
||||||
.join(
|
.join(`${T.PERMISSIONS} AS p`, 'p.id', 'rp.permission_id')
|
||||||
`${T.ROLE_PERMISSION} AS rp`,
|
.where('gu.user_id', '=', userId);
|
||||||
'rp.role_id',
|
});
|
||||||
'gr.role_id',
|
|
||||||
)
|
|
||||||
.join(`${T.PERMISSIONS} AS p`, 'p.id', 'rp.permission_id')
|
|
||||||
.where('gu.user_id', '=', userId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const rows = await userPermissionQuery;
|
const rows = await userPermissionQuery;
|
||||||
stopTimer();
|
stopTimer();
|
||||||
return rows.map(this.mapUserPermission);
|
return rows.map(this.mapUserPermission);
|
||||||
|
@ -57,12 +57,7 @@ export const createStores = (
|
|||||||
tagStore: new TagStore(db, eventBus, getLogger),
|
tagStore: new TagStore(db, eventBus, getLogger),
|
||||||
tagTypeStore: new TagTypeStore(db, eventBus, getLogger),
|
tagTypeStore: new TagTypeStore(db, eventBus, getLogger),
|
||||||
addonStore: new AddonStore(db, eventBus, getLogger),
|
addonStore: new AddonStore(db, eventBus, getLogger),
|
||||||
accessStore: new AccessStore(
|
accessStore: new AccessStore(db, eventBus, getLogger),
|
||||||
db,
|
|
||||||
eventBus,
|
|
||||||
getLogger,
|
|
||||||
config?.experimental?.userGroups,
|
|
||||||
),
|
|
||||||
apiTokenStore: new ApiTokenStore(db, eventBus, getLogger),
|
apiTokenStore: new ApiTokenStore(db, eventBus, getLogger),
|
||||||
resetTokenStore: new ResetTokenStore(db, eventBus, getLogger),
|
resetTokenStore: new ResetTokenStore(db, eventBus, getLogger),
|
||||||
sessionStore: new SessionStore(db, eventBus, getLogger),
|
sessionStore: new SessionStore(db, eventBus, getLogger),
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
export interface IExperimentalOptions {
|
|
||||||
metricsV2?: IExperimentalToggle;
|
|
||||||
clientFeatureMemoize?: IExperimentalToggle;
|
|
||||||
userGroups?: boolean;
|
|
||||||
anonymiseEventLog?: boolean;
|
|
||||||
embedProxy?: boolean;
|
|
||||||
batchMetrics?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IExperimentalToggle {
|
|
||||||
enabled: boolean;
|
|
||||||
}
|
|
@ -42,7 +42,8 @@ const apiAccessMiddleware = (
|
|||||||
if (
|
if (
|
||||||
(apiUser.type === CLIENT && !isClientApi(req)) ||
|
(apiUser.type === CLIENT && !isClientApi(req)) ||
|
||||||
(apiUser.type === FRONTEND && !isProxyApi(req)) ||
|
(apiUser.type === FRONTEND && !isProxyApi(req)) ||
|
||||||
(apiUser.type === FRONTEND && !experimental.embedProxy)
|
(apiUser.type === FRONTEND &&
|
||||||
|
!experimental.flags.embedProxy)
|
||||||
) {
|
) {
|
||||||
res.status(403).send({ message: TOKEN_TYPE_ERROR_MESSAGE });
|
res.status(403).send({ message: TOKEN_TYPE_ERROR_MESSAGE });
|
||||||
return;
|
return;
|
||||||
|
@ -1,4 +1,21 @@
|
|||||||
import { allowRequestOrigin } from './cors-origin-middleware';
|
import { allowRequestOrigin } from './cors-origin-middleware';
|
||||||
|
import FakeSettingStore from '../../test/fixtures/fake-setting-store';
|
||||||
|
import SettingService from '../services/setting-service';
|
||||||
|
import { createTestConfig } from '../../test/config/test-config';
|
||||||
|
import FakeEventStore from '../../test/fixtures/fake-event-store';
|
||||||
|
import { randomId } from '../util/random-id';
|
||||||
|
import { frontendSettingsKey } from '../types/settings/frontend-settings';
|
||||||
|
|
||||||
|
const createSettingService = (frontendApiOrigins: string[]): SettingService => {
|
||||||
|
const config = createTestConfig({ frontendApiOrigins });
|
||||||
|
|
||||||
|
const stores = {
|
||||||
|
settingStore: new FakeSettingStore(),
|
||||||
|
eventStore: new FakeEventStore(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return new SettingService(stores, config);
|
||||||
|
};
|
||||||
|
|
||||||
test('allowRequestOrigin', () => {
|
test('allowRequestOrigin', () => {
|
||||||
const dotCom = 'https://example.com';
|
const dotCom = 'https://example.com';
|
||||||
@ -16,3 +33,54 @@ test('allowRequestOrigin', () => {
|
|||||||
expect(allowRequestOrigin(dotCom, [dotOrg, '*'])).toEqual(true);
|
expect(allowRequestOrigin(dotCom, [dotOrg, '*'])).toEqual(true);
|
||||||
expect(allowRequestOrigin(dotCom, [dotCom, dotOrg, '*'])).toEqual(true);
|
expect(allowRequestOrigin(dotCom, [dotCom, dotOrg, '*'])).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('corsOriginMiddleware origin validation', async () => {
|
||||||
|
const service = createSettingService([]);
|
||||||
|
const userName = randomId();
|
||||||
|
await expect(() =>
|
||||||
|
service.setFrontendSettings({ frontendApiOrigins: ['a'] }, userName),
|
||||||
|
).rejects.toThrow('Invalid origin: a');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('corsOriginMiddleware without config', async () => {
|
||||||
|
const service = createSettingService([]);
|
||||||
|
const userName = randomId();
|
||||||
|
expect(await service.getFrontendSettings()).toEqual({
|
||||||
|
frontendApiOrigins: [],
|
||||||
|
});
|
||||||
|
await service.setFrontendSettings({ frontendApiOrigins: [] }, userName);
|
||||||
|
expect(await service.getFrontendSettings()).toEqual({
|
||||||
|
frontendApiOrigins: [],
|
||||||
|
});
|
||||||
|
await service.setFrontendSettings({ frontendApiOrigins: ['*'] }, userName);
|
||||||
|
expect(await service.getFrontendSettings()).toEqual({
|
||||||
|
frontendApiOrigins: ['*'],
|
||||||
|
});
|
||||||
|
await service.delete(frontendSettingsKey, userName);
|
||||||
|
expect(await service.getFrontendSettings()).toEqual({
|
||||||
|
frontendApiOrigins: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('corsOriginMiddleware with config', async () => {
|
||||||
|
const service = createSettingService(['*']);
|
||||||
|
const userName = randomId();
|
||||||
|
expect(await service.getFrontendSettings()).toEqual({
|
||||||
|
frontendApiOrigins: ['*'],
|
||||||
|
});
|
||||||
|
await service.setFrontendSettings({ frontendApiOrigins: [] }, userName);
|
||||||
|
expect(await service.getFrontendSettings()).toEqual({
|
||||||
|
frontendApiOrigins: [],
|
||||||
|
});
|
||||||
|
await service.setFrontendSettings(
|
||||||
|
{ frontendApiOrigins: ['https://example.com', 'https://example.org'] },
|
||||||
|
userName,
|
||||||
|
);
|
||||||
|
expect(await service.getFrontendSettings()).toEqual({
|
||||||
|
frontendApiOrigins: ['https://example.com', 'https://example.org'],
|
||||||
|
});
|
||||||
|
await service.delete(frontendSettingsKey, userName);
|
||||||
|
expect(await service.getFrontendSettings()).toEqual({
|
||||||
|
frontendApiOrigins: ['*'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -1,25 +1,33 @@
|
|||||||
import { RequestHandler } from 'express';
|
import { RequestHandler } from 'express';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
|
import { IUnleashServices } from '../types';
|
||||||
const ANY_ORIGIN = '*';
|
|
||||||
|
|
||||||
export const allowRequestOrigin = (
|
export const allowRequestOrigin = (
|
||||||
requestOrigin: string,
|
requestOrigin: string,
|
||||||
allowedOrigins: string[],
|
allowedOrigins: string[],
|
||||||
): boolean => {
|
): boolean => {
|
||||||
return allowedOrigins.some((allowedOrigin) => {
|
return allowedOrigins.some((allowedOrigin) => {
|
||||||
return allowedOrigin === requestOrigin || allowedOrigin === ANY_ORIGIN;
|
return allowedOrigin === requestOrigin || allowedOrigin === '*';
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check the request's Origin header against a list of allowed origins.
|
// Check the request's Origin header against a list of allowed origins.
|
||||||
// The list may include '*', which `cors` does not support natively.
|
// The list may include '*', which `cors` does not support natively.
|
||||||
export const corsOriginMiddleware = (
|
export const corsOriginMiddleware = ({
|
||||||
allowedOrigins: string[],
|
settingService,
|
||||||
): RequestHandler => {
|
}: Pick<IUnleashServices, 'settingService'>): RequestHandler => {
|
||||||
return cors((req, callback) => {
|
return cors(async (req, callback) => {
|
||||||
callback(null, {
|
try {
|
||||||
origin: allowRequestOrigin(req.header('Origin'), allowedOrigins),
|
const { frontendApiOrigins = [] } =
|
||||||
});
|
await settingService.getFrontendSettings();
|
||||||
|
callback(null, {
|
||||||
|
origin: allowRequestOrigin(
|
||||||
|
req.header('Origin'),
|
||||||
|
frontendApiOrigins,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
callback(error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -110,6 +110,7 @@ import { proxyFeaturesSchema } from './spec/proxy-features-schema';
|
|||||||
import { proxyFeatureSchema } from './spec/proxy-feature-schema';
|
import { proxyFeatureSchema } from './spec/proxy-feature-schema';
|
||||||
import { proxyClientSchema } from './spec/proxy-client-schema';
|
import { proxyClientSchema } from './spec/proxy-client-schema';
|
||||||
import { proxyMetricsSchema } from './spec/proxy-metrics-schema';
|
import { proxyMetricsSchema } from './spec/proxy-metrics-schema';
|
||||||
|
import { setUiConfigSchema } from './spec/set-ui-config-schema';
|
||||||
|
|
||||||
// All schemas in `openapi/spec` should be listed here.
|
// All schemas in `openapi/spec` should be listed here.
|
||||||
export const schemas = {
|
export const schemas = {
|
||||||
@ -187,6 +188,7 @@ export const schemas = {
|
|||||||
searchEventsSchema,
|
searchEventsSchema,
|
||||||
segmentSchema,
|
segmentSchema,
|
||||||
setStrategySortOrderSchema,
|
setStrategySortOrderSchema,
|
||||||
|
setUiConfigSchema,
|
||||||
sortOrderSchema,
|
sortOrderSchema,
|
||||||
splashSchema,
|
splashSchema,
|
||||||
stateSchema,
|
stateSchema,
|
||||||
|
24
src/lib/openapi/spec/set-ui-config-schema.ts
Normal file
24
src/lib/openapi/spec/set-ui-config-schema.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { FromSchema } from 'json-schema-to-ts';
|
||||||
|
|
||||||
|
export const setUiConfigSchema = {
|
||||||
|
$id: '#/components/schemas/setUiConfigSchema',
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: false,
|
||||||
|
properties: {
|
||||||
|
frontendSettings: {
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: false,
|
||||||
|
required: ['frontendApiOrigins'],
|
||||||
|
properties: {
|
||||||
|
frontendApiOrigins: {
|
||||||
|
type: 'array',
|
||||||
|
additionalProperties: false,
|
||||||
|
items: { type: 'string' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type SetUiConfigSchema = FromSchema<typeof setUiConfigSchema>;
|
@ -37,6 +37,12 @@ export const uiConfigSchema = {
|
|||||||
strategySegmentsLimit: {
|
strategySegmentsLimit: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
},
|
},
|
||||||
|
frontendApiOrigins: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
flags: {
|
flags: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
additionalProperties: {
|
additionalProperties: {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Response } from 'express';
|
||||||
|
import { AuthedRequest } from '../../types/core';
|
||||||
import { IUnleashServices } from '../../types/services';
|
import { IUnleashServices } from '../../types/services';
|
||||||
import { IAuthType, IUnleashConfig } from '../../types/option';
|
import { IAuthType, IUnleashConfig } from '../../types/option';
|
||||||
import version from '../../util/version';
|
import version from '../../util/version';
|
||||||
@ -6,10 +7,10 @@ import Controller from '../controller';
|
|||||||
import VersionService from '../../services/version-service';
|
import VersionService from '../../services/version-service';
|
||||||
import SettingService from '../../services/setting-service';
|
import SettingService from '../../services/setting-service';
|
||||||
import {
|
import {
|
||||||
simpleAuthKey,
|
simpleAuthSettingsKey,
|
||||||
SimpleAuthSettings,
|
SimpleAuthSettings,
|
||||||
} from '../../types/settings/simple-auth-settings';
|
} from '../../types/settings/simple-auth-settings';
|
||||||
import { NONE } from '../../types/permissions';
|
import { ADMIN, NONE } from '../../types/permissions';
|
||||||
import { createResponseSchema } from '../../openapi/util/create-response-schema';
|
import { createResponseSchema } from '../../openapi/util/create-response-schema';
|
||||||
import {
|
import {
|
||||||
uiConfigSchema,
|
uiConfigSchema,
|
||||||
@ -17,6 +18,12 @@ import {
|
|||||||
} from '../../openapi/spec/ui-config-schema';
|
} from '../../openapi/spec/ui-config-schema';
|
||||||
import { OpenApiService } from '../../services/openapi-service';
|
import { OpenApiService } from '../../services/openapi-service';
|
||||||
import { EmailService } from '../../services/email-service';
|
import { EmailService } from '../../services/email-service';
|
||||||
|
import { emptyResponse } from '../../openapi/util/standard-responses';
|
||||||
|
import { IAuthRequest } from '../unleash-types';
|
||||||
|
import { extractUsername } from '../../util/extract-user';
|
||||||
|
import NotFoundError from '../../error/notfound-error';
|
||||||
|
import { SetUiConfigSchema } from '../../openapi/spec/set-ui-config-schema';
|
||||||
|
import { createRequestSchema } from '../../openapi/util/create-request-schema';
|
||||||
|
|
||||||
class ConfigController extends Controller {
|
class ConfigController extends Controller {
|
||||||
private versionService: VersionService;
|
private versionService: VersionService;
|
||||||
@ -51,33 +58,56 @@ class ConfigController extends Controller {
|
|||||||
this.route({
|
this.route({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
path: '',
|
path: '',
|
||||||
handler: this.getUIConfig,
|
handler: this.getUiConfig,
|
||||||
permission: NONE,
|
permission: NONE,
|
||||||
middleware: [
|
middleware: [
|
||||||
openApiService.validPath({
|
openApiService.validPath({
|
||||||
tags: ['Admin UI'],
|
tags: ['Admin UI'],
|
||||||
operationId: 'getUIConfig',
|
operationId: 'getUiConfig',
|
||||||
responses: {
|
responses: {
|
||||||
200: createResponseSchema('uiConfigSchema'),
|
200: createResponseSchema('uiConfigSchema'),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.route({
|
||||||
|
method: 'post',
|
||||||
|
path: '',
|
||||||
|
handler: this.setUiConfig,
|
||||||
|
permission: ADMIN,
|
||||||
|
middleware: [
|
||||||
|
openApiService.validPath({
|
||||||
|
tags: ['Admin UI'],
|
||||||
|
operationId: 'setUiConfig',
|
||||||
|
requestBody: createRequestSchema('setUiConfigSchema'),
|
||||||
|
responses: { 200: emptyResponse },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUIConfig(
|
async getUiConfig(
|
||||||
req: Request,
|
req: AuthedRequest,
|
||||||
res: Response<UiConfigSchema>,
|
res: Response<UiConfigSchema>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const simpleAuthSettings =
|
const [frontendSettings, simpleAuthSettings] = await Promise.all([
|
||||||
await this.settingService.get<SimpleAuthSettings>(simpleAuthKey);
|
this.settingService.getFrontendSettings(),
|
||||||
|
this.settingService.get<SimpleAuthSettings>(simpleAuthSettingsKey),
|
||||||
|
]);
|
||||||
|
|
||||||
const disablePasswordAuth =
|
const disablePasswordAuth =
|
||||||
simpleAuthSettings?.disabled ||
|
simpleAuthSettings?.disabled ||
|
||||||
this.config.authentication.type == IAuthType.NONE;
|
this.config.authentication.type == IAuthType.NONE;
|
||||||
|
|
||||||
|
const expFlags = this.config.flagResolver.getAll({
|
||||||
|
email: req.user.email,
|
||||||
|
});
|
||||||
|
const flags = { ...this.config.ui.flags, ...expFlags };
|
||||||
|
|
||||||
const response: UiConfigSchema = {
|
const response: UiConfigSchema = {
|
||||||
...this.config.ui,
|
...this.config.ui,
|
||||||
|
flags,
|
||||||
version,
|
version,
|
||||||
emailEnabled: this.emailService.isEnabled(),
|
emailEnabled: this.emailService.isEnabled(),
|
||||||
unleashUrl: this.config.server.unleashUrl,
|
unleashUrl: this.config.server.unleashUrl,
|
||||||
@ -85,9 +115,10 @@ class ConfigController extends Controller {
|
|||||||
authenticationType: this.config.authentication?.type,
|
authenticationType: this.config.authentication?.type,
|
||||||
segmentValuesLimit: this.config.segmentValuesLimit,
|
segmentValuesLimit: this.config.segmentValuesLimit,
|
||||||
strategySegmentsLimit: this.config.strategySegmentsLimit,
|
strategySegmentsLimit: this.config.strategySegmentsLimit,
|
||||||
|
frontendApiOrigins: frontendSettings.frontendApiOrigins,
|
||||||
versionInfo: this.versionService.getVersionInfo(),
|
versionInfo: this.versionService.getVersionInfo(),
|
||||||
disablePasswordAuth,
|
disablePasswordAuth,
|
||||||
embedProxy: this.config.experimental.embedProxy,
|
embedProxy: this.config.experimental.flags.embedProxy,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.openApiService.respondWithValidation(
|
this.openApiService.respondWithValidation(
|
||||||
@ -97,5 +128,22 @@ class ConfigController extends Controller {
|
|||||||
response,
|
response,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setUiConfig(
|
||||||
|
req: IAuthRequest<void, void, SetUiConfigSchema>,
|
||||||
|
res: Response<string>,
|
||||||
|
): Promise<void> {
|
||||||
|
if (req.body.frontendSettings) {
|
||||||
|
await this.settingService.setFrontendSettings(
|
||||||
|
req.body.frontendSettings,
|
||||||
|
extractUsername(req),
|
||||||
|
);
|
||||||
|
res.sendStatus(204);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new NotFoundError();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ConfigController;
|
export default ConfigController;
|
||||||
|
@ -21,12 +21,13 @@ import {
|
|||||||
import { getStandardResponses } from '../../../lib/openapi/util/standard-responses';
|
import { getStandardResponses } from '../../../lib/openapi/util/standard-responses';
|
||||||
import { createRequestSchema } from '../../openapi/util/create-request-schema';
|
import { createRequestSchema } from '../../openapi/util/create-request-schema';
|
||||||
import { SearchEventsSchema } from '../../openapi/spec/search-events-schema';
|
import { SearchEventsSchema } from '../../openapi/spec/search-events-schema';
|
||||||
|
import { IFlagResolver } from '../../types/experimental';
|
||||||
|
|
||||||
const version = 1;
|
const version = 1;
|
||||||
export default class EventController extends Controller {
|
export default class EventController extends Controller {
|
||||||
private eventService: EventService;
|
private eventService: EventService;
|
||||||
|
|
||||||
private anonymise: boolean = false;
|
private flagResolver: IFlagResolver;
|
||||||
|
|
||||||
private openApiService: OpenApiService;
|
private openApiService: OpenApiService;
|
||||||
|
|
||||||
@ -39,7 +40,7 @@ export default class EventController extends Controller {
|
|||||||
) {
|
) {
|
||||||
super(config);
|
super(config);
|
||||||
this.eventService = eventService;
|
this.eventService = eventService;
|
||||||
this.anonymise = config.experimental?.anonymiseEventLog;
|
this.flagResolver = config.flagResolver;
|
||||||
this.openApiService = openApiService;
|
this.openApiService = openApiService;
|
||||||
|
|
||||||
this.route({
|
this.route({
|
||||||
@ -106,7 +107,7 @@ export default class EventController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
maybeAnonymiseEvents(events: IEvent[]): IEvent[] {
|
maybeAnonymiseEvents(events: IEvent[]): IEvent[] {
|
||||||
if (this.anonymise) {
|
if (this.flagResolver.isEnabled('anonymiseEventLog')) {
|
||||||
return events.map((e: IEvent) => ({
|
return events.map((e: IEvent) => ({
|
||||||
...e,
|
...e,
|
||||||
createdBy: anonymise(e.createdBy),
|
createdBy: anonymise(e.createdBy),
|
||||||
|
@ -12,7 +12,7 @@ async function getSetup(anonymise: boolean = false) {
|
|||||||
const stores = createStores();
|
const stores = createStores();
|
||||||
const config = createTestConfig({
|
const config = createTestConfig({
|
||||||
server: { baseUriPath: base },
|
server: { baseUriPath: base },
|
||||||
experimental: { anonymiseEventLog: anonymise },
|
experimental: { flags: { anonymiseEventLog: anonymise } },
|
||||||
});
|
});
|
||||||
const services = createServices(stores, config);
|
const services = createServices(stores, config);
|
||||||
const app = await getApp(config, stores, services);
|
const app = await getApp(config, stores, services);
|
||||||
|
@ -10,7 +10,7 @@ import ResetTokenService from '../../services/reset-token-service';
|
|||||||
import { IAuthRequest } from '../unleash-types';
|
import { IAuthRequest } from '../unleash-types';
|
||||||
import SettingService from '../../services/setting-service';
|
import SettingService from '../../services/setting-service';
|
||||||
import { IUser, SimpleAuthSettings } from '../../server-impl';
|
import { IUser, SimpleAuthSettings } from '../../server-impl';
|
||||||
import { simpleAuthKey } from '../../types/settings/simple-auth-settings';
|
import { simpleAuthSettingsKey } from '../../types/settings/simple-auth-settings';
|
||||||
import { anonymise } from '../../util/anonymise';
|
import { anonymise } from '../../util/anonymise';
|
||||||
import { OpenApiService } from '../../services/openapi-service';
|
import { OpenApiService } from '../../services/openapi-service';
|
||||||
import { createRequestSchema } from '../../openapi/util/create-request-schema';
|
import { createRequestSchema } from '../../openapi/util/create-request-schema';
|
||||||
@ -37,9 +37,10 @@ import {
|
|||||||
usersGroupsBaseSchema,
|
usersGroupsBaseSchema,
|
||||||
} from '../../openapi/spec/users-groups-base-schema';
|
} from '../../openapi/spec/users-groups-base-schema';
|
||||||
import { IGroup } from '../../types/group';
|
import { IGroup } from '../../types/group';
|
||||||
|
import { IFlagResolver } from '../../types/experimental';
|
||||||
|
|
||||||
export default class UserAdminController extends Controller {
|
export default class UserAdminController extends Controller {
|
||||||
private anonymise: boolean = false;
|
private flagResolver: IFlagResolver;
|
||||||
|
|
||||||
private userService: UserService;
|
private userService: UserService;
|
||||||
|
|
||||||
@ -90,7 +91,7 @@ export default class UserAdminController extends Controller {
|
|||||||
this.groupService = groupService;
|
this.groupService = groupService;
|
||||||
this.logger = config.getLogger('routes/user-controller.ts');
|
this.logger = config.getLogger('routes/user-controller.ts');
|
||||||
this.unleashUrl = config.server.unleashUrl;
|
this.unleashUrl = config.server.unleashUrl;
|
||||||
this.anonymise = config.experimental?.anonymiseEventLog;
|
this.flagResolver = config.flagResolver;
|
||||||
|
|
||||||
this.route({
|
this.route({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
@ -294,7 +295,7 @@ export default class UserAdminController extends Controller {
|
|||||||
typeof q === 'string' && q.length > 1
|
typeof q === 'string' && q.length > 1
|
||||||
? await this.userService.search(q)
|
? await this.userService.search(q)
|
||||||
: [];
|
: [];
|
||||||
if (this.anonymise) {
|
if (this.flagResolver.isEnabled('anonymiseEventLog')) {
|
||||||
users = this.anonymiseUsers(users);
|
users = this.anonymiseUsers(users);
|
||||||
}
|
}
|
||||||
this.openApiService.respondWithValidation(
|
this.openApiService.respondWithValidation(
|
||||||
@ -368,7 +369,9 @@ export default class UserAdminController extends Controller {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const passwordAuthSettings =
|
const passwordAuthSettings =
|
||||||
await this.settingService.get<SimpleAuthSettings>(simpleAuthKey);
|
await this.settingService.get<SimpleAuthSettings>(
|
||||||
|
simpleAuthSettingsKey,
|
||||||
|
);
|
||||||
|
|
||||||
let inviteLink: string;
|
let inviteLink: string;
|
||||||
if (!passwordAuthSettings?.disabled) {
|
if (!passwordAuthSettings?.disabled) {
|
||||||
|
@ -82,9 +82,7 @@ test('should accept client metrics with yes/no', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should accept client metrics with yes/no with metricsV2', async () => {
|
test('should accept client metrics with yes/no with metricsV2', async () => {
|
||||||
const testRunner = await getSetup({
|
const testRunner = await getSetup();
|
||||||
experimental: { metricsV2: { enabled: true } },
|
|
||||||
});
|
|
||||||
await testRunner.request
|
await testRunner.request
|
||||||
.post('/api/client/metrics')
|
.post('/api/client/metrics')
|
||||||
.send({
|
.send({
|
||||||
|
@ -28,7 +28,7 @@ class IndexRouter extends Controller {
|
|||||||
this.use('/api/admin', new AdminApi(config, services).router);
|
this.use('/api/admin', new AdminApi(config, services).router);
|
||||||
this.use('/api/client', new ClientApi(config, services).router);
|
this.use('/api/client', new ClientApi(config, services).router);
|
||||||
|
|
||||||
if (config.experimental.embedProxy) {
|
if (config.experimental.flags.embedProxy) {
|
||||||
this.use(
|
this.use(
|
||||||
'/api/frontend',
|
'/api/frontend',
|
||||||
new ProxyController(config, services).router,
|
new ProxyController(config, services).router,
|
||||||
|
@ -2,9 +2,7 @@ import { Response, Request } from 'express';
|
|||||||
import Controller from '../controller';
|
import Controller from '../controller';
|
||||||
import { IUnleashConfig, IUnleashServices } from '../../types';
|
import { IUnleashConfig, IUnleashServices } from '../../types';
|
||||||
import { Logger } from '../../logger';
|
import { Logger } from '../../logger';
|
||||||
import { OpenApiService } from '../../services/openapi-service';
|
|
||||||
import { NONE } from '../../types/permissions';
|
import { NONE } from '../../types/permissions';
|
||||||
import { ProxyService } from '../../services/proxy-service';
|
|
||||||
import ApiUser from '../../types/api-user';
|
import ApiUser from '../../types/api-user';
|
||||||
import {
|
import {
|
||||||
proxyFeaturesSchema,
|
proxyFeaturesSchema,
|
||||||
@ -28,29 +26,25 @@ interface ApiUserRequest<
|
|||||||
user: ApiUser;
|
user: ApiUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Services = Pick<
|
||||||
|
IUnleashServices,
|
||||||
|
'settingService' | 'proxyService' | 'openApiService'
|
||||||
|
>;
|
||||||
|
|
||||||
export default class ProxyController extends Controller {
|
export default class ProxyController extends Controller {
|
||||||
private readonly logger: Logger;
|
private readonly logger: Logger;
|
||||||
|
|
||||||
private proxyService: ProxyService;
|
private services: Services;
|
||||||
|
|
||||||
private openApiService: OpenApiService;
|
constructor(config: IUnleashConfig, services: Services) {
|
||||||
|
|
||||||
constructor(
|
|
||||||
config: IUnleashConfig,
|
|
||||||
{
|
|
||||||
proxyService,
|
|
||||||
openApiService,
|
|
||||||
}: Pick<IUnleashServices, 'proxyService' | 'openApiService'>,
|
|
||||||
) {
|
|
||||||
super(config);
|
super(config);
|
||||||
this.logger = config.getLogger('client-api/feature.js');
|
this.logger = config.getLogger('client-api/feature.js');
|
||||||
this.proxyService = proxyService;
|
this.services = services;
|
||||||
this.openApiService = openApiService;
|
|
||||||
|
|
||||||
if (config.frontendApiOrigins.length > 0) {
|
if (config.frontendApiOrigins.length > 0) {
|
||||||
// Support CORS requests for the frontend endpoints.
|
// Support CORS requests for the frontend endpoints.
|
||||||
// Preflight requests are handled in `app.ts`.
|
// Preflight requests are handled in `app.ts`.
|
||||||
this.app.use(corsOriginMiddleware(config.frontendApiOrigins));
|
this.app.use(corsOriginMiddleware(services));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.route({
|
this.route({
|
||||||
@ -59,7 +53,7 @@ export default class ProxyController extends Controller {
|
|||||||
handler: this.getProxyFeatures,
|
handler: this.getProxyFeatures,
|
||||||
permission: NONE,
|
permission: NONE,
|
||||||
middleware: [
|
middleware: [
|
||||||
this.openApiService.validPath({
|
this.services.openApiService.validPath({
|
||||||
tags: ['Unstable'],
|
tags: ['Unstable'],
|
||||||
operationId: 'getFrontendFeatures',
|
operationId: 'getFrontendFeatures',
|
||||||
responses: {
|
responses: {
|
||||||
@ -89,7 +83,7 @@ export default class ProxyController extends Controller {
|
|||||||
handler: this.registerProxyMetrics,
|
handler: this.registerProxyMetrics,
|
||||||
permission: NONE,
|
permission: NONE,
|
||||||
middleware: [
|
middleware: [
|
||||||
this.openApiService.validPath({
|
this.services.openApiService.validPath({
|
||||||
tags: ['Unstable'],
|
tags: ['Unstable'],
|
||||||
operationId: 'registerFrontendMetrics',
|
operationId: 'registerFrontendMetrics',
|
||||||
requestBody: createRequestSchema('proxyMetricsSchema'),
|
requestBody: createRequestSchema('proxyMetricsSchema'),
|
||||||
@ -104,7 +98,7 @@ export default class ProxyController extends Controller {
|
|||||||
handler: ProxyController.registerProxyClient,
|
handler: ProxyController.registerProxyClient,
|
||||||
permission: NONE,
|
permission: NONE,
|
||||||
middleware: [
|
middleware: [
|
||||||
this.openApiService.validPath({
|
this.services.openApiService.validPath({
|
||||||
tags: ['Unstable'],
|
tags: ['Unstable'],
|
||||||
operationId: 'registerFrontendClient',
|
operationId: 'registerFrontendClient',
|
||||||
requestBody: createRequestSchema('proxyClientSchema'),
|
requestBody: createRequestSchema('proxyClientSchema'),
|
||||||
@ -141,11 +135,11 @@ export default class ProxyController extends Controller {
|
|||||||
req: ApiUserRequest,
|
req: ApiUserRequest,
|
||||||
res: Response<ProxyFeaturesSchema>,
|
res: Response<ProxyFeaturesSchema>,
|
||||||
) {
|
) {
|
||||||
const toggles = await this.proxyService.getProxyFeatures(
|
const toggles = await this.services.proxyService.getProxyFeatures(
|
||||||
req.user,
|
req.user,
|
||||||
ProxyController.createContext(req),
|
ProxyController.createContext(req),
|
||||||
);
|
);
|
||||||
this.openApiService.respondWithValidation(
|
this.services.openApiService.respondWithValidation(
|
||||||
200,
|
200,
|
||||||
res,
|
res,
|
||||||
proxyFeaturesSchema.$id,
|
proxyFeaturesSchema.$id,
|
||||||
@ -157,7 +151,7 @@ export default class ProxyController extends Controller {
|
|||||||
req: ApiUserRequest<unknown, unknown, ProxyMetricsSchema>,
|
req: ApiUserRequest<unknown, unknown, ProxyMetricsSchema>,
|
||||||
res: Response,
|
res: Response,
|
||||||
) {
|
) {
|
||||||
await this.proxyService.registerProxyMetrics(
|
await this.services.proxyService.registerProxyMetrics(
|
||||||
req.user,
|
req.user,
|
||||||
req.body,
|
req.body,
|
||||||
req.ip,
|
req.ip,
|
||||||
|
@ -16,7 +16,6 @@ import ApiUser from '../../types/api-user';
|
|||||||
import { ALL } from '../../types/models/api-token';
|
import { ALL } from '../../types/models/api-token';
|
||||||
import User from '../../types/user';
|
import User from '../../types/user';
|
||||||
import { collapseHourlyMetrics } from '../../util/collapseHourlyMetrics';
|
import { collapseHourlyMetrics } from '../../util/collapseHourlyMetrics';
|
||||||
import { IExperimentalOptions } from '../../experimental';
|
|
||||||
|
|
||||||
export default class ClientMetricsServiceV2 {
|
export default class ClientMetricsServiceV2 {
|
||||||
private timers: NodeJS.Timeout[] = [];
|
private timers: NodeJS.Timeout[] = [];
|
||||||
@ -27,7 +26,7 @@ export default class ClientMetricsServiceV2 {
|
|||||||
|
|
||||||
private featureToggleStore: IFeatureToggleStore;
|
private featureToggleStore: IFeatureToggleStore;
|
||||||
|
|
||||||
private experimental: IExperimentalOptions;
|
private batchMetricsEnabled: boolean;
|
||||||
|
|
||||||
private eventBus: EventEmitter;
|
private eventBus: EventEmitter;
|
||||||
|
|
||||||
@ -47,13 +46,13 @@ export default class ClientMetricsServiceV2 {
|
|||||||
) {
|
) {
|
||||||
this.featureToggleStore = featureToggleStore;
|
this.featureToggleStore = featureToggleStore;
|
||||||
this.clientMetricsStoreV2 = clientMetricsStoreV2;
|
this.clientMetricsStoreV2 = clientMetricsStoreV2;
|
||||||
this.experimental = experimental;
|
this.batchMetricsEnabled = experimental.flags.batchMetrics;
|
||||||
this.eventBus = eventBus;
|
this.eventBus = eventBus;
|
||||||
this.logger = getLogger(
|
this.logger = getLogger(
|
||||||
'/services/client-metrics/client-metrics-service-v2.ts',
|
'/services/client-metrics/client-metrics-service-v2.ts',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.experimental.batchMetrics) {
|
if (this.batchMetricsEnabled) {
|
||||||
this.timers.push(
|
this.timers.push(
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
this.bulkAdd().catch(console.error);
|
this.bulkAdd().catch(console.error);
|
||||||
@ -91,7 +90,7 @@ export default class ClientMetricsServiceV2 {
|
|||||||
}))
|
}))
|
||||||
.filter((item) => !(item.yes === 0 && item.no === 0));
|
.filter((item) => !(item.yes === 0 && item.no === 0));
|
||||||
|
|
||||||
if (this.experimental.batchMetrics) {
|
if (this.batchMetricsEnabled) {
|
||||||
this.unsavedMetrics = collapseHourlyMetrics([
|
this.unsavedMetrics = collapseHourlyMetrics([
|
||||||
...this.unsavedMetrics,
|
...this.unsavedMetrics,
|
||||||
...clientMetrics,
|
...clientMetrics,
|
||||||
@ -104,7 +103,7 @@ export default class ClientMetricsServiceV2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async bulkAdd(): Promise<void> {
|
async bulkAdd(): Promise<void> {
|
||||||
if (this.experimental.batchMetrics && this.unsavedMetrics.length > 0) {
|
if (this.batchMetricsEnabled && this.unsavedMetrics.length > 0) {
|
||||||
// Make a copy of `unsavedMetrics` in case new metrics
|
// Make a copy of `unsavedMetrics` in case new metrics
|
||||||
// arrive while awaiting `batchInsertMetrics`.
|
// arrive while awaiting `batchInsertMetrics`.
|
||||||
const copy = [...this.unsavedMetrics];
|
const copy = [...this.unsavedMetrics];
|
||||||
|
@ -294,12 +294,11 @@ export default class ProjectService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Remove the optional nature of createdBy - this in place to make sure enterprise is compatible
|
|
||||||
async addUser(
|
async addUser(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
roleId: number,
|
roleId: number,
|
||||||
userId: number,
|
userId: number,
|
||||||
createdBy?: string,
|
createdBy: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const [roles, users] = await this.accessService.getProjectRoleAccess(
|
const [roles, users] = await this.accessService.getProjectRoleAccess(
|
||||||
projectId,
|
projectId,
|
||||||
@ -323,7 +322,7 @@ export default class ProjectService {
|
|||||||
await this.eventStore.store(
|
await this.eventStore.store(
|
||||||
new ProjectUserAddedEvent({
|
new ProjectUserAddedEvent({
|
||||||
project: projectId,
|
project: projectId,
|
||||||
createdBy,
|
createdBy: createdBy || 'system-user',
|
||||||
data: {
|
data: {
|
||||||
roleId,
|
roleId,
|
||||||
userId,
|
userId,
|
||||||
@ -334,12 +333,11 @@ export default class ProjectService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Remove the optional nature of createdBy - this in place to make sure enterprise is compatible
|
|
||||||
async removeUser(
|
async removeUser(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
roleId: number,
|
roleId: number,
|
||||||
userId: number,
|
userId: number,
|
||||||
createdBy?: string,
|
createdBy: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const role = await this.findProjectRole(projectId, roleId);
|
const role = await this.findProjectRole(projectId, roleId);
|
||||||
|
|
||||||
@ -367,7 +365,7 @@ export default class ProjectService {
|
|||||||
projectId: string,
|
projectId: string,
|
||||||
roleId: number,
|
roleId: number,
|
||||||
groupId: number,
|
groupId: number,
|
||||||
modifiedBy?: string,
|
modifiedBy: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const role = await this.accessService.getRole(roleId);
|
const role = await this.accessService.getRole(roleId);
|
||||||
const group = await this.groupService.getGroup(groupId);
|
const group = await this.groupService.getGroup(groupId);
|
||||||
@ -397,7 +395,7 @@ export default class ProjectService {
|
|||||||
projectId: string,
|
projectId: string,
|
||||||
roleId: number,
|
roleId: number,
|
||||||
groupId: number,
|
groupId: number,
|
||||||
modifiedBy?: string,
|
modifiedBy: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const group = await this.groupService.getGroup(groupId);
|
const group = await this.groupService.getGroup(groupId);
|
||||||
const role = await this.accessService.getRole(roleId);
|
const role = await this.accessService.getRole(roleId);
|
||||||
|
@ -8,31 +8,48 @@ import {
|
|||||||
SettingDeletedEvent,
|
SettingDeletedEvent,
|
||||||
SettingUpdatedEvent,
|
SettingUpdatedEvent,
|
||||||
} from '../types/events';
|
} from '../types/events';
|
||||||
|
import { validateOrigins } from '../util/validateOrigin';
|
||||||
|
import {
|
||||||
|
FrontendSettings,
|
||||||
|
frontendSettingsKey,
|
||||||
|
} from '../types/settings/frontend-settings';
|
||||||
|
import BadDataError from '../error/bad-data-error';
|
||||||
|
|
||||||
export default class SettingService {
|
export default class SettingService {
|
||||||
|
private config: IUnleashConfig;
|
||||||
|
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
private settingStore: ISettingStore;
|
private settingStore: ISettingStore;
|
||||||
|
|
||||||
private eventStore: IEventStore;
|
private eventStore: IEventStore;
|
||||||
|
|
||||||
|
// SettingService.getFrontendSettings is called on every request to the
|
||||||
|
// frontend API. Keep fetched settings in a cache for fewer DB queries.
|
||||||
|
private cache = new Map<string, unknown>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
{
|
{
|
||||||
settingStore,
|
settingStore,
|
||||||
eventStore,
|
eventStore,
|
||||||
}: Pick<IUnleashStores, 'settingStore' | 'eventStore'>,
|
}: Pick<IUnleashStores, 'settingStore' | 'eventStore'>,
|
||||||
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
|
config: IUnleashConfig,
|
||||||
) {
|
) {
|
||||||
this.logger = getLogger('services/setting-service.ts');
|
this.config = config;
|
||||||
|
this.logger = config.getLogger('services/setting-service.ts');
|
||||||
this.settingStore = settingStore;
|
this.settingStore = settingStore;
|
||||||
this.eventStore = eventStore;
|
this.eventStore = eventStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
async get<T>(id: string): Promise<T> {
|
async get<T>(id: string, defaultValue?: T): Promise<T> {
|
||||||
return this.settingStore.get(id);
|
if (!this.cache.has(id)) {
|
||||||
|
this.cache.set(id, await this.settingStore.get(id));
|
||||||
|
}
|
||||||
|
return (this.cache.get(id) as T) || defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
async insert(id: string, value: object, createdBy: string): Promise<void> {
|
async insert(id: string, value: object, createdBy: string): Promise<void> {
|
||||||
|
this.cache.delete(id);
|
||||||
const exists = await this.settingStore.exists(id);
|
const exists = await this.settingStore.exists(id);
|
||||||
if (exists) {
|
if (exists) {
|
||||||
await this.settingStore.updateRow(id, value);
|
await this.settingStore.updateRow(id, value);
|
||||||
@ -54,6 +71,7 @@ export default class SettingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async delete(id: string, createdBy: string): Promise<void> {
|
async delete(id: string, createdBy: string): Promise<void> {
|
||||||
|
this.cache.delete(id);
|
||||||
await this.settingStore.delete(id);
|
await this.settingStore.delete(id);
|
||||||
await this.eventStore.store(
|
await this.eventStore.store(
|
||||||
new SettingDeletedEvent({
|
new SettingDeletedEvent({
|
||||||
@ -64,6 +82,28 @@ export default class SettingService {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteAll(): Promise<void> {
|
||||||
|
this.cache.clear();
|
||||||
|
await this.settingStore.deleteAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
async setFrontendSettings(
|
||||||
|
value: FrontendSettings,
|
||||||
|
createdBy: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const error = validateOrigins(value.frontendApiOrigins);
|
||||||
|
if (error) {
|
||||||
|
throw new BadDataError(error);
|
||||||
|
}
|
||||||
|
await this.insert(frontendSettingsKey, value, createdBy);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFrontendSettings(): Promise<FrontendSettings> {
|
||||||
|
return this.get(frontendSettingsKey, {
|
||||||
|
frontendApiOrigins: this.config.frontendApiOrigins,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = SettingService;
|
module.exports = SettingService;
|
||||||
|
@ -22,7 +22,7 @@ import { IUserStore } from '../types/stores/user-store';
|
|||||||
import { RoleName } from '../types/model';
|
import { RoleName } from '../types/model';
|
||||||
import SettingService from './setting-service';
|
import SettingService from './setting-service';
|
||||||
import { SimpleAuthSettings } from '../server-impl';
|
import { SimpleAuthSettings } from '../server-impl';
|
||||||
import { simpleAuthKey } from '../types/settings/simple-auth-settings';
|
import { simpleAuthSettingsKey } from '../types/settings/simple-auth-settings';
|
||||||
import DisabledError from '../error/disabled-error';
|
import DisabledError from '../error/disabled-error';
|
||||||
import PasswordMismatch from '../error/password-mismatch';
|
import PasswordMismatch from '../error/password-mismatch';
|
||||||
import BadDataError from '../error/bad-data-error';
|
import BadDataError from '../error/bad-data-error';
|
||||||
@ -276,7 +276,7 @@ class UserService {
|
|||||||
|
|
||||||
async loginUser(usernameOrEmail: string, password: string): Promise<IUser> {
|
async loginUser(usernameOrEmail: string, password: string): Promise<IUser> {
|
||||||
const settings = await this.settingService.get<SimpleAuthSettings>(
|
const settings = await this.settingService.get<SimpleAuthSettings>(
|
||||||
simpleAuthKey,
|
simpleAuthSettingsKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (settings?.disabled) {
|
if (settings?.disabled) {
|
||||||
|
43
src/lib/types/experimental.ts
Normal file
43
src/lib/types/experimental.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { parseEnvVarBoolean } from '../util/parseEnvVar';
|
||||||
|
|
||||||
|
export type IFlags = Partial<Record<string, boolean>>;
|
||||||
|
|
||||||
|
export const defaultExperimentalOptions = {
|
||||||
|
flags: {
|
||||||
|
ENABLE_DARK_MODE_SUPPORT: false,
|
||||||
|
anonymiseEventLog: false,
|
||||||
|
embedProxy: parseEnvVarBoolean(
|
||||||
|
process.env.UNLEASH_EXPERIMENTAL_EMBED_PROXY,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
batchMetrics: parseEnvVarBoolean(
|
||||||
|
process.env.UNLEASH_EXPERIMENTAL_BATCH_METRICS,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
externalResolver: { isEnabled: (): boolean => false },
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface IExperimentalOptions {
|
||||||
|
flags: {
|
||||||
|
[key: string]: boolean;
|
||||||
|
ENABLE_DARK_MODE_SUPPORT?: boolean;
|
||||||
|
embedProxy?: boolean;
|
||||||
|
batchMetrics?: boolean;
|
||||||
|
anonymiseEventLog?: boolean;
|
||||||
|
};
|
||||||
|
externalResolver: IExternalFlagResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFlagContext {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFlagResolver {
|
||||||
|
getAll: (context?: IFlagContext) => IFlags;
|
||||||
|
isEnabled: (expName: string, context?: IFlagContext) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IExternalFlagResolver {
|
||||||
|
isEnabled: (flagName: string, context?: IFlagContext) => boolean;
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
import { LogLevel, LogProvider } from '../logger';
|
import { LogLevel, LogProvider } from '../logger';
|
||||||
import { ILegacyApiTokenCreate } from './models/api-token';
|
import { ILegacyApiTokenCreate } from './models/api-token';
|
||||||
import { IExperimentalOptions } from '../experimental';
|
import { IFlagResolver, IExperimentalOptions, IFlags } from './experimental';
|
||||||
import SMTPTransport from 'nodemailer/lib/smtp-transport';
|
import SMTPTransport from 'nodemailer/lib/smtp-transport';
|
||||||
|
|
||||||
export type EventHook = (eventName: string, data: object) => void;
|
export type EventHook = (eventName: string, data: object) => void;
|
||||||
@ -102,7 +102,7 @@ export interface IUnleashOptions {
|
|||||||
authentication?: Partial<IAuthOption>;
|
authentication?: Partial<IAuthOption>;
|
||||||
ui?: object;
|
ui?: object;
|
||||||
import?: Partial<IImportOption>;
|
import?: Partial<IImportOption>;
|
||||||
experimental?: IExperimentalOptions;
|
experimental?: Partial<IExperimentalOptions>;
|
||||||
email?: Partial<IEmailOption>;
|
email?: Partial<IEmailOption>;
|
||||||
secureHeaders?: boolean;
|
secureHeaders?: boolean;
|
||||||
additionalCspAllowedDomains?: ICspDomainOptions;
|
additionalCspAllowedDomains?: ICspDomainOptions;
|
||||||
@ -139,7 +139,6 @@ export interface IListeningHost {
|
|||||||
export interface IUIConfig {
|
export interface IUIConfig {
|
||||||
slogan?: string;
|
slogan?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
flags?: { [key: string]: boolean };
|
|
||||||
links?: [
|
links?: [
|
||||||
{
|
{
|
||||||
value: string;
|
value: string;
|
||||||
@ -148,6 +147,7 @@ export interface IUIConfig {
|
|||||||
title: string;
|
title: string;
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
flags?: IFlags;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICspDomainOptions {
|
export interface ICspDomainOptions {
|
||||||
@ -177,6 +177,7 @@ export interface IUnleashConfig {
|
|||||||
ui: IUIConfig;
|
ui: IUIConfig;
|
||||||
import: IImportOption;
|
import: IImportOption;
|
||||||
experimental?: IExperimentalOptions;
|
experimental?: IExperimentalOptions;
|
||||||
|
flagResolver: IFlagResolver;
|
||||||
email: IEmailOption;
|
email: IEmailOption;
|
||||||
secureHeaders: boolean;
|
secureHeaders: boolean;
|
||||||
additionalCspAllowedDomains: ICspDomainConfig;
|
additionalCspAllowedDomains: ICspDomainConfig;
|
||||||
|
5
src/lib/types/settings/frontend-settings.ts
Normal file
5
src/lib/types/settings/frontend-settings.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { IUnleashConfig } from '../option';
|
||||||
|
|
||||||
|
export const frontendSettingsKey = 'unleash.frontend';
|
||||||
|
|
||||||
|
export type FrontendSettings = Pick<IUnleashConfig, 'frontendApiOrigins'>;
|
@ -1,4 +1,5 @@
|
|||||||
export const simpleAuthKey = 'unleash.auth.simple';
|
export const simpleAuthSettingsKey = 'unleash.auth.simple';
|
||||||
|
|
||||||
export interface SimpleAuthSettings {
|
export interface SimpleAuthSettings {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
}
|
}
|
||||||
|
101
src/lib/util/flag-resolver.test.ts
Normal file
101
src/lib/util/flag-resolver.test.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { defaultExperimentalOptions } from '../types/experimental';
|
||||||
|
import FlagResolver from './flag-resolver';
|
||||||
|
|
||||||
|
test('should produce empty exposed flags', () => {
|
||||||
|
const resolver = new FlagResolver(defaultExperimentalOptions);
|
||||||
|
|
||||||
|
const result = resolver.getAll();
|
||||||
|
|
||||||
|
expect(result.ENABLE_DARK_MODE_SUPPORT).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should produce UI flags with extra dynamic flags', () => {
|
||||||
|
const config = {
|
||||||
|
...defaultExperimentalOptions,
|
||||||
|
flags: { extraFlag: false },
|
||||||
|
};
|
||||||
|
const resolver = new FlagResolver(config);
|
||||||
|
const result = resolver.getAll();
|
||||||
|
|
||||||
|
expect(result.extraFlag).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should use external resolver for dynamic flags', () => {
|
||||||
|
const externalResolver = {
|
||||||
|
isEnabled: (name: string) => {
|
||||||
|
if (name === 'extraFlag') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolver = new FlagResolver({
|
||||||
|
flags: {
|
||||||
|
extraFlag: false,
|
||||||
|
},
|
||||||
|
externalResolver,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = resolver.getAll();
|
||||||
|
|
||||||
|
expect(result.extraFlag).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not use external resolver for enabled experiments', () => {
|
||||||
|
const externalResolver = {
|
||||||
|
isEnabled: () => {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolver = new FlagResolver({
|
||||||
|
flags: {
|
||||||
|
should_be_enabled: true,
|
||||||
|
extraFlag: false,
|
||||||
|
},
|
||||||
|
externalResolver,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = resolver.getAll();
|
||||||
|
|
||||||
|
expect(result.should_be_enabled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should load experimental flags', () => {
|
||||||
|
const externalResolver = {
|
||||||
|
isEnabled: () => {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const resolver = new FlagResolver({
|
||||||
|
flags: {
|
||||||
|
extraFlag: false,
|
||||||
|
someFlag: true,
|
||||||
|
},
|
||||||
|
externalResolver,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resolver.isEnabled('someFlag')).toBe(true);
|
||||||
|
expect(resolver.isEnabled('extraFlag')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should load experimental flags from external provider', () => {
|
||||||
|
const externalResolver = {
|
||||||
|
isEnabled: (name: string) => {
|
||||||
|
if (name === 'extraFlag') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolver = new FlagResolver({
|
||||||
|
flags: {
|
||||||
|
extraFlag: false,
|
||||||
|
someFlag: true,
|
||||||
|
},
|
||||||
|
externalResolver,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resolver.isEnabled('someFlag')).toBe(true);
|
||||||
|
expect(resolver.isEnabled('extraFlag')).toBe(true);
|
||||||
|
});
|
38
src/lib/util/flag-resolver.ts
Normal file
38
src/lib/util/flag-resolver.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import {
|
||||||
|
IExperimentalOptions,
|
||||||
|
IExternalFlagResolver,
|
||||||
|
IFlagContext,
|
||||||
|
IFlags,
|
||||||
|
IFlagResolver,
|
||||||
|
} from '../types/experimental';
|
||||||
|
export default class FlagResolver implements IFlagResolver {
|
||||||
|
private experiments: IFlags;
|
||||||
|
|
||||||
|
private externalResolver: IExternalFlagResolver;
|
||||||
|
|
||||||
|
constructor(expOpt: IExperimentalOptions) {
|
||||||
|
this.experiments = expOpt.flags;
|
||||||
|
this.externalResolver = expOpt.externalResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAll(context?: IFlagContext): IFlags {
|
||||||
|
const flags: IFlags = { ...this.experiments };
|
||||||
|
|
||||||
|
Object.keys(flags).forEach((flagName) => {
|
||||||
|
if (!this.experiments[flagName])
|
||||||
|
flags[flagName] = this.externalResolver.isEnabled(
|
||||||
|
flagName,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(expName: string, context?: IFlagContext): boolean {
|
||||||
|
if (this.experiments[expName]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return this.externalResolver.isEnabled(expName, context);
|
||||||
|
}
|
||||||
|
}
|
@ -29,9 +29,11 @@ test('parseEnvVarBoolean', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('parseEnvVarStringList', () => {
|
test('parseEnvVarStringList', () => {
|
||||||
|
expect(parseEnvVarStrings(undefined, [])).toEqual([]);
|
||||||
|
expect(parseEnvVarStrings(undefined, ['a'])).toEqual(['a']);
|
||||||
|
expect(parseEnvVarStrings('', ['a'])).toEqual([]);
|
||||||
expect(parseEnvVarStrings('', [])).toEqual([]);
|
expect(parseEnvVarStrings('', [])).toEqual([]);
|
||||||
expect(parseEnvVarStrings(' ', [])).toEqual([]);
|
expect(parseEnvVarStrings(' ', [])).toEqual([]);
|
||||||
expect(parseEnvVarStrings('', ['*'])).toEqual(['*']);
|
|
||||||
expect(parseEnvVarStrings('a', ['*'])).toEqual(['a']);
|
expect(parseEnvVarStrings('a', ['*'])).toEqual(['a']);
|
||||||
expect(parseEnvVarStrings('a,b,c', [])).toEqual(['a', 'b', 'c']);
|
expect(parseEnvVarStrings('a,b,c', [])).toEqual(['a', 'b', 'c']);
|
||||||
expect(parseEnvVarStrings('a,b,c', [])).toEqual(['a', 'b', 'c']);
|
expect(parseEnvVarStrings('a,b,c', [])).toEqual(['a', 'b', 'c']);
|
||||||
|
@ -20,10 +20,10 @@ export function parseEnvVarBoolean(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function parseEnvVarStrings(
|
export function parseEnvVarStrings(
|
||||||
envVar: string,
|
envVar: string | undefined,
|
||||||
defaultVal: string[],
|
defaultVal: string[],
|
||||||
): string[] {
|
): string[] {
|
||||||
if (envVar) {
|
if (typeof envVar === 'string') {
|
||||||
return envVar
|
return envVar
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((item) => item.trim())
|
.map((item) => item.trim())
|
||||||
|
24
src/lib/util/validateOrigin.test.ts
Normal file
24
src/lib/util/validateOrigin.test.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { validateOrigin } from './validateOrigin';
|
||||||
|
|
||||||
|
test('validateOrigin', () => {
|
||||||
|
expect(validateOrigin(undefined)).toEqual(false);
|
||||||
|
expect(validateOrigin('')).toEqual(false);
|
||||||
|
expect(validateOrigin(' ')).toEqual(false);
|
||||||
|
expect(validateOrigin('a')).toEqual(false);
|
||||||
|
expect(validateOrigin('**')).toEqual(false);
|
||||||
|
expect(validateOrigin('localhost')).toEqual(false);
|
||||||
|
expect(validateOrigin('localhost:8080')).toEqual(false);
|
||||||
|
expect(validateOrigin('//localhost:8080')).toEqual(false);
|
||||||
|
expect(validateOrigin('http://localhost/')).toEqual(false);
|
||||||
|
expect(validateOrigin('http://localhost/a')).toEqual(false);
|
||||||
|
expect(validateOrigin('https://example.com/a')).toEqual(false);
|
||||||
|
expect(validateOrigin('https://example.com ')).toEqual(false);
|
||||||
|
expect(validateOrigin('https://*.example.com ')).toEqual(false);
|
||||||
|
expect(validateOrigin('*.example.com')).toEqual(false);
|
||||||
|
|
||||||
|
expect(validateOrigin('*')).toEqual(true);
|
||||||
|
expect(validateOrigin('http://localhost')).toEqual(true);
|
||||||
|
expect(validateOrigin('http://localhost:8080')).toEqual(true);
|
||||||
|
expect(validateOrigin('https://example.com')).toEqual(true);
|
||||||
|
expect(validateOrigin('https://example.com:8080')).toEqual(true);
|
||||||
|
});
|
24
src/lib/util/validateOrigin.ts
Normal file
24
src/lib/util/validateOrigin.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
export const validateOrigin = (origin: string): boolean => {
|
||||||
|
if (origin === '*') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (origin?.includes('*')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = new URL(origin);
|
||||||
|
return parsed.origin && parsed.origin === origin;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateOrigins = (origins: string[]): string | undefined => {
|
||||||
|
for (const origin of origins) {
|
||||||
|
if (!validateOrigin(origin)) {
|
||||||
|
return `Invalid origin: ${origin}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@ -31,11 +31,12 @@ process.nextTick(async () => {
|
|||||||
enable: false,
|
enable: false,
|
||||||
},
|
},
|
||||||
experimental: {
|
experimental: {
|
||||||
metricsV2: { enabled: true },
|
// externalResolver: unleash,
|
||||||
anonymiseEventLog: false,
|
flags: {
|
||||||
userGroups: true,
|
embedProxy: true,
|
||||||
embedProxy: true,
|
batchMetrics: true,
|
||||||
batchMetrics: true,
|
anonymiseEventLog: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
authentication: {
|
authentication: {
|
||||||
initApiTokens: [
|
initApiTokens: [
|
||||||
|
@ -23,9 +23,10 @@ export function createTestConfig(config?: IUnleashOptions): IUnleashConfig {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
experimental: {
|
experimental: {
|
||||||
userGroups: true,
|
flags: {
|
||||||
embedProxy: true,
|
embedProxy: true,
|
||||||
batchMetrics: true,
|
batchMetrics: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const options = mergeAll<IUnleashOptions>([testConfig, config]);
|
const options = mergeAll<IUnleashOptions>([testConfig, config]);
|
||||||
|
@ -9,9 +9,7 @@ let db: ITestDb;
|
|||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
db = await dbInit('client_metrics_serial', getLogger);
|
db = await dbInit('client_metrics_serial', getLogger);
|
||||||
app = await setupAppWithCustomConfig(db.stores, {
|
app = await setupAppWithCustomConfig(db.stores, {});
|
||||||
experimental: { metricsV2: { enabled: true } },
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import dbInit, { ITestDb } from '../../helpers/database-init';
|
import dbInit, { ITestDb } from '../../helpers/database-init';
|
||||||
import { setupApp } from '../../helpers/test-helper';
|
import { IUnleashTest, setupApp } from '../../helpers/test-helper';
|
||||||
import getLogger from '../../../fixtures/no-logger';
|
import getLogger from '../../../fixtures/no-logger';
|
||||||
import { simpleAuthKey } from '../../../../lib/types/settings/simple-auth-settings';
|
import { simpleAuthSettingsKey } from '../../../../lib/types/settings/simple-auth-settings';
|
||||||
|
import { randomId } from '../../../../lib/util/random-id';
|
||||||
|
|
||||||
let db: ITestDb;
|
let db: ITestDb;
|
||||||
let app;
|
let app: IUnleashTest;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
db = await dbInit('config_api_serial', getLogger);
|
db = await dbInit('config_api_serial', getLogger);
|
||||||
@ -16,24 +17,71 @@ afterAll(async () => {
|
|||||||
await db.destroy();
|
await db.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await app.services.settingService.deleteAll();
|
||||||
|
});
|
||||||
|
|
||||||
test('gets ui config fields', async () => {
|
test('gets ui config fields', async () => {
|
||||||
const { body } = await app.request
|
const { body } = await app.request
|
||||||
.get('/api/admin/ui-config')
|
.get('/api/admin/ui-config')
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(body.unleashUrl).toBe('http://localhost:4242');
|
expect(body.unleashUrl).toBe('http://localhost:4242');
|
||||||
expect(body.version).toBeDefined();
|
expect(body.version).toBeDefined();
|
||||||
expect(body.emailEnabled).toBe(false);
|
expect(body.emailEnabled).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('gets ui config with disablePasswordAuth', async () => {
|
test('gets ui config with disablePasswordAuth', async () => {
|
||||||
await db.stores.settingStore.insert(simpleAuthKey, { disabled: true });
|
await db.stores.settingStore.insert(simpleAuthSettingsKey, {
|
||||||
|
disabled: true,
|
||||||
|
});
|
||||||
const { body } = await app.request
|
const { body } = await app.request
|
||||||
.get('/api/admin/ui-config')
|
.get('/api/admin/ui-config')
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(body.disablePasswordAuth).toBe(true);
|
expect(body.disablePasswordAuth).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('gets ui config with frontendSettings', async () => {
|
||||||
|
const frontendApiOrigins = ['https://example.net'];
|
||||||
|
await app.services.settingService.setFrontendSettings(
|
||||||
|
{ frontendApiOrigins },
|
||||||
|
randomId(),
|
||||||
|
);
|
||||||
|
await app.request
|
||||||
|
.get('/api/admin/ui-config')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
.expect((res) =>
|
||||||
|
expect(res.body.frontendApiOrigins).toEqual(frontendApiOrigins),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sets ui config with frontendSettings', async () => {
|
||||||
|
const frontendApiOrigins = ['https://example.org'];
|
||||||
|
await app.request
|
||||||
|
.get('/api/admin/ui-config')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
.expect((res) => expect(res.body.frontendApiOrigins).toEqual(['*']));
|
||||||
|
await app.request
|
||||||
|
.post('/api/admin/ui-config')
|
||||||
|
.send({ frontendSettings: { frontendApiOrigins: [] } })
|
||||||
|
.expect(204);
|
||||||
|
await app.request
|
||||||
|
.get('/api/admin/ui-config')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
.expect((res) => expect(res.body.frontendApiOrigins).toEqual([]));
|
||||||
|
await app.request
|
||||||
|
.post('/api/admin/ui-config')
|
||||||
|
.send({ frontendSettings: { frontendApiOrigins } })
|
||||||
|
.expect(204);
|
||||||
|
await app.request
|
||||||
|
.get('/api/admin/ui-config')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
.expect((res) =>
|
||||||
|
expect(res.body.frontendApiOrigins).toEqual(frontendApiOrigins),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
@ -11,9 +11,7 @@ let defaultToken;
|
|||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
db = await dbInit('metrics_two_api_client', getLogger);
|
db = await dbInit('metrics_two_api_client', getLogger);
|
||||||
app = await setupAppWithAuth(db.stores, {
|
app = await setupAppWithAuth(db.stores, {});
|
||||||
experimental: { metricsV2: { enabled: true } },
|
|
||||||
});
|
|
||||||
defaultToken = await app.services.apiTokenService.createApiToken({
|
defaultToken = await app.services.apiTokenService.createApiToken({
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
project: 'default',
|
project: 'default',
|
||||||
|
@ -2498,6 +2498,28 @@ Object {
|
|||||||
},
|
},
|
||||||
"type": "array",
|
"type": "array",
|
||||||
},
|
},
|
||||||
|
"setUiConfigSchema": Object {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": Object {
|
||||||
|
"frontendSettings": Object {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": Object {
|
||||||
|
"frontendApiOrigins": Object {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"items": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": Array [
|
||||||
|
"frontendApiOrigins",
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
"sortOrderSchema": Object {
|
"sortOrderSchema": Object {
|
||||||
"additionalProperties": Object {
|
"additionalProperties": Object {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
@ -2829,6 +2851,12 @@ Object {
|
|||||||
},
|
},
|
||||||
"type": "object",
|
"type": "object",
|
||||||
},
|
},
|
||||||
|
"frontendApiOrigins": Object {
|
||||||
|
"items": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
},
|
||||||
"links": Object {
|
"links": Object {
|
||||||
"items": Object {
|
"items": Object {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@ -6172,7 +6200,7 @@ If the provided project does not exist, the list of events will be empty.",
|
|||||||
},
|
},
|
||||||
"/api/admin/ui-config": Object {
|
"/api/admin/ui-config": Object {
|
||||||
"get": Object {
|
"get": Object {
|
||||||
"operationId": "getUIConfig",
|
"operationId": "getUiConfig",
|
||||||
"responses": Object {
|
"responses": Object {
|
||||||
"200": Object {
|
"200": Object {
|
||||||
"content": Object {
|
"content": Object {
|
||||||
@ -6189,6 +6217,28 @@ If the provided project does not exist, the list of events will be empty.",
|
|||||||
"Admin UI",
|
"Admin UI",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
"post": Object {
|
||||||
|
"operationId": "setUiConfig",
|
||||||
|
"requestBody": Object {
|
||||||
|
"content": Object {
|
||||||
|
"application/json": Object {
|
||||||
|
"schema": Object {
|
||||||
|
"$ref": "#/components/schemas/setUiConfigSchema",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"description": "setUiConfigSchema",
|
||||||
|
"required": true,
|
||||||
|
},
|
||||||
|
"responses": Object {
|
||||||
|
"200": Object {
|
||||||
|
"description": "This response has no body.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"tags": Array [
|
||||||
|
"Admin UI",
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"/api/admin/user": Object {
|
"/api/admin/user": Object {
|
||||||
"get": Object {
|
"get": Object {
|
||||||
|
@ -254,8 +254,18 @@ test('should add a member user to the project', async () => {
|
|||||||
|
|
||||||
const memberRole = await stores.roleStore.getRoleByName(RoleName.MEMBER);
|
const memberRole = await stores.roleStore.getRoleByName(RoleName.MEMBER);
|
||||||
|
|
||||||
await projectService.addUser(project.id, memberRole.id, projectMember1.id);
|
await projectService.addUser(
|
||||||
await projectService.addUser(project.id, memberRole.id, projectMember2.id);
|
project.id,
|
||||||
|
memberRole.id,
|
||||||
|
projectMember1.id,
|
||||||
|
'test',
|
||||||
|
);
|
||||||
|
await projectService.addUser(
|
||||||
|
project.id,
|
||||||
|
memberRole.id,
|
||||||
|
projectMember2.id,
|
||||||
|
'test',
|
||||||
|
);
|
||||||
|
|
||||||
const { users } = await projectService.getAccessToProject(project.id);
|
const { users } = await projectService.getAccessToProject(project.id);
|
||||||
const memberUsers = users.filter((u) => u.roleId === memberRole.id);
|
const memberUsers = users.filter((u) => u.roleId === memberRole.id);
|
||||||
@ -286,8 +296,18 @@ test('should add admin users to the project', async () => {
|
|||||||
|
|
||||||
const ownerRole = await stores.roleStore.getRoleByName(RoleName.OWNER);
|
const ownerRole = await stores.roleStore.getRoleByName(RoleName.OWNER);
|
||||||
|
|
||||||
await projectService.addUser(project.id, ownerRole.id, projectAdmin1.id);
|
await projectService.addUser(
|
||||||
await projectService.addUser(project.id, ownerRole.id, projectAdmin2.id);
|
project.id,
|
||||||
|
ownerRole.id,
|
||||||
|
projectAdmin1.id,
|
||||||
|
'test',
|
||||||
|
);
|
||||||
|
await projectService.addUser(
|
||||||
|
project.id,
|
||||||
|
ownerRole.id,
|
||||||
|
projectAdmin2.id,
|
||||||
|
'test',
|
||||||
|
);
|
||||||
|
|
||||||
const { users } = await projectService.getAccessToProject(project.id);
|
const { users } = await projectService.getAccessToProject(project.id);
|
||||||
|
|
||||||
@ -315,10 +335,20 @@ test('add user should fail if user already have access', async () => {
|
|||||||
|
|
||||||
const memberRole = await stores.roleStore.getRoleByName(RoleName.MEMBER);
|
const memberRole = await stores.roleStore.getRoleByName(RoleName.MEMBER);
|
||||||
|
|
||||||
await projectService.addUser(project.id, memberRole.id, projectMember1.id);
|
await projectService.addUser(
|
||||||
|
project.id,
|
||||||
|
memberRole.id,
|
||||||
|
projectMember1.id,
|
||||||
|
'test',
|
||||||
|
);
|
||||||
|
|
||||||
await expect(async () =>
|
await expect(async () =>
|
||||||
projectService.addUser(project.id, memberRole.id, projectMember1.id),
|
projectService.addUser(
|
||||||
|
project.id,
|
||||||
|
memberRole.id,
|
||||||
|
projectMember1.id,
|
||||||
|
'test',
|
||||||
|
),
|
||||||
).rejects.toThrow(
|
).rejects.toThrow(
|
||||||
new Error('User already has access to project=add-users-twice'),
|
new Error('User already has access to project=add-users-twice'),
|
||||||
);
|
);
|
||||||
@ -339,11 +369,17 @@ test('should remove user from the project', async () => {
|
|||||||
|
|
||||||
const memberRole = await stores.roleStore.getRoleByName(RoleName.MEMBER);
|
const memberRole = await stores.roleStore.getRoleByName(RoleName.MEMBER);
|
||||||
|
|
||||||
await projectService.addUser(project.id, memberRole.id, projectMember1.id);
|
await projectService.addUser(
|
||||||
|
project.id,
|
||||||
|
memberRole.id,
|
||||||
|
projectMember1.id,
|
||||||
|
'test',
|
||||||
|
);
|
||||||
await projectService.removeUser(
|
await projectService.removeUser(
|
||||||
project.id,
|
project.id,
|
||||||
memberRole.id,
|
memberRole.id,
|
||||||
projectMember1.id,
|
projectMember1.id,
|
||||||
|
'test',
|
||||||
);
|
);
|
||||||
|
|
||||||
const { users } = await projectService.getAccessToProject(project.id);
|
const { users } = await projectService.getAccessToProject(project.id);
|
||||||
@ -364,7 +400,12 @@ test('should not remove user from the project', async () => {
|
|||||||
const ownerRole = roles.find((r) => r.name === RoleName.OWNER);
|
const ownerRole = roles.find((r) => r.name === RoleName.OWNER);
|
||||||
|
|
||||||
await expect(async () => {
|
await expect(async () => {
|
||||||
await projectService.removeUser(project.id, ownerRole.id, user.id);
|
await projectService.removeUser(
|
||||||
|
project.id,
|
||||||
|
ownerRole.id,
|
||||||
|
user.id,
|
||||||
|
'test',
|
||||||
|
);
|
||||||
}).rejects.toThrowError(
|
}).rejects.toThrowError(
|
||||||
new Error('A project must have at least one owner'),
|
new Error('A project must have at least one owner'),
|
||||||
);
|
);
|
||||||
@ -617,7 +658,12 @@ test('should add a user to the project with a custom role', async () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
await projectService.addUser(project.id, customRole.id, projectMember1.id);
|
await projectService.addUser(
|
||||||
|
project.id,
|
||||||
|
customRole.id,
|
||||||
|
projectMember1.id,
|
||||||
|
'test',
|
||||||
|
);
|
||||||
|
|
||||||
const { users } = await projectService.getAccessToProject(project.id);
|
const { users } = await projectService.getAccessToProject(project.id);
|
||||||
|
|
||||||
@ -668,8 +714,8 @@ test('should delete role entries when deleting project', async () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
await projectService.addUser(project.id, customRole.id, user1.id);
|
await projectService.addUser(project.id, customRole.id, user1.id, 'test');
|
||||||
await projectService.addUser(project.id, customRole.id, user2.id);
|
await projectService.addUser(project.id, customRole.id, user2.id, 'test');
|
||||||
|
|
||||||
let usersForRole = await accessService.getUsersForRole(customRole.id);
|
let usersForRole = await accessService.getUsersForRole(customRole.id);
|
||||||
expect(usersForRole.length).toBe(2);
|
expect(usersForRole.length).toBe(2);
|
||||||
@ -715,15 +761,25 @@ test('should change a users role in the project', async () => {
|
|||||||
});
|
});
|
||||||
const member = await stores.roleStore.getRoleByName(RoleName.MEMBER);
|
const member = await stores.roleStore.getRoleByName(RoleName.MEMBER);
|
||||||
|
|
||||||
await projectService.addUser(project.id, member.id, projectUser.id);
|
await projectService.addUser(project.id, member.id, projectUser.id, 'test');
|
||||||
const { users } = await projectService.getAccessToProject(project.id);
|
const { users } = await projectService.getAccessToProject(project.id);
|
||||||
let memberUser = users.filter((u) => u.roleId === member.id);
|
let memberUser = users.filter((u) => u.roleId === member.id);
|
||||||
|
|
||||||
expect(memberUser).toHaveLength(1);
|
expect(memberUser).toHaveLength(1);
|
||||||
expect(memberUser[0].id).toBe(projectUser.id);
|
expect(memberUser[0].id).toBe(projectUser.id);
|
||||||
expect(memberUser[0].name).toBe(projectUser.name);
|
expect(memberUser[0].name).toBe(projectUser.name);
|
||||||
await projectService.removeUser(project.id, member.id, projectUser.id);
|
await projectService.removeUser(
|
||||||
await projectService.addUser(project.id, customRole.id, projectUser.id);
|
project.id,
|
||||||
|
member.id,
|
||||||
|
projectUser.id,
|
||||||
|
'test',
|
||||||
|
);
|
||||||
|
await projectService.addUser(
|
||||||
|
project.id,
|
||||||
|
customRole.id,
|
||||||
|
projectUser.id,
|
||||||
|
'test',
|
||||||
|
);
|
||||||
|
|
||||||
let { users: updatedUsers } = await projectService.getAccessToProject(
|
let { users: updatedUsers } = await projectService.getAccessToProject(
|
||||||
project.id,
|
project.id,
|
||||||
@ -751,7 +807,12 @@ test('should update role for user on project', async () => {
|
|||||||
const memberRole = await stores.roleStore.getRoleByName(RoleName.MEMBER);
|
const memberRole = await stores.roleStore.getRoleByName(RoleName.MEMBER);
|
||||||
const ownerRole = await stores.roleStore.getRoleByName(RoleName.OWNER);
|
const ownerRole = await stores.roleStore.getRoleByName(RoleName.OWNER);
|
||||||
|
|
||||||
await projectService.addUser(project.id, memberRole.id, projectMember1.id);
|
await projectService.addUser(
|
||||||
|
project.id,
|
||||||
|
memberRole.id,
|
||||||
|
projectMember1.id,
|
||||||
|
'test',
|
||||||
|
);
|
||||||
await projectService.changeRole(
|
await projectService.changeRole(
|
||||||
project.id,
|
project.id,
|
||||||
ownerRole.id,
|
ownerRole.id,
|
||||||
@ -788,7 +849,12 @@ test('should able to assign role without existing members', async () => {
|
|||||||
|
|
||||||
const memberRole = await stores.roleStore.getRoleByName(RoleName.MEMBER);
|
const memberRole = await stores.roleStore.getRoleByName(RoleName.MEMBER);
|
||||||
|
|
||||||
await projectService.addUser(project.id, memberRole.id, projectMember1.id);
|
await projectService.addUser(
|
||||||
|
project.id,
|
||||||
|
memberRole.id,
|
||||||
|
projectMember1.id,
|
||||||
|
'test',
|
||||||
|
);
|
||||||
await projectService.changeRole(
|
await projectService.changeRole(
|
||||||
project.id,
|
project.id,
|
||||||
testRole.id,
|
testRole.id,
|
||||||
@ -819,7 +885,12 @@ test('should not update role for user on project when she is the owner', async (
|
|||||||
|
|
||||||
const memberRole = await stores.roleStore.getRoleByName(RoleName.MEMBER);
|
const memberRole = await stores.roleStore.getRoleByName(RoleName.MEMBER);
|
||||||
|
|
||||||
await projectService.addUser(project.id, memberRole.id, projectMember1.id);
|
await projectService.addUser(
|
||||||
|
project.id,
|
||||||
|
memberRole.id,
|
||||||
|
projectMember1.id,
|
||||||
|
'test',
|
||||||
|
);
|
||||||
|
|
||||||
await expect(async () => {
|
await expect(async () => {
|
||||||
await projectService.changeRole(
|
await projectService.changeRole(
|
||||||
|
@ -11,9 +11,10 @@ import NotFoundError from '../../../lib/error/notfound-error';
|
|||||||
import { IRole } from '../../../lib/types/stores/access-store';
|
import { IRole } from '../../../lib/types/stores/access-store';
|
||||||
import { RoleName } from '../../../lib/types/model';
|
import { RoleName } from '../../../lib/types/model';
|
||||||
import SettingService from '../../../lib/services/setting-service';
|
import SettingService from '../../../lib/services/setting-service';
|
||||||
import { simpleAuthKey } from '../../../lib/types/settings/simple-auth-settings';
|
import { simpleAuthSettingsKey } from '../../../lib/types/settings/simple-auth-settings';
|
||||||
import { addDays, minutesToMilliseconds } from 'date-fns';
|
import { addDays, minutesToMilliseconds } from 'date-fns';
|
||||||
import { GroupService } from '../../../lib/services/group-service';
|
import { GroupService } from '../../../lib/services/group-service';
|
||||||
|
import { randomId } from '../../../lib/util/random-id';
|
||||||
|
|
||||||
let db;
|
let db;
|
||||||
let stores;
|
let stores;
|
||||||
@ -22,6 +23,7 @@ let userStore: UserStore;
|
|||||||
let adminRole: IRole;
|
let adminRole: IRole;
|
||||||
let viewerRole: IRole;
|
let viewerRole: IRole;
|
||||||
let sessionService: SessionService;
|
let sessionService: SessionService;
|
||||||
|
let settingService: SettingService;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
db = await dbInit('user_service_serial', getLogger);
|
db = await dbInit('user_service_serial', getLogger);
|
||||||
@ -32,7 +34,7 @@ beforeAll(async () => {
|
|||||||
const resetTokenService = new ResetTokenService(stores, config);
|
const resetTokenService = new ResetTokenService(stores, config);
|
||||||
const emailService = new EmailService(undefined, config.getLogger);
|
const emailService = new EmailService(undefined, config.getLogger);
|
||||||
sessionService = new SessionService(stores, config);
|
sessionService = new SessionService(stores, config);
|
||||||
const settingService = new SettingService(stores, config);
|
settingService = new SettingService(stores, config);
|
||||||
|
|
||||||
userService = new UserService(stores, config, {
|
userService = new UserService(stores, config, {
|
||||||
accessService,
|
accessService,
|
||||||
@ -101,7 +103,11 @@ test('should create user with password', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should not login user if simple auth is disabled', async () => {
|
test('should not login user if simple auth is disabled', async () => {
|
||||||
await db.stores.settingStore.insert(simpleAuthKey, { disabled: true });
|
await settingService.insert(
|
||||||
|
simpleAuthSettingsKey,
|
||||||
|
{ disabled: true },
|
||||||
|
randomId(),
|
||||||
|
);
|
||||||
|
|
||||||
await userService.createUser({
|
await userService.createUser({
|
||||||
username: 'test_no_pass',
|
username: 'test_no_pass',
|
||||||
|
@ -36,13 +36,16 @@
|
|||||||
"url-loader": "4.1.1"
|
"url-loader": "4.1.1"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
|
"async": "^3.2.4",
|
||||||
"trim": "^1.0.0",
|
"trim": "^1.0.0",
|
||||||
"glob-parent": "^6.0.0",
|
"glob-parent": "^6.0.0",
|
||||||
"browserslist": "^4.16.5",
|
"browserslist": "^4.16.5",
|
||||||
"set-value": "^4.0.1",
|
"set-value": "^4.0.1",
|
||||||
"immer": "^9.0.6",
|
"immer": "^9.0.6",
|
||||||
"ansi-regex": "^5.0.1",
|
"ansi-regex": "^5.0.1",
|
||||||
"nth-check": "^2.0.1"
|
"nth-check": "^2.0.1",
|
||||||
|
"shelljs": "^0.8.5",
|
||||||
|
"trim-newlines": "^3.0.1"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
@ -70,6 +73,6 @@
|
|||||||
"enhanced-resolve": "5.10.0",
|
"enhanced-resolve": "5.10.0",
|
||||||
"react-router": "6.3.0",
|
"react-router": "6.3.0",
|
||||||
"storybook-addon-root-attribute": "1.0.2",
|
"storybook-addon-root-attribute": "1.0.2",
|
||||||
"typescript": "4.7.4"
|
"typescript": "4.8.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4583,17 +4583,10 @@ async-each@^1.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf"
|
resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf"
|
||||||
integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==
|
integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==
|
||||||
|
|
||||||
async@2.6.3:
|
async@2.6.3, async@3.2.1, async@^3.2.4:
|
||||||
version "2.6.3"
|
version "3.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
|
resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c"
|
||||||
integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==
|
integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==
|
||||||
dependencies:
|
|
||||||
lodash "^4.17.14"
|
|
||||||
|
|
||||||
async@3.2.1:
|
|
||||||
version "3.2.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/async/-/async-3.2.1.tgz#d3274ec66d107a47476a4c49136aacdb00665fc8"
|
|
||||||
integrity sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg==
|
|
||||||
|
|
||||||
asynckit@^0.4.0:
|
asynckit@^0.4.0:
|
||||||
version "0.4.0"
|
version "0.4.0"
|
||||||
@ -9200,7 +9193,7 @@ lodash.uniq@4.5.0, lodash.uniq@^4.5.0:
|
|||||||
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
||||||
integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==
|
integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==
|
||||||
|
|
||||||
lodash@4.17.21, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4:
|
lodash@4.17.21, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4:
|
||||||
version "4.17.21"
|
version "4.17.21"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||||
@ -12649,16 +12642,7 @@ shell-quote@^1.7.3:
|
|||||||
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123"
|
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123"
|
||||||
integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==
|
integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==
|
||||||
|
|
||||||
shelljs@0.8.4:
|
shelljs@0.8.4, shelljs@^0.8.5:
|
||||||
version "0.8.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2"
|
|
||||||
integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ==
|
|
||||||
dependencies:
|
|
||||||
glob "^7.0.0"
|
|
||||||
interpret "^1.0.0"
|
|
||||||
rechoir "^0.6.2"
|
|
||||||
|
|
||||||
shelljs@^0.8.5:
|
|
||||||
version "0.8.5"
|
version "0.8.5"
|
||||||
resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c"
|
resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c"
|
||||||
integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==
|
integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==
|
||||||
@ -13549,10 +13533,10 @@ trim-lines@^3.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338"
|
resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338"
|
||||||
integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==
|
integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==
|
||||||
|
|
||||||
trim-newlines@^1.0.0:
|
trim-newlines@^1.0.0, trim-newlines@^3.0.1:
|
||||||
version "1.0.0"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
|
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144"
|
||||||
integrity sha512-Nm4cF79FhSTzrLKGDMi3I4utBtFv8qKy4sq1enftf2gMdpqI8oVQTAfySkTz5r49giVzDj88SVZXP4CeYQwjaw==
|
integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==
|
||||||
|
|
||||||
trim-trailing-lines@^1.0.0:
|
trim-trailing-lines@^1.0.0:
|
||||||
version "1.1.4"
|
version "1.1.4"
|
||||||
@ -13651,10 +13635,10 @@ typedarray@^0.0.6:
|
|||||||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
||||||
integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==
|
integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==
|
||||||
|
|
||||||
typescript@4.7.4:
|
typescript@4.8.2:
|
||||||
version "4.7.4"
|
version "4.8.2"
|
||||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.2.tgz#e3b33d5ccfb5914e4eeab6699cf208adee3fd790"
|
||||||
integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
|
integrity sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw==
|
||||||
|
|
||||||
ua-parser-js@^0.7.30:
|
ua-parser-js@^0.7.30:
|
||||||
version "0.7.31"
|
version "0.7.31"
|
||||||
|
107
yarn.lock
107
yarn.lock
@ -1352,14 +1352,14 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/yargs-parser" "*"
|
"@types/yargs-parser" "*"
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin@5.34.0":
|
"@typescript-eslint/eslint-plugin@5.35.1":
|
||||||
version "5.34.0"
|
version "5.35.1"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.34.0.tgz#d690f60e335596f38b01792e8f4b361d9bd0cb35"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.35.1.tgz#0d822bfea7469904dfc1bb8f13cabd362b967c93"
|
||||||
integrity sha512-eRfPPcasO39iwjlUAMtjeueRGuIrW3TQ9WseIDl7i5UWuFbf83yYaU7YPs4j8+4CxUMIsj1k+4kV+E+G+6ypDQ==
|
integrity sha512-RBZZXZlI4XCY4Wzgy64vB+0slT9+yAPQRjj/HSaRwUot33xbDjF1oN9BLwOLTewoOI0jothIltZRe9uJCHf8gg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/scope-manager" "5.34.0"
|
"@typescript-eslint/scope-manager" "5.35.1"
|
||||||
"@typescript-eslint/type-utils" "5.34.0"
|
"@typescript-eslint/type-utils" "5.35.1"
|
||||||
"@typescript-eslint/utils" "5.34.0"
|
"@typescript-eslint/utils" "5.35.1"
|
||||||
debug "^4.3.4"
|
debug "^4.3.4"
|
||||||
functional-red-black-tree "^1.0.1"
|
functional-red-black-tree "^1.0.1"
|
||||||
ignore "^5.2.0"
|
ignore "^5.2.0"
|
||||||
@ -1367,69 +1367,69 @@
|
|||||||
semver "^7.3.7"
|
semver "^7.3.7"
|
||||||
tsutils "^3.21.0"
|
tsutils "^3.21.0"
|
||||||
|
|
||||||
"@typescript-eslint/parser@5.34.0":
|
"@typescript-eslint/parser@5.35.1":
|
||||||
version "5.34.0"
|
version "5.35.1"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.34.0.tgz#ca710858ea85dbfd30c9b416a335dc49e82dbc07"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.35.1.tgz#bf2ee2ebeaa0a0567213748243fb4eec2857f04f"
|
||||||
integrity sha512-SZ3NEnK4usd2CXkoV3jPa/vo1mWX1fqRyIVUQZR4As1vyp4fneknBNJj+OFtV8WAVgGf+rOHMSqQbs2Qn3nFZQ==
|
integrity sha512-XL2TBTSrh3yWAsMYpKseBYTVpvudNf69rPOWXWVBI08My2JVT5jR66eTt4IgQFHA/giiKJW5dUD4x/ZviCKyGg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/scope-manager" "5.34.0"
|
"@typescript-eslint/scope-manager" "5.35.1"
|
||||||
"@typescript-eslint/types" "5.34.0"
|
"@typescript-eslint/types" "5.35.1"
|
||||||
"@typescript-eslint/typescript-estree" "5.34.0"
|
"@typescript-eslint/typescript-estree" "5.35.1"
|
||||||
debug "^4.3.4"
|
debug "^4.3.4"
|
||||||
|
|
||||||
"@typescript-eslint/scope-manager@5.34.0":
|
"@typescript-eslint/scope-manager@5.35.1":
|
||||||
version "5.34.0"
|
version "5.35.1"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.34.0.tgz#14efd13dc57602937e25f188fd911f118781e527"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.35.1.tgz#ccb69d54b7fd0f2d0226a11a75a8f311f525ff9e"
|
||||||
integrity sha512-HNvASMQlah5RsBW6L6c7IJ0vsm+8Sope/wu5sEAf7joJYWNb1LDbJipzmdhdUOnfrDFE6LR1j57x1EYVxrY4ow==
|
integrity sha512-kCYRSAzIW9ByEIzmzGHE50NGAvAP3wFTaZevgWva7GpquDyFPFcmvVkFJGWJJktg/hLwmys/FZwqM9EKr2u24Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/types" "5.34.0"
|
"@typescript-eslint/types" "5.35.1"
|
||||||
"@typescript-eslint/visitor-keys" "5.34.0"
|
"@typescript-eslint/visitor-keys" "5.35.1"
|
||||||
|
|
||||||
"@typescript-eslint/type-utils@5.34.0":
|
"@typescript-eslint/type-utils@5.35.1":
|
||||||
version "5.34.0"
|
version "5.35.1"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.34.0.tgz#7a324ab9ddd102cd5e1beefc94eea6f3eb32d32d"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.35.1.tgz#d50903b56758c5c8fc3be52b3be40569f27f9c4a"
|
||||||
integrity sha512-Pxlno9bjsQ7hs1pdWRUv9aJijGYPYsHpwMeCQ/Inavhym3/XaKt1ZKAA8FIw4odTBfowBdZJDMxf2aavyMDkLg==
|
integrity sha512-8xT8ljvo43Mp7BiTn1vxLXkjpw8wS4oAc00hMSB4L1/jIiYbjjnc3Qp2GAUOG/v8zsNCd1qwcqfCQ0BuishHkw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/utils" "5.34.0"
|
"@typescript-eslint/utils" "5.35.1"
|
||||||
debug "^4.3.4"
|
debug "^4.3.4"
|
||||||
tsutils "^3.21.0"
|
tsutils "^3.21.0"
|
||||||
|
|
||||||
"@typescript-eslint/types@5.34.0":
|
"@typescript-eslint/types@5.35.1":
|
||||||
version "5.34.0"
|
version "5.35.1"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.34.0.tgz#217bf08049e9e7b86694d982e88a2c1566330c78"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.35.1.tgz#af355fe52a0cc88301e889bc4ada72f279b63d61"
|
||||||
integrity sha512-49fm3xbbUPuzBIOcy2CDpYWqy/X7VBkxVN+DC21e0zIm3+61Z0NZi6J9mqPmSW1BDVk9FIOvuCFyUPjXz93sjA==
|
integrity sha512-FDaujtsH07VHzG0gQ6NDkVVhi1+rhq0qEvzHdJAQjysN+LHDCKDKCBRlZFFE0ec0jKxiv0hN63SNfExy0KrbQQ==
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree@5.34.0":
|
"@typescript-eslint/typescript-estree@5.35.1":
|
||||||
version "5.34.0"
|
version "5.35.1"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.34.0.tgz#ba7b83f4bf8ccbabf074bbf1baca7a58de3ccb9a"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.35.1.tgz#db878a39a0dbdc9bb133f11cdad451770bfba211"
|
||||||
integrity sha512-mXHAqapJJDVzxauEkfJI96j3D10sd567LlqroyCeJaHnu42sDbjxotGb3XFtGPYKPD9IyLjhsoULML1oI3M86A==
|
integrity sha512-JUqE1+VRTGyoXlDWWjm6MdfpBYVq+hixytrv1oyjYIBEOZhBCwtpp5ZSvBt4wIA1MKWlnaC2UXl2XmYGC3BoQA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/types" "5.34.0"
|
"@typescript-eslint/types" "5.35.1"
|
||||||
"@typescript-eslint/visitor-keys" "5.34.0"
|
"@typescript-eslint/visitor-keys" "5.35.1"
|
||||||
debug "^4.3.4"
|
debug "^4.3.4"
|
||||||
globby "^11.1.0"
|
globby "^11.1.0"
|
||||||
is-glob "^4.0.3"
|
is-glob "^4.0.3"
|
||||||
semver "^7.3.7"
|
semver "^7.3.7"
|
||||||
tsutils "^3.21.0"
|
tsutils "^3.21.0"
|
||||||
|
|
||||||
"@typescript-eslint/utils@5.34.0":
|
"@typescript-eslint/utils@5.35.1":
|
||||||
version "5.34.0"
|
version "5.35.1"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.34.0.tgz#0cae98f48d8f9e292e5caa9343611b6faf49e743"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.35.1.tgz#ae1399afbfd6aa7d0ed1b7d941e9758d950250eb"
|
||||||
integrity sha512-kWRYybU4Rn++7lm9yu8pbuydRyQsHRoBDIo11k7eqBWTldN4xUdVUMCsHBiE7aoEkFzrUEaZy3iH477vr4xHAQ==
|
integrity sha512-v6F8JNXgeBWI4pzZn36hT2HXXzoBBBJuOYvoQiaQaEEjdi5STzux3Yj8v7ODIpx36i/5s8TdzuQ54TPc5AITQQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/json-schema" "^7.0.9"
|
"@types/json-schema" "^7.0.9"
|
||||||
"@typescript-eslint/scope-manager" "5.34.0"
|
"@typescript-eslint/scope-manager" "5.35.1"
|
||||||
"@typescript-eslint/types" "5.34.0"
|
"@typescript-eslint/types" "5.35.1"
|
||||||
"@typescript-eslint/typescript-estree" "5.34.0"
|
"@typescript-eslint/typescript-estree" "5.35.1"
|
||||||
eslint-scope "^5.1.1"
|
eslint-scope "^5.1.1"
|
||||||
eslint-utils "^3.0.0"
|
eslint-utils "^3.0.0"
|
||||||
|
|
||||||
"@typescript-eslint/visitor-keys@5.34.0":
|
"@typescript-eslint/visitor-keys@5.35.1":
|
||||||
version "5.34.0"
|
version "5.35.1"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.34.0.tgz#d0fb3e31033e82ddd5de048371ad39eb342b2d40"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.35.1.tgz#285e9e34aed7c876f16ff646a3984010035898e6"
|
||||||
integrity sha512-O1moYjOSrab0a2fUvFpsJe0QHtvTC+cR+ovYpgKrAVXzqQyc74mv76TgY6z+aEtjQE2vgZux3CQVtGryqdcOAw==
|
integrity sha512-cEB1DvBVo1bxbW/S5axbGPE6b7FIMAbo3w+AGq6zNDA7+NYJOIkKj/sInfTv4edxd4PxJSgdN4t6/pbvgA+n5g==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/types" "5.34.0"
|
"@typescript-eslint/types" "5.35.1"
|
||||||
eslint-visitor-keys "^3.3.0"
|
eslint-visitor-keys "^3.3.0"
|
||||||
|
|
||||||
"@unleash/express-openapi@^0.2.0":
|
"@unleash/express-openapi@^0.2.0":
|
||||||
@ -7187,10 +7187,10 @@ typedarray@^0.0.6:
|
|||||||
resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz"
|
resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz"
|
||||||
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
|
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
|
||||||
|
|
||||||
typescript@4.7.4:
|
typescript@4.8.2:
|
||||||
version "4.7.4"
|
version "4.8.2"
|
||||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.2.tgz#e3b33d5ccfb5914e4eeab6699cf208adee3fd790"
|
||||||
integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
|
integrity sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw==
|
||||||
|
|
||||||
uid-safe@~2.1.5:
|
uid-safe@~2.1.5:
|
||||||
version "2.1.5"
|
version "2.1.5"
|
||||||
@ -7243,6 +7243,11 @@ unleash-client@3.15.0:
|
|||||||
murmurhash3js "^3.0.1"
|
murmurhash3js "^3.0.1"
|
||||||
semver "^7.3.5"
|
semver "^7.3.5"
|
||||||
|
|
||||||
|
unleash-frontend@4.14.8:
|
||||||
|
version "4.14.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/unleash-frontend/-/unleash-frontend-4.14.8.tgz#c31ded48d8f6de859bde39833fc78ad8bd74c770"
|
||||||
|
integrity sha512-CcqyFhIZyb8qCHe6iX3saHTdIfN0TzAES2PaSWDCRPt17L9KcV1t+fns1nFvCIzH/XmM416uQC4HgKyoLtR9tw==
|
||||||
|
|
||||||
unpipe@1.0.0, unpipe@~1.0.0:
|
unpipe@1.0.0, unpipe@~1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz"
|
resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz"
|
||||||
|
Loading…
Reference in New Issue
Block a user