Skip to content

Commit

Permalink
Added handling for session expiration. Fixed for document height issu…
Browse files Browse the repository at this point in the history
…e in Safari. (#772)
  • Loading branch information
azaslonov authored Jul 22, 2020
1 parent 99788cf commit f61bf64
Show file tree
Hide file tree
Showing 17 changed files with 121 additions and 48 deletions.
6 changes: 3 additions & 3 deletions src/apim.design.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand All @@ -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);
}
}
2 changes: 1 addition & 1 deletion src/apim.runtime.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
22 changes: 19 additions & 3 deletions src/components/content/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -21,11 +23,25 @@ export class ContentWorkshop {
}

public async publish(): Promise<void> {
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");
}
Expand Down
28 changes: 21 additions & 7 deletions src/components/defaultAuthenticator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ export class DefaultAuthenticator implements IAuthenticator {
});
}

public async setAccessToken(accessToken: string): Promise<void> {
return new Promise<void>((resolve) => {
sessionStorage.setItem("accessToken", accessToken);
resolve();
});
public async setAccessToken(accessToken: string): Promise<void> {
if (!this.isTokenValid(accessToken)) {
console.warn(`Cannot set invalid or expired access token.`);
return;
}

sessionStorage.setItem("accessToken", accessToken);
}

public async refreshAccessTokenFromHeader(responseHeaders: HttpHeader[] = []): Promise<string> {
Expand All @@ -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);
});
}
Expand Down Expand Up @@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion src/components/users/signin/ko/runtime/signin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
13 changes: 13 additions & 0 deletions src/errors/appError.ts
Original file line number Diff line number Diff line change
@@ -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} `;
}
}
3 changes: 3 additions & 0 deletions src/errors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./appError";
export * from "./sessionExpirationErrorHandler";
export * from "./unhandledErrorHandler";
File renamed without changes.
30 changes: 30 additions & 0 deletions src/errors/sessionExpirationErrorHandler.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}

Expand All @@ -16,6 +14,7 @@ export class UnhandledErrorHandler {
}

public handlerPromiseRejection(event: PromiseRejectionEvent): void {
debugger;
this.logger.traceError(event.reason);
}
}
25 changes: 13 additions & 12 deletions src/persistence/mapiObjectStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"];
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand All @@ -214,7 +215,7 @@ export class MapiObjectStorage implements IObjectStorage {
await this.mapiClient.put<T>(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);
}
}

Expand Down Expand Up @@ -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);
}
}

Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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);
}
}

Expand All @@ -333,7 +334,7 @@ export class MapiObjectStorage implements IObjectStorage {
await this.mapiClient.put<T>(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);
}
}

Expand Down Expand Up @@ -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.`);
}
}

Expand Down Expand Up @@ -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);
}
}

Expand Down
17 changes: 6 additions & 11 deletions src/services/mapiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -153,10 +153,10 @@ export class MapiClient {
console.error("Refresh token error: ", error);
}

return this.handleResponse<T>(response, httpRequest.url);
return await this.handleResponse<T>(response, httpRequest.url);
}

private handleResponse<T>(response: HttpResponse<T>, url: string): T {
private async handleResponse<T>(response: HttpResponse<T>, url: string): Promise<T> {
let contentType = "";

if (response.headers) {
Expand All @@ -174,13 +174,13 @@ export class MapiClient {
return <any>text;
}
} else {
this.handleError(response, url);
await this.handleError(response, url);
}
}

private handleError(errorResponse: HttpResponse<any>, requestedUrl: string): void {
private async handleError(errorResponse: HttpResponse<any>, requestedUrl: string): Promise<void> {
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) {
Expand All @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion src/services/oauthService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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);
}
}

Expand Down
Loading

0 comments on commit f61bf64

Please sign in to comment.