apim-developer-portal2
Version:
API management developer portal
291 lines (231 loc) • 10.5 kB
text/typescript
import * as _ from "lodash";
import * as Constants from "./../constants";
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 "../errors/mapiError";
import { IAuthenticator, AccessToken } from "../authentication";
import { KnownHttpHeaders } from "../models/knownHttpHeaders";
import { developerPortalType, portalHeaderName } from "./../constants";
export interface IHttpBatchResponses {
responses: IHttpBatchResponse[];
}
export interface IHttpBatchResponse {
httpStatusCode: number;
headers: {
[key: string]: string;
};
content: any;
}
export class MapiClient {
private managementApiUrl: string;
private environment: string;
private initializePromise: Promise<void>;
private requestCache: TtlCache = new TtlCache();
constructor(
private readonly httpClient: HttpClient,
private readonly authenticator: IAuthenticator,
private readonly settingsProvider: ISettingsProvider
) { }
private async ensureInitialized(): Promise<void> {
if (!this.initializePromise) {
this.initializePromise = this.initialize();
}
return this.initializePromise;
}
private async initialize(): Promise<void> {
const settings = await this.settingsProvider.getSettings();
const managementApiUrl = settings[Constants.SettingNames.managementApiUrl];
if (!managementApiUrl) {
throw new Error(`Management API URL ("${Constants.SettingNames.managementApiUrl}") setting is missing in configuration file.`);
}
this.managementApiUrl = Utils.ensureUrlArmified(managementApiUrl);
const managementApiAccessToken = settings[Constants.SettingNames.managementApiAccessToken];
if (managementApiAccessToken) {
const accessToken = AccessToken.parse(managementApiAccessToken);
await this.authenticator.setAccessToken(accessToken);
}
else if (this.environment === "development") {
console.warn(`Development mode: Please specify ${Constants.SettingNames.managementApiAccessToken}" in configuration file.`);
return;
}
this.environment = settings["environment"];
}
private async requestInternal<T>(httpRequest: HttpRequest): Promise<T> {
if (!httpRequest.url) {
throw new Error("Request URL cannot be empty.");
}
await this.ensureInitialized();
httpRequest.headers = httpRequest.headers || [];
if (httpRequest.body && !httpRequest.headers.some(x => x.name === "Content-Type")) {
httpRequest.headers.push({ name: "Content-Type", value: "application/json" });
}
if (!httpRequest.headers.some(x => x.name === "Accept")) {
httpRequest.headers.push({ name: "Accept", value: "*/*" });
}
if (typeof (httpRequest.body) === "object") {
httpRequest.body = JSON.stringify(httpRequest.body);
}
const call = () => this.makeRequest<T>(httpRequest);
const requestKey = this.getRequestKey(httpRequest);
if (requestKey) {
return this.requestCache.getOrAddAsync<T>(requestKey, call, 1000);
}
return call();
}
private getRequestKey(httpRequest: HttpRequest): string {
if (httpRequest.method !== HttpMethod.get && httpRequest.method !== HttpMethod.head && httpRequest.method !== "OPTIONS") { // TODO: HttpMethod.options) {
return null;
}
let key = `${httpRequest.method}:${httpRequest.url}`;
if (httpRequest.headers) {
key += ":" + httpRequest.headers.sort().map(k => `${k}=${httpRequest.headers.join(",")}`).join("&");
}
return key;
}
protected async makeRequest<T>(httpRequest: HttpRequest): Promise<T> {
const authHeader = httpRequest.headers.find(header => header.name === KnownHttpHeaders.Authorization);
if (!authHeader || !authHeader.value) {
const authToken = await this.authenticator.getAccessToken();
if (authToken) {
httpRequest.headers.push({ name: KnownHttpHeaders.Authorization, value: `${authToken}` });
}
}
const portalHeader = httpRequest.headers.find(header => header.name === portalHeaderName);
if (!portalHeader) {
httpRequest.headers.push(MapiClient.getPortalHeader());
}
httpRequest.url = `${this.managementApiUrl}${Utils.ensureLeadingSlash(httpRequest.url)}`;
httpRequest.url = Utils.addQueryParameter(httpRequest.url, `api-version=${Constants.managementApiVersion}`);
let response: HttpResponse<T>;
try {
response = await this.httpClient.send<T>(httpRequest);
}
catch (error) {
throw new Error(`Unable to complete request. Error: ${error.message}`);
}
try {
await this.authenticator.refreshAccessTokenFromHeader(response.headers);
}
catch (error) {
console.error("Refresh token error: ", error);
}
return await this.handleResponse<T>(response, httpRequest.url);
}
private async handleResponse<T>(response: HttpResponse<T>, url: string): Promise<T> {
let contentType = "";
if (response.headers) {
const contentTypeHeader = response.headers.find(h => h.name.toLowerCase() === "content-type");
contentType = contentTypeHeader ? contentTypeHeader.value.toLowerCase() : "";
}
const text = response.toText();
if (response.statusCode >= 200 && response.statusCode < 300) {
if (_.includes(contentType, "json") && text.length > 0) {
return JSON.parse(text) as T;
}
else {
return <any>text;
}
} else {
await this.handleError(response, url);
}
}
private async handleError(errorResponse: HttpResponse<any>, requestedUrl: string): Promise<void> {
if (errorResponse.statusCode === 429) {
throw new MapiError("too_many_logins", "Too many attempts. Please try later.");
}
if (errorResponse.statusCode === 401) {
const authHeader = errorResponse.headers.find(h => h.name.toLowerCase() === "www-authenticate");
if (authHeader && authHeader.value.indexOf("Basic") !== -1) {
if (authHeader.value.indexOf("identity_not_confirmed") !== -1) {
throw new MapiError("identity_not_confirmed", "User status is Pending. Please check confirmation email.");
}
if (authHeader.value.indexOf("invalid_identity") !== -1) {
throw new MapiError("invalid_identity", "Invalid email or password.");
}
}
}
const error = this.createMapiError(errorResponse.statusCode, requestedUrl, () => errorResponse.toObject().error);
if (error) {
error.response = errorResponse;
throw error;
}
throw new MapiError("Unhandled", "Unhandled error");
}
private createMapiError(statusCode: number, url: string, getError: () => any): any {
switch (statusCode) {
case 400:
return getError();
case 401:
this.authenticator.clearAccessToken();
return new MapiError("Unauthorized", "Unauthorized request.");
case 403:
return new MapiError("Forbidden", "You're not authorized to perform this operation.");
case 404:
return new MapiError("ResourceNotFound", `Resource not found: ${url}`);
case 408:
return new MapiError("RequestTimeout", "Could not complete the request. Please try again later.");
case 409:
return getError();
case 500:
return new MapiError("ServerError", "Internal server error.");
default:
return new MapiError("Unhandled", `Unexpected status code in SMAPI response: ${statusCode}.`);
}
}
public get<TResponse>(url: string, headers?: HttpHeader[]): Promise<TResponse> {
return this.requestInternal<TResponse>({
method: HttpMethod.get,
url: url,
headers: headers
});
}
public post<TResponse>(url: string, headers?: HttpHeader[], body?: any): Promise<TResponse> {
return this.requestInternal<TResponse>({
method: HttpMethod.post,
url: url,
headers: headers,
body: body
});
}
public patch<TResponse>(url: string, headers?: HttpHeader[], body?: any): Promise<TResponse> {
return this.requestInternal<TResponse>({
method: HttpMethod.patch,
url: url,
headers: headers,
body: body
});
}
public put<TResponse>(url: string, headers?: HttpHeader[], body?: any): Promise<TResponse> {
return this.requestInternal<TResponse>({
method: HttpMethod.put,
url: url,
headers: headers,
body: body
});
}
public delete<TResponse>(url: string, headers?: HttpHeader[]): Promise<TResponse> {
return this.requestInternal<TResponse>({
method: HttpMethod.delete,
url: url,
headers: headers
});
}
public head<T>(url: string, headers?: HttpHeader[]): Promise<T> {
return this.requestInternal<T>({
method: HttpMethod.head,
url: url,
headers: headers
});
}
public static getPortalHeader(eventName?: string): HttpHeader {
let host = "";
try {
host = window.location.host;
} catch (error){
host = "publishing";
}
return { name: portalHeaderName, value: `${developerPortalType}|${host}|${eventName || ""}` };
}
}