diff --git a/src/apim.design.module.ts b/src/apim.design.module.ts index 63be5ee17..498ec4f4e 100644 --- a/src/apim.design.module.ts +++ b/src/apim.design.module.ts @@ -51,7 +51,7 @@ import { ChangePasswordModule } from "./components/users/change-password/ko/chan import { ChangePasswordEditorModule } from "./components/users/change-password/ko/changePasswordEditor.module"; import { TenantService } from "./services/tenantService"; import { ValidationSummaryModule } from "./components/users/validation-summary/validationSummary.module"; -import { ValidationSummaryDesignModule} from "./components/users/validation-summary/validationSummary.design.module" +import { ValidationSummaryDesignModule } from "./components/users/validation-summary/validationSummary.design.module" import { BackendService } from "./services/backendService"; import { StaticRoleService } from "./services/roleService"; import { ProvisionService } from "./services/provisioningService"; @@ -109,7 +109,7 @@ export class ApimDesignModule implements IInjectorModule { injector.bindSingleton("app", App); injector.bindSingleton("logger", ConsoleLogger); injector.bindSingleton("blobStorage", AzureBlobStorage); - injector.bindSingleton("tenantService", TenantService); + injector.bindSingleton("tenantService", TenantService); injector.bindSingleton("backendService", BackendService); injector.bindSingleton("roleService", StaticRoleService); injector.bindSingleton("provisioningService", ProvisionService); @@ -119,7 +119,7 @@ export class ApimDesignModule implements IInjectorModule { injector.bindSingleton("authenticator", DefaultAuthenticator); injector.bindSingleton("objectStorage", MapiObjectStorage); injector.bindToCollection("routeGuards", UnsavedChangesRouteGuard); - injector.bindInstance("reservedPermalinks", Constants.reservedPermalinks); + injector.bindInstance("reservedPermalinks", Constants.reservedPermalinks); injector.bindSingleton("oauthService", OAuthService); } } \ No newline at end of file diff --git a/src/apim.runtime.module.ts b/src/apim.runtime.module.ts index 8ad72a3de..f875a99da 100644 --- a/src/apim.runtime.module.ts +++ b/src/apim.runtime.module.ts @@ -55,7 +55,7 @@ import { ResetPassword } from "./components/users/reset-password/ko/runtime/rese import { ConfirmPassword } from "./components/users/confirm-password/ko/runtime/confirm-password"; import { ChangePassword } from "./components/users/change-password/ko/runtime/change-password"; import { Reports } from "./components/reports/ko/runtime/reports"; -import { UnhandledErrorHandler } from "./services/unhandledErrorHandler"; +import { UnhandledErrorHandler } from "./errors/unhandledErrorHandler"; import { ProductListDropdown } from "./components/products/product-list/ko/runtime/product-list-dropdown"; import { ValidationSummary } from "./components/users/validation-summary/ko/runtime/validation-summary"; import { TypeDefinitionViewModel } from "./components/operations/operation-details/ko/runtime/type-definition"; diff --git a/src/components/content/content.ts b/src/components/content/content.ts index c68f03fa6..7cdb0dd00 100644 --- a/src/components/content/content.ts +++ b/src/components/content/content.ts @@ -4,6 +4,8 @@ import { Component } from "@paperbits/common/ko/decorators"; import { HttpClient } from "@paperbits/common/http"; import { Logger } from "@paperbits/common/logging"; import { IAuthenticator } from "../../authentication/IAuthenticator"; +import { AppError } from "./../../errors/appError"; +import { MapiError } from "../../errors/mapiError"; @Component({ @@ -21,11 +23,25 @@ export class ContentWorkshop { } public async publish(): Promise { - try { - this.logger.traceEvent("Click: Publish website"); + this.logger.traceEvent("Click: Publish website"); + + if (!await this.authenticator.isAuthenticated()) { + throw new AppError("Cannot publish website", new MapiError("Unauthorized", "You're not authorized.")); + } + try { const accessToken = await this.authenticator.getAccessToken(); - await this.httpClient.send({ url: "/publish", method: "POST", headers: [{ name: "Authorization", value: accessToken }] }); + + const response = await this.httpClient.send({ + url: "/publish", + method: "POST", + headers: [{ name: "Authorization", value: accessToken }] + }); + + 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/components/defaultAuthenticator.ts b/src/components/defaultAuthenticator.ts index 0d0d5702c..236e3726e 100644 --- a/src/components/defaultAuthenticator.ts +++ b/src/components/defaultAuthenticator.ts @@ -11,11 +11,13 @@ export class DefaultAuthenticator implements IAuthenticator { }); } - public async setAccessToken(accessToken: string): Promise { - return new Promise((resolve) => { - sessionStorage.setItem("accessToken", accessToken); - resolve(); - }); + public async setAccessToken(accessToken: string): Promise { + if (!this.isTokenValid(accessToken)) { + console.warn(`Cannot set invalid or expired access token.`); + return; + } + + sessionStorage.setItem("accessToken", accessToken); } public async refreshAccessTokenFromHeader(responseHeaders: HttpHeader[] = []): Promise { @@ -32,12 +34,12 @@ export class DefaultAuthenticator implements IAuthenticator { const accessToken = `SharedAccessSignature ${accessTokenHeader.value}`; const current = sessionStorage.getItem("accessToken"); if (current !== accessToken) { - sessionStorage.setItem("accessToken", accessToken); + sessionStorage.setItem("accessToken", accessToken); resolve(accessToken); return; } } - + resolve(undefined); }); } @@ -117,4 +119,16 @@ export class DefaultAuthenticator implements IAuthenticator { throw new Error(`Access token format is not valid. Please use "Bearer" or "SharedAccessSignature".`); } } + + private isTokenValid(accessToken: string): boolean { + try { + const parsedToken = this.parseAccessToken(accessToken); + const utcNow = Utils.getUtcDateTime(); + + return (utcNow < parsedToken.expires); + } + catch (error) { + return false; + } + } } \ No newline at end of file diff --git a/src/components/users/signin-social/ko/runtime/signin-aad-b2c.ts b/src/components/users/signin-social/ko/runtime/signin-aad-b2c.ts index 67fa6d3b9..1418cc2de 100644 --- a/src/components/users/signin-social/ko/runtime/signin-aad-b2c.ts +++ b/src/components/users/signin-social/ko/runtime/signin-aad-b2c.ts @@ -1,4 +1,3 @@ -import { MapiError } from "./../../../../../services/mapiError"; import * as ko from "knockout"; import template from "./signin-aad-b2c.html"; import { Component, RuntimeComponent, OnMounted, Param } from "@paperbits/common/ko/decorators"; diff --git a/src/components/users/signin/ko/runtime/signin.ts b/src/components/users/signin/ko/runtime/signin.ts index c78b5a05b..04cc9ac7e 100644 --- a/src/components/users/signin/ko/runtime/signin.ts +++ b/src/components/users/signin/ko/runtime/signin.ts @@ -4,7 +4,7 @@ import template from "./signin.html"; import { EventManager } from "@paperbits/common/events"; import { Component, RuntimeComponent, OnMounted, Param } from "@paperbits/common/ko/decorators"; import { UsersService } from "../../../../../services/usersService"; -import { MapiError } from "../../../../../services/mapiError"; +import { MapiError } from "../../../../../errors/mapiError"; import { ValidationReport } from "../../../../../contracts/validationReport"; import { RouteHelper } from "../../../../../routing/routeHelper"; import { Router } from "@paperbits/common/routing/router"; diff --git a/src/errors/appError.ts b/src/errors/appError.ts new file mode 100644 index 000000000..ad77f7670 --- /dev/null +++ b/src/errors/appError.ts @@ -0,0 +1,13 @@ +export class AppError extends Error { + constructor( + public readonly message: string, + public readonly innerError?: Error + ) { + super(); + Object.setPrototypeOf(this, AppError.prototype); + } + + public toString(): string { + return `${this.stack} `; + } +} \ No newline at end of file diff --git a/src/errors/index.ts b/src/errors/index.ts new file mode 100644 index 000000000..8ccfe36f5 --- /dev/null +++ b/src/errors/index.ts @@ -0,0 +1,3 @@ +export * from "./appError"; +export * from "./sessionExpirationErrorHandler"; +export * from "./unhandledErrorHandler"; \ No newline at end of file diff --git a/src/services/mapiError.ts b/src/errors/mapiError.ts similarity index 100% rename from src/services/mapiError.ts rename to src/errors/mapiError.ts diff --git a/src/errors/sessionExpirationErrorHandler.ts b/src/errors/sessionExpirationErrorHandler.ts new file mode 100644 index 000000000..13e11857a --- /dev/null +++ b/src/errors/sessionExpirationErrorHandler.ts @@ -0,0 +1,30 @@ +import { ViewManager } from "@paperbits/common/ui"; +import { EventManager } from "@paperbits/common/events"; + + +export class SessionExpirationErrorHandler { + constructor(private readonly viewManager: ViewManager, eventManager: EventManager) { + eventManager.addEventListener("error", this.handlerError.bind(this)); + window.addEventListener("unhandledrejection", this.handlerPromiseRejection.bind(this), true); + } + + private handleSessionExpiration(error: any): void { + if (error?.innerError?.code !== "Unauthorized") { + return; + } + + event.stopImmediatePropagation(); + this.viewManager.hideToolboxes(); + this.viewManager.addToast("Session expired", `Please re-authenticate through Azure portal.`); + this.viewManager.setShutter(); + return; + } + + public handlerError(event: ErrorEvent): void { + this.handleSessionExpiration(event.error); + } + + public handlerPromiseRejection(event: PromiseRejectionEvent): void { + this.handleSessionExpiration(event.reason); + } +} \ No newline at end of file diff --git a/src/services/unhandledErrorHandler.ts b/src/errors/unhandledErrorHandler.ts similarity index 73% rename from src/services/unhandledErrorHandler.ts rename to src/errors/unhandledErrorHandler.ts index cb48f1a2c..a11c80621 100644 --- a/src/services/unhandledErrorHandler.ts +++ b/src/errors/unhandledErrorHandler.ts @@ -1,13 +1,11 @@ -import { Router } from "@paperbits/common/routing"; import { Logger } from "@paperbits/common/logging"; -import { pageUrl500 } from "../constants"; + export class UnhandledErrorHandler { constructor( - private readonly logger: Logger, - private readonly router: Router + private readonly logger: Logger ) { - window.addEventListener("error", this.handlerError.bind(this), true); + window.addEventListener("error", this.handlerError.bind(this), true,); window.addEventListener("unhandledrejection", this.handlerPromiseRejection.bind(this), true); } @@ -16,6 +14,7 @@ export class UnhandledErrorHandler { } public handlerPromiseRejection(event: PromiseRejectionEvent): void { + debugger; this.logger.traceError(event.reason); } } \ No newline at end of file diff --git a/src/persistence/mapiObjectStorage.ts b/src/persistence/mapiObjectStorage.ts index 0162c5bf9..fdf89111d 100644 --- a/src/persistence/mapiObjectStorage.ts +++ b/src/persistence/mapiObjectStorage.ts @@ -5,6 +5,7 @@ import { MapiClient } from "../services/mapiClient"; import { Page } from "../models/page"; import { HttpHeader } from "@paperbits/common/http"; import { ArmResource } from "../contracts/armResource"; +import { AppError } from "../errors"; const localizedContentTypes = ["page", "layout", "blogpost", "navigation", "block"]; @@ -25,7 +26,7 @@ export class MapiObjectStorage implements IObjectStorage { return contentType; } - throw new Error(`Could not determine content type by resource: ${resource}`); + throw new AppError(`Could not determine content type by resource: ${resource}`); } private delocalizeBlock(contract: any): void { @@ -112,7 +113,7 @@ export class MapiObjectStorage implements IObjectStorage { break; default: - throw new Error(`Unknown content type: "${mapiContentType}"`); + throw new AppError(`Unknown content type: "${mapiContentType}"`); } let key = contentType; @@ -192,7 +193,7 @@ export class MapiObjectStorage implements IObjectStorage { break; default: - // throw new Error(`Unknown content type: "${contentType}"`); + // throw new AppError(`Unknown content type: "${contentType}"`); return key; } @@ -214,7 +215,7 @@ export class MapiObjectStorage implements IObjectStorage { await this.mapiClient.put(resource, headers, converted); } catch (error) { - throw new Error(`Could not add object '${path}'. Error: ${error.message}`); + throw new AppError(`Could not add object '${path}'.`, error); } } @@ -249,11 +250,11 @@ export class MapiObjectStorage implements IObjectStorage { return converted; } catch (error) { - if (error && error.code === "ResourceNotFound") { + if (error?.code === "ResourceNotFound") { return null; } - throw new Error(`Could not get object '${key}'. Error: ${error.message}`); + throw new AppError(`Could not get object '${key}'.`, error); } } @@ -267,7 +268,7 @@ export class MapiObjectStorage implements IObjectStorage { await this.mapiClient.delete(resource, headers); } catch (error) { - throw new Error(`Could not delete object '${path}'. Error: ${error.message}`); + throw new AppError(`Could not delete object '${path}'.`, error); } } @@ -315,11 +316,11 @@ export class MapiObjectStorage implements IObjectStorage { exists = true; } catch (error) { - if (error && error.code === "ResourceNotFound") { + if (error?.code === "ResourceNotFound") { exists = false; } else { - throw new Error(`Could not update object '${key}'. Error: ${error.message}`); + throw new AppError(`Could not update object '${key}'.`, error); } } @@ -333,7 +334,7 @@ export class MapiObjectStorage implements IObjectStorage { await this.mapiClient.put(resource, headers, armContract); } catch (error) { - throw new Error(`Could not update object '${key}'. Error: ${error.message}`); + throw new AppError(`Could not update object '${key}'.`, error); } } @@ -368,7 +369,7 @@ export class MapiObjectStorage implements IObjectStorage { break; default: - throw new Error(`Cannot translate operator into OData query.`); + throw new AppError(`Cannot translate operator into OData query.`); } } @@ -401,7 +402,7 @@ export class MapiObjectStorage implements IObjectStorage { } } catch (error) { - throw new Error(`Could not search object '${key}'. Error: ${error.message}`); + throw new AppError(`Could not search object '${key}'. Error: ${error.message}`, error); } } diff --git a/src/services/mapiClient.ts b/src/services/mapiClient.ts index e69b01235..2f5cce506 100644 --- a/src/services/mapiClient.ts +++ b/src/services/mapiClient.ts @@ -4,7 +4,7 @@ import { ISettingsProvider } from "@paperbits/common/configuration"; import { Utils } from "../utils"; import { TtlCache } from "./ttlCache"; import { HttpClient, HttpRequest, HttpResponse, HttpMethod, HttpHeader } from "@paperbits/common/http"; -import { MapiError } from "./mapiError"; +import { MapiError } from "../errors/mapiError"; import { IAuthenticator } from "../authentication/IAuthenticator"; import { KnownHttpHeaders } from "../models/knownHttpHeaders"; @@ -153,10 +153,10 @@ export class MapiClient { console.error("Refresh token error: ", error); } - return this.handleResponse(response, httpRequest.url); + return await this.handleResponse(response, httpRequest.url); } - private handleResponse(response: HttpResponse, url: string): T { + private async handleResponse(response: HttpResponse, url: string): Promise { let contentType = ""; if (response.headers) { @@ -174,13 +174,13 @@ export class MapiClient { return text; } } else { - this.handleError(response, url); + await this.handleError(response, url); } } - private handleError(errorResponse: HttpResponse, requestedUrl: string): void { + private async handleError(errorResponse: HttpResponse, requestedUrl: string): Promise { if (errorResponse.statusCode === 429) { - throw new MapiError("to_many_logins", "Too many attempts. Please try later."); + throw new MapiError("too_many_logins", "Too many attempts. Please try later."); } if (errorResponse.statusCode === 401) { @@ -194,11 +194,6 @@ export class MapiClient { throw new MapiError("invalid_identity", "Invalid email or password."); } } - - if (this.environment === "production") { - window.location.assign("/signin"); - return; - } } const error = this.createMapiError(errorResponse.statusCode, requestedUrl, () => errorResponse.toObject().error); diff --git a/src/services/oauthService.ts b/src/services/oauthService.ts index d070bfa39..abdd0dd13 100644 --- a/src/services/oauthService.ts +++ b/src/services/oauthService.ts @@ -9,6 +9,7 @@ import { AuthorizationServer } from "../models/authorizationServer"; import { PageContract } from "../contracts/page"; import { OpenIdConnectProviderContract } from "../contracts/openIdConnectProvider"; import { OpenIdConnectProvider } from "./../models/openIdConnectProvider"; +import { AppError } from "../errors"; export class OAuthService { constructor( @@ -22,7 +23,7 @@ export class OAuthService { return pageOfAuthservers.value.map(authServer => new OpenIdConnectProvider(authServer)); } catch (error) { - throw new Error(`Unable to fetch configured authorization servers.`); + throw new AppError(`Unable to fetch configured authorization servers.`, error); } } diff --git a/src/services/usersService.ts b/src/services/usersService.ts index 94bb7e6e0..8383d1803 100644 --- a/src/services/usersService.ts +++ b/src/services/usersService.ts @@ -9,7 +9,7 @@ import { Utils } from "../utils"; import { Identity } from "../contracts/identity"; import { UserContract, UserPropertiesContract, } from "../contracts/user"; import { MapiSignupRequest } from "../contracts/signupRequest"; -import { MapiError } from "./mapiError"; +import { MapiError } from "../errors/mapiError"; import { AppType } from "./../constants"; diff --git a/src/startup.design.ts b/src/startup.design.ts index c93d20481..e28b9547e 100644 --- a/src/startup.design.ts +++ b/src/startup.design.ts @@ -5,11 +5,13 @@ import { CoreDesignModule } from "@paperbits/core/core.design.module"; import { StylesDesignModule } from "@paperbits/styles/styles.design.module"; import { ProseMirrorModule } from "@paperbits/prosemirror/prosemirror.module"; import { OfflineModule } from "@paperbits/common/persistence/offline.module"; +import { SessionExpirationErrorHandler } from "./errors/sessionExpirationErrorHandler"; import { ApimDesignModule } from "./apim.design.module"; /* Initializing dependency injection container */ const injector = new InversifyInjector(); +injector.bindToCollection("autostart", SessionExpirationErrorHandler); injector.bindModule(new CoreDesignModule()); injector.bindModule(new StylesDesignModule()); injector.bindModule(new ProseMirrorModule()); diff --git a/src/themes/website/styles/layouts.scss b/src/themes/website/styles/layouts.scss index ed673ba74..46d52ba66 100644 --- a/src/themes/website/styles/layouts.scss +++ b/src/themes/website/styles/layouts.scss @@ -4,8 +4,8 @@ body, html { - height: 100%; - width: 100%; + min-height: 100%; + min-width: 100%; display: flex; flex-direction: column; }