diff --git a/src/lib/routes/admin-api/index.ts b/src/lib/routes/admin-api/index.ts index 5e966f699c..e77903c510 100644 --- a/src/lib/routes/admin-api/index.ts +++ b/src/lib/routes/admin-api/index.ts @@ -8,7 +8,7 @@ import EventController from './event.js'; import PlaygroundController from '../../features/playground/playground.js'; import MetricsController from './metrics.js'; import UserController from './user/user.js'; -import ConfigController from './config.js'; +import UiConfigController from '../../ui-config/ui-config-controller.js'; import { ContextController } from '../../features/context/context.js'; import ClientMetricsController from '../../features/metrics/client-metrics/client-metrics.js'; import TagController from './tag.js'; @@ -90,7 +90,7 @@ export class AdminApi extends Controller { this.app.use( '/ui-config', - new ConfigController(config, services).router, + new UiConfigController(config, services).router, ); this.app.use( '/context', diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index 1871512156..600b485e0e 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -170,6 +170,7 @@ import type { IPrivateProjectChecker } from '../features/private-project/private import { UnknownFlagsService } from '../features/metrics/unknown-flags/unknown-flags-service.js'; import type FeatureLinkService from '../features/feature-links/feature-link-service.js'; import { createUserService } from '../features/users/createUserService.js'; +import { UiConfigService } from '../ui-config/ui-config-service.js'; export const createServices = ( stores: IUnleashStores, @@ -441,6 +442,15 @@ export const createServices = ( ? withTransactional(createUserSubscriptionsService(config), db) : withFakeTransactional(createFakeUserSubscriptionsService(config)); + const uiConfigService = new UiConfigService(config, { + versionService, + settingService, + emailService, + frontendApiService, + maintenanceService, + sessionService, + }); + return { transactionalAccessService, accessService, @@ -514,6 +524,7 @@ export const createServices = ( transactionalFeatureLinkService, featureLinkService, unknownFlagsService, + uiConfigService, }; }; @@ -645,4 +656,5 @@ export interface IUnleashServices { transactionalFeatureLinkService: WithTransactional; featureLinkService: FeatureLinkService; unknownFlagsService: UnknownFlagsService; + uiConfigService: UiConfigService; } diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index 1e88e9fa3f..8cf0df5506 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -59,7 +59,8 @@ export type IFlagKey = | 'fetchMode' | 'optimizeLifecycle' | 'newStrategyModal' - | 'globalChangeRequestList'; + | 'globalChangeRequestList' + | 'newUiConfigService'; export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>; @@ -273,6 +274,10 @@ const flags: IFlags = { process.env.UNLEASH_EXPERIMENTAL_GLOBAL_CHANGE_REQUEST_LIST, false, ), + newUiConfigService: parseEnvVarBoolean( + process.env.UNLEASH_EXPERIMENTAL_NEW_UI_CONFIG_SERVICE, + false, + ), }; export const defaultExperimentalOptions: IExperimentalOptions = { diff --git a/src/lib/routes/admin-api/config.ts b/src/lib/ui-config/ui-config-controller.ts similarity index 73% rename from src/lib/routes/admin-api/config.ts rename to src/lib/ui-config/ui-config-controller.ts index c4feb6c6c7..90ee7204fb 100644 --- a/src/lib/routes/admin-api/config.ts +++ b/src/lib/ui-config/ui-config-controller.ts @@ -1,37 +1,34 @@ import type { Response } from 'express'; -import type { AuthedRequest } from '../../types/core.js'; -import type { IUnleashServices } from '../../services/index.js'; -import { IAuthType, type IUnleashConfig } from '../../types/option.js'; -import version from '../../util/version.js'; -import Controller from '../controller.js'; -import type VersionService from '../../services/version-service.js'; -import type SettingService from '../../services/setting-service.js'; +import type { AuthedRequest } from '../types/core.js'; +import type { IUnleashServices } from '../services/index.js'; +import { IAuthType, type IUnleashConfig } from '../types/option.js'; +import version from '../util/version.js'; +import Controller from '../routes/controller.js'; +import type VersionService from '../services/version-service.js'; +import type SettingService from '../services/setting-service.js'; import { type SimpleAuthSettings, simpleAuthSettingsKey, -} from '../../types/settings/simple-auth-settings.js'; -import { ADMIN, NONE, UPDATE_CORS } from '../../types/permissions.js'; -import { createResponseSchema } from '../../openapi/util/create-response-schema.js'; +} from '../types/settings/simple-auth-settings.js'; +import { ADMIN, NONE, UPDATE_CORS } from '../types/permissions.js'; +import { createResponseSchema } from '../openapi/util/create-response-schema.js'; import { uiConfigSchema, type UiConfigSchema, -} from '../../openapi/spec/ui-config-schema.js'; -import type { OpenApiService } from '../../services/openapi-service.js'; -import type { EmailService } from '../../services/email-service.js'; -import { emptyResponse } from '../../openapi/util/standard-responses.js'; -import type { IAuthRequest } from '../unleash-types.js'; -import NotFoundError from '../../error/notfound-error.js'; -import type { SetCorsSchema } from '../../openapi/spec/set-cors-schema.js'; -import { createRequestSchema } from '../../openapi/util/create-request-schema.js'; -import type { - FrontendApiService, - SessionService, -} from '../../services/index.js'; -import type MaintenanceService from '../../features/maintenance/maintenance-service.js'; -import type ClientInstanceService from '../../features/metrics/instance/instance-service.js'; -import type { IFlagResolver } from '../../types/index.js'; +} from '../openapi/spec/ui-config-schema.js'; +import type { OpenApiService } from '../services/openapi-service.js'; +import type { EmailService } from '../services/email-service.js'; +import { emptyResponse } from '../openapi/util/standard-responses.js'; +import type { IAuthRequest } from '../routes/unleash-types.js'; +import NotFoundError from '../error/notfound-error.js'; +import type { SetCorsSchema } from '../openapi/spec/set-cors-schema.js'; +import { createRequestSchema } from '../openapi/util/create-request-schema.js'; +import type { FrontendApiService, SessionService } from '../services/index.js'; +import type MaintenanceService from '../features/maintenance/maintenance-service.js'; +import type { IFlagResolver } from '../types/index.js'; +import type { UiConfigService } from './ui-config-service.js'; -class ConfigController extends Controller { +class UiConfigController extends Controller { private versionService: VersionService; private settingService: SettingService; @@ -40,14 +37,14 @@ class ConfigController extends Controller { private emailService: EmailService; - private clientInstanceService: ClientInstanceService; - private sessionService: SessionService; private maintenanceService: MaintenanceService; private flagResolver: IFlagResolver; + private uiConfigService: UiConfigService; + private readonly openApiService: OpenApiService; constructor( @@ -59,8 +56,8 @@ class ConfigController extends Controller { openApiService, frontendApiService, maintenanceService, - clientInstanceService, sessionService, + uiConfigService, }: Pick< IUnleashServices, | 'versionService' @@ -71,18 +68,20 @@ class ConfigController extends Controller { | 'maintenanceService' | 'clientInstanceService' | 'sessionService' + | 'uiConfigService' >, ) { super(config); + this.flagResolver = config.flagResolver; + this.openApiService = openApiService; + this.uiConfigService = uiConfigService; this.versionService = versionService; this.settingService = settingService; this.emailService = emailService; - this.openApiService = openApiService; this.frontendApiService = frontendApiService; this.maintenanceService = maintenanceService; - this.clientInstanceService = clientInstanceService; this.sessionService = sessionService; - this.flagResolver = config.flagResolver; + this.route({ method: 'get', path: '', @@ -125,6 +124,17 @@ class ConfigController extends Controller { req: AuthedRequest, res: Response, ): Promise { + if (this.flagResolver.isEnabled('newUiConfigService')) { + const uiConfig = await this.uiConfigService.getUiConfig(req.user); + + return this.openApiService.respondWithValidation( + 200, + res, + uiConfigSchema.$id, + uiConfig, + ); + } + const getMaxSessionsCount = async () => { if (this.flagResolver.isEnabled('showUserDeviceCount')) { return this.sessionService.getMaxSessionsCount(); @@ -207,4 +217,4 @@ class ConfigController extends Controller { } } -export default ConfigController; +export default UiConfigController; diff --git a/src/lib/ui-config/ui-config-service.ts b/src/lib/ui-config/ui-config-service.ts new file mode 100644 index 0000000000..d376cb6b40 --- /dev/null +++ b/src/lib/ui-config/ui-config-service.ts @@ -0,0 +1,127 @@ +import type { IUnleashConfig } from '../types/option.js'; +import type { UiConfigSchema } from '../openapi/index.js'; +import { + IAuthType, + type EmailService, + type FrontendApiService, + type IFlagResolver, + type IUnleashServices, + type SessionService, + type SettingService, + type User, + type VersionService, +} from '../server-impl.js'; +import type MaintenanceService from '../features/maintenance/maintenance-service.js'; +import { + type SimpleAuthSettings, + simpleAuthSettingsKey, +} from '../types/settings/simple-auth-settings.js'; +import version from '../util/version.js'; + +export class UiConfigService { + private config: IUnleashConfig; + + private versionService: VersionService; + + private settingService: SettingService; + + private frontendApiService: FrontendApiService; + + private emailService: EmailService; + + private sessionService: SessionService; + + private maintenanceService: MaintenanceService; + + private flagResolver: IFlagResolver; + + constructor( + config: IUnleashConfig, + { + versionService, + settingService, + emailService, + frontendApiService, + maintenanceService, + sessionService, + }: Pick< + IUnleashServices, + | 'versionService' + | 'settingService' + | 'emailService' + | 'frontendApiService' + | 'maintenanceService' + | 'sessionService' + >, + ) { + this.config = config; + this.flagResolver = config.flagResolver; + this.versionService = versionService; + this.settingService = settingService; + this.emailService = emailService; + this.frontendApiService = frontendApiService; + this.maintenanceService = maintenanceService; + this.sessionService = sessionService; + } + + async getMaxSessionsCount(): Promise { + if (this.flagResolver.isEnabled('showUserDeviceCount')) { + return this.sessionService.getMaxSessionsCount(); + } + return 0; + } + + async getUiConfig(user: User): Promise { + const [ + frontendSettings, + simpleAuthSettings, + maintenanceMode, + maxSessionsCount, + ] = await Promise.all([ + this.frontendApiService.getFrontendSettings(false), + this.settingService.get(simpleAuthSettingsKey), + this.maintenanceService.isMaintenanceMode(), + this.getMaxSessionsCount(), + ]); + + const disablePasswordAuth = + simpleAuthSettings?.disabled || + this.config.authentication.type === IAuthType.NONE; + + const expFlags = this.config.flagResolver.getAll({ + email: user.email, + }); + + const flags = { + ...this.config.ui.flags, + ...expFlags, + }; + + const unleashContext = { + ...this.flagResolver.getStaticContext(), + email: user.email, + userId: user.id, + }; + + const uiConfig: UiConfigSchema = { + ...this.config.ui, + flags, + version, + emailEnabled: this.emailService.isEnabled(), + unleashUrl: this.config.server.unleashUrl, + baseUriPath: this.config.server.baseUriPath, + authenticationType: this.config.authentication?.type, + frontendApiOrigins: frontendSettings.frontendApiOrigins, + versionInfo: await this.versionService.getVersionInfo(), + prometheusAPIAvailable: this.config.prometheusApi !== undefined, + resourceLimits: this.config.resourceLimits, + disablePasswordAuth, + maintenanceMode, + feedbackUriPath: this.config.feedbackUriPath, + maxSessionsCount, + unleashContext: unleashContext, + }; + + return uiConfig; + } +} diff --git a/src/lib/routes/admin-api/config.test.ts b/src/lib/ui-config/ui-config.test.ts similarity index 86% rename from src/lib/routes/admin-api/config.test.ts rename to src/lib/ui-config/ui-config.test.ts index eae2618b37..1a6d958877 100644 --- a/src/lib/routes/admin-api/config.test.ts +++ b/src/lib/ui-config/ui-config.test.ts @@ -1,15 +1,15 @@ import supertest, { type Test } from 'supertest'; -import { createTestConfig } from '../../../test/config/test-config.js'; +import { createTestConfig } from '../../test/config/test-config.js'; -import createStores from '../../../test/fixtures/store.js'; -import getApp from '../../app.js'; -import { createServices } from '../../services/index.js'; +import createStores from '../../test/fixtures/store.js'; +import getApp from '../app.js'; +import { createServices } from '../services/index.js'; import { DEFAULT_SEGMENT_VALUES_LIMIT, DEFAULT_STRATEGY_SEGMENTS_LIMIT, -} from '../../util/segments.js'; +} from '../util/segments.js'; import type TestAgent from 'supertest/lib/agent.d.ts'; -import type { IUnleashStores } from '../../types/index.js'; +import type { IUnleashStores } from '../types/index.js'; const uiConfig = { headerBackground: 'red', diff --git a/src/server-dev.ts b/src/server-dev.ts index 8ac7b96cd7..70c36fcf3b 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -55,6 +55,7 @@ process.nextTick(async () => { lifecycleGraphs: true, newStrategyModal: true, globalChangeRequestList: true, + newUiConfigService: true, }, }, authentication: {