diff --git a/src/apim.runtime.module.ts b/src/apim.runtime.module.ts index c4d9b5e57..679a73240 100644 --- a/src/apim.runtime.module.ts +++ b/src/apim.runtime.module.ts @@ -1,9 +1,9 @@ +import "./polyfills"; import "@paperbits/core/ko/bindingHandlers/bindingHandlers.activate"; import "@paperbits/core/ko/bindingHandlers/bindingHandlers.component"; import "@paperbits/core/ko/bindingHandlers/bindingHandlers.dialog"; import "@paperbits/core/ko/bindingHandlers/bindingHandlers.focus"; import "@paperbits/core/ko/bindingHandlers/bindingHandlers.scrollable"; -import { DefaultSettingsProvider } from "@paperbits/common/configuration"; import { IInjector, IInjectorModule } from "@paperbits/common/injection"; import { ConsoleLogger } from "@paperbits/common/logging"; import { DefaultSessionManager } from "@paperbits/common/persistence/defaultSessionManager"; @@ -53,7 +53,6 @@ import { Signup } from "./components/users/signup/ko/runtime/signup"; import { Subscriptions } from "./components/users/subscriptions/ko/runtime/subscriptions"; import { ValidationSummary } from "./components/users/validation-summary/ko/runtime/validation-summary"; import { UnhandledErrorHandler } from "./errors/unhandledErrorHandler"; -import "./polyfills"; import { AadSignOutRouteGuard } from "./routing/aadSignoutRouteGuard"; import { RouteHelper } from "./routing/routeHelper"; import { SignOutRouteGuard } from "./routing/signOutRouteGuard"; @@ -69,6 +68,7 @@ import { ProvisionService } from "./services/provisioningService"; import { TagService } from "./services/tagService"; import { TenantService } from "./services/tenantService"; import { UsersService } from "./services/usersService"; +import { ApimSettingsProvider } from "./configuration/apimSettingsProvider"; export class ApimRuntimeModule implements IInjectorModule { public register(injector: IInjector): void { @@ -122,7 +122,7 @@ export class ApimRuntimeModule implements IInjectorModule { injector.bindSingleton("backendService", BackendService); injector.bindSingleton("aadService", AadService); injector.bindSingleton("mapiClient", MapiClient); - injector.bindSingleton("settingsProvider", DefaultSettingsProvider); + injector.bindSingleton("settingsProvider", ApimSettingsProvider); injector.bindSingleton("authenticator", DefaultAuthenticator); injector.bindSingleton("routeHelper", RouteHelper); injector.bindSingleton("userService", StaticUserService); diff --git a/src/components/app/app.ts b/src/components/app/app.ts index 9304d59f8..98a34ca09 100644 --- a/src/components/app/app.ts +++ b/src/components/app/app.ts @@ -1,3 +1,4 @@ +import { EventManager } from "@paperbits/common/events"; import { AccessToken } from "./../../authentication/accessToken"; import template from "./app.html"; import { ViewManager } from "@paperbits/common/ui"; @@ -5,7 +6,6 @@ import { Component, OnMounted } from "@paperbits/common/ko/decorators"; import { ISettingsProvider } from "@paperbits/common/configuration"; import { ISiteService } from "@paperbits/common/sites"; import { IAuthenticator } from "../../authentication"; -import { Utils } from "../../utils"; const startupError = `Unable to start the portal`; @@ -18,7 +18,8 @@ export class App { private readonly settingsProvider: ISettingsProvider, private readonly authenticator: IAuthenticator, private readonly viewManager: ViewManager, - private readonly siteService: ISiteService + private readonly siteService: ISiteService, + private readonly eventManager: EventManager ) { } @OnMounted() @@ -70,6 +71,11 @@ export class App { this.viewManager.setHost({ name: "page-host" }); this.viewManager.showToolboxes(); + + setTimeout(() => this.eventManager.dispatchEvent("displayHint", { + key: "a69b", + content: `When you're in the administrative view, you still can navigate any website hyperlink by clicking on it holding Ctrl (Windows) or ⌘ (Mac) key.` + }), 5000); } catch (error) { this.viewManager.addToast(startupError, `Check if the settings specified in the configuration file config.design.json are correct or refer to the frequently asked questions.`); diff --git a/src/components/content/content.ts b/src/components/content/content.ts index fb5d9303f..a54898cb8 100644 --- a/src/components/content/content.ts +++ b/src/components/content/content.ts @@ -1,12 +1,13 @@ import template from "./content.html"; +import * as moment from "moment"; +import * as Constants from "../../constants"; import { ViewManager, View } from "@paperbits/common/ui"; import { Component } from "@paperbits/common/ko/decorators"; -import { HttpClient } from "@paperbits/common/http"; -import { ISettingsProvider } from "@paperbits/common/configuration"; import { Logger } from "@paperbits/common/logging"; import { IAuthenticator } from "../../authentication/IAuthenticator"; import { AppError } from "./../../errors/appError"; import { MapiError } from "../../errors/mapiError"; +import { MapiClient } from "../../services"; @Component({ @@ -16,9 +17,8 @@ import { MapiError } from "../../errors/mapiError"; export class ContentWorkshop { constructor( private readonly viewManager: ViewManager, - private readonly httpClient: HttpClient, + private readonly mapiClient: MapiClient, private readonly authenticator: IAuthenticator, - private readonly settingsProvider: ISettingsProvider, private readonly logger: Logger ) { } @@ -30,20 +30,12 @@ export class ContentWorkshop { } try { - const accessToken = await this.authenticator.getAccessTokenAsString(); + const revisionName = moment.utc().format(Constants.releaseNameFormat); - const publishRootUrl = await this.settingsProvider.getSetting("backendUrl") || ""; - - const response = await this.httpClient.send({ - url: publishRootUrl + "/publish", - method: "POST", - headers: [{ name: "Authorization", value: accessToken }] + await this.mapiClient.put(`/portalRevisions/${revisionName}`, null, { + properties: { description: "" } }); - if (response.statusCode !== 200) { - throw MapiError.fromResponse(response); - } - this.viewManager.notifySuccess("Operations", `The website is being published...`); this.viewManager.closeWorkshop("content-workshop"); } diff --git a/src/configuration/apimSettingsProvider.ts b/src/configuration/apimSettingsProvider.ts new file mode 100644 index 000000000..074c400f5 --- /dev/null +++ b/src/configuration/apimSettingsProvider.ts @@ -0,0 +1,72 @@ +import * as Objects from "@paperbits/common/objects"; +import { EventManager } from "@paperbits/common/events"; +import { HttpClient } from "@paperbits/common/http"; +import { ISettingsProvider } from "@paperbits/common/configuration"; +import { SessionManager } from "@paperbits/common/persistence/sessionManager"; + + +export class ApimSettingsProvider implements ISettingsProvider { + private configuration: Object; + private initializePromise: Promise; + + constructor( + private readonly httpClient: HttpClient, + private readonly eventManager: EventManager, + private readonly sessionManager: SessionManager + ) { } + + private async ensureInitialized(): Promise { + if (!this.initializePromise) { + this.initializePromise = this.loadSettings(); + } + return this.initializePromise; + } + + private async loadSettings(): Promise { + const commonConfigurationResponse = await this.httpClient.send({ url: "/config.json" }); + const commonConfiguration = commonConfigurationResponse.toObject(); + + const searializedDesignTimeSettings = await this.sessionManager?.getItem("designTimeSettings"); + + if (searializedDesignTimeSettings) { + const designTimeSettings = searializedDesignTimeSettings; + Object.assign(commonConfiguration, designTimeSettings); + } + else { + const apimsConfigurationResponse = await this.httpClient.send({ url: "/config-apim.json" }); + + if (apimsConfigurationResponse.statusCode === 200) { + const apimConfiguration = apimsConfigurationResponse.toObject(); + Object.assign(commonConfiguration, apimConfiguration); + } + } + + this.configuration = commonConfiguration; + } + + public async getSetting(name: string): Promise { + await this.ensureInitialized(); + return Objects.getObjectAt(name, this.configuration); + } + + public onSettingChange(name: string, eventHandler: (value: T) => void): void { + this.eventManager.addEventListener("onSettingChange", (setting) => { + if (setting.name === name) { + eventHandler(setting.value); + } + }); + } + + public async setSetting(name: string, value: T): Promise { + await this.ensureInitialized(); + + Objects.setValue(name, this.configuration, value); + this.eventManager.dispatchEvent("onSettingChange", { name: name, value: value }); + } + + public async getSettings(): Promise { + await this.ensureInitialized(); + + return this.configuration; + } +} \ No newline at end of file diff --git a/src/constants.ts b/src/constants.ts index 0ec9f0d36..84ebdc16f 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -134,4 +134,6 @@ export const developerPortalType = "self-hosted-portal"; /** * Header name to track developer portal type. */ -export const portalHeaderName = "x-ms-apim-client"; \ No newline at end of file +export const portalHeaderName = "x-ms-apim-client"; + +export const releaseNameFormat = "YYYYMMDDHHmm"; \ No newline at end of file diff --git a/src/contracts/oauthSession.ts b/src/contracts/oauthSession.ts new file mode 100644 index 000000000..c48338005 --- /dev/null +++ b/src/contracts/oauthSession.ts @@ -0,0 +1,11 @@ +export interface OAuthSession { + authenticationFlow: string; + authenticationCallback: (accessToken: string) => void; + authenticationErrorCallback: (error: Error) => void; + loginUrl: string; + redirectUri: string; + clientId: string; + issuer: string; + tokenEndpoint: string; + scope: string; +} \ No newline at end of file diff --git a/src/models/service.ts b/src/models/service.ts new file mode 100644 index 000000000..656cbf01a --- /dev/null +++ b/src/models/service.ts @@ -0,0 +1,15 @@ +import { ServiceDescriptionContract, HostnameConfiguration, ServiceSku } from "./../contracts/service"; + +export class ServiceDescription { + public name: string; + public hostnameConfigurations: HostnameConfiguration[]; + public sku: ServiceSku; + public gatewayUrl: string; + + constructor(contract: ServiceDescriptionContract) { + this.name = contract.name; + this.sku = contract.sku; + this.gatewayUrl = contract.properties.gatewayUrl; + this.hostnameConfigurations = contract.properties.hostnameConfigurations; + } +} \ No newline at end of file diff --git a/src/modules.d.ts b/src/modules.d.ts index d4dabb326..6e4c92570 100644 --- a/src/modules.d.ts +++ b/src/modules.d.ts @@ -11,4 +11,9 @@ declare module "*.liquid" { declare module "*.txt" { const content: string; export default content; +} + +declare module "*.raw" { + const content: string; + export default content; } \ No newline at end of file diff --git a/src/persistence/cachedObjectStorage.ts b/src/persistence/cachedObjectStorage.ts new file mode 100644 index 000000000..7b4413fce --- /dev/null +++ b/src/persistence/cachedObjectStorage.ts @@ -0,0 +1,42 @@ +import { LruCache } from "@paperbits/common/caching"; +import { IObjectStorage, Page, Query } from "@paperbits/common/persistence"; + +export class CachedObjectStorage { + private readonly cache: LruCache>; + + constructor( + private readonly underlyingStorage: IObjectStorage + ) { + this.cache = new LruCache(100, () => { return; }); + } + + public addObject(key: string, dataObject: T): Promise { + throw new Error("Not supported."); + } + + public getObject(key: string): Promise { + const cachedItemPromise = this.cache.getItem(key); + + if (cachedItemPromise) { + return cachedItemPromise; + } + + const fetchPromise = this.underlyingStorage.getObject(key); + + this.cache.setItem(key, fetchPromise); + + return fetchPromise; + } + + public deleteObject(key: string): Promise { + throw new Error("Not supported."); + } + + public updateObject(key: string, dataObject: T): Promise { + throw new Error("Not supported."); + } + + public async searchObjects(key: string, query?: Query): Promise> { + return await this.underlyingStorage.searchObjects(key, query); + } +} \ No newline at end of file diff --git a/src/persistence/publishingCacheModule.ts b/src/persistence/publishingCacheModule.ts new file mode 100644 index 000000000..da9bcb675 --- /dev/null +++ b/src/persistence/publishingCacheModule.ts @@ -0,0 +1,14 @@ +import { IObjectStorage } from "@paperbits/common/persistence"; +import { IInjector, IInjectorModule } from "@paperbits/common/injection"; +import { CachedObjectStorage } from "./cachedObjectStorage"; + + +export class PublishingCacheModule implements IInjectorModule { + public register(injector: IInjector): void { + const underlyingObjectStorage = injector.resolve("objectStorage"); + + injector.bindSingletonFactory("objectStorage", (ctx: IInjector) => { + return new CachedObjectStorage(underlyingObjectStorage); + }); + } +} \ No newline at end of file diff --git a/src/publishing/aadConfigPublisher.ts b/src/publishing/aadConfigPublisher.ts index b3b92a613..e2b0762f0 100644 --- a/src/publishing/aadConfigPublisher.ts +++ b/src/publishing/aadConfigPublisher.ts @@ -13,14 +13,10 @@ import { RuntimeConfigBuilder } from "./runtimeConfigBuilder"; export class AadConfigPublisher implements IPublisher { constructor( private readonly runtimeConfigBuilder: RuntimeConfigBuilder, - private readonly identityService: IdentityService, - private readonly settingsProvider: ISettingsProvider + private readonly identityService: IdentityService ) { } public async publish(): Promise { - const managementApiUrl = await this.settingsProvider.getSetting(SettingNames.managementApiUrl); - this.runtimeConfigBuilder.addSetting(SettingNames.managementApiUrl, managementApiUrl); - const identityProviders = await this.identityService.getIdentityProviders(); const aadIdentityProvider = identityProviders.find(x => x.type === SettingNames.aadClientConfig); diff --git a/src/publishing/runtimeConfigPublisher.ts b/src/publishing/runtimeConfigPublisher.ts index 53da084f6..fafe721d5 100644 --- a/src/publishing/runtimeConfigPublisher.ts +++ b/src/publishing/runtimeConfigPublisher.ts @@ -16,6 +16,6 @@ export class RuntimeConfigPublisher implements IPublisher { const configuration = this.runtimeConfigBuilder.build(); const content = Utils.stringToUnit8Array(JSON.stringify(configuration)); - await this.outputBlobStorage.uploadBlob("/config.json", content); + await this.outputBlobStorage.uploadBlob("/config-apim.json", content); } } \ No newline at end of file diff --git a/src/services/runtimeConfigurator.ts b/src/services/runtimeConfigurator.ts index 09bf36873..a84633a72 100644 --- a/src/services/runtimeConfigurator.ts +++ b/src/services/runtimeConfigurator.ts @@ -63,6 +63,6 @@ export class RuntimeConfigurator { designTimeSettings[SettingNames.aadB2CClientConfig] = aadB2CConfig; } - this.sessionManager.setItem("designTimeSettings", JSON.stringify(designTimeSettings)); + this.sessionManager.setItem("designTimeSettings", designTimeSettings); } } \ No newline at end of file diff --git a/src/startup.publish.ts b/src/startup.publish.ts index 13d3c12e0..afbb17715 100644 --- a/src/startup.publish.ts +++ b/src/startup.publish.ts @@ -9,6 +9,7 @@ import { ProseMirrorModule } from "@paperbits/prosemirror/prosemirror.module"; import { StaticSettingsProvider } from "./components/staticSettingsProvider"; import { FileSystemBlobStorage } from "./components/filesystemBlobStorage"; import { ApimPublishModule } from "./apim.publish.module"; +import { PublishingCacheModule } from "./persistence/publishingCacheModule"; /* Reading settings from configuration file */ const configFile = path.resolve(__dirname, "./config.json"); @@ -27,6 +28,7 @@ injector.bindModule(new ProseMirrorModule()); injector.bindModule(new ApimPublishModule()); injector.bindInstance("settingsProvider", settingsProvider); injector.bindInstance("outputBlobStorage", outputBlobStorage); +injector.bindModule(new PublishingCacheModule()); injector.resolve("autostart"); /* Allowing self-signed certificates for HTTP requests */