Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added handling for session expiration. Fixed document height issue in Safari. #772

Merged
merged 3 commits into from
Jul 22, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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