From de1d582bf2ed7062fd400459f30d463d47ef9982 Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 24 Aug 2020 22:59:45 +0800 Subject: ... --- Timeline/ClientApp/src/app/http/common.ts | 322 +++--- Timeline/ClientApp/src/app/http/mock/common.ts | 156 +-- Timeline/ClientApp/src/app/http/mock/install.ts | 22 +- Timeline/ClientApp/src/app/http/mock/timeline.ts | 1316 +++++++++++----------- Timeline/ClientApp/src/app/http/mock/token.ts | 106 +- Timeline/ClientApp/src/app/http/mock/user.ts | 280 ++--- Timeline/ClientApp/src/app/http/timeline.ts | 1086 +++++++++--------- Timeline/ClientApp/src/app/http/token.ts | 144 +-- Timeline/ClientApp/src/app/http/user.ts | 268 ++--- 9 files changed, 1850 insertions(+), 1850 deletions(-) (limited to 'Timeline/ClientApp/src/app/http') diff --git a/Timeline/ClientApp/src/app/http/common.ts b/Timeline/ClientApp/src/app/http/common.ts index 1f904106..54203d1a 100644 --- a/Timeline/ClientApp/src/app/http/common.ts +++ b/Timeline/ClientApp/src/app/http/common.ts @@ -1,161 +1,161 @@ -import { AxiosError, AxiosResponse } from 'axios'; - -export const apiBaseUrl = '/api'; - -export function base64(blob: Blob): Promise { - return new Promise((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 -): 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(res: AxiosResponse): T { - return res.data; -} - -export function catchIfStatusCodeIs< - TResult, - TErrorHandlerResult extends TResult | PromiseLike | null | undefined ->( - statusCode: number, - errorHandler: (error: AxiosError) => TErrorHandlerResult -): (error: AxiosError) => TErrorHandlerResult { - return (error: AxiosError) => { - if (extractStatusCode(error) == statusCode) { - return errorHandler(error); - } else { - throw error; - } - }; -} - -export function convertToIfStatusCodeIs( - statusCode: number, - newErrorType: { - new (innerError: AxiosError): NewError; - } -): (error: AxiosError) => never { - return catchIfStatusCodeIs(statusCode, (error) => { - throw new newErrorType(error); - }); -} - -export function catchIfErrorCodeIs< - TResult, - TErrorHandlerResult extends TResult | PromiseLike | null | undefined ->( - errorCode: number, - errorHandler: (error: AxiosError) => TErrorHandlerResult -): (error: AxiosError) => TErrorHandlerResult { - return (error: AxiosError) => { - if (extractErrorCode(error) == errorCode) { - return errorHandler(error); - } else { - throw error; - } - }; -} -export function convertToIfErrorCodeIs( - errorCode: number, - newErrorType: { - new (innerError: AxiosError): NewError; - } -): (error: AxiosError) => never { - return catchIfErrorCodeIs(errorCode, (error) => { - throw new newErrorType(error); - }); -} - -export function convertToNetworkError( - error: AxiosError -): never { - if (error.isAxiosError && error.response == null) { - throw new HttpNetworkError(error); - } else { - throw error; - } -} - -export 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; - } -} - -export function convertToNotModified( - error: AxiosError -): NotModified { - if ( - error.isAxiosError && - error.response != null && - error.response.status == 304 - ) { - return new NotModified(); - } else { - throw error; - } -} - -export function convertToBlobWithEtag(res: AxiosResponse): BlobWithEtag { - return { - data: res.data, - etag: (res.headers as Record<'etag', string>)['etag'], - }; -} +import { AxiosError, AxiosResponse } from "axios"; + +export const apiBaseUrl = "/api"; + +export function base64(blob: Blob): Promise { + return new Promise((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 +): 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(res: AxiosResponse): T { + return res.data; +} + +export function catchIfStatusCodeIs< + TResult, + TErrorHandlerResult extends TResult | PromiseLike | null | undefined +>( + statusCode: number, + errorHandler: (error: AxiosError) => TErrorHandlerResult +): (error: AxiosError) => TErrorHandlerResult { + return (error: AxiosError) => { + if (extractStatusCode(error) == statusCode) { + return errorHandler(error); + } else { + throw error; + } + }; +} + +export function convertToIfStatusCodeIs( + statusCode: number, + newErrorType: { + new (innerError: AxiosError): NewError; + } +): (error: AxiosError) => never { + return catchIfStatusCodeIs(statusCode, (error) => { + throw new newErrorType(error); + }); +} + +export function catchIfErrorCodeIs< + TResult, + TErrorHandlerResult extends TResult | PromiseLike | null | undefined +>( + errorCode: number, + errorHandler: (error: AxiosError) => TErrorHandlerResult +): (error: AxiosError) => TErrorHandlerResult { + return (error: AxiosError) => { + if (extractErrorCode(error) == errorCode) { + return errorHandler(error); + } else { + throw error; + } + }; +} +export function convertToIfErrorCodeIs( + errorCode: number, + newErrorType: { + new (innerError: AxiosError): NewError; + } +): (error: AxiosError) => never { + return catchIfErrorCodeIs(errorCode, (error) => { + throw new newErrorType(error); + }); +} + +export function convertToNetworkError( + error: AxiosError +): never { + if (error.isAxiosError && error.response == null) { + throw new HttpNetworkError(error); + } else { + throw error; + } +} + +export 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; + } +} + +export function convertToNotModified( + error: AxiosError +): NotModified { + if ( + error.isAxiosError && + error.response != null && + error.response.status == 304 + ) { + return new NotModified(); + } else { + throw error; + } +} + +export function convertToBlobWithEtag(res: AxiosResponse): BlobWithEtag { + return { + data: res.data, + etag: (res.headers as Record<"etag", string>)["etag"], + }; +} diff --git a/Timeline/ClientApp/src/app/http/mock/common.ts b/Timeline/ClientApp/src/app/http/mock/common.ts index 3dce8117..787d81bd 100644 --- a/Timeline/ClientApp/src/app/http/mock/common.ts +++ b/Timeline/ClientApp/src/app/http/mock/common.ts @@ -1,78 +1,78 @@ -import localforage from 'localforage'; -import { SHA1 } from 'crypto-js'; - -import { HttpNetworkError } from '../common'; - -export const mockStorage = localforage.createInstance({ - name: 'mock-backend', - description: 'Database for mock back end.', - driver: localforage.INDEXEDDB, -}); - -export async function sha1(data: Blob): Promise { - const s = await new Promise((resolve) => { - const fileReader = new FileReader(); - fileReader.readAsBinaryString(data); - fileReader.onload = () => { - resolve(fileReader.result as string); - }; - }); - - return SHA1(s).toString(); -} - -const disableNetworkKey = 'mockServer.disableNetwork'; -const networkLatencyKey = 'mockServer.networkLatency'; - -let disableNetwork: boolean = - localStorage.getItem(disableNetworkKey) === 'true' ? true : false; - -const savedNetworkLatency = localStorage.getItem(networkLatencyKey); - -let networkLatency: number | null = - savedNetworkLatency != null ? Number(savedNetworkLatency) : null; - -Object.defineProperty(window, 'disableNetwork', { - get: () => disableNetwork, - set: (value) => { - if (value) { - disableNetwork = true; - localStorage.setItem(disableNetworkKey, 'true'); - } else { - disableNetwork = false; - localStorage.setItem(disableNetworkKey, 'false'); - } - }, -}); - -Object.defineProperty(window, 'networkLatency', { - get: () => networkLatency, - set: (value) => { - if (typeof value === 'number') { - networkLatency = value; - localStorage.setItem(networkLatencyKey, value.toString()); - } else if (value == null) { - networkLatency = null; - localStorage.removeItem(networkLatencyKey); - } - }, -}); - -export async function mockPrepare(key: string): Promise { - console.log(`Recieve request: ${key}`); - - if (disableNetwork) { - console.warn('Network is disabled for mock server.'); - throw new HttpNetworkError(); - } - if (networkLatency != null) { - await new Promise((resolve) => { - window.setTimeout(() => { - resolve(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - }, networkLatency! * 1000); - }); - } - - await Promise.resolve(); -} +import localforage from "localforage"; +import { SHA1 } from "crypto-js"; + +import { HttpNetworkError } from "../common"; + +export const mockStorage = localforage.createInstance({ + name: "mock-backend", + description: "Database for mock back end.", + driver: localforage.INDEXEDDB, +}); + +export async function sha1(data: Blob): Promise { + const s = await new Promise((resolve) => { + const fileReader = new FileReader(); + fileReader.readAsBinaryString(data); + fileReader.onload = () => { + resolve(fileReader.result as string); + }; + }); + + return SHA1(s).toString(); +} + +const disableNetworkKey = "mockServer.disableNetwork"; +const networkLatencyKey = "mockServer.networkLatency"; + +let disableNetwork: boolean = + localStorage.getItem(disableNetworkKey) === "true" ? true : false; + +const savedNetworkLatency = localStorage.getItem(networkLatencyKey); + +let networkLatency: number | null = + savedNetworkLatency != null ? Number(savedNetworkLatency) : null; + +Object.defineProperty(window, "disableNetwork", { + get: () => disableNetwork, + set: (value) => { + if (value) { + disableNetwork = true; + localStorage.setItem(disableNetworkKey, "true"); + } else { + disableNetwork = false; + localStorage.setItem(disableNetworkKey, "false"); + } + }, +}); + +Object.defineProperty(window, "networkLatency", { + get: () => networkLatency, + set: (value) => { + if (typeof value === "number") { + networkLatency = value; + localStorage.setItem(networkLatencyKey, value.toString()); + } else if (value == null) { + networkLatency = null; + localStorage.removeItem(networkLatencyKey); + } + }, +}); + +export async function mockPrepare(key: string): Promise { + console.log(`Recieve request: ${key}`); + + if (disableNetwork) { + console.warn("Network is disabled for mock server."); + throw new HttpNetworkError(); + } + if (networkLatency != null) { + await new Promise((resolve) => { + window.setTimeout(() => { + resolve(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + }, networkLatency! * 1000); + }); + } + + await Promise.resolve(); +} diff --git a/Timeline/ClientApp/src/app/http/mock/install.ts b/Timeline/ClientApp/src/app/http/mock/install.ts index 66174d41..17b7cc13 100644 --- a/Timeline/ClientApp/src/app/http/mock/install.ts +++ b/Timeline/ClientApp/src/app/http/mock/install.ts @@ -1,11 +1,11 @@ -import { setHttpTokenClient } from '../token'; -import { setHttpUserClient } from '../user'; -import { setHttpTimelineClient } from '../timeline'; - -import { MockHttpTokenClient } from './token'; -import { MockHttpUserClient } from './user'; -import { MockHttpTimelineClient } from './timeline'; - -setHttpTokenClient(new MockHttpTokenClient()); -setHttpUserClient(new MockHttpUserClient()); -setHttpTimelineClient(new MockHttpTimelineClient()); +import { setHttpTokenClient } from "../token"; +import { setHttpUserClient } from "../user"; +import { setHttpTimelineClient } from "../timeline"; + +import { MockHttpTokenClient } from "./token"; +import { MockHttpUserClient } from "./user"; +import { MockHttpTimelineClient } from "./timeline"; + +setHttpTokenClient(new MockHttpTokenClient()); +setHttpUserClient(new MockHttpUserClient()); +setHttpTimelineClient(new MockHttpTimelineClient()); diff --git a/Timeline/ClientApp/src/app/http/mock/timeline.ts b/Timeline/ClientApp/src/app/http/mock/timeline.ts index 9434f666..0330b303 100644 --- a/Timeline/ClientApp/src/app/http/mock/timeline.ts +++ b/Timeline/ClientApp/src/app/http/mock/timeline.ts @@ -1,658 +1,658 @@ -import { random, without, range } from 'lodash'; - -import { BlobWithEtag, NotModified } from '../common'; -import { - IHttpTimelineClient, - HttpTimelineInfo, - TimelineVisibility, - HttpTimelineListQuery, - HttpTimelineNotExistError, - HttpTimelinePostRequest, - HttpTimelineNameConflictError, - HttpTimelinePatchRequest, - HttpTimelinePostInfo, - HttpTimelinePostContent, - HttpTimelinePostPostRequest, - HttpTimelinePostNotExistError, - HttpTimelineGenericPostInfo, -} from '../timeline'; -import { HttpUser } from '../user'; - -import { mockStorage, sha1, mockPrepare } from './common'; -import { getUser, MockUserNotExistError, checkToken } from './user'; - -async function getTimelineNameList(): Promise { - return (await mockStorage.getItem('timelines')) ?? []; -} - -async function setTimelineNameList(newOne: string[]): Promise { - await mockStorage.setItem('timelines', newOne); -} - -type TimelinePropertyKey = - | 'uniqueId' - | 'lastModified' - | 'owner' - | 'description' - | 'visibility' - | 'members' - | 'currentPostId'; - -function getTimelinePropertyKey( - name: string, - property: TimelinePropertyKey -): string { - return `timeline.${name}.${property}`; -} - -function getTimelinePropertyValue( - name: string, - property: TimelinePropertyKey -): Promise { - return mockStorage.getItem(getTimelinePropertyKey(name, property)); -} - -function setTimelinePropertyValue( - name: string, - property: TimelinePropertyKey, - value: T -): Promise { - return mockStorage - .setItem(getTimelinePropertyKey(name, property), value) - .then(); -} - -function updateTimelineLastModified(name: string): Promise { - return setTimelinePropertyValue( - name, - 'lastModified', - new Date().toISOString() - ); -} - -interface HttpTimelineInfoEx extends HttpTimelineInfo { - memberUsernames: string[]; -} - -function createUniqueId(): string { - const s = 'abcdefghijklmnopqrstuvwxz0123456789'; - let result = ''; - for (let i = 0; i < 16; i++) { - result += s[random(0, s.length - 1)]; - } - return result; -} - -class MockTimelineNotExistError extends Error { - constructor() { - super('Timeline not exist.'); - } -} - -class MockTimelineAlreadyExistError extends Error { - constructor() { - super('Timeline already exist.'); - } -} - -async function getTimelineInfo(name: string): Promise { - let owner: HttpUser; - if (name.startsWith('@')) { - const ownerUsername = name.substr(1); - owner = await getUser(ownerUsername); - const optionalUniqueId = await getTimelinePropertyValue( - name, - 'uniqueId' - ); - if (optionalUniqueId == null) { - await setTimelineNameList([...(await getTimelineNameList()), name]); - await setTimelinePropertyValue(name, 'uniqueId', createUniqueId()); - await updateTimelineLastModified(name); - } - } else { - const optionalOwnerUsername = await getTimelinePropertyValue( - name, - 'owner' - ); - if (optionalOwnerUsername == null) { - throw new MockTimelineNotExistError(); - } else { - owner = await getUser(optionalOwnerUsername); - } - } - - const memberUsernames = - (await getTimelinePropertyValue(name, 'members')) ?? []; - const members = await Promise.all( - memberUsernames.map(async (username) => { - return await getUser(username); - }) - ); - - return { - name, - uniqueId: await getTimelinePropertyValue(name, 'uniqueId'), - owner, - description: - (await getTimelinePropertyValue(name, 'description')) ?? - '', - visibility: - (await getTimelinePropertyValue( - name, - 'visibility' - )) ?? 'Register', - lastModified: new Date( - await getTimelinePropertyValue(name, 'lastModified') - ), - members, - memberUsernames, - }; -} - -async function createTimeline(name: string, owner: string): Promise { - const optionalOwnerUsername = await getTimelinePropertyValue( - name, - 'owner' - ); - if (optionalOwnerUsername != null) { - throw new MockTimelineAlreadyExistError(); - } - - await setTimelineNameList([...(await getTimelineNameList()), name]); - await setTimelinePropertyValue(name, 'uniqueId', createUniqueId()); - await setTimelinePropertyValue(name, 'owner', owner); - await updateTimelineLastModified(name); -} - -type TimelinePostPropertyKey = - | 'type' - | 'data' - | 'etag' - | 'author' - | 'time' - | 'lastUpdated'; - -function getTimelinePostPropertyKey( - timelineName: string, - id: number, - propertyKey: TimelinePostPropertyKey -): string { - return `timeline.${timelineName}.posts.${id}.${propertyKey}`; -} - -function getTimelinePostPropertyValue( - timelineName: string, - id: number, - propertyKey: TimelinePostPropertyKey -): Promise { - return mockStorage.getItem( - getTimelinePostPropertyKey(timelineName, id, propertyKey) - ); -} - -function setTimelinePostPropertyValue( - timelineName: string, - id: number, - propertyKey: TimelinePostPropertyKey, - value: T -): Promise { - return mockStorage.setItem( - getTimelinePostPropertyKey(timelineName, id, propertyKey), - value - ); -} - -function removeTimelinePostProperty( - timelineName: string, - id: number, - propertyKey: TimelinePostPropertyKey -): Promise { - return mockStorage.removeItem( - getTimelinePostPropertyKey(timelineName, id, propertyKey) - ); -} - -async function getTimelinePostInfo( - timelineName: string, - id: number -): Promise { - const currentPostId = await getTimelinePropertyValue( - timelineName, - 'currentPostId' - ); - if (currentPostId == null || id > currentPostId) { - throw new HttpTimelinePostNotExistError(); - } - - const type = await getTimelinePostPropertyValue( - timelineName, - id, - 'type' - ); - - if (type == null) { - return { - id, - author: await getUser( - await getTimelinePostPropertyValue(timelineName, id, 'author') - ), - time: new Date( - await getTimelinePostPropertyValue(timelineName, id, 'time') - ), - lastUpdated: new Date( - await getTimelinePostPropertyValue( - timelineName, - id, - 'lastUpdated' - ) - ), - deleted: true, - }; - } else { - let content: HttpTimelinePostContent; - if (type === 'text') { - content = { - type: 'text', - text: await getTimelinePostPropertyValue(timelineName, id, 'data'), - }; - } else { - content = { - type: 'image', - }; - } - - return { - id, - author: await getUser( - await getTimelinePostPropertyValue(timelineName, id, 'author') - ), - time: new Date( - await getTimelinePostPropertyValue(timelineName, id, 'time') - ), - lastUpdated: new Date( - await getTimelinePostPropertyValue( - timelineName, - id, - 'lastUpdated' - ) - ), - content, - deleted: false, - }; - } -} - -export class MockHttpTimelineClient implements IHttpTimelineClient { - async listTimeline( - query: HttpTimelineListQuery - ): Promise { - await mockPrepare('timeline.list'); - return ( - await Promise.all( - (await getTimelineNameList()).map((name) => getTimelineInfo(name)) - ) - ).filter((timeline) => { - if ( - query.visibility != null && - query.visibility !== timeline.visibility - ) { - return false; - } - if (query.relate != null) { - if (query.relateType === 'own') { - if (timeline.owner.username !== query.relate) { - return false; - } - } else if (query.relateType === 'join') { - if (!timeline.memberUsernames.includes(query.relate)) { - return false; - } - } else if ( - timeline.owner.username !== query.relate && - !timeline.memberUsernames.includes(query.relate) - ) { - return false; - } - } - return true; - }); - } - - getTimeline(timelineName: string): Promise; - getTimeline( - timelineName: string, - query: { - checkUniqueId?: string; - } - ): Promise; - getTimeline( - timelineName: string, - query: { - checkUniqueId?: string; - ifModifiedSince: Date; - } - ): Promise; - async getTimeline( - timelineName: string, - query?: { - checkUniqueId?: string; - ifModifiedSince?: Date; - } - ): Promise { - await mockPrepare('timeline.get'); - try { - const timeline = await getTimelineInfo(timelineName); - if (query != null && query.ifModifiedSince != null) { - if (timeline.lastModified >= query.ifModifiedSince) { - return timeline; - } else { - if ( - query.checkUniqueId != null && - timeline.uniqueId != query.checkUniqueId - ) { - return timeline; - } else { - return new NotModified(); - } - } - } - - return timeline; - } catch (e) { - if ( - e instanceof MockTimelineNotExistError || - e instanceof MockUserNotExistError - ) { - throw new HttpTimelineNotExistError(); - } - throw e; - } - } - - async postTimeline( - req: HttpTimelinePostRequest, - token: string - ): Promise { - await mockPrepare('timeline.post'); - const user = checkToken(token); - try { - await createTimeline(req.name, user); - } catch (e) { - if (e instanceof MockTimelineAlreadyExistError) { - throw new HttpTimelineNameConflictError(); - } - throw e; - } - return await getTimelineInfo(req.name); - } - - async patchTimeline( - timelineName: string, - req: HttpTimelinePatchRequest, - _token: string - ): Promise { - await mockPrepare('timeline.patch'); - let modified = false; - if (req.description != null) { - modified = true; - await setTimelinePropertyValue( - timelineName, - 'description', - req.description - ); - } - if (req.visibility != null) { - modified = true; - await setTimelinePropertyValue( - timelineName, - 'visibility', - req.visibility - ); - } - if (modified) { - await updateTimelineLastModified(timelineName); - } - return await getTimelineInfo(timelineName); - } - - async deleteTimeline(timelineName: string, _token: string): Promise { - await mockPrepare('timeline.delete'); - await setTimelineNameList( - without(await getTimelineNameList(), timelineName) - ); - await mockStorage.removeItem( - getTimelinePropertyKey(timelineName, 'uniqueId') - ); - - // TODO: remove other things - } - - async memberPut( - timelineName: string, - username: string, - _token: string - ): Promise { - await mockPrepare('timeline.member.put'); - const oldMembers = - (await getTimelinePropertyValue( - timelineName, - 'members' - )) ?? []; - if (!oldMembers.includes(username)) { - await setTimelinePropertyValue(timelineName, 'members', [ - ...oldMembers, - username, - ]); - await updateTimelineLastModified(timelineName); - } - } - - async memberDelete( - timelineName: string, - username: string, - _token: string - ): Promise { - await mockPrepare('timeline.member.delete'); - const oldMembers = - (await getTimelinePropertyValue( - timelineName, - 'members' - )) ?? []; - if (oldMembers.includes(username)) { - await setTimelinePropertyValue( - timelineName, - 'members', - without(oldMembers, username) - ); - await updateTimelineLastModified(timelineName); - } - } - - listPost( - timelineName: string, - token?: string - ): Promise; - listPost( - timelineName: string, - token: string | undefined, - query: { - modifiedSince?: Date; - includeDeleted?: false; - } - ): Promise; - listPost( - timelineName: string, - token: string | undefined, - query: { - modifiedSince?: Date; - includeDeleted: true; - } - ): Promise; - async listPost( - timelineName: string, - _token?: string, - query?: { - modifiedSince?: Date; - includeDeleted?: boolean; - } - ): Promise { - await mockPrepare('timeline.post.list'); - // TODO: Permission check. - - const currentPostId = await getTimelinePropertyValue( - timelineName, - 'currentPostId' - ); - - return ( - await Promise.all( - range(1, currentPostId == null ? 1 : currentPostId + 1).map( - async (id) => { - return await getTimelinePostInfo(timelineName, id); - } - ) - ) - ) - .filter((post) => { - if (query?.includeDeleted !== true && post.deleted) { - return false; - } - return true; - }) - .filter((post) => { - if (query?.modifiedSince != null) { - return post.lastUpdated >= query.modifiedSince; - } - return true; - }); - } - - getPostData( - timelineName: string, - postId: number, - token: string - ): Promise; - async getPostData( - timelineName: string, - postId: number, - _token?: string, - etag?: string - ): Promise { - await mockPrepare('timeline.post.data.get'); - // TODO: Permission check. - - const optionalSavedEtag = await getTimelinePostPropertyValue( - timelineName, - postId, - 'etag' - ); - - if (optionalSavedEtag == null) { - const optionalType = await getTimelinePostPropertyValue( - timelineName, - postId, - 'type' - ); - - if (optionalType != null) { - throw new Error('Post of this type has no data.'); - } else { - throw new HttpTimelinePostNotExistError(); - } - } - - if (etag === optionalSavedEtag) { - return new NotModified(); - } - - return { - data: await getTimelinePostPropertyValue( - timelineName, - postId, - 'data' - ), - etag: optionalSavedEtag, - }; - } - - async postPost( - timelineName: string, - req: HttpTimelinePostPostRequest, - token: string - ): Promise { - await mockPrepare('timeline.post.post'); - const user = checkToken(token); - - const savedId = await getTimelinePropertyValue( - timelineName, - 'currentPostId' - ); - const id = savedId ? savedId + 1 : 1; - await setTimelinePropertyValue(timelineName, 'currentPostId', id); - - await setTimelinePostPropertyValue(timelineName, id, 'author', user); - - const currentTimeString = new Date().toISOString(); - await setTimelinePostPropertyValue( - timelineName, - id, - 'lastUpdated', - currentTimeString - ); - - await setTimelinePostPropertyValue( - timelineName, - id, - 'time', - req.time != null ? req.time.toISOString() : currentTimeString - ); - - const { content } = req; - if (content.type === 'text') { - await setTimelinePostPropertyValue(timelineName, id, 'type', 'text'); - await setTimelinePostPropertyValue( - timelineName, - id, - 'data', - content.text - ); - } else { - await setTimelinePostPropertyValue(timelineName, id, 'type', 'image'); - await setTimelinePostPropertyValue( - timelineName, - id, - 'data', - content.data - ); - await setTimelinePostPropertyValue( - timelineName, - id, - 'etag', - await sha1(content.data) - ); - } - - return (await getTimelinePostInfo( - timelineName, - id - )) as HttpTimelinePostInfo; - } - - async deletePost( - timelineName: string, - postId: number, - _token: string - ): Promise { - await mockPrepare('timeline.post.delete'); - // TODO: permission check - await removeTimelinePostProperty(timelineName, postId, 'type'); - await removeTimelinePostProperty(timelineName, postId, 'data'); - await removeTimelinePostProperty(timelineName, postId, 'etag'); - await setTimelinePostPropertyValue( - timelineName, - postId, - 'lastUpdated', - new Date().toISOString() - ); - } -} +import { random, without, range } from "lodash"; + +import { BlobWithEtag, NotModified } from "../common"; +import { + IHttpTimelineClient, + HttpTimelineInfo, + TimelineVisibility, + HttpTimelineListQuery, + HttpTimelineNotExistError, + HttpTimelinePostRequest, + HttpTimelineNameConflictError, + HttpTimelinePatchRequest, + HttpTimelinePostInfo, + HttpTimelinePostContent, + HttpTimelinePostPostRequest, + HttpTimelinePostNotExistError, + HttpTimelineGenericPostInfo, +} from "../timeline"; +import { HttpUser } from "../user"; + +import { mockStorage, sha1, mockPrepare } from "./common"; +import { getUser, MockUserNotExistError, checkToken } from "./user"; + +async function getTimelineNameList(): Promise { + return (await mockStorage.getItem("timelines")) ?? []; +} + +async function setTimelineNameList(newOne: string[]): Promise { + await mockStorage.setItem("timelines", newOne); +} + +type TimelinePropertyKey = + | "uniqueId" + | "lastModified" + | "owner" + | "description" + | "visibility" + | "members" + | "currentPostId"; + +function getTimelinePropertyKey( + name: string, + property: TimelinePropertyKey +): string { + return `timeline.${name}.${property}`; +} + +function getTimelinePropertyValue( + name: string, + property: TimelinePropertyKey +): Promise { + return mockStorage.getItem(getTimelinePropertyKey(name, property)); +} + +function setTimelinePropertyValue( + name: string, + property: TimelinePropertyKey, + value: T +): Promise { + return mockStorage + .setItem(getTimelinePropertyKey(name, property), value) + .then(); +} + +function updateTimelineLastModified(name: string): Promise { + return setTimelinePropertyValue( + name, + "lastModified", + new Date().toISOString() + ); +} + +interface HttpTimelineInfoEx extends HttpTimelineInfo { + memberUsernames: string[]; +} + +function createUniqueId(): string { + const s = "abcdefghijklmnopqrstuvwxz0123456789"; + let result = ""; + for (let i = 0; i < 16; i++) { + result += s[random(0, s.length - 1)]; + } + return result; +} + +class MockTimelineNotExistError extends Error { + constructor() { + super("Timeline not exist."); + } +} + +class MockTimelineAlreadyExistError extends Error { + constructor() { + super("Timeline already exist."); + } +} + +async function getTimelineInfo(name: string): Promise { + let owner: HttpUser; + if (name.startsWith("@")) { + const ownerUsername = name.substr(1); + owner = await getUser(ownerUsername); + const optionalUniqueId = await getTimelinePropertyValue( + name, + "uniqueId" + ); + if (optionalUniqueId == null) { + await setTimelineNameList([...(await getTimelineNameList()), name]); + await setTimelinePropertyValue(name, "uniqueId", createUniqueId()); + await updateTimelineLastModified(name); + } + } else { + const optionalOwnerUsername = await getTimelinePropertyValue( + name, + "owner" + ); + if (optionalOwnerUsername == null) { + throw new MockTimelineNotExistError(); + } else { + owner = await getUser(optionalOwnerUsername); + } + } + + const memberUsernames = + (await getTimelinePropertyValue(name, "members")) ?? []; + const members = await Promise.all( + memberUsernames.map(async (username) => { + return await getUser(username); + }) + ); + + return { + name, + uniqueId: await getTimelinePropertyValue(name, "uniqueId"), + owner, + description: + (await getTimelinePropertyValue(name, "description")) ?? + "", + visibility: + (await getTimelinePropertyValue( + name, + "visibility" + )) ?? "Register", + lastModified: new Date( + await getTimelinePropertyValue(name, "lastModified") + ), + members, + memberUsernames, + }; +} + +async function createTimeline(name: string, owner: string): Promise { + const optionalOwnerUsername = await getTimelinePropertyValue( + name, + "owner" + ); + if (optionalOwnerUsername != null) { + throw new MockTimelineAlreadyExistError(); + } + + await setTimelineNameList([...(await getTimelineNameList()), name]); + await setTimelinePropertyValue(name, "uniqueId", createUniqueId()); + await setTimelinePropertyValue(name, "owner", owner); + await updateTimelineLastModified(name); +} + +type TimelinePostPropertyKey = + | "type" + | "data" + | "etag" + | "author" + | "time" + | "lastUpdated"; + +function getTimelinePostPropertyKey( + timelineName: string, + id: number, + propertyKey: TimelinePostPropertyKey +): string { + return `timeline.${timelineName}.posts.${id}.${propertyKey}`; +} + +function getTimelinePostPropertyValue( + timelineName: string, + id: number, + propertyKey: TimelinePostPropertyKey +): Promise { + return mockStorage.getItem( + getTimelinePostPropertyKey(timelineName, id, propertyKey) + ); +} + +function setTimelinePostPropertyValue( + timelineName: string, + id: number, + propertyKey: TimelinePostPropertyKey, + value: T +): Promise { + return mockStorage.setItem( + getTimelinePostPropertyKey(timelineName, id, propertyKey), + value + ); +} + +function removeTimelinePostProperty( + timelineName: string, + id: number, + propertyKey: TimelinePostPropertyKey +): Promise { + return mockStorage.removeItem( + getTimelinePostPropertyKey(timelineName, id, propertyKey) + ); +} + +async function getTimelinePostInfo( + timelineName: string, + id: number +): Promise { + const currentPostId = await getTimelinePropertyValue( + timelineName, + "currentPostId" + ); + if (currentPostId == null || id > currentPostId) { + throw new HttpTimelinePostNotExistError(); + } + + const type = await getTimelinePostPropertyValue( + timelineName, + id, + "type" + ); + + if (type == null) { + return { + id, + author: await getUser( + await getTimelinePostPropertyValue(timelineName, id, "author") + ), + time: new Date( + await getTimelinePostPropertyValue(timelineName, id, "time") + ), + lastUpdated: new Date( + await getTimelinePostPropertyValue( + timelineName, + id, + "lastUpdated" + ) + ), + deleted: true, + }; + } else { + let content: HttpTimelinePostContent; + if (type === "text") { + content = { + type: "text", + text: await getTimelinePostPropertyValue(timelineName, id, "data"), + }; + } else { + content = { + type: "image", + }; + } + + return { + id, + author: await getUser( + await getTimelinePostPropertyValue(timelineName, id, "author") + ), + time: new Date( + await getTimelinePostPropertyValue(timelineName, id, "time") + ), + lastUpdated: new Date( + await getTimelinePostPropertyValue( + timelineName, + id, + "lastUpdated" + ) + ), + content, + deleted: false, + }; + } +} + +export class MockHttpTimelineClient implements IHttpTimelineClient { + async listTimeline( + query: HttpTimelineListQuery + ): Promise { + await mockPrepare("timeline.list"); + return ( + await Promise.all( + (await getTimelineNameList()).map((name) => getTimelineInfo(name)) + ) + ).filter((timeline) => { + if ( + query.visibility != null && + query.visibility !== timeline.visibility + ) { + return false; + } + if (query.relate != null) { + if (query.relateType === "own") { + if (timeline.owner.username !== query.relate) { + return false; + } + } else if (query.relateType === "join") { + if (!timeline.memberUsernames.includes(query.relate)) { + return false; + } + } else if ( + timeline.owner.username !== query.relate && + !timeline.memberUsernames.includes(query.relate) + ) { + return false; + } + } + return true; + }); + } + + getTimeline(timelineName: string): Promise; + getTimeline( + timelineName: string, + query: { + checkUniqueId?: string; + } + ): Promise; + getTimeline( + timelineName: string, + query: { + checkUniqueId?: string; + ifModifiedSince: Date; + } + ): Promise; + async getTimeline( + timelineName: string, + query?: { + checkUniqueId?: string; + ifModifiedSince?: Date; + } + ): Promise { + await mockPrepare("timeline.get"); + try { + const timeline = await getTimelineInfo(timelineName); + if (query != null && query.ifModifiedSince != null) { + if (timeline.lastModified >= query.ifModifiedSince) { + return timeline; + } else { + if ( + query.checkUniqueId != null && + timeline.uniqueId != query.checkUniqueId + ) { + return timeline; + } else { + return new NotModified(); + } + } + } + + return timeline; + } catch (e) { + if ( + e instanceof MockTimelineNotExistError || + e instanceof MockUserNotExistError + ) { + throw new HttpTimelineNotExistError(); + } + throw e; + } + } + + async postTimeline( + req: HttpTimelinePostRequest, + token: string + ): Promise { + await mockPrepare("timeline.post"); + const user = checkToken(token); + try { + await createTimeline(req.name, user); + } catch (e) { + if (e instanceof MockTimelineAlreadyExistError) { + throw new HttpTimelineNameConflictError(); + } + throw e; + } + return await getTimelineInfo(req.name); + } + + async patchTimeline( + timelineName: string, + req: HttpTimelinePatchRequest, + _token: string + ): Promise { + await mockPrepare("timeline.patch"); + let modified = false; + if (req.description != null) { + modified = true; + await setTimelinePropertyValue( + timelineName, + "description", + req.description + ); + } + if (req.visibility != null) { + modified = true; + await setTimelinePropertyValue( + timelineName, + "visibility", + req.visibility + ); + } + if (modified) { + await updateTimelineLastModified(timelineName); + } + return await getTimelineInfo(timelineName); + } + + async deleteTimeline(timelineName: string, _token: string): Promise { + await mockPrepare("timeline.delete"); + await setTimelineNameList( + without(await getTimelineNameList(), timelineName) + ); + await mockStorage.removeItem( + getTimelinePropertyKey(timelineName, "uniqueId") + ); + + // TODO: remove other things + } + + async memberPut( + timelineName: string, + username: string, + _token: string + ): Promise { + await mockPrepare("timeline.member.put"); + const oldMembers = + (await getTimelinePropertyValue( + timelineName, + "members" + )) ?? []; + if (!oldMembers.includes(username)) { + await setTimelinePropertyValue(timelineName, "members", [ + ...oldMembers, + username, + ]); + await updateTimelineLastModified(timelineName); + } + } + + async memberDelete( + timelineName: string, + username: string, + _token: string + ): Promise { + await mockPrepare("timeline.member.delete"); + const oldMembers = + (await getTimelinePropertyValue( + timelineName, + "members" + )) ?? []; + if (oldMembers.includes(username)) { + await setTimelinePropertyValue( + timelineName, + "members", + without(oldMembers, username) + ); + await updateTimelineLastModified(timelineName); + } + } + + listPost( + timelineName: string, + token?: string + ): Promise; + listPost( + timelineName: string, + token: string | undefined, + query: { + modifiedSince?: Date; + includeDeleted?: false; + } + ): Promise; + listPost( + timelineName: string, + token: string | undefined, + query: { + modifiedSince?: Date; + includeDeleted: true; + } + ): Promise; + async listPost( + timelineName: string, + _token?: string, + query?: { + modifiedSince?: Date; + includeDeleted?: boolean; + } + ): Promise { + await mockPrepare("timeline.post.list"); + // TODO: Permission check. + + const currentPostId = await getTimelinePropertyValue( + timelineName, + "currentPostId" + ); + + return ( + await Promise.all( + range(1, currentPostId == null ? 1 : currentPostId + 1).map( + async (id) => { + return await getTimelinePostInfo(timelineName, id); + } + ) + ) + ) + .filter((post) => { + if (query?.includeDeleted !== true && post.deleted) { + return false; + } + return true; + }) + .filter((post) => { + if (query?.modifiedSince != null) { + return post.lastUpdated >= query.modifiedSince; + } + return true; + }); + } + + getPostData( + timelineName: string, + postId: number, + token: string + ): Promise; + async getPostData( + timelineName: string, + postId: number, + _token?: string, + etag?: string + ): Promise { + await mockPrepare("timeline.post.data.get"); + // TODO: Permission check. + + const optionalSavedEtag = await getTimelinePostPropertyValue( + timelineName, + postId, + "etag" + ); + + if (optionalSavedEtag == null) { + const optionalType = await getTimelinePostPropertyValue( + timelineName, + postId, + "type" + ); + + if (optionalType != null) { + throw new Error("Post of this type has no data."); + } else { + throw new HttpTimelinePostNotExistError(); + } + } + + if (etag === optionalSavedEtag) { + return new NotModified(); + } + + return { + data: await getTimelinePostPropertyValue( + timelineName, + postId, + "data" + ), + etag: optionalSavedEtag, + }; + } + + async postPost( + timelineName: string, + req: HttpTimelinePostPostRequest, + token: string + ): Promise { + await mockPrepare("timeline.post.post"); + const user = checkToken(token); + + const savedId = await getTimelinePropertyValue( + timelineName, + "currentPostId" + ); + const id = savedId ? savedId + 1 : 1; + await setTimelinePropertyValue(timelineName, "currentPostId", id); + + await setTimelinePostPropertyValue(timelineName, id, "author", user); + + const currentTimeString = new Date().toISOString(); + await setTimelinePostPropertyValue( + timelineName, + id, + "lastUpdated", + currentTimeString + ); + + await setTimelinePostPropertyValue( + timelineName, + id, + "time", + req.time != null ? req.time.toISOString() : currentTimeString + ); + + const { content } = req; + if (content.type === "text") { + await setTimelinePostPropertyValue(timelineName, id, "type", "text"); + await setTimelinePostPropertyValue( + timelineName, + id, + "data", + content.text + ); + } else { + await setTimelinePostPropertyValue(timelineName, id, "type", "image"); + await setTimelinePostPropertyValue( + timelineName, + id, + "data", + content.data + ); + await setTimelinePostPropertyValue( + timelineName, + id, + "etag", + await sha1(content.data) + ); + } + + return (await getTimelinePostInfo( + timelineName, + id + )) as HttpTimelinePostInfo; + } + + async deletePost( + timelineName: string, + postId: number, + _token: string + ): Promise { + await mockPrepare("timeline.post.delete"); + // TODO: permission check + await removeTimelinePostProperty(timelineName, postId, "type"); + await removeTimelinePostProperty(timelineName, postId, "data"); + await removeTimelinePostProperty(timelineName, postId, "etag"); + await setTimelinePostPropertyValue( + timelineName, + postId, + "lastUpdated", + new Date().toISOString() + ); + } +} diff --git a/Timeline/ClientApp/src/app/http/mock/token.ts b/Timeline/ClientApp/src/app/http/mock/token.ts index 6929be2a..0a350894 100644 --- a/Timeline/ClientApp/src/app/http/mock/token.ts +++ b/Timeline/ClientApp/src/app/http/mock/token.ts @@ -1,53 +1,53 @@ -import { AxiosError } from 'axios'; - -import { - IHttpTokenClient, - HttpCreateTokenRequest, - HttpCreateTokenResponse, - HttpVerifyTokenRequest, - HttpVerifyTokenResponse, -} from '../token'; - -import { mockPrepare } from './common'; -import { getUser, MockUserNotExistError, checkToken } from './user'; - -export class MockHttpTokenClient implements IHttpTokenClient { - // TODO: Mock bad credentials error. - async create(req: HttpCreateTokenRequest): Promise { - await mockPrepare('token.create'); - try { - const user = await getUser(req.username); - return { - user, - token: `token-${req.username}`, - }; - } catch (e) { - if (e instanceof MockUserNotExistError) { - throw { - isAxiosError: true, - response: { - status: 400, - }, - } as Partial; - } - throw e; - } - } - - async verify(req: HttpVerifyTokenRequest): Promise { - await mockPrepare('token.verify'); - try { - const user = await getUser(checkToken(req.token)); - return { - user, - }; - } catch (e) { - throw { - isAxiosError: true, - response: { - status: 400, - }, - } as Partial; - } - } -} +import { AxiosError } from "axios"; + +import { + IHttpTokenClient, + HttpCreateTokenRequest, + HttpCreateTokenResponse, + HttpVerifyTokenRequest, + HttpVerifyTokenResponse, +} from "../token"; + +import { mockPrepare } from "./common"; +import { getUser, MockUserNotExistError, checkToken } from "./user"; + +export class MockHttpTokenClient implements IHttpTokenClient { + // TODO: Mock bad credentials error. + async create(req: HttpCreateTokenRequest): Promise { + await mockPrepare("token.create"); + try { + const user = await getUser(req.username); + return { + user, + token: `token-${req.username}`, + }; + } catch (e) { + if (e instanceof MockUserNotExistError) { + throw { + isAxiosError: true, + response: { + status: 400, + }, + } as Partial; + } + throw e; + } + } + + async verify(req: HttpVerifyTokenRequest): Promise { + await mockPrepare("token.verify"); + try { + const user = await getUser(checkToken(req.token)); + return { + user, + }; + } catch (e) { + throw { + isAxiosError: true, + response: { + status: 400, + }, + } as Partial; + } + } +} diff --git a/Timeline/ClientApp/src/app/http/mock/user.ts b/Timeline/ClientApp/src/app/http/mock/user.ts index 76a35f17..1ecd7365 100644 --- a/Timeline/ClientApp/src/app/http/mock/user.ts +++ b/Timeline/ClientApp/src/app/http/mock/user.ts @@ -1,140 +1,140 @@ -import axios from 'axios'; - -import { BlobWithEtag, NotModified } from '../common'; -import { - IHttpUserClient, - HttpUser, - HttpUserNotExistError, - HttpUserPatchRequest, - HttpChangePasswordRequest, -} from '../user'; - -import { mockStorage, sha1, mockPrepare } from './common'; - -import defaultAvatarUrl from './default-avatar.png'; - -let _defaultAvatar: BlobWithEtag | undefined = undefined; - -async function getDefaultAvatar(): Promise { - if (_defaultAvatar == null) { - const blob = ( - await axios.get(defaultAvatarUrl, { - responseType: 'blob', - }) - ).data; - const etag = await sha1(blob); - _defaultAvatar = { - data: blob, - etag, - }; - } - return _defaultAvatar; -} - -export class MockTokenError extends Error { - constructor() { - super('Token bad format.'); - } -} - -export class MockUserNotExistError extends Error { - constructor() { - super('Only two user "user" and "admin".'); - } -} - -export function checkUsername( - username: string -): asserts username is 'user' | 'admin' { - if (!['user', 'admin'].includes(username)) throw new MockUserNotExistError(); -} - -export function checkToken(token: string): string { - if (!token.startsWith('token-')) { - throw new MockTokenError(); - } - return token.substr(6); -} - -const uniqueIdMap = { - user: 'e4c80127d092d9b2fc19c5e04612d4c0', - admin: '5640fa45435f9a55077b9f77c42a77bb', -}; - -export async function getUser( - username: 'user' | 'admin' | string -): Promise { - checkUsername(username); - const savedNickname = await mockStorage.getItem( - `user.${username}.nickname` - ); - return { - uniqueId: uniqueIdMap[username], - username: username, - nickname: - savedNickname == null || savedNickname === '' ? username : savedNickname, - administrator: username === 'admin', - }; -} - -export class MockHttpUserClient implements IHttpUserClient { - async get(username: string): Promise { - await mockPrepare('user.get'); - return await getUser(username).catch((e) => { - if (e instanceof MockUserNotExistError) { - throw new HttpUserNotExistError(); - } else { - throw e; - } - }); - } - - async patch( - username: string, - req: HttpUserPatchRequest, - _token: string - ): Promise { - await mockPrepare('user.patch'); - if (req.nickname != null) { - await mockStorage.setItem(`user.${username}.nickname`, req.nickname); - } - return await getUser(username); - } - - getAvatar(username: string): Promise; - async getAvatar( - username: string, - etag?: string - ): Promise { - await mockPrepare('user.avatar.get'); - - const savedEtag = await mockStorage.getItem(`user.${username}.avatar.etag`); - if (savedEtag == null) { - return await getDefaultAvatar(); - } - - if (savedEtag === etag) { - return new NotModified(); - } - - return { - data: await mockStorage.getItem(`user.${username}.avatar.data`), - etag: await mockStorage.getItem(`user.${username}.avatar.etag`), - }; - } - - async putAvatar(username: string, data: Blob, _token: string): Promise { - await mockPrepare('user.avatar.put'); - const etag = await sha1(data); - await mockStorage.setItem(`user.${username}.avatar.data`, data); - await mockStorage.setItem(`user.${username}.avatar.etag`, etag); - } - - async changePassword( - _req: HttpChangePasswordRequest, - _token: string - ): Promise { - await mockPrepare('userop.changepassowrd'); - throw new Error('Not Implemented.'); - } -} +import axios from "axios"; + +import { BlobWithEtag, NotModified } from "../common"; +import { + IHttpUserClient, + HttpUser, + HttpUserNotExistError, + HttpUserPatchRequest, + HttpChangePasswordRequest, +} from "../user"; + +import { mockStorage, sha1, mockPrepare } from "./common"; + +import defaultAvatarUrl from "./default-avatar.png"; + +let _defaultAvatar: BlobWithEtag | undefined = undefined; + +async function getDefaultAvatar(): Promise { + if (_defaultAvatar == null) { + const blob = ( + await axios.get(defaultAvatarUrl, { + responseType: "blob", + }) + ).data; + const etag = await sha1(blob); + _defaultAvatar = { + data: blob, + etag, + }; + } + return _defaultAvatar; +} + +export class MockTokenError extends Error { + constructor() { + super("Token bad format."); + } +} + +export class MockUserNotExistError extends Error { + constructor() { + super('Only two user "user" and "admin".'); + } +} + +export function checkUsername( + username: string +): asserts username is "user" | "admin" { + if (!["user", "admin"].includes(username)) throw new MockUserNotExistError(); +} + +export function checkToken(token: string): string { + if (!token.startsWith("token-")) { + throw new MockTokenError(); + } + return token.substr(6); +} + +const uniqueIdMap = { + user: "e4c80127d092d9b2fc19c5e04612d4c0", + admin: "5640fa45435f9a55077b9f77c42a77bb", +}; + +export async function getUser( + username: "user" | "admin" | string +): Promise { + checkUsername(username); + const savedNickname = await mockStorage.getItem( + `user.${username}.nickname` + ); + return { + uniqueId: uniqueIdMap[username], + username: username, + nickname: + savedNickname == null || savedNickname === "" ? username : savedNickname, + administrator: username === "admin", + }; +} + +export class MockHttpUserClient implements IHttpUserClient { + async get(username: string): Promise { + await mockPrepare("user.get"); + return await getUser(username).catch((e) => { + if (e instanceof MockUserNotExistError) { + throw new HttpUserNotExistError(); + } else { + throw e; + } + }); + } + + async patch( + username: string, + req: HttpUserPatchRequest, + _token: string + ): Promise { + await mockPrepare("user.patch"); + if (req.nickname != null) { + await mockStorage.setItem(`user.${username}.nickname`, req.nickname); + } + return await getUser(username); + } + + getAvatar(username: string): Promise; + async getAvatar( + username: string, + etag?: string + ): Promise { + await mockPrepare("user.avatar.get"); + + const savedEtag = await mockStorage.getItem(`user.${username}.avatar.etag`); + if (savedEtag == null) { + return await getDefaultAvatar(); + } + + if (savedEtag === etag) { + return new NotModified(); + } + + return { + data: await mockStorage.getItem(`user.${username}.avatar.data`), + etag: await mockStorage.getItem(`user.${username}.avatar.etag`), + }; + } + + async putAvatar(username: string, data: Blob, _token: string): Promise { + await mockPrepare("user.avatar.put"); + const etag = await sha1(data); + await mockStorage.setItem(`user.${username}.avatar.data`, data); + await mockStorage.setItem(`user.${username}.avatar.etag`, etag); + } + + async changePassword( + _req: HttpChangePasswordRequest, + _token: string + ): Promise { + await mockPrepare("userop.changepassowrd"); + throw new Error("Not Implemented."); + } +} diff --git a/Timeline/ClientApp/src/app/http/timeline.ts b/Timeline/ClientApp/src/app/http/timeline.ts index c4ebdee9..4a325bca 100644 --- a/Timeline/ClientApp/src/app/http/timeline.ts +++ b/Timeline/ClientApp/src/app/http/timeline.ts @@ -1,543 +1,543 @@ -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: HttpTimelinePostContent; - 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; - getTimeline(timelineName: string): Promise; - getTimeline( - timelineName: string, - query: { - checkUniqueId?: string; - } - ): Promise; - getTimeline( - timelineName: string, - query: { - checkUniqueId?: string; - ifModifiedSince: Date; - } - ): Promise; - postTimeline( - req: HttpTimelinePostRequest, - token: string - ): Promise; - patchTimeline( - timelineName: string, - req: HttpTimelinePatchRequest, - token: string - ): Promise; - deleteTimeline(timelineName: string, token: string): Promise; - memberPut( - timelineName: string, - username: string, - token: string - ): Promise; - memberDelete( - timelineName: string, - username: string, - token: string - ): Promise; - listPost( - timelineName: string, - token?: string - ): Promise; - listPost( - timelineName: string, - token: string | undefined, - query: { - modifiedSince?: Date; - includeDeleted?: false; - } - ): Promise; - listPost( - timelineName: string, - token: string | undefined, - query: { - modifiedSince?: Date; - includeDeleted: true; - } - ): Promise; - getPostData( - timelineName: string, - postId: number, - token?: string - ): Promise; - getPostData( - timelineName: string, - postId: number, - token: string | undefined, - etag: string - ): Promise; - postPost( - timelineName: string, - req: HttpTimelinePostPostRequest, - token: string - ): Promise; - deletePost( - timelineName: string, - postId: number, - token: string - ): Promise; -} - -export class HttpTimelineClient implements IHttpTimelineClient { - listTimeline(query: HttpTimelineListQuery): Promise { - return axios - .get( - applyQueryParameters(`${apiBaseUrl}/timelines`, query) - ) - .then(extractResponseData) - .then((list) => list.map(processRawTimelineInfo)) - .catch(convertToNetworkError); - } - - getTimeline(timelineName: string): Promise; - getTimeline( - timelineName: string, - query: { - checkUniqueId?: string; - } - ): Promise; - getTimeline( - timelineName: string, - query: { - checkUniqueId?: string; - ifModifiedSince: Date; - } - ): Promise; - getTimeline( - timelineName: string, - query?: { - checkUniqueId?: string; - ifModifiedSince?: Date; - } - ): Promise { - return axios - .get( - 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 { - return axios - .post(`${apiBaseUrl}/timelines?token=${token}`, req) - .then(extractResponseData) - .then(processRawTimelineInfo) - .catch(convertToIfErrorCodeIs(11040101, HttpTimelineNameConflictError)) - .catch(convertToNetworkError); - } - - patchTimeline( - timelineName: string, - req: HttpTimelinePatchRequest, - token: string - ): Promise { - return axios - .patch( - `${apiBaseUrl}/timelines/${timelineName}?token=${token}`, - req - ) - .then(extractResponseData) - .then(processRawTimelineInfo) - .catch(convertToNetworkError); - } - - deleteTimeline(timelineName: string, token: string): Promise { - return axios - .delete(`${apiBaseUrl}/timelines/${timelineName}?token=${token}`) - .catch(convertToNetworkError) - .then(); - } - - memberPut( - timelineName: string, - username: string, - token: string - ): Promise { - return axios - .put( - `${apiBaseUrl}/timelines/${timelineName}/members/${username}?token=${token}` - ) - .catch(convertToNetworkError) - .then(); - } - - memberDelete( - timelineName: string, - username: string, - token: string - ): Promise { - return axios - .delete( - `${apiBaseUrl}/timelines/${timelineName}/members/${username}?token=${token}` - ) - .catch(convertToNetworkError) - .then(); - } - - listPost( - timelineName: string, - token?: string - ): Promise; - listPost( - timelineName: string, - token: string | undefined, - query: { - modifiedSince?: Date; - includeDeleted?: false; - } - ): Promise; - listPost( - timelineName: string, - token: string | undefined, - query: { - modifiedSince?: Date; - includeDeleted: true; - } - ): Promise; - listPost( - timelineName: string, - token?: string, - query?: { - modifiedSince?: Date; - includeDeleted?: boolean; - } - ): Promise { - 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(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; - getPostData( - timelineName: string, - postId: number, - token?: string, - etag?: string - ): Promise { - 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 { - 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( - `${apiBaseUrl}/timelines/${timelineName}/posts?token=${token}`, - rawReq - ) - .then(extractResponseData) - .catch(convertToNetworkError) - .then((rawPost) => processRawTimelinePostInfo(rawPost)); - } - - deletePost( - timelineName: string, - postId: number, - token: string - ): Promise { - 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; -} +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: HttpTimelinePostContent; + 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; + getTimeline(timelineName: string): Promise; + getTimeline( + timelineName: string, + query: { + checkUniqueId?: string; + } + ): Promise; + getTimeline( + timelineName: string, + query: { + checkUniqueId?: string; + ifModifiedSince: Date; + } + ): Promise; + postTimeline( + req: HttpTimelinePostRequest, + token: string + ): Promise; + patchTimeline( + timelineName: string, + req: HttpTimelinePatchRequest, + token: string + ): Promise; + deleteTimeline(timelineName: string, token: string): Promise; + memberPut( + timelineName: string, + username: string, + token: string + ): Promise; + memberDelete( + timelineName: string, + username: string, + token: string + ): Promise; + listPost( + timelineName: string, + token?: string + ): Promise; + listPost( + timelineName: string, + token: string | undefined, + query: { + modifiedSince?: Date; + includeDeleted?: false; + } + ): Promise; + listPost( + timelineName: string, + token: string | undefined, + query: { + modifiedSince?: Date; + includeDeleted: true; + } + ): Promise; + getPostData( + timelineName: string, + postId: number, + token?: string + ): Promise; + getPostData( + timelineName: string, + postId: number, + token: string | undefined, + etag: string + ): Promise; + postPost( + timelineName: string, + req: HttpTimelinePostPostRequest, + token: string + ): Promise; + deletePost( + timelineName: string, + postId: number, + token: string + ): Promise; +} + +export class HttpTimelineClient implements IHttpTimelineClient { + listTimeline(query: HttpTimelineListQuery): Promise { + return axios + .get( + applyQueryParameters(`${apiBaseUrl}/timelines`, query) + ) + .then(extractResponseData) + .then((list) => list.map(processRawTimelineInfo)) + .catch(convertToNetworkError); + } + + getTimeline(timelineName: string): Promise; + getTimeline( + timelineName: string, + query: { + checkUniqueId?: string; + } + ): Promise; + getTimeline( + timelineName: string, + query: { + checkUniqueId?: string; + ifModifiedSince: Date; + } + ): Promise; + getTimeline( + timelineName: string, + query?: { + checkUniqueId?: string; + ifModifiedSince?: Date; + } + ): Promise { + return axios + .get( + 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 { + return axios + .post(`${apiBaseUrl}/timelines?token=${token}`, req) + .then(extractResponseData) + .then(processRawTimelineInfo) + .catch(convertToIfErrorCodeIs(11040101, HttpTimelineNameConflictError)) + .catch(convertToNetworkError); + } + + patchTimeline( + timelineName: string, + req: HttpTimelinePatchRequest, + token: string + ): Promise { + return axios + .patch( + `${apiBaseUrl}/timelines/${timelineName}?token=${token}`, + req + ) + .then(extractResponseData) + .then(processRawTimelineInfo) + .catch(convertToNetworkError); + } + + deleteTimeline(timelineName: string, token: string): Promise { + return axios + .delete(`${apiBaseUrl}/timelines/${timelineName}?token=${token}`) + .catch(convertToNetworkError) + .then(); + } + + memberPut( + timelineName: string, + username: string, + token: string + ): Promise { + return axios + .put( + `${apiBaseUrl}/timelines/${timelineName}/members/${username}?token=${token}` + ) + .catch(convertToNetworkError) + .then(); + } + + memberDelete( + timelineName: string, + username: string, + token: string + ): Promise { + return axios + .delete( + `${apiBaseUrl}/timelines/${timelineName}/members/${username}?token=${token}` + ) + .catch(convertToNetworkError) + .then(); + } + + listPost( + timelineName: string, + token?: string + ): Promise; + listPost( + timelineName: string, + token: string | undefined, + query: { + modifiedSince?: Date; + includeDeleted?: false; + } + ): Promise; + listPost( + timelineName: string, + token: string | undefined, + query: { + modifiedSince?: Date; + includeDeleted: true; + } + ): Promise; + listPost( + timelineName: string, + token?: string, + query?: { + modifiedSince?: Date; + includeDeleted?: boolean; + } + ): Promise { + 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(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; + getPostData( + timelineName: string, + postId: number, + token?: string, + etag?: string + ): Promise { + 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 { + 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( + `${apiBaseUrl}/timelines/${timelineName}/posts?token=${token}`, + rawReq + ) + .then(extractResponseData) + .catch(convertToNetworkError) + .then((rawPost) => processRawTimelinePostInfo(rawPost)); + } + + deletePost( + timelineName: string, + postId: number, + token: string + ): Promise { + 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/Timeline/ClientApp/src/app/http/token.ts b/Timeline/ClientApp/src/app/http/token.ts index 26f7891f..ae0cf3f6 100644 --- a/Timeline/ClientApp/src/app/http/token.ts +++ b/Timeline/ClientApp/src/app/http/token.ts @@ -1,72 +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; - verify(req: HttpVerifyTokenRequest): Promise; -} - -export class HttpTokenClient implements IHttpTokenClient { - create(req: HttpCreateTokenRequest): Promise { - return axios - .post(`${apiBaseUrl}/token/create`, req) - .then(extractResponseData) - .catch( - convertToIfErrorCodeIs(11010101, HttpCreateTokenBadCredentialError) - ) - .catch(convertToNetworkError); - } - - verify(req: HttpVerifyTokenRequest): Promise { - return axios - .post(`${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; -} +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; + verify(req: HttpVerifyTokenRequest): Promise; +} + +export class HttpTokenClient implements IHttpTokenClient { + create(req: HttpCreateTokenRequest): Promise { + return axios + .post(`${apiBaseUrl}/token/create`, req) + .then(extractResponseData) + .catch( + convertToIfErrorCodeIs(11010101, HttpCreateTokenBadCredentialError) + ) + .catch(convertToNetworkError); + } + + verify(req: HttpVerifyTokenRequest): Promise { + return axios + .post(`${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/Timeline/ClientApp/src/app/http/user.ts b/Timeline/ClientApp/src/app/http/user.ts index baa831aa..a0a02cce 100644 --- a/Timeline/ClientApp/src/app/http/user.ts +++ b/Timeline/ClientApp/src/app/http/user.ts @@ -1,134 +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; - patch( - username: string, - req: HttpUserPatchRequest, - token: string - ): Promise; - getAvatar(username: string): Promise; - getAvatar( - username: string, - etag: string - ): Promise; - putAvatar(username: string, data: Blob, token: string): Promise; - changePassword(req: HttpChangePasswordRequest, token: string): Promise; -} - -export class HttpUserClient implements IHttpUserClient { - get(username: string): Promise { - return axios - .get(`${apiBaseUrl}/users/${username}`) - .then(extractResponseData) - .catch(convertToIfStatusCodeIs(404, HttpUserNotExistError)) - .catch(convertToNetworkError); - } - - patch( - username: string, - req: HttpUserPatchRequest, - token: string - ): Promise { - return axios - .patch(`${apiBaseUrl}/users/${username}?token=${token}`, req) - .then(extractResponseData) - .catch(convertToNetworkError); - } - - getAvatar(username: string): Promise; - getAvatar( - username: string, - etag?: string - ): Promise { - 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 { - return axios - .put(`${apiBaseUrl}/users/${username}/avatar?token=${token}`, data, { - headers: { - 'Content-Type': data.type, - }, - }) - .catch(convertToNetworkError) - .then(); - } - - changePassword(req: HttpChangePasswordRequest, token: string): Promise { - 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; -} +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; + patch( + username: string, + req: HttpUserPatchRequest, + token: string + ): Promise; + getAvatar(username: string): Promise; + getAvatar( + username: string, + etag: string + ): Promise; + putAvatar(username: string, data: Blob, token: string): Promise; + changePassword(req: HttpChangePasswordRequest, token: string): Promise; +} + +export class HttpUserClient implements IHttpUserClient { + get(username: string): Promise { + return axios + .get(`${apiBaseUrl}/users/${username}`) + .then(extractResponseData) + .catch(convertToIfStatusCodeIs(404, HttpUserNotExistError)) + .catch(convertToNetworkError); + } + + patch( + username: string, + req: HttpUserPatchRequest, + token: string + ): Promise { + return axios + .patch(`${apiBaseUrl}/users/${username}?token=${token}`, req) + .then(extractResponseData) + .catch(convertToNetworkError); + } + + getAvatar(username: string): Promise; + getAvatar( + username: string, + etag?: string + ): Promise { + 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 { + return axios + .put(`${apiBaseUrl}/users/${username}/avatar?token=${token}`, data, { + headers: { + "Content-Type": data.type, + }, + }) + .catch(convertToNetworkError) + .then(); + } + + changePassword(req: HttpChangePasswordRequest, token: string): Promise { + 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; +} -- cgit v1.2.3