aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src/app/http
diff options
context:
space:
mode:
Diffstat (limited to 'FrontEnd/src/app/http')
-rw-r--r--FrontEnd/src/app/http/common.ts161
-rw-r--r--FrontEnd/src/app/http/timeline.ts544
-rw-r--r--FrontEnd/src/app/http/token.ts72
-rw-r--r--FrontEnd/src/app/http/user.ts134
4 files changed, 911 insertions, 0 deletions
diff --git a/FrontEnd/src/app/http/common.ts b/FrontEnd/src/app/http/common.ts
new file mode 100644
index 00000000..54203d1a
--- /dev/null
+++ b/FrontEnd/src/app/http/common.ts
@@ -0,0 +1,161 @@
+import { AxiosError, AxiosResponse } from "axios";
+
+export const apiBaseUrl = "/api";
+
+export function base64(blob: Blob): Promise<string> {
+ return new Promise<string>((resolve) => {
+ const reader = new FileReader();
+ reader.onload = function () {
+ resolve((reader.result as string).replace(/^data:.+;base64,/, ""));
+ };
+ reader.readAsDataURL(blob);
+ });
+}
+
+export function extractStatusCode(error: AxiosError): number | null {
+ if (error.isAxiosError) {
+ const code = error?.response?.status;
+ if (typeof code === "number") {
+ return code;
+ }
+ }
+ return null;
+}
+
+export interface CommonErrorResponse {
+ code: number;
+ message: string;
+}
+
+export function extractErrorCode(
+ error: AxiosError<CommonErrorResponse>
+): number | null {
+ if (error.isAxiosError) {
+ const code = error.response?.data?.code;
+ if (typeof code === "number") {
+ return code;
+ }
+ }
+ return null;
+}
+
+export class HttpNetworkError extends Error {
+ constructor(public innerError?: AxiosError) {
+ super();
+ }
+}
+
+export class HttpForbiddenError extends Error {
+ constructor(public innerError?: AxiosError) {
+ super();
+ }
+}
+
+export class NotModified {}
+
+export interface BlobWithEtag {
+ data: Blob;
+ etag: string;
+}
+
+export function extractResponseData<T>(res: AxiosResponse<T>): T {
+ return res.data;
+}
+
+export function catchIfStatusCodeIs<
+ TResult,
+ TErrorHandlerResult extends TResult | PromiseLike<TResult> | null | undefined
+>(
+ statusCode: number,
+ errorHandler: (error: AxiosError<CommonErrorResponse>) => TErrorHandlerResult
+): (error: AxiosError<CommonErrorResponse>) => TErrorHandlerResult {
+ return (error: AxiosError<CommonErrorResponse>) => {
+ if (extractStatusCode(error) == statusCode) {
+ return errorHandler(error);
+ } else {
+ throw error;
+ }
+ };
+}
+
+export function convertToIfStatusCodeIs<NewError>(
+ statusCode: number,
+ newErrorType: {
+ new (innerError: AxiosError): NewError;
+ }
+): (error: AxiosError<CommonErrorResponse>) => never {
+ return catchIfStatusCodeIs(statusCode, (error) => {
+ throw new newErrorType(error);
+ });
+}
+
+export function catchIfErrorCodeIs<
+ TResult,
+ TErrorHandlerResult extends TResult | PromiseLike<TResult> | null | undefined
+>(
+ errorCode: number,
+ errorHandler: (error: AxiosError<CommonErrorResponse>) => TErrorHandlerResult
+): (error: AxiosError<CommonErrorResponse>) => TErrorHandlerResult {
+ return (error: AxiosError<CommonErrorResponse>) => {
+ if (extractErrorCode(error) == errorCode) {
+ return errorHandler(error);
+ } else {
+ throw error;
+ }
+ };
+}
+export function convertToIfErrorCodeIs<NewError>(
+ errorCode: number,
+ newErrorType: {
+ new (innerError: AxiosError): NewError;
+ }
+): (error: AxiosError<CommonErrorResponse>) => never {
+ return catchIfErrorCodeIs(errorCode, (error) => {
+ throw new newErrorType(error);
+ });
+}
+
+export function convertToNetworkError(
+ error: AxiosError<CommonErrorResponse>
+): never {
+ if (error.isAxiosError && error.response == null) {
+ throw new HttpNetworkError(error);
+ } else {
+ throw error;
+ }
+}
+
+export function convertToForbiddenError(
+ error: AxiosError<CommonErrorResponse>
+): never {
+ if (
+ error.isAxiosError &&
+ error.response != null &&
+ (error.response.status == 401 || error.response.status == 403)
+ ) {
+ throw new HttpForbiddenError(error);
+ } else {
+ throw error;
+ }
+}
+
+export function convertToNotModified(
+ error: AxiosError<CommonErrorResponse>
+): NotModified {
+ if (
+ error.isAxiosError &&
+ error.response != null &&
+ error.response.status == 304
+ ) {
+ return new NotModified();
+ } else {
+ throw error;
+ }
+}
+
+export function convertToBlobWithEtag(res: AxiosResponse<Blob>): BlobWithEtag {
+ return {
+ data: res.data,
+ etag: (res.headers as Record<"etag", string>)["etag"],
+ };
+}
diff --git a/FrontEnd/src/app/http/timeline.ts b/FrontEnd/src/app/http/timeline.ts
new file mode 100644
index 00000000..eb7d5065
--- /dev/null
+++ b/FrontEnd/src/app/http/timeline.ts
@@ -0,0 +1,544 @@
+import axios, { AxiosError } from "axios";
+
+import { updateQueryString, applyQueryParameters } from "../utilities/url";
+
+import {
+ apiBaseUrl,
+ extractResponseData,
+ convertToNetworkError,
+ base64,
+ convertToIfStatusCodeIs,
+ convertToIfErrorCodeIs,
+ BlobWithEtag,
+ NotModified,
+ convertToNotModified,
+ convertToForbiddenError,
+ convertToBlobWithEtag,
+} from "./common";
+import { HttpUser } from "./user";
+
+export const kTimelineVisibilities = ["Public", "Register", "Private"] as const;
+
+export type TimelineVisibility = typeof kTimelineVisibilities[number];
+
+export interface HttpTimelineInfo {
+ uniqueId: string;
+ name: string;
+ description: string;
+ owner: HttpUser;
+ visibility: TimelineVisibility;
+ lastModified: Date;
+ members: HttpUser[];
+}
+
+export interface HttpTimelineListQuery {
+ visibility?: TimelineVisibility;
+ relate?: string;
+ relateType?: "own" | "join";
+}
+
+export interface HttpTimelinePostRequest {
+ name: string;
+}
+
+export interface HttpTimelinePostTextContent {
+ type: "text";
+ text: string;
+}
+
+export interface HttpTimelinePostImageContent {
+ type: "image";
+}
+
+export type HttpTimelinePostContent =
+ | HttpTimelinePostTextContent
+ | HttpTimelinePostImageContent;
+
+export interface HttpTimelinePostInfo {
+ id: number;
+ content: HttpTimelinePostContent;
+ time: Date;
+ lastUpdated: Date;
+ author: HttpUser;
+ deleted: false;
+}
+
+export interface HttpTimelineDeletedPostInfo {
+ id: number;
+ time: Date;
+ lastUpdated: Date;
+ author?: HttpUser;
+ deleted: true;
+}
+
+export type HttpTimelineGenericPostInfo =
+ | HttpTimelinePostInfo
+ | HttpTimelineDeletedPostInfo;
+
+export interface HttpTimelinePostPostRequestTextContent {
+ type: "text";
+ text: string;
+}
+
+export interface HttpTimelinePostPostRequestImageContent {
+ type: "image";
+ data: Blob;
+}
+
+export type HttpTimelinePostPostRequestContent =
+ | HttpTimelinePostPostRequestTextContent
+ | HttpTimelinePostPostRequestImageContent;
+
+export interface HttpTimelinePostPostRequest {
+ content: HttpTimelinePostPostRequestContent;
+ time?: Date;
+}
+
+export interface HttpTimelinePatchRequest {
+ visibility?: TimelineVisibility;
+ description?: string;
+}
+
+export class HttpTimelineNotExistError extends Error {
+ constructor(public innerError?: AxiosError) {
+ super();
+ }
+}
+
+export class HttpTimelinePostNotExistError extends Error {
+ constructor(public innerError?: AxiosError) {
+ super();
+ }
+}
+
+export class HttpTimelineNameConflictError extends Error {
+ constructor(public innerError?: AxiosError) {
+ super();
+ }
+}
+
+//-------------------- begin: internal model --------------------
+
+interface RawTimelineInfo {
+ uniqueId: string;
+ name: string;
+ description: string;
+ owner: HttpUser;
+ visibility: TimelineVisibility;
+ lastModified: string;
+ members: HttpUser[];
+}
+
+interface RawTimelinePostTextContent {
+ type: "text";
+ text: string;
+}
+
+interface RawTimelinePostImageContent {
+ type: "image";
+ url: string;
+}
+
+type RawTimelinePostContent =
+ | RawTimelinePostTextContent
+ | RawTimelinePostImageContent;
+
+interface RawTimelinePostInfo {
+ id: number;
+ content: RawTimelinePostContent;
+ time: string;
+ lastUpdated: string;
+ author: HttpUser;
+ deleted: false;
+}
+
+interface RawTimelineDeletedPostInfo {
+ id: number;
+ time: string;
+ lastUpdated: string;
+ author: HttpUser;
+ deleted: true;
+}
+
+type RawTimelineGenericPostInfo =
+ | RawTimelinePostInfo
+ | RawTimelineDeletedPostInfo;
+
+interface RawTimelinePostPostRequestTextContent {
+ type: "text";
+ text: string;
+}
+
+interface RawTimelinePostPostRequestImageContent {
+ type: "image";
+ data: string;
+}
+
+type RawTimelinePostPostRequestContent =
+ | RawTimelinePostPostRequestTextContent
+ | RawTimelinePostPostRequestImageContent;
+
+interface RawTimelinePostPostRequest {
+ content: RawTimelinePostPostRequestContent;
+ time?: string;
+}
+
+//-------------------- end: internal model --------------------
+
+function processRawTimelineInfo(raw: RawTimelineInfo): HttpTimelineInfo {
+ return {
+ ...raw,
+ lastModified: new Date(raw.lastModified),
+ };
+}
+
+function processRawTimelinePostInfo(
+ raw: RawTimelinePostInfo
+): HttpTimelinePostInfo;
+function processRawTimelinePostInfo(
+ raw: RawTimelineGenericPostInfo
+): HttpTimelineGenericPostInfo;
+function processRawTimelinePostInfo(
+ raw: RawTimelineGenericPostInfo
+): HttpTimelineGenericPostInfo {
+ return {
+ ...raw,
+ time: new Date(raw.time),
+ lastUpdated: new Date(raw.lastUpdated),
+ };
+}
+
+export interface IHttpTimelineClient {
+ listTimeline(query: HttpTimelineListQuery): Promise<HttpTimelineInfo[]>;
+ getTimeline(timelineName: string): Promise<HttpTimelineInfo>;
+ getTimeline(
+ timelineName: string,
+ query: {
+ checkUniqueId?: string;
+ }
+ ): Promise<HttpTimelineInfo>;
+ getTimeline(
+ timelineName: string,
+ query: {
+ checkUniqueId?: string;
+ ifModifiedSince: Date;
+ }
+ ): Promise<HttpTimelineInfo | NotModified>;
+ postTimeline(
+ req: HttpTimelinePostRequest,
+ token: string
+ ): Promise<HttpTimelineInfo>;
+ patchTimeline(
+ timelineName: string,
+ req: HttpTimelinePatchRequest,
+ token: string
+ ): Promise<HttpTimelineInfo>;
+ deleteTimeline(timelineName: string, token: string): Promise<void>;
+ memberPut(
+ timelineName: string,
+ username: string,
+ token: string
+ ): Promise<void>;
+ memberDelete(
+ timelineName: string,
+ username: string,
+ token: string
+ ): Promise<void>;
+ listPost(
+ timelineName: string,
+ token?: string
+ ): Promise<HttpTimelinePostInfo[]>;
+ listPost(
+ timelineName: string,
+ token: string | undefined,
+ query: {
+ modifiedSince?: Date;
+ includeDeleted?: false;
+ }
+ ): Promise<HttpTimelinePostInfo[]>;
+ listPost(
+ timelineName: string,
+ token: string | undefined,
+ query: {
+ modifiedSince?: Date;
+ includeDeleted: true;
+ }
+ ): Promise<HttpTimelineGenericPostInfo[]>;
+ getPostData(
+ timelineName: string,
+ postId: number,
+ token?: string
+ ): Promise<BlobWithEtag>;
+ getPostData(
+ timelineName: string,
+ postId: number,
+ token: string | undefined,
+ etag: string
+ ): Promise<BlobWithEtag | NotModified>;
+ postPost(
+ timelineName: string,
+ req: HttpTimelinePostPostRequest,
+ token: string
+ ): Promise<HttpTimelinePostInfo>;
+ deletePost(
+ timelineName: string,
+ postId: number,
+ token: string
+ ): Promise<void>;
+}
+
+export class HttpTimelineClient implements IHttpTimelineClient {
+ listTimeline(query: HttpTimelineListQuery): Promise<HttpTimelineInfo[]> {
+ return axios
+ .get<RawTimelineInfo[]>(
+ applyQueryParameters(`${apiBaseUrl}/timelines`, query)
+ )
+ .then(extractResponseData)
+ .then((list) => list.map(processRawTimelineInfo))
+ .catch(convertToNetworkError);
+ }
+
+ getTimeline(timelineName: string): Promise<HttpTimelineInfo>;
+ getTimeline(
+ timelineName: string,
+ query: {
+ checkUniqueId?: string;
+ }
+ ): Promise<HttpTimelineInfo>;
+ getTimeline(
+ timelineName: string,
+ query: {
+ checkUniqueId?: string;
+ ifModifiedSince: Date;
+ }
+ ): Promise<HttpTimelineInfo | NotModified>;
+ getTimeline(
+ timelineName: string,
+ query?: {
+ checkUniqueId?: string;
+ ifModifiedSince?: Date;
+ }
+ ): Promise<HttpTimelineInfo | NotModified> {
+ return axios
+ .get<RawTimelineInfo>(
+ applyQueryParameters(`${apiBaseUrl}/timelines/${timelineName}`, query)
+ )
+ .then((res) => {
+ if (res.status === 304) {
+ return new NotModified();
+ } else {
+ return processRawTimelineInfo(res.data);
+ }
+ })
+ .catch(convertToIfStatusCodeIs(404, HttpTimelineNotExistError))
+ .catch(convertToNetworkError);
+ }
+
+ postTimeline(
+ req: HttpTimelinePostRequest,
+ token: string
+ ): Promise<HttpTimelineInfo> {
+ return axios
+ .post<RawTimelineInfo>(`${apiBaseUrl}/timelines?token=${token}`, req)
+ .then(extractResponseData)
+ .then(processRawTimelineInfo)
+ .catch(convertToIfErrorCodeIs(11040101, HttpTimelineNameConflictError))
+ .catch(convertToNetworkError);
+ }
+
+ patchTimeline(
+ timelineName: string,
+ req: HttpTimelinePatchRequest,
+ token: string
+ ): Promise<HttpTimelineInfo> {
+ return axios
+ .patch<RawTimelineInfo>(
+ `${apiBaseUrl}/timelines/${timelineName}?token=${token}`,
+ req
+ )
+ .then(extractResponseData)
+ .then(processRawTimelineInfo)
+ .catch(convertToNetworkError);
+ }
+
+ deleteTimeline(timelineName: string, token: string): Promise<void> {
+ return axios
+ .delete(`${apiBaseUrl}/timelines/${timelineName}?token=${token}`)
+ .catch(convertToNetworkError)
+ .then();
+ }
+
+ memberPut(
+ timelineName: string,
+ username: string,
+ token: string
+ ): Promise<void> {
+ return axios
+ .put(
+ `${apiBaseUrl}/timelines/${timelineName}/members/${username}?token=${token}`
+ )
+ .catch(convertToNetworkError)
+ .then();
+ }
+
+ memberDelete(
+ timelineName: string,
+ username: string,
+ token: string
+ ): Promise<void> {
+ return axios
+ .delete(
+ `${apiBaseUrl}/timelines/${timelineName}/members/${username}?token=${token}`
+ )
+ .catch(convertToNetworkError)
+ .then();
+ }
+
+ listPost(
+ timelineName: string,
+ token?: string
+ ): Promise<HttpTimelinePostInfo[]>;
+ listPost(
+ timelineName: string,
+ token: string | undefined,
+ query: {
+ modifiedSince?: Date;
+ includeDeleted?: false;
+ }
+ ): Promise<HttpTimelinePostInfo[]>;
+ listPost(
+ timelineName: string,
+ token: string | undefined,
+ query: {
+ modifiedSince?: Date;
+ includeDeleted: true;
+ }
+ ): Promise<HttpTimelineGenericPostInfo[]>;
+ listPost(
+ timelineName: string,
+ token?: string,
+ query?: {
+ modifiedSince?: Date;
+ includeDeleted?: boolean;
+ }
+ ): Promise<HttpTimelineGenericPostInfo[]> {
+ let url = `${apiBaseUrl}/timelines/${timelineName}/posts`;
+ url = updateQueryString("token", token, url);
+ if (query != null) {
+ if (query.modifiedSince != null) {
+ url = updateQueryString(
+ "modifiedSince",
+ query.modifiedSince.toISOString(),
+ url
+ );
+ }
+ if (query.includeDeleted != null) {
+ url = updateQueryString(
+ "includeDeleted",
+ query.includeDeleted ? "true" : "false",
+ url
+ );
+ }
+ }
+
+ return axios
+ .get<RawTimelineGenericPostInfo[]>(url)
+ .then(extractResponseData)
+ .catch(convertToIfStatusCodeIs(404, HttpTimelineNotExistError))
+ .catch(convertToForbiddenError)
+ .catch(convertToNetworkError)
+ .then((rawPosts) =>
+ rawPosts.map((raw) => processRawTimelinePostInfo(raw))
+ );
+ }
+
+ getPostData(
+ timelineName: string,
+ postId: number,
+ token: string
+ ): Promise<BlobWithEtag>;
+ getPostData(
+ timelineName: string,
+ postId: number,
+ token?: string,
+ etag?: string
+ ): Promise<BlobWithEtag | NotModified> {
+ const headers =
+ etag != null
+ ? {
+ "If-None-Match": etag,
+ }
+ : undefined;
+
+ let url = `${apiBaseUrl}/timelines/${timelineName}/posts/${postId}/data`;
+ url = updateQueryString("token", token, url);
+
+ return axios
+ .get(url, {
+ responseType: "blob",
+ headers,
+ })
+ .then(convertToBlobWithEtag)
+ .catch(convertToNotModified)
+ .catch(convertToIfStatusCodeIs(404, HttpTimelinePostNotExistError))
+ .catch(convertToNetworkError);
+ }
+
+ async postPost(
+ timelineName: string,
+ req: HttpTimelinePostPostRequest,
+ token: string
+ ): Promise<HttpTimelinePostInfo> {
+ let content: RawTimelinePostPostRequestContent;
+ if (req.content.type === "image") {
+ const base64Data = await base64(req.content.data);
+ content = {
+ ...req.content,
+ data: base64Data,
+ } as RawTimelinePostPostRequestImageContent;
+ } else {
+ content = req.content;
+ }
+ const rawReq: RawTimelinePostPostRequest = {
+ content,
+ };
+ if (req.time != null) {
+ rawReq.time = req.time.toISOString();
+ }
+ return await axios
+ .post<RawTimelinePostInfo>(
+ `${apiBaseUrl}/timelines/${timelineName}/posts?token=${token}`,
+ rawReq
+ )
+ .then(extractResponseData)
+ .catch(convertToNetworkError)
+ .then((rawPost) => processRawTimelinePostInfo(rawPost));
+ }
+
+ deletePost(
+ timelineName: string,
+ postId: number,
+ token: string
+ ): Promise<void> {
+ return axios
+ .delete(
+ `${apiBaseUrl}/timelines/${timelineName}/posts/${postId}?token=${token}`
+ )
+ .catch(convertToNetworkError)
+ .then();
+ }
+}
+
+let client: IHttpTimelineClient = new HttpTimelineClient();
+
+export function getHttpTimelineClient(): IHttpTimelineClient {
+ return client;
+}
+
+export function setHttpTimelineClient(
+ newClient: IHttpTimelineClient
+): IHttpTimelineClient {
+ const old = client;
+ client = newClient;
+ return old;
+}
diff --git a/FrontEnd/src/app/http/token.ts b/FrontEnd/src/app/http/token.ts
new file mode 100644
index 00000000..ae0cf3f6
--- /dev/null
+++ b/FrontEnd/src/app/http/token.ts
@@ -0,0 +1,72 @@
+import axios, { AxiosError } from "axios";
+
+import {
+ apiBaseUrl,
+ convertToNetworkError,
+ convertToIfErrorCodeIs,
+ extractResponseData,
+} from "./common";
+import { HttpUser } from "./user";
+
+export interface HttpCreateTokenRequest {
+ username: string;
+ password: string;
+ expire: number;
+}
+
+export interface HttpCreateTokenResponse {
+ token: string;
+ user: HttpUser;
+}
+
+export interface HttpVerifyTokenRequest {
+ token: string;
+}
+
+export interface HttpVerifyTokenResponse {
+ user: HttpUser;
+}
+
+export class HttpCreateTokenBadCredentialError extends Error {
+ constructor(public innerError?: AxiosError) {
+ super();
+ }
+}
+
+export interface IHttpTokenClient {
+ create(req: HttpCreateTokenRequest): Promise<HttpCreateTokenResponse>;
+ verify(req: HttpVerifyTokenRequest): Promise<HttpVerifyTokenResponse>;
+}
+
+export class HttpTokenClient implements IHttpTokenClient {
+ create(req: HttpCreateTokenRequest): Promise<HttpCreateTokenResponse> {
+ return axios
+ .post<HttpCreateTokenResponse>(`${apiBaseUrl}/token/create`, req)
+ .then(extractResponseData)
+ .catch(
+ convertToIfErrorCodeIs(11010101, HttpCreateTokenBadCredentialError)
+ )
+ .catch(convertToNetworkError);
+ }
+
+ verify(req: HttpVerifyTokenRequest): Promise<HttpVerifyTokenResponse> {
+ return axios
+ .post<HttpVerifyTokenResponse>(`${apiBaseUrl}/token/verify`, req)
+ .then(extractResponseData)
+ .catch(convertToNetworkError);
+ }
+}
+
+let client: IHttpTokenClient = new HttpTokenClient();
+
+export function getHttpTokenClient(): IHttpTokenClient {
+ return client;
+}
+
+export function setHttpTokenClient(
+ newClient: IHttpTokenClient
+): IHttpTokenClient {
+ const old = client;
+ client = newClient;
+ return old;
+}
diff --git a/FrontEnd/src/app/http/user.ts b/FrontEnd/src/app/http/user.ts
new file mode 100644
index 00000000..a0a02cce
--- /dev/null
+++ b/FrontEnd/src/app/http/user.ts
@@ -0,0 +1,134 @@
+import axios, { AxiosError } from "axios";
+
+import {
+ apiBaseUrl,
+ convertToNetworkError,
+ extractResponseData,
+ convertToIfStatusCodeIs,
+ convertToIfErrorCodeIs,
+ NotModified,
+ BlobWithEtag,
+ convertToBlobWithEtag,
+ convertToNotModified,
+} from "./common";
+
+export interface HttpUser {
+ uniqueId: string;
+ username: string;
+ administrator: boolean;
+ nickname: string;
+}
+
+export interface HttpUserPatchRequest {
+ nickname?: string;
+}
+
+export interface HttpChangePasswordRequest {
+ oldPassword: string;
+ newPassword: string;
+}
+
+export class HttpUserNotExistError extends Error {
+ constructor(public innerError?: AxiosError) {
+ super();
+ }
+}
+
+export class HttpChangePasswordBadCredentialError extends Error {
+ constructor(public innerError?: AxiosError) {
+ super();
+ }
+}
+
+export interface IHttpUserClient {
+ get(username: string): Promise<HttpUser>;
+ patch(
+ username: string,
+ req: HttpUserPatchRequest,
+ token: string
+ ): Promise<HttpUser>;
+ getAvatar(username: string): Promise<BlobWithEtag>;
+ getAvatar(
+ username: string,
+ etag: string
+ ): Promise<BlobWithEtag | NotModified>;
+ putAvatar(username: string, data: Blob, token: string): Promise<void>;
+ changePassword(req: HttpChangePasswordRequest, token: string): Promise<void>;
+}
+
+export class HttpUserClient implements IHttpUserClient {
+ get(username: string): Promise<HttpUser> {
+ return axios
+ .get<HttpUser>(`${apiBaseUrl}/users/${username}`)
+ .then(extractResponseData)
+ .catch(convertToIfStatusCodeIs(404, HttpUserNotExistError))
+ .catch(convertToNetworkError);
+ }
+
+ patch(
+ username: string,
+ req: HttpUserPatchRequest,
+ token: string
+ ): Promise<HttpUser> {
+ return axios
+ .patch<HttpUser>(`${apiBaseUrl}/users/${username}?token=${token}`, req)
+ .then(extractResponseData)
+ .catch(convertToNetworkError);
+ }
+
+ getAvatar(username: string): Promise<BlobWithEtag>;
+ getAvatar(
+ username: string,
+ etag?: string
+ ): Promise<BlobWithEtag | NotModified> {
+ const headers =
+ etag != null
+ ? {
+ "If-None-Match": etag,
+ }
+ : undefined;
+
+ return axios
+ .get(`${apiBaseUrl}/users/${username}/avatar`, {
+ responseType: "blob",
+ headers,
+ })
+ .then(convertToBlobWithEtag)
+ .catch(convertToNotModified)
+ .catch(convertToIfStatusCodeIs(404, HttpUserNotExistError))
+ .catch(convertToNetworkError);
+ }
+
+ putAvatar(username: string, data: Blob, token: string): Promise<void> {
+ return axios
+ .put(`${apiBaseUrl}/users/${username}/avatar?token=${token}`, data, {
+ headers: {
+ "Content-Type": data.type,
+ },
+ })
+ .catch(convertToNetworkError)
+ .then();
+ }
+
+ changePassword(req: HttpChangePasswordRequest, token: string): Promise<void> {
+ return axios
+ .post(`${apiBaseUrl}/userop/changepassword?token=${token}`, req)
+ .catch(
+ convertToIfErrorCodeIs(11020201, HttpChangePasswordBadCredentialError)
+ )
+ .catch(convertToNetworkError)
+ .then();
+ }
+}
+
+let client: IHttpUserClient = new HttpUserClient();
+
+export function getHttpUserClient(): IHttpUserClient {
+ return client;
+}
+
+export function setHttpUserClient(newClient: IHttpUserClient): IHttpUserClient {
+ const old = client;
+ client = newClient;
+ return old;
+}