182 lines
		
	
	
		
			4.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			182 lines
		
	
	
		
			4.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import { AuthApi } from "./AuthApi";
 | 
						|
 | 
						|
interface RequestParams {
 | 
						|
  uri: string;
 | 
						|
  method: "GET" | "POST" | "DELETE" | "PATCH" | "PUT";
 | 
						|
  allowFail?: boolean;
 | 
						|
  jsonData?: any;
 | 
						|
  formData?: FormData;
 | 
						|
  upProgress?: (progress: number) => void;
 | 
						|
  downProgress?: (e: { progress: number; total: number }) => void;
 | 
						|
}
 | 
						|
 | 
						|
interface APIResponse {
 | 
						|
  data: any;
 | 
						|
  status: number;
 | 
						|
}
 | 
						|
 | 
						|
export class ApiError extends Error {
 | 
						|
  constructor(message: string, public code: number, public data: any) {
 | 
						|
    super(`HTTP status: ${code}\nMessage: ${message}\nData=${data}`);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
export class APIClient {
 | 
						|
  /**
 | 
						|
   * Get backend URL
 | 
						|
   */
 | 
						|
  static backendURL(): string {
 | 
						|
    const URL = String(import.meta.env.VITE_APP_BACKEND ?? "");
 | 
						|
    if (URL.length === 0) throw new Error("Backend URL undefined!");
 | 
						|
    return URL;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Check out whether the backend is accessed through
 | 
						|
   * HTTPS or not
 | 
						|
   */
 | 
						|
  static IsBackendSecure(): boolean {
 | 
						|
    return this.backendURL().startsWith("https");
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Perform a request on the backend
 | 
						|
   */
 | 
						|
  static async exec(args: RequestParams): Promise<APIResponse> {
 | 
						|
    let body: string | undefined | FormData = undefined;
 | 
						|
    const headers: any = {};
 | 
						|
 | 
						|
    // JSON request
 | 
						|
    if (args.jsonData) {
 | 
						|
      headers["Content-Type"] = "application/json";
 | 
						|
      body = JSON.stringify(args.jsonData);
 | 
						|
    }
 | 
						|
 | 
						|
    // Form data request
 | 
						|
    else if (args.formData) {
 | 
						|
      body = args.formData;
 | 
						|
    }
 | 
						|
 | 
						|
    const url = this.backendURL() + args.uri;
 | 
						|
 | 
						|
    let data;
 | 
						|
    let status: number;
 | 
						|
 | 
						|
    // Make the request with XMLHttpRequest
 | 
						|
    if (args.upProgress) {
 | 
						|
      const res: XMLHttpRequest = await new Promise((resolve, reject) => {
 | 
						|
        const xhr = new XMLHttpRequest();
 | 
						|
        xhr.upload.addEventListener("progress", (e) => {
 | 
						|
          args.upProgress!(e.loaded / e.total);
 | 
						|
        });
 | 
						|
        xhr.addEventListener("load", () => {
 | 
						|
          resolve(xhr);
 | 
						|
        });
 | 
						|
        xhr.addEventListener("error", () => {
 | 
						|
          reject(new Error("File upload failed"));
 | 
						|
        });
 | 
						|
        xhr.addEventListener("abort", () => {
 | 
						|
          reject(new Error("File upload aborted"));
 | 
						|
        });
 | 
						|
        xhr.addEventListener("timeout", () => {
 | 
						|
          reject(new Error("File upload timeout"));
 | 
						|
        });
 | 
						|
        xhr.open(args.method, url, true);
 | 
						|
        xhr.withCredentials = true;
 | 
						|
        for (const key in headers) {
 | 
						|
          // eslint-disable-next-line no-prototype-builtins
 | 
						|
          if (headers.hasOwnProperty(key))
 | 
						|
            xhr.setRequestHeader(key, headers[key]);
 | 
						|
        }
 | 
						|
        xhr.send(body);
 | 
						|
      });
 | 
						|
 | 
						|
      status = res.status;
 | 
						|
      if (res.responseType === "json") data = JSON.parse(res.responseText);
 | 
						|
      else data = res.response;
 | 
						|
    }
 | 
						|
 | 
						|
    // Make the request with fetch
 | 
						|
    else {
 | 
						|
      const res = await fetch(url, {
 | 
						|
        method: args.method,
 | 
						|
        body: body,
 | 
						|
        headers: headers,
 | 
						|
        credentials: "include",
 | 
						|
        signal: AbortSignal.timeout(50 * 1000 * 1000),
 | 
						|
      });
 | 
						|
 | 
						|
      // Process response
 | 
						|
      // JSON response
 | 
						|
      if (res.headers.get("content-type") === "application/json")
 | 
						|
        data = await res.json();
 | 
						|
      // Text / XML response
 | 
						|
      else if (
 | 
						|
        ["application/xml", "text/plain"].includes(
 | 
						|
          res.headers.get("content-type") ?? ""
 | 
						|
        )
 | 
						|
      )
 | 
						|
        data = await res.text();
 | 
						|
      // Binary file, tracking download progress
 | 
						|
      else if (res.body !== null && args.downProgress) {
 | 
						|
        // Track download progress
 | 
						|
        const contentEncoding = res.headers.get("content-encoding");
 | 
						|
        const contentLength = contentEncoding
 | 
						|
          ? null
 | 
						|
          : res.headers.get("content-length");
 | 
						|
 | 
						|
        const total = parseInt(contentLength ?? "0", 10);
 | 
						|
        let loaded = 0;
 | 
						|
 | 
						|
        const resInt = new Response(
 | 
						|
          new ReadableStream({
 | 
						|
            start(controller) {
 | 
						|
              const reader = res.body!.getReader();
 | 
						|
 | 
						|
              const read = async () => {
 | 
						|
                try {
 | 
						|
                  const ret = await reader.read();
 | 
						|
                  if (ret.done) {
 | 
						|
                    controller.close();
 | 
						|
                    return;
 | 
						|
                  }
 | 
						|
                  loaded += ret.value.byteLength;
 | 
						|
                  args.downProgress!({ progress: loaded, total });
 | 
						|
                  controller.enqueue(ret.value);
 | 
						|
                  read();
 | 
						|
                } catch (e) {
 | 
						|
                  console.error(e);
 | 
						|
                  controller.error(e);
 | 
						|
                }
 | 
						|
              };
 | 
						|
 | 
						|
              read();
 | 
						|
            },
 | 
						|
          })
 | 
						|
        );
 | 
						|
 | 
						|
        data = await resInt.blob();
 | 
						|
      }
 | 
						|
 | 
						|
      // Do not track progress (binary file)
 | 
						|
      else data = await res.blob();
 | 
						|
 | 
						|
      status = res.status;
 | 
						|
    }
 | 
						|
 | 
						|
    // Handle expired tokens
 | 
						|
    if (status === 412) {
 | 
						|
      AuthApi.UnsetAuthenticated();
 | 
						|
      window.location.href = "/";
 | 
						|
    }
 | 
						|
 | 
						|
    if (!args.allowFail && (status < 200 || status > 299))
 | 
						|
      throw new ApiError("Request failed!", status, data);
 | 
						|
 | 
						|
    return {
 | 
						|
      data: data,
 | 
						|
      status: status,
 | 
						|
    };
 | 
						|
  }
 | 
						|
}
 |