mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-06 00:07:44 +01:00
09c87c755f
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>
203 lines
5.7 KiB
TypeScript
203 lines
5.7 KiB
TypeScript
import { IUnleashConfig, IUnleashServices, IUnleashStores } from '../types';
|
|
import { Logger } from '../logger';
|
|
import { ProxyFeatureSchema, ProxyMetricsSchema } from '../openapi';
|
|
import ApiUser from '../types/api-user';
|
|
import {
|
|
Context,
|
|
InMemStorageProvider,
|
|
startUnleash,
|
|
Unleash,
|
|
UnleashEvents,
|
|
} from 'unleash-client';
|
|
import { ProxyRepository } from '../proxy';
|
|
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' | 'frontendApiOrigins'
|
|
>;
|
|
|
|
type Stores = Pick<IUnleashStores, 'projectStore' | 'eventStore'>;
|
|
|
|
type Services = Pick<
|
|
IUnleashServices,
|
|
| 'featureToggleServiceV2'
|
|
| 'segmentService'
|
|
| 'clientMetricsServiceV2'
|
|
| 'settingService'
|
|
>;
|
|
|
|
export class ProxyService {
|
|
private readonly config: Config;
|
|
|
|
private readonly logger: Logger;
|
|
|
|
private readonly stores: Stores;
|
|
|
|
private readonly services: Services;
|
|
|
|
private readonly clients: Map<ApiUser['secret'], Unleash> = new Map();
|
|
|
|
private cachedFrontendSettings?: FrontendSettings;
|
|
|
|
private timer: NodeJS.Timeout;
|
|
|
|
constructor(config: Config, stores: Stores, services: Services) {
|
|
this.config = config;
|
|
this.logger = config.getLogger('services/proxy-service.ts');
|
|
this.stores = stores;
|
|
this.services = services;
|
|
|
|
this.timer = setInterval(
|
|
() => this.fetchFrontendSettings(),
|
|
minutesToMilliseconds(2),
|
|
).unref();
|
|
}
|
|
|
|
async getProxyFeatures(
|
|
token: ApiUser,
|
|
context: Context,
|
|
): Promise<ProxyFeatureSchema[]> {
|
|
const client = await this.clientForProxyToken(token);
|
|
const definitions = client.getFeatureToggleDefinitions() || [];
|
|
|
|
return definitions
|
|
.filter((feature) => client.isEnabled(feature.name, context))
|
|
.map((feature) => ({
|
|
name: feature.name,
|
|
enabled: Boolean(feature.enabled),
|
|
variant: client.forceGetVariant(feature.name, context),
|
|
impressionData: Boolean(feature.impressionData),
|
|
}));
|
|
}
|
|
|
|
async getAllProxyFeatures(
|
|
token: ApiUser,
|
|
context: Context,
|
|
): Promise<ProxyFeatureSchema[]> {
|
|
const client = await this.clientForProxyToken(token);
|
|
const definitions = client.getFeatureToggleDefinitions() || [];
|
|
|
|
return definitions.map((feature) => ({
|
|
name: feature.name,
|
|
enabled: Boolean(feature.enabled),
|
|
variant: client.forceGetVariant(feature.name, context),
|
|
impressionData: Boolean(feature.impressionData),
|
|
}));
|
|
}
|
|
|
|
async registerProxyMetrics(
|
|
token: ApiUser,
|
|
metrics: ProxyMetricsSchema,
|
|
ip: string,
|
|
): Promise<void> {
|
|
ProxyService.assertExpectedTokenType(token);
|
|
|
|
const environment =
|
|
this.services.clientMetricsServiceV2.resolveMetricsEnvironment(
|
|
token,
|
|
metrics,
|
|
);
|
|
|
|
await this.services.clientMetricsServiceV2.registerClientMetrics(
|
|
{ ...metrics, environment },
|
|
ip,
|
|
);
|
|
}
|
|
|
|
private async clientForProxyToken(token: ApiUser): Promise<Unleash> {
|
|
ProxyService.assertExpectedTokenType(token);
|
|
|
|
if (!this.clients.has(token.secret)) {
|
|
this.clients.set(
|
|
token.secret,
|
|
await this.createClientForProxyToken(token),
|
|
);
|
|
}
|
|
|
|
return this.clients.get(token.secret);
|
|
}
|
|
|
|
private async createClientForProxyToken(token: ApiUser): Promise<Unleash> {
|
|
const repository = new ProxyRepository(
|
|
this.config,
|
|
this.stores,
|
|
this.services,
|
|
token,
|
|
);
|
|
|
|
const client = await startUnleash({
|
|
appName: 'proxy',
|
|
url: 'unused',
|
|
storageProvider: new InMemStorageProvider(),
|
|
disableMetrics: true,
|
|
repository,
|
|
});
|
|
|
|
client.on(UnleashEvents.Error, (error) => {
|
|
this.logger.error(error);
|
|
});
|
|
|
|
return client;
|
|
}
|
|
|
|
deleteClientForProxyToken(secret: string): void {
|
|
this.clients.delete(secret);
|
|
}
|
|
|
|
stopAll(): void {
|
|
this.clients.forEach((client) => client.destroy());
|
|
}
|
|
|
|
private static assertExpectedTokenType({ type }: ApiUser) {
|
|
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;
|
|
}
|
|
}
|