diff options
author | crupest <crupest@outlook.com> | 2021-06-15 14:14:28 +0800 |
---|---|---|
committer | crupest <crupest@outlook.com> | 2021-06-15 14:14:28 +0800 |
commit | 47587812b809fee2a95c76266d9d0e42fc4ac1ca (patch) | |
tree | bfaa7320c838e21edf88b5a037263f89a8012222 /FrontEnd/src/http | |
parent | da26373c7fc13cded47428b27638b349b0432437 (diff) | |
download | timeline-47587812b809fee2a95c76266d9d0e42fc4ac1ca.tar.gz timeline-47587812b809fee2a95c76266d9d0e42fc4ac1ca.tar.bz2 timeline-47587812b809fee2a95c76266d9d0e42fc4ac1ca.zip |
...
Diffstat (limited to 'FrontEnd/src/http')
-rw-r--r-- | FrontEnd/src/http/bookmark.ts | 49 | ||||
-rw-r--r-- | FrontEnd/src/http/common.ts | 214 | ||||
-rw-r--r-- | FrontEnd/src/http/highlight.ts | 49 | ||||
-rw-r--r-- | FrontEnd/src/http/search.ts | 36 | ||||
-rw-r--r-- | FrontEnd/src/http/timeline.ts | 234 | ||||
-rw-r--r-- | FrontEnd/src/http/token.ts | 71 | ||||
-rw-r--r-- | FrontEnd/src/http/user.ts | 161 |
7 files changed, 814 insertions, 0 deletions
diff --git a/FrontEnd/src/http/bookmark.ts b/FrontEnd/src/http/bookmark.ts new file mode 100644 index 00000000..3e5be229 --- /dev/null +++ b/FrontEnd/src/http/bookmark.ts @@ -0,0 +1,49 @@ +import { axios, apiBaseUrl, extractResponseData } from "./common"; + +import { HttpTimelineInfo } from "./timeline"; + +export interface HttpHighlightMoveRequest { + timeline: string; + newPosition: number; +} + +export interface IHttpBookmarkClient { + list(): Promise<HttpTimelineInfo[]>; + put(timeline: string): Promise<void>; + delete(timeline: string): Promise<void>; + move(req: HttpHighlightMoveRequest): Promise<void>; +} + +export class HttpHighlightClient implements IHttpBookmarkClient { + list(): Promise<HttpTimelineInfo[]> { + return axios + .get<HttpTimelineInfo[]>(`${apiBaseUrl}/bookmarks`) + .then(extractResponseData); + } + + put(timeline: string): Promise<void> { + return axios.put(`${apiBaseUrl}/bookmarks/${timeline}`).then(); + } + + delete(timeline: string): Promise<void> { + return axios.delete(`${apiBaseUrl}/bookmarks/${timeline}`).then(); + } + + move(req: HttpHighlightMoveRequest): Promise<void> { + return axios.post(`${apiBaseUrl}/bookmarkop/move`, req).then(); + } +} + +let client: IHttpBookmarkClient = new HttpHighlightClient(); + +export function getHttpBookmarkClient(): IHttpBookmarkClient { + return client; +} + +export function setHttpBookmarkClient( + newClient: IHttpBookmarkClient +): IHttpBookmarkClient { + const old = client; + client = newClient; + return old; +} diff --git a/FrontEnd/src/http/common.ts b/FrontEnd/src/http/common.ts new file mode 100644 index 00000000..e1672985 --- /dev/null +++ b/FrontEnd/src/http/common.ts @@ -0,0 +1,214 @@ +import rawAxios, { AxiosError, AxiosResponse } from "axios"; +import { Base64 } from "js-base64"; +import { BehaviorSubject, Observable } from "rxjs"; + +export const apiBaseUrl = "/api"; + +export const axios = rawAxios.create(); + +function convertToNetworkError(error: AxiosError): never { + if (error.isAxiosError && error.response == null) { + throw new HttpNetworkError(error); + } else { + throw error; + } +} + +function convertToForbiddenError(error: AxiosError): never { + if ( + error.isAxiosError && + error.response != null && + (error.response.status == 401 || error.response.status == 403) + ) { + throw new HttpForbiddenError(error); + } else { + throw error; + } +} + +function convertToNotFoundError(error: AxiosError): never { + if ( + error.isAxiosError && + error.response != null && + error.response.status == 404 + ) { + throw new HttpNotFoundError(error); + } else { + throw error; + } +} + +rawAxios.interceptors.response.use(undefined, convertToNetworkError); +rawAxios.interceptors.response.use(undefined, convertToForbiddenError); +rawAxios.interceptors.response.use(undefined, convertToNotFoundError); +axios.interceptors.response.use(undefined, convertToNetworkError); +axios.interceptors.response.use(undefined, convertToForbiddenError); +axios.interceptors.response.use(undefined, convertToNotFoundError); + +const tokenSubject = new BehaviorSubject<string | null>(null); + +export function getHttpToken(): string | null { + return tokenSubject.value; +} + +export function setHttpToken(token: string | null): void { + tokenSubject.next(token); + + if (token == null) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + delete axios.defaults.headers.common["Authorization"]; + } else { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + axios.defaults.headers.common["Authorization"] = `Bearer ${token}`; + } +} + +export const token$: Observable<string | null> = tokenSubject.asObservable(); + +export function base64(blob: Blob | string): Promise<string> { + if (typeof blob === "string") { + return Promise.resolve(Base64.encode(blob)); + } + + 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 HttpNotFoundError 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 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"], + }; +} + +export function extractEtag(res: AxiosResponse): string { + return (res.headers as Record<"etag", string>)["etag"]; +} diff --git a/FrontEnd/src/http/highlight.ts b/FrontEnd/src/http/highlight.ts new file mode 100644 index 00000000..fddf0729 --- /dev/null +++ b/FrontEnd/src/http/highlight.ts @@ -0,0 +1,49 @@ +import { axios, apiBaseUrl, extractResponseData } from "./common"; + +import { HttpTimelineInfo } from "./timeline"; + +export interface HttpHighlightMoveRequest { + timeline: string; + newPosition: number; +} + +export interface IHttpHighlightClient { + list(): Promise<HttpTimelineInfo[]>; + put(timeline: string): Promise<void>; + delete(timeline: string): Promise<void>; + move(req: HttpHighlightMoveRequest): Promise<void>; +} + +export class HttpHighlightClient implements IHttpHighlightClient { + list(): Promise<HttpTimelineInfo[]> { + return axios + .get<HttpTimelineInfo[]>(`${apiBaseUrl}/highlights`) + .then(extractResponseData); + } + + put(timeline: string): Promise<void> { + return axios.put(`${apiBaseUrl}/highlights/${timeline}`).then(); + } + + delete(timeline: string): Promise<void> { + return axios.delete(`${apiBaseUrl}/highlights/${timeline}`).then(); + } + + move(req: HttpHighlightMoveRequest): Promise<void> { + return axios.post(`${apiBaseUrl}/highlightop/move`, req).then(); + } +} + +let client: IHttpHighlightClient = new HttpHighlightClient(); + +export function getHttpHighlightClient(): IHttpHighlightClient { + return client; +} + +export function setHttpHighlightClient( + newClient: IHttpHighlightClient +): IHttpHighlightClient { + const old = client; + client = newClient; + return old; +} diff --git a/FrontEnd/src/http/search.ts b/FrontEnd/src/http/search.ts new file mode 100644 index 00000000..8ca48fe9 --- /dev/null +++ b/FrontEnd/src/http/search.ts @@ -0,0 +1,36 @@ +import { apiBaseUrl, axios, extractResponseData } from "./common"; +import { HttpTimelineInfo } from "./timeline"; +import { HttpUser } from "./user"; + +export interface IHttpSearchClient { + searchTimelines(query: string): Promise<HttpTimelineInfo[]>; + searchUsers(query: string): Promise<HttpUser[]>; +} + +export class HttpSearchClient implements IHttpSearchClient { + searchTimelines(query: string): Promise<HttpTimelineInfo[]> { + return axios + .get<HttpTimelineInfo[]>(`${apiBaseUrl}/search/timelines?q=${query}`) + .then(extractResponseData); + } + + searchUsers(query: string): Promise<HttpUser[]> { + return axios + .get<HttpUser[]>(`${apiBaseUrl}/search/users?q=${query}`) + .then(extractResponseData); + } +} + +let client: IHttpSearchClient = new HttpSearchClient(); + +export function getHttpSearchClient(): IHttpSearchClient { + return client; +} + +export function setHttpSearchClient( + newClient: IHttpSearchClient +): IHttpSearchClient { + const old = client; + client = newClient; + return old; +} diff --git a/FrontEnd/src/http/timeline.ts b/FrontEnd/src/http/timeline.ts new file mode 100644 index 00000000..9697c1a0 --- /dev/null +++ b/FrontEnd/src/http/timeline.ts @@ -0,0 +1,234 @@ +import { AxiosError } from "axios"; + +import { applyQueryParameters } from "../utilities/url"; + +import { + axios, + apiBaseUrl, + extractResponseData, + convertToIfErrorCodeIs, + getHttpToken, +} 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; + title: string; + name: string; + description: string; + owner: HttpUser; + visibility: TimelineVisibility; + color: string; + lastModified: string; + members: HttpUser[]; + isHighlight: boolean; + isBookmark: boolean; + manageable: boolean; + postable: boolean; +} + +export interface HttpTimelineListQuery { + visibility?: TimelineVisibility; + relate?: string; + relateType?: "own" | "join"; +} + +export interface HttpTimelinePostRequest { + name: string; +} + +export interface HttpTimelinePostDataDigest { + kind: string; + eTag: string; + lastUpdated: string; +} + +export interface HttpTimelinePostInfo { + id: number; + time: string; + author: HttpUser; + dataList: HttpTimelinePostDataDigest[]; + color: string; + lastUpdated: string; + timelineName: string; + editable: boolean; +} + +export interface HttpTimelinePostPostRequestData { + contentType: string; + data: string; +} + +export interface HttpTimelinePostPostRequest { + time?: string; + color?: string; + dataList: HttpTimelinePostPostRequestData[]; +} + +export interface HttpTimelinePatchRequest { + name?: string; + title?: string; + color?: string; + visibility?: TimelineVisibility; + description?: string; +} + +export interface HttpTimelinePostPatchRequest { + time?: string; + color?: string; +} + +export class HttpTimelineNameConflictError extends Error { + constructor(public innerError?: AxiosError) { + super(); + } +} + +export interface IHttpTimelineClient { + listTimeline(query: HttpTimelineListQuery): Promise<HttpTimelineInfo[]>; + getTimeline(timelineName: string): Promise<HttpTimelineInfo>; + postTimeline(req: HttpTimelinePostRequest): Promise<HttpTimelineInfo>; + patchTimeline( + timelineName: string, + req: HttpTimelinePatchRequest + ): Promise<HttpTimelineInfo>; + deleteTimeline(timelineName: string): Promise<void>; + memberPut(timelineName: string, username: string): Promise<void>; + memberDelete(timelineName: string, username: string): Promise<void>; + listPost(timelineName: string): Promise<HttpTimelinePostInfo[]>; + generatePostDataUrl(timelineName: string, postId: number): string; + getPostDataAsString(timelineName: string, postId: number): Promise<string>; + postPost( + timelineName: string, + req: HttpTimelinePostPostRequest + ): Promise<HttpTimelinePostInfo>; + patchPost( + timelineName: string, + postId: number, + req: HttpTimelinePostPatchRequest + ): Promise<HttpTimelinePostInfo>; + deletePost(timelineName: string, postId: number): Promise<void>; +} + +export class HttpTimelineClient implements IHttpTimelineClient { + listTimeline(query: HttpTimelineListQuery): Promise<HttpTimelineInfo[]> { + return axios + .get<HttpTimelineInfo[]>( + applyQueryParameters(`${apiBaseUrl}/timelines`, query) + ) + .then(extractResponseData); + } + + getTimeline(timelineName: string): Promise<HttpTimelineInfo> { + return axios + .get<HttpTimelineInfo>(`${apiBaseUrl}/timelines/${timelineName}`) + .then(extractResponseData); + } + + postTimeline(req: HttpTimelinePostRequest): Promise<HttpTimelineInfo> { + return axios + .post<HttpTimelineInfo>(`${apiBaseUrl}/timelines`, req) + .then(extractResponseData) + .catch(convertToIfErrorCodeIs(11040101, HttpTimelineNameConflictError)); + } + + patchTimeline( + timelineName: string, + req: HttpTimelinePatchRequest + ): Promise<HttpTimelineInfo> { + return axios + .patch<HttpTimelineInfo>(`${apiBaseUrl}/timelines/${timelineName}`, req) + .then(extractResponseData); + } + + deleteTimeline(timelineName: string): Promise<void> { + return axios.delete(`${apiBaseUrl}/timelines/${timelineName}`).then(); + } + + memberPut(timelineName: string, username: string): Promise<void> { + return axios + .put(`${apiBaseUrl}/timelines/${timelineName}/members/${username}`) + .then(); + } + + memberDelete(timelineName: string, username: string): Promise<void> { + return axios + .delete(`${apiBaseUrl}/timelines/${timelineName}/members/${username}`) + .then(); + } + + listPost(timelineName: string): Promise<HttpTimelinePostInfo[]> { + return axios + .get<HttpTimelinePostInfo[]>( + `${apiBaseUrl}/timelines/${timelineName}/posts` + ) + .then(extractResponseData); + } + + generatePostDataUrl(timelineName: string, postId: number): string { + return applyQueryParameters( + `${apiBaseUrl}/timelines/${timelineName}/posts/${postId}/data`, + { token: getHttpToken() } + ); + } + + getPostDataAsString(timelineName: string, postId: number): Promise<string> { + return axios + .get<string>( + `${apiBaseUrl}/timelines/${timelineName}/posts/${postId}/data`, + { + responseType: "text", + } + ) + .then(extractResponseData); + } + + postPost( + timelineName: string, + req: HttpTimelinePostPostRequest + ): Promise<HttpTimelinePostInfo> { + return axios + .post<HttpTimelinePostInfo>( + `${apiBaseUrl}/timelines/${timelineName}/posts`, + req + ) + .then(extractResponseData); + } + + patchPost( + timelineName: string, + postId: number, + req: HttpTimelinePostPatchRequest + ): Promise<HttpTimelinePostInfo> { + return axios + .patch<HttpTimelinePostInfo>( + `${apiBaseUrl}/timelines/${timelineName}/posts/${postId}`, + req + ) + .then(extractResponseData); + } + + deletePost(timelineName: string, postId: number): Promise<void> { + return axios + .delete(`${apiBaseUrl}/timelines/${timelineName}/posts/${postId}`) + .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/http/token.ts b/FrontEnd/src/http/token.ts new file mode 100644 index 00000000..f8b09d63 --- /dev/null +++ b/FrontEnd/src/http/token.ts @@ -0,0 +1,71 @@ +// Don't use axios in common because it will contains +// authorization header, which shouldn't be used in token apis. +import axios, { AxiosError } from "axios"; + +import { + apiBaseUrl, + 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) + ); + } + + verify(req: HttpVerifyTokenRequest): Promise<HttpVerifyTokenResponse> { + return axios + .post<HttpVerifyTokenResponse>(`${apiBaseUrl}/token/verify`, req) + .then(extractResponseData); + } +} + +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/http/user.ts b/FrontEnd/src/http/user.ts new file mode 100644 index 00000000..dcf24cba --- /dev/null +++ b/FrontEnd/src/http/user.ts @@ -0,0 +1,161 @@ +import { AxiosError } from "axios"; + +import { + axios, + apiBaseUrl, + extractResponseData, + convertToIfStatusCodeIs, + convertToIfErrorCodeIs, + extractEtag, +} from "./common"; + +export const kUserManagement = "UserManagement"; +export const kAllTimelineManagement = "AllTimelineManagement"; +export const kHighlightTimelineManagement = "HighlightTimelineManagement"; + +export const kUserPermissionList = [ + kUserManagement, + kAllTimelineManagement, + kHighlightTimelineManagement, +] as const; + +export type UserPermission = typeof kUserPermissionList[number]; + +export interface HttpUser { + uniqueId: string; + username: string; + permissions: UserPermission[]; + nickname: string; +} + +export interface HttpUserPatchRequest { + username?: string; + password?: string; + nickname?: string; +} + +export interface HttpChangePasswordRequest { + oldPassword: string; + newPassword: string; +} + +export interface HttpCreateUserRequest { + username: string; + password: string; +} + +export class HttpUserNotExistError extends Error { + constructor(public innerError?: AxiosError) { + super(); + } +} + +export class HttpChangePasswordBadCredentialError extends Error { + constructor(public innerError?: AxiosError) { + super(); + } +} + +export interface IHttpUserClient { + list(): Promise<HttpUser[]>; + get(username: string): Promise<HttpUser>; + post(req: HttpCreateUserRequest): Promise<HttpUser>; + patch(username: string, req: HttpUserPatchRequest): Promise<HttpUser>; + delete(username: string): Promise<void>; + generateAvatarUrl(username: string): string; + putAvatar(username: string, data: Blob): Promise<string>; + changePassword(req: HttpChangePasswordRequest): Promise<void>; + putUserPermission( + username: string, + permission: UserPermission + ): Promise<void>; + deleteUserPermission( + username: string, + permission: UserPermission + ): Promise<void>; +} + +export class HttpUserClient implements IHttpUserClient { + list(): Promise<HttpUser[]> { + return axios + .get<HttpUser[]>(`${apiBaseUrl}/users`) + .then(extractResponseData); + } + + get(username: string): Promise<HttpUser> { + return axios + .get<HttpUser>(`${apiBaseUrl}/users/${username}`) + .then(extractResponseData) + .catch(convertToIfStatusCodeIs(404, HttpUserNotExistError)); + } + + post(req: HttpCreateUserRequest): Promise<HttpUser> { + return axios + .post<HttpUser>(`${apiBaseUrl}/users`, req) + .then(extractResponseData) + .then(); + } + + patch(username: string, req: HttpUserPatchRequest): Promise<HttpUser> { + return axios + .patch<HttpUser>(`${apiBaseUrl}/users/${username}`, req) + .then(extractResponseData); + } + + delete(username: string): Promise<void> { + return axios.delete(`${apiBaseUrl}/users/${username}`).then(); + } + + generateAvatarUrl(username: string): string { + return `${apiBaseUrl}/users/${username}/avatar`; + } + + putAvatar(username: string, data: Blob): Promise<string> { + return axios + .put(`${apiBaseUrl}/users/${username}/avatar`, data, { + headers: { + "Content-Type": data.type, + }, + }) + .then(extractEtag); + } + + changePassword(req: HttpChangePasswordRequest): Promise<void> { + return axios + .post(`${apiBaseUrl}/userop/changepassword`, req) + .catch( + convertToIfErrorCodeIs(11020201, HttpChangePasswordBadCredentialError) + ) + .then(); + } + + putUserPermission( + username: string, + permission: UserPermission + ): Promise<void> { + return axios + .put(`${apiBaseUrl}/users/${username}/permissions/${permission}`) + .then(); + } + + deleteUserPermission( + username: string, + permission: UserPermission + ): Promise<void> { + return axios + .delete(`${apiBaseUrl}/users/${username}/permissions/${permission}`) + .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; +} |