mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-20 00:08:02 +01:00
Fix/remove settings cache (#2694)
In this PR we remove the general SettingService cache, as it will not work across multiple horizontal unleash instances, events are not published across. We also fix the CORS origin to: - Access-Control-Allow-Origin set to "*" if no Origin is configured - Access-Control-Allow-Origin set to "*" if any Origin is configured to "*" - - Access-Control-Allow-Origin set to array and have the "cors" middleware to return an exact match on the user provided Origin. Co-authored-by: Fredrik Oseberg <fredrik.no@gmail.com>
This commit is contained in:
parent
db7b39af2d
commit
09c87c755f
@ -6,6 +6,7 @@ import { useUiConfigApi } from 'hooks/api/actions/useUiConfigApi/useUiConfigApi'
|
|||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
import { useId } from 'hooks/useId';
|
import { useId } from 'hooks/useId';
|
||||||
|
import { fontSize } from '@mui/system';
|
||||||
|
|
||||||
interface ICorsFormProps {
|
interface ICorsFormProps {
|
||||||
frontendApiOrigins: string[] | undefined;
|
frontendApiOrigins: string[] | undefined;
|
||||||
@ -35,8 +36,23 @@ export const CorsForm = ({ frontendApiOrigins }: ICorsFormProps) => {
|
|||||||
<Box sx={{ display: 'grid', gap: 1 }}>
|
<Box sx={{ display: 'grid', gap: 1 }}>
|
||||||
<label htmlFor={inputFieldId}>
|
<label htmlFor={inputFieldId}>
|
||||||
Which origins should be allowed to call the Frontend API?
|
Which origins should be allowed to call the Frontend API?
|
||||||
Add only one origin per line.
|
Add only one origin per line. The CORS specification does
|
||||||
|
not support wildcard for subdomains, it needs to be a fully
|
||||||
|
qualified domain, including the protocol.
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
If you specify "*" it will be the chosen origin.
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
Example:
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<code style={{ fontSize: '0.7em' }}>
|
||||||
|
https://www.example.com
|
||||||
|
<br />
|
||||||
|
https://www.example2.com
|
||||||
|
</code>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
id={inputFieldId}
|
id={inputFieldId}
|
||||||
aria-describedby={helpTextId}
|
aria-describedby={helpTextId}
|
||||||
|
@ -17,6 +17,15 @@ export const CorsHelpAlert = () => {
|
|||||||
An asterisk (<code>*</code>) may be used to allow API calls from
|
An asterisk (<code>*</code>) may be used to allow API calls from
|
||||||
any origin.
|
any origin.
|
||||||
</p>
|
</p>
|
||||||
|
<br />
|
||||||
|
<p>
|
||||||
|
Be aware that changes here will take up to two minutes to be
|
||||||
|
updated. In addition, there is a maxAge on the
|
||||||
|
Access-Control-Allow-Origin header that will instruct browsers
|
||||||
|
to cache this header for some time. The cache period is set to
|
||||||
|
the maxium that the browser allows (2h for Chrome, 24h for
|
||||||
|
Firefox).
|
||||||
|
</p>
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,86 +1,152 @@
|
|||||||
import { allowRequestOrigin } from './cors-origin-middleware';
|
import { resolveOrigin } from './cors-origin-middleware';
|
||||||
import FakeSettingStore from '../../test/fixtures/fake-setting-store';
|
import FakeSettingStore from '../../test/fixtures/fake-setting-store';
|
||||||
import SettingService from '../services/setting-service';
|
|
||||||
import { createTestConfig } from '../../test/config/test-config';
|
import { createTestConfig } from '../../test/config/test-config';
|
||||||
import FakeEventStore from '../../test/fixtures/fake-event-store';
|
import FakeEventStore from '../../test/fixtures/fake-event-store';
|
||||||
import { randomId } from '../util/random-id';
|
import { randomId } from '../util/random-id';
|
||||||
import { frontendSettingsKey } from '../types/settings/frontend-settings';
|
import FakeProjectStore from '../../test/fixtures/fake-project-store';
|
||||||
|
import { ProxyService, SettingService } from '../../lib/services';
|
||||||
|
import { ISettingStore } from '../../lib/types';
|
||||||
|
import { frontendSettingsKey } from '../../lib/types/settings/frontend-settings';
|
||||||
|
import { minutesToMilliseconds } from 'date-fns';
|
||||||
|
|
||||||
const createSettingService = (frontendApiOrigins: string[]): SettingService => {
|
const createSettingService = (
|
||||||
|
frontendApiOrigins: string[],
|
||||||
|
): { proxyService: ProxyService; settingStore: ISettingStore } => {
|
||||||
const config = createTestConfig({ frontendApiOrigins });
|
const config = createTestConfig({ frontendApiOrigins });
|
||||||
|
|
||||||
const stores = {
|
const stores = {
|
||||||
settingStore: new FakeSettingStore(),
|
settingStore: new FakeSettingStore(),
|
||||||
eventStore: new FakeEventStore(),
|
eventStore: new FakeEventStore(),
|
||||||
|
projectStore: new FakeProjectStore(),
|
||||||
};
|
};
|
||||||
|
|
||||||
return new SettingService(stores, config);
|
const services = {
|
||||||
|
settingService: new SettingService(stores, config),
|
||||||
};
|
};
|
||||||
|
|
||||||
test('allowRequestOrigin', () => {
|
return {
|
||||||
|
//@ts-ignore
|
||||||
|
proxyService: new ProxyService(config, stores, services),
|
||||||
|
settingStore: stores.settingStore,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
test('resolveOrigin', () => {
|
||||||
const dotCom = 'https://example.com';
|
const dotCom = 'https://example.com';
|
||||||
const dotOrg = 'https://example.org';
|
const dotOrg = 'https://example.org';
|
||||||
|
|
||||||
expect(allowRequestOrigin('', [])).toEqual(false);
|
expect(resolveOrigin([])).toEqual('*');
|
||||||
expect(allowRequestOrigin(dotCom, [])).toEqual(false);
|
expect(resolveOrigin(['*'])).toEqual('*');
|
||||||
expect(allowRequestOrigin(dotCom, [dotOrg])).toEqual(false);
|
expect(resolveOrigin([dotOrg])).toEqual([dotOrg]);
|
||||||
|
expect(resolveOrigin([dotCom, dotOrg])).toEqual([dotCom, dotOrg]);
|
||||||
expect(allowRequestOrigin(dotCom, [dotCom, dotOrg])).toEqual(true);
|
expect(resolveOrigin([dotOrg, '*'])).toEqual('*');
|
||||||
expect(allowRequestOrigin(dotCom, [dotOrg, dotCom])).toEqual(true);
|
|
||||||
expect(allowRequestOrigin(dotCom, [dotCom, dotCom])).toEqual(true);
|
|
||||||
|
|
||||||
expect(allowRequestOrigin(dotCom, ['*'])).toEqual(true);
|
|
||||||
expect(allowRequestOrigin(dotCom, [dotOrg, '*'])).toEqual(true);
|
|
||||||
expect(allowRequestOrigin(dotCom, [dotCom, dotOrg, '*'])).toEqual(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('corsOriginMiddleware origin validation', async () => {
|
test('corsOriginMiddleware origin validation', async () => {
|
||||||
const service = createSettingService([]);
|
const { proxyService } = createSettingService([]);
|
||||||
const userName = randomId();
|
const userName = randomId();
|
||||||
await expect(() =>
|
await expect(() =>
|
||||||
service.setFrontendSettings({ frontendApiOrigins: ['a'] }, userName),
|
proxyService.setFrontendSettings(
|
||||||
|
{ frontendApiOrigins: ['a'] },
|
||||||
|
userName,
|
||||||
|
),
|
||||||
).rejects.toThrow('Invalid origin: a');
|
).rejects.toThrow('Invalid origin: a');
|
||||||
|
proxyService.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('corsOriginMiddleware without config', async () => {
|
test('corsOriginMiddleware without config', async () => {
|
||||||
const service = createSettingService([]);
|
const { proxyService, settingStore } = createSettingService([]);
|
||||||
const userName = randomId();
|
const userName = randomId();
|
||||||
expect(await service.getFrontendSettings()).toEqual({
|
expect(await proxyService.getFrontendSettings(false)).toEqual({
|
||||||
frontendApiOrigins: [],
|
frontendApiOrigins: [],
|
||||||
});
|
});
|
||||||
await service.setFrontendSettings({ frontendApiOrigins: [] }, userName);
|
await proxyService.setFrontendSettings(
|
||||||
expect(await service.getFrontendSettings()).toEqual({
|
{ frontendApiOrigins: [] },
|
||||||
|
userName,
|
||||||
|
);
|
||||||
|
expect(await proxyService.getFrontendSettings(false)).toEqual({
|
||||||
frontendApiOrigins: [],
|
frontendApiOrigins: [],
|
||||||
});
|
});
|
||||||
await service.setFrontendSettings({ frontendApiOrigins: ['*'] }, userName);
|
await proxyService.setFrontendSettings(
|
||||||
expect(await service.getFrontendSettings()).toEqual({
|
{ frontendApiOrigins: ['*'] },
|
||||||
|
userName,
|
||||||
|
);
|
||||||
|
expect(await proxyService.getFrontendSettings(false)).toEqual({
|
||||||
frontendApiOrigins: ['*'],
|
frontendApiOrigins: ['*'],
|
||||||
});
|
});
|
||||||
await service.delete(frontendSettingsKey, userName);
|
await settingStore.delete(frontendSettingsKey);
|
||||||
expect(await service.getFrontendSettings()).toEqual({
|
expect(await proxyService.getFrontendSettings(false)).toEqual({
|
||||||
frontendApiOrigins: [],
|
frontendApiOrigins: [],
|
||||||
});
|
});
|
||||||
|
proxyService.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('corsOriginMiddleware with config', async () => {
|
test('corsOriginMiddleware with config', async () => {
|
||||||
const service = createSettingService(['*']);
|
const { proxyService, settingStore } = createSettingService(['*']);
|
||||||
const userName = randomId();
|
const userName = randomId();
|
||||||
expect(await service.getFrontendSettings()).toEqual({
|
expect(await proxyService.getFrontendSettings(false)).toEqual({
|
||||||
frontendApiOrigins: ['*'],
|
frontendApiOrigins: ['*'],
|
||||||
});
|
});
|
||||||
await service.setFrontendSettings({ frontendApiOrigins: [] }, userName);
|
await proxyService.setFrontendSettings(
|
||||||
expect(await service.getFrontendSettings()).toEqual({
|
{ frontendApiOrigins: [] },
|
||||||
|
userName,
|
||||||
|
);
|
||||||
|
expect(await proxyService.getFrontendSettings(false)).toEqual({
|
||||||
frontendApiOrigins: [],
|
frontendApiOrigins: [],
|
||||||
});
|
});
|
||||||
await service.setFrontendSettings(
|
await proxyService.setFrontendSettings(
|
||||||
{ frontendApiOrigins: ['https://example.com', 'https://example.org'] },
|
{ frontendApiOrigins: ['https://example.com', 'https://example.org'] },
|
||||||
userName,
|
userName,
|
||||||
);
|
);
|
||||||
expect(await service.getFrontendSettings()).toEqual({
|
expect(await proxyService.getFrontendSettings(false)).toEqual({
|
||||||
frontendApiOrigins: ['https://example.com', 'https://example.org'],
|
frontendApiOrigins: ['https://example.com', 'https://example.org'],
|
||||||
});
|
});
|
||||||
await service.delete(frontendSettingsKey, userName);
|
await settingStore.delete(frontendSettingsKey);
|
||||||
expect(await service.getFrontendSettings()).toEqual({
|
expect(await proxyService.getFrontendSettings(false)).toEqual({
|
||||||
frontendApiOrigins: ['*'],
|
frontendApiOrigins: ['*'],
|
||||||
});
|
});
|
||||||
|
proxyService.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('corsOriginMiddleware with caching enabled', async () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
const { proxyService } = createSettingService([]);
|
||||||
|
|
||||||
|
const userName = randomId();
|
||||||
|
expect(await proxyService.getFrontendSettings()).toEqual({
|
||||||
|
frontendApiOrigins: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
//setting
|
||||||
|
await proxyService.setFrontendSettings(
|
||||||
|
{ frontendApiOrigins: ['*'] },
|
||||||
|
userName,
|
||||||
|
);
|
||||||
|
|
||||||
|
//still get cached value
|
||||||
|
expect(await proxyService.getFrontendSettings()).toEqual({
|
||||||
|
frontendApiOrigins: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.advanceTimersByTime(minutesToMilliseconds(2));
|
||||||
|
|
||||||
|
jest.useRealTimers();
|
||||||
|
|
||||||
|
/*
|
||||||
|
This is needed because it is not enough to fake time to test the
|
||||||
|
updated cache, we also need to make sure that all promises are
|
||||||
|
executed and completed, in the right order.
|
||||||
|
*/
|
||||||
|
await new Promise<void>((resolve) =>
|
||||||
|
process.nextTick(async () => {
|
||||||
|
const settings = await proxyService.getFrontendSettings();
|
||||||
|
|
||||||
|
expect(settings).toEqual({
|
||||||
|
frontendApiOrigins: ['*'],
|
||||||
|
});
|
||||||
|
resolve();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
proxyService.destroy();
|
||||||
});
|
});
|
||||||
|
@ -2,32 +2,32 @@ import { RequestHandler } from 'express';
|
|||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import { IUnleashConfig, IUnleashServices } from '../types';
|
import { IUnleashConfig, IUnleashServices } from '../types';
|
||||||
|
|
||||||
export const allowRequestOrigin = (
|
export const resolveOrigin = (allowedOrigins: string[]): string | string[] => {
|
||||||
requestOrigin: string,
|
if (allowedOrigins.length === 0) {
|
||||||
allowedOrigins: string[],
|
return '*';
|
||||||
): boolean => {
|
}
|
||||||
return allowedOrigins.some((allowedOrigin) => {
|
if (allowedOrigins.some((origin: string) => origin === '*')) {
|
||||||
return allowedOrigin === requestOrigin || allowedOrigin === '*';
|
return '*';
|
||||||
});
|
} else {
|
||||||
|
return allowedOrigins;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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 = (
|
||||||
{ settingService }: Pick<IUnleashServices, 'settingService'>,
|
{ proxyService }: Pick<IUnleashServices, 'proxyService'>,
|
||||||
config: IUnleashConfig,
|
config: IUnleashConfig,
|
||||||
): RequestHandler => {
|
): RequestHandler => {
|
||||||
return cors(async (req, callback) => {
|
return cors(async (req, callback) => {
|
||||||
try {
|
try {
|
||||||
const { frontendApiOrigins = [] } =
|
const { frontendApiOrigins = [] } =
|
||||||
await settingService.getFrontendSettings();
|
await proxyService.getFrontendSettings();
|
||||||
callback(null, {
|
callback(null, {
|
||||||
origin: allowRequestOrigin(
|
origin: resolveOrigin(frontendApiOrigins),
|
||||||
req.header('Origin'),
|
|
||||||
frontendApiOrigins,
|
|
||||||
),
|
|
||||||
maxAge: config.accessControlMaxAge,
|
maxAge: config.accessControlMaxAge,
|
||||||
exposedHeaders: 'ETag',
|
exposedHeaders: 'ETag',
|
||||||
|
credentials: true,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
callback(error);
|
callback(error);
|
||||||
|
@ -24,12 +24,15 @@ import { extractUsername } from '../../util/extract-user';
|
|||||||
import NotFoundError from '../../error/notfound-error';
|
import NotFoundError from '../../error/notfound-error';
|
||||||
import { SetUiConfigSchema } from '../../openapi/spec/set-ui-config-schema';
|
import { SetUiConfigSchema } from '../../openapi/spec/set-ui-config-schema';
|
||||||
import { createRequestSchema } from '../../openapi/util/create-request-schema';
|
import { createRequestSchema } from '../../openapi/util/create-request-schema';
|
||||||
|
import { ProxyService } from 'lib/services';
|
||||||
|
|
||||||
class ConfigController extends Controller {
|
class ConfigController extends Controller {
|
||||||
private versionService: VersionService;
|
private versionService: VersionService;
|
||||||
|
|
||||||
private settingService: SettingService;
|
private settingService: SettingService;
|
||||||
|
|
||||||
|
private proxyService: ProxyService;
|
||||||
|
|
||||||
private emailService: EmailService;
|
private emailService: EmailService;
|
||||||
|
|
||||||
private readonly openApiService: OpenApiService;
|
private readonly openApiService: OpenApiService;
|
||||||
@ -41,12 +44,14 @@ class ConfigController extends Controller {
|
|||||||
settingService,
|
settingService,
|
||||||
emailService,
|
emailService,
|
||||||
openApiService,
|
openApiService,
|
||||||
|
proxyService,
|
||||||
}: Pick<
|
}: Pick<
|
||||||
IUnleashServices,
|
IUnleashServices,
|
||||||
| 'versionService'
|
| 'versionService'
|
||||||
| 'settingService'
|
| 'settingService'
|
||||||
| 'emailService'
|
| 'emailService'
|
||||||
| 'openApiService'
|
| 'openApiService'
|
||||||
|
| 'proxyService'
|
||||||
>,
|
>,
|
||||||
) {
|
) {
|
||||||
super(config);
|
super(config);
|
||||||
@ -54,6 +59,7 @@ class ConfigController extends Controller {
|
|||||||
this.settingService = settingService;
|
this.settingService = settingService;
|
||||||
this.emailService = emailService;
|
this.emailService = emailService;
|
||||||
this.openApiService = openApiService;
|
this.openApiService = openApiService;
|
||||||
|
this.proxyService = proxyService;
|
||||||
|
|
||||||
this.route({
|
this.route({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
@ -92,7 +98,7 @@ class ConfigController extends Controller {
|
|||||||
res: Response<UiConfigSchema>,
|
res: Response<UiConfigSchema>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const [frontendSettings, simpleAuthSettings] = await Promise.all([
|
const [frontendSettings, simpleAuthSettings] = await Promise.all([
|
||||||
this.settingService.getFrontendSettings(),
|
this.proxyService.getFrontendSettings(false),
|
||||||
this.settingService.get<SimpleAuthSettings>(simpleAuthSettingsKey),
|
this.settingService.get<SimpleAuthSettings>(simpleAuthSettingsKey),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -133,7 +139,7 @@ class ConfigController extends Controller {
|
|||||||
res: Response<string>,
|
res: Response<string>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (req.body.frontendSettings) {
|
if (req.body.frontendSettings) {
|
||||||
await this.settingService.setFrontendSettings(
|
await this.proxyService.setFrontendSettings(
|
||||||
req.body.frontendSettings,
|
req.body.frontendSettings,
|
||||||
extractUsername(req),
|
extractUsername(req),
|
||||||
);
|
);
|
||||||
|
@ -55,6 +55,7 @@ async function createApp(
|
|||||||
metricsMonitor.stopMonitoring();
|
metricsMonitor.stopMonitoring();
|
||||||
stores.clientInstanceStore.destroy();
|
stores.clientInstanceStore.destroy();
|
||||||
services.clientMetricsServiceV2.destroy();
|
services.clientMetricsServiceV2.destroy();
|
||||||
|
services.proxyService.destroy();
|
||||||
await db.destroy();
|
await db.destroy();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -109,6 +109,7 @@ export const createServices = (
|
|||||||
featureToggleServiceV2,
|
featureToggleServiceV2,
|
||||||
clientMetricsServiceV2,
|
clientMetricsServiceV2,
|
||||||
segmentService,
|
segmentService,
|
||||||
|
settingService,
|
||||||
});
|
});
|
||||||
|
|
||||||
const edgeService = new EdgeService(stores, config);
|
const edgeService = new EdgeService(stores, config);
|
||||||
|
@ -10,16 +10,29 @@ import {
|
|||||||
UnleashEvents,
|
UnleashEvents,
|
||||||
} from 'unleash-client';
|
} from 'unleash-client';
|
||||||
import { ProxyRepository } from '../proxy';
|
import { ProxyRepository } from '../proxy';
|
||||||
import assert from 'assert';
|
|
||||||
import { ApiTokenType } from '../types/models/api-token';
|
import { ApiTokenType } from '../types/models/api-token';
|
||||||
|
import {
|
||||||
|
FrontendSettings,
|
||||||
|
frontendSettingsKey,
|
||||||
|
} from '../types/settings/frontend-settings';
|
||||||
|
import { validateOrigins } from '../util';
|
||||||
|
import { BadDataError } from '../error';
|
||||||
|
import assert from 'assert';
|
||||||
|
import { minutesToMilliseconds } from 'date-fns';
|
||||||
|
|
||||||
type Config = Pick<IUnleashConfig, 'getLogger' | 'frontendApi'>;
|
type Config = Pick<
|
||||||
|
IUnleashConfig,
|
||||||
|
'getLogger' | 'frontendApi' | 'frontendApiOrigins'
|
||||||
|
>;
|
||||||
|
|
||||||
type Stores = Pick<IUnleashStores, 'projectStore' | 'eventStore'>;
|
type Stores = Pick<IUnleashStores, 'projectStore' | 'eventStore'>;
|
||||||
|
|
||||||
type Services = Pick<
|
type Services = Pick<
|
||||||
IUnleashServices,
|
IUnleashServices,
|
||||||
'featureToggleServiceV2' | 'segmentService' | 'clientMetricsServiceV2'
|
| 'featureToggleServiceV2'
|
||||||
|
| 'segmentService'
|
||||||
|
| 'clientMetricsServiceV2'
|
||||||
|
| 'settingService'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export class ProxyService {
|
export class ProxyService {
|
||||||
@ -33,11 +46,20 @@ export class ProxyService {
|
|||||||
|
|
||||||
private readonly clients: Map<ApiUser['secret'], Unleash> = new Map();
|
private readonly clients: Map<ApiUser['secret'], Unleash> = new Map();
|
||||||
|
|
||||||
|
private cachedFrontendSettings?: FrontendSettings;
|
||||||
|
|
||||||
|
private timer: NodeJS.Timeout;
|
||||||
|
|
||||||
constructor(config: Config, stores: Stores, services: Services) {
|
constructor(config: Config, stores: Stores, services: Services) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.logger = config.getLogger('services/proxy-service.ts');
|
this.logger = config.getLogger('services/proxy-service.ts');
|
||||||
this.stores = stores;
|
this.stores = stores;
|
||||||
this.services = services;
|
this.services = services;
|
||||||
|
|
||||||
|
this.timer = setInterval(
|
||||||
|
() => this.fetchFrontendSettings(),
|
||||||
|
minutesToMilliseconds(2),
|
||||||
|
).unref();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProxyFeatures(
|
async getProxyFeatures(
|
||||||
@ -138,4 +160,43 @@ export class ProxyService {
|
|||||||
private static assertExpectedTokenType({ type }: ApiUser) {
|
private static assertExpectedTokenType({ type }: ApiUser) {
|
||||||
assert(type === ApiTokenType.FRONTEND || type === ApiTokenType.ADMIN);
|
assert(type === ApiTokenType.FRONTEND || type === ApiTokenType.ADMIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setFrontendSettings(
|
||||||
|
value: FrontendSettings,
|
||||||
|
createdBy: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const error = validateOrigins(value.frontendApiOrigins);
|
||||||
|
if (error) {
|
||||||
|
throw new BadDataError(error);
|
||||||
|
}
|
||||||
|
await this.services.settingService.insert(
|
||||||
|
frontendSettingsKey,
|
||||||
|
value,
|
||||||
|
createdBy,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchFrontendSettings(): Promise<FrontendSettings> {
|
||||||
|
this.cachedFrontendSettings = await this.services.settingService.get(
|
||||||
|
frontendSettingsKey,
|
||||||
|
{
|
||||||
|
frontendApiOrigins: this.config.frontendApiOrigins,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return this.cachedFrontendSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFrontendSettings(
|
||||||
|
useCache: boolean = true,
|
||||||
|
): Promise<FrontendSettings> {
|
||||||
|
if (useCache && this.cachedFrontendSettings) {
|
||||||
|
return this.cachedFrontendSettings;
|
||||||
|
}
|
||||||
|
return this.fetchFrontendSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
clearInterval(this.timer);
|
||||||
|
this.timer = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,12 +8,6 @@ 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 config: IUnleashConfig;
|
||||||
@ -24,10 +18,6 @@ export default class SettingService {
|
|||||||
|
|
||||||
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,
|
||||||
@ -42,14 +32,11 @@ export default class SettingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async get<T>(id: string, defaultValue?: T): Promise<T> {
|
async get<T>(id: string, defaultValue?: T): Promise<T> {
|
||||||
if (!this.cache.has(id)) {
|
const value = await this.settingStore.get(id);
|
||||||
this.cache.set(id, await this.settingStore.get(id));
|
return value || defaultValue;
|
||||||
}
|
|
||||||
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);
|
||||||
@ -71,7 +58,6 @@ 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({
|
||||||
@ -84,26 +70,6 @@ export default class SettingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteAll(): Promise<void> {
|
async deleteAll(): Promise<void> {
|
||||||
this.cache.clear();
|
|
||||||
await this.settingStore.deleteAll();
|
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;
|
|
||||||
|
@ -44,7 +44,7 @@ test('gets ui config with disablePasswordAuth', async () => {
|
|||||||
|
|
||||||
test('gets ui config with frontendSettings', async () => {
|
test('gets ui config with frontendSettings', async () => {
|
||||||
const frontendApiOrigins = ['https://example.net'];
|
const frontendApiOrigins = ['https://example.net'];
|
||||||
await app.services.settingService.setFrontendSettings(
|
await app.services.proxyService.setFrontendSettings(
|
||||||
{ frontendApiOrigins },
|
{ frontendApiOrigins },
|
||||||
randomId(),
|
randomId(),
|
||||||
);
|
);
|
||||||
|
@ -47,6 +47,7 @@ async function createApp(
|
|||||||
services.clientInstanceService.destroy();
|
services.clientInstanceService.destroy();
|
||||||
services.clientMetricsServiceV2.destroy();
|
services.clientMetricsServiceV2.destroy();
|
||||||
services.apiTokenService.destroy();
|
services.apiTokenService.destroy();
|
||||||
|
services.proxyService.destroy();
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: use create from server-impl instead?
|
// TODO: use create from server-impl instead?
|
||||||
|
Loading…
Reference in New Issue
Block a user