Files
SCM-Manager/scm-ui/ui-components/src/apiclient.ts

308 lines
8.5 KiB
TypeScript
Raw Normal View History

/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import { contextPath } from "./urls";
2020-08-20 18:57:58 +02:00
import {
createBackendError,
ForbiddenError,
isBackendError,
UnauthorizedError,
BackendErrorContent,
TOKEN_EXPIRED_ERROR_CODE
} from "./errors";
2019-12-11 10:11:40 +01:00
type SubscriptionEvent = {
type: string;
};
type OpenEvent = SubscriptionEvent;
type ErrorEvent = SubscriptionEvent & {
error: Error;
};
type MessageEvent = SubscriptionEvent & {
data: string;
lastEventId?: string;
};
type MessageListeners = {
[eventType: string]: (event: MessageEvent) => void;
};
type SubscriptionContext = {
onOpen?: OpenEvent;
onMessage: MessageListeners;
onError?: ErrorEvent;
};
type SubscriptionArgument = MessageListeners | SubscriptionContext;
type Cancel = () => void;
const sessionId = (
Date.now().toString(36) +
Math.random()
.toString(36)
.substr(2, 5)
).toUpperCase();
2019-11-18 11:45:48 +01:00
const extractXsrfTokenFromJwt = (jwt: string) => {
const parts = jwt.split(".");
if (parts.length === 3) {
return JSON.parse(atob(parts[1])).xsrf;
}
};
// @VisibleForTesting
export const extractXsrfTokenFromCookie = (cookieString?: string) => {
if (cookieString) {
const cookies = cookieString.split(";");
for (const c of cookies) {
const parts = c.trim().split("=");
if (parts[0] === "X-Bearer-Token") {
return extractXsrfTokenFromJwt(parts[1]);
}
}
}
};
const extractXsrfToken = () => {
return extractXsrfTokenFromCookie(document.cookie);
};
2019-12-11 10:11:40 +01:00
const createRequestHeaders = () => {
const headers: { [key: string]: string } = {
// disable caching for now
Cache: "no-cache",
// identify the request as ajax request
"X-Requested-With": "XMLHttpRequest",
// identify the web interface
"X-SCM-Client": "WUI",
// identify the window session
"X-SCM-Session-ID": sessionId
};
2019-11-18 11:45:48 +01:00
const xsrf = extractXsrfToken();
if (xsrf) {
headers["X-XSRF-Token"] = xsrf;
}
2019-12-11 10:11:40 +01:00
return headers;
};
2019-11-18 11:45:48 +01:00
2019-12-11 10:11:40 +01:00
const applyFetchOptions: (p: RequestInit) => RequestInit = o => {
if (o.headers) {
o.headers = {
...createRequestHeaders()
};
} else {
o.headers = createRequestHeaders();
}
o.credentials = "same-origin";
return o;
};
2018-11-15 21:39:08 +01:00
function handleFailure(response: Response) {
if (!response.ok) {
if (response.status === 401) {
throw new UnauthorizedError("Unauthorized", 401);
} else if (response.status === 403) {
throw new ForbiddenError("Forbidden", 403);
} else if (isBackendError(response)) {
return response.json().then((content: BackendErrorContent) => {
throw createBackendError(content, response.status);
});
2018-11-15 21:39:08 +01:00
} else {
throw new Error("server returned status code " + response.status);
}
}
return response;
}
export function createUrl(url: string) {
if (url.includes("://")) {
return url;
}
let urlWithStartingSlash = url;
if (url.indexOf("/") !== 0) {
urlWithStartingSlash = "/" + urlWithStartingSlash;
2018-07-11 22:01:36 +02:00
}
2018-10-01 17:22:03 +02:00
return `${contextPath}/api/v2${urlWithStartingSlash}`;
}
export function createUrlWithIdentifiers(url: string): string {
return createUrl(url) + "?X-SCM-Client=WUI&X-SCM-Session-ID=" + sessionId;
}
type ErrorListener = (error: Error) => void;
class ApiClient {
errorListeners: ErrorListener[] = [];
get = (url: string): Promise<Response> => {
return fetch(createUrl(url), applyFetchOptions({}))
.then(handleFailure)
.catch(this.notifyAndRethrow);
};
post = (
url: string,
payload?: any,
contentType = "application/json",
additionalHeaders: Record<string, string> = {}
) => {
return this.httpRequestWithJSONBody("POST", url, contentType, additionalHeaders, payload);
};
postText = (url: string, payload: string, additionalHeaders: Record<string, string> = {}) => {
2019-11-20 08:59:57 +01:00
return this.httpRequestWithTextBody("POST", url, additionalHeaders, payload);
};
2019-11-20 08:59:57 +01:00
putText = (url: string, payload: string, additionalHeaders: Record<string, string> = {}) => {
2019-11-20 08:59:57 +01:00
return this.httpRequestWithTextBody("PUT", url, additionalHeaders, payload);
};
postBinary = (url: string, fileAppender: (p: FormData) => void, additionalHeaders: Record<string, string> = {}) => {
2019-10-21 10:57:56 +02:00
const formData = new FormData();
2019-08-30 09:11:14 +02:00
fileAppender(formData);
2019-10-21 10:57:56 +02:00
const options: RequestInit = {
method: "POST",
body: formData,
headers: additionalHeaders
};
2019-08-30 09:11:14 +02:00
return this.httpRequestWithBinaryBody(options, url);
};
2019-11-20 10:44:11 +01:00
put(url: string, payload: any, contentType = "application/json", additionalHeaders: Record<string, string> = {}) {
return this.httpRequestWithJSONBody("PUT", url, contentType, additionalHeaders, payload);
2018-07-11 17:02:38 +02:00
}
head = (url: string) => {
let options: RequestInit = {
method: "HEAD"
2018-10-25 13:45:52 +02:00
};
options = applyFetchOptions(options);
return fetch(createUrl(url), options)
.then(handleFailure)
.catch(this.notifyAndRethrow);
};
2018-10-15 16:45:44 +02:00
delete = (url: string): Promise<Response> => {
let options: RequestInit = {
method: "DELETE"
};
options = applyFetchOptions(options);
return fetch(createUrl(url), options)
.then(handleFailure)
.catch(this.notifyAndRethrow);
};
httpRequestWithJSONBody = (
method: string,
url: string,
contentType: string,
2019-11-20 10:44:11 +01:00
additionalHeaders: Record<string, string>,
payload?: any
): Promise<Response> => {
2019-10-21 10:57:56 +02:00
const options: RequestInit = {
method: method,
headers: additionalHeaders
};
2019-11-05 16:10:41 +01:00
if (payload) {
options.body = JSON.stringify(payload);
}
2019-08-30 09:11:14 +02:00
return this.httpRequestWithBinaryBody(options, url, contentType);
};
httpRequestWithTextBody = (
2019-11-20 10:44:11 +01:00
method: string,
url: string,
additionalHeaders: Record<string, string> = {},
payload: string
) => {
2019-11-20 08:59:57 +01:00
const options: RequestInit = {
method: method,
headers: additionalHeaders
};
options.body = payload;
return this.httpRequestWithBinaryBody(options, url, "text/plain");
};
2019-11-20 08:59:57 +01:00
httpRequestWithBinaryBody = (options: RequestInit, url: string, contentType?: string) => {
options = applyFetchOptions(options);
2019-08-30 09:11:14 +02:00
if (contentType) {
if (!options.headers) {
options.headers = {};
}
2019-12-11 10:11:40 +01:00
// @ts-ignore We are sure that here we only get headers of type {[name:string]: string}
options.headers["Content-Type"] = contentType;
2019-08-30 09:11:14 +02:00
}
return fetch(createUrl(url), options)
.then(handleFailure)
.catch(this.notifyAndRethrow);
};
2019-12-11 10:11:40 +01:00
subscribe(url: string, argument: SubscriptionArgument): Cancel {
const es = new EventSource(createUrlWithIdentifiers(url), {
withCredentials: true
2019-12-11 10:11:40 +01:00
});
let listeners: MessageListeners;
// type guard, to identify that argument is of type SubscriptionContext
if ("onMessage" in argument) {
listeners = (argument as SubscriptionContext).onMessage;
if (argument.onError) {
// @ts-ignore typing of EventSource is weird
2019-12-11 10:11:40 +01:00
es.onerror = argument.onError;
}
if (argument.onOpen) {
// @ts-ignore typing of EventSource is weird
2019-12-11 10:11:40 +01:00
es.onopen = argument.onOpen;
}
} else {
listeners = argument;
}
for (const type in listeners) {
// @ts-ignore typing of EventSource is weird
2019-12-11 10:11:40 +01:00
es.addEventListener(type, listeners[type]);
}
2019-12-17 09:30:21 +01:00
return () => es.close();
2019-12-11 10:11:40 +01:00
}
onError = (errorListener: ErrorListener) => {
this.errorListeners.push(errorListener);
};
private notifyAndRethrow = (error: Error): never => {
this.errorListeners.forEach(errorListener => errorListener(error));
throw error;
};
}
2019-10-21 10:57:56 +02:00
export const apiClient = new ApiClient();