From 38c28864b1ae948c0a258290c092dce94a948575 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 27 Oct 2020 18:46:34 +0800 Subject: build(front): Upgrade packages. --- Timeline/ClientApp/src/app/views/login/index.tsx | 2 +- Timeline/ClientApp/src/app/views/timeline-common/TimelineItem.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'Timeline/ClientApp/src') diff --git a/Timeline/ClientApp/src/app/views/login/index.tsx b/Timeline/ClientApp/src/app/views/login/index.tsx index 265c2172..61b9a525 100644 --- a/Timeline/ClientApp/src/app/views/login/index.tsx +++ b/Timeline/ClientApp/src/app/views/login/index.tsx @@ -124,7 +124,7 @@ const LoginPage: React.FC = (_) => { type="checkbox" checked={rememberMe} onChange={(e) => { - setRememberMe(e.target.checked); + setRememberMe(e.currentTarget.checked); }} label={t("user.rememberMe")} /> diff --git a/Timeline/ClientApp/src/app/views/timeline-common/TimelineItem.tsx b/Timeline/ClientApp/src/app/views/timeline-common/TimelineItem.tsx index 327b6833..4db23371 100644 --- a/Timeline/ClientApp/src/app/views/timeline-common/TimelineItem.tsx +++ b/Timeline/ClientApp/src/app/views/timeline-common/TimelineItem.tsx @@ -105,7 +105,7 @@ const TimelineItem: React.FC = (props) => { { + onClick={(e) => { more.toggle(); e.stopPropagation(); }} @@ -148,7 +148,7 @@ const TimelineItem: React.FC = (props) => { { + onClick={(e) => { toggleDeleteDialog(); e.stopPropagation(); }} -- cgit v1.2.3 From 03eebbf710bfc7148493cc5a598e6cc096ee0632 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 27 Oct 2020 18:48:48 +0800 Subject: fix: Fix a bug in http. --- Timeline/ClientApp/src/app/http/timeline.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'Timeline/ClientApp/src') diff --git a/Timeline/ClientApp/src/app/http/timeline.ts b/Timeline/ClientApp/src/app/http/timeline.ts index a9511c64..eb7d5065 100644 --- a/Timeline/ClientApp/src/app/http/timeline.ts +++ b/Timeline/ClientApp/src/app/http/timeline.ts @@ -145,7 +145,7 @@ type RawTimelinePostContent = interface RawTimelinePostInfo { id: number; - content: HttpTimelinePostContent; + content: RawTimelinePostContent; time: string; lastUpdated: string; author: HttpUser; -- cgit v1.2.3 From 14e5848c23c643cea9b5d709770747d98c3d75e2 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 27 Oct 2020 18:52:01 +0800 Subject: refactor: Remove mock backend. --- Timeline/ClientApp/package.json | 3 - Timeline/ClientApp/src/app/http/mock/common.ts | 78 --- .../ClientApp/src/app/http/mock/default-avatar.png | Bin 26442 -> 0 bytes Timeline/ClientApp/src/app/http/mock/install.ts | 11 - Timeline/ClientApp/src/app/http/mock/timeline.ts | 660 --------------------- Timeline/ClientApp/src/app/http/mock/token.ts | 53 -- Timeline/ClientApp/src/app/http/mock/user.ts | 139 ----- Timeline/ClientApp/webpack.config.dev.js | 10 +- 8 files changed, 1 insertion(+), 953 deletions(-) delete mode 100644 Timeline/ClientApp/src/app/http/mock/common.ts delete mode 100644 Timeline/ClientApp/src/app/http/mock/default-avatar.png delete mode 100644 Timeline/ClientApp/src/app/http/mock/install.ts delete mode 100644 Timeline/ClientApp/src/app/http/mock/timeline.ts delete mode 100644 Timeline/ClientApp/src/app/http/mock/token.ts delete mode 100644 Timeline/ClientApp/src/app/http/mock/user.ts (limited to 'Timeline/ClientApp/src') diff --git a/Timeline/ClientApp/package.json b/Timeline/ClientApp/package.json index 84b5b7d5..65c5cbe2 100644 --- a/Timeline/ClientApp/package.json +++ b/Timeline/ClientApp/package.json @@ -37,7 +37,6 @@ }, "scripts": { "start": "webpack-dev-server --config ./webpack.config.dev.js", - "start:mock": "webpack-dev-server --config ./webpack.config.dev.js --env.TIMELINE_USE_MOCK_BACKEND", "build": "webpack --config ./webpack.config.prod.js", "lint": "eslint src/ --ext .js --ext .jsx --ext .ts --ext .tsx" }, @@ -65,7 +64,6 @@ "@babel/preset-typescript": "^7.12.1", "@hot-loader/react-dom": "^17.0.0", "@types/classnames": "^2.2.10", - "@types/crypto-js": "^4.0.1", "@types/lodash": "^4.14.162", "@types/node": "^14.14.5", "@types/react": "^16.9.53", @@ -83,7 +81,6 @@ "babel-plugin-transform-builtin-extend": "^1.1.2", "clean-webpack-plugin": "^3.0.0", "copy-webpack-plugin": "^6.2.1", - "crypto-js": "^4.0.0", "css-loader": "^5.0.0", "eslint": "^7.12.1", "eslint-config-prettier": "^6.14.0", diff --git a/Timeline/ClientApp/src/app/http/mock/common.ts b/Timeline/ClientApp/src/app/http/mock/common.ts deleted file mode 100644 index 787d81bd..00000000 --- a/Timeline/ClientApp/src/app/http/mock/common.ts +++ /dev/null @@ -1,78 +0,0 @@ -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/default-avatar.png b/Timeline/ClientApp/src/app/http/mock/default-avatar.png deleted file mode 100644 index 4086e1d2..00000000 Binary files a/Timeline/ClientApp/src/app/http/mock/default-avatar.png and /dev/null differ diff --git a/Timeline/ClientApp/src/app/http/mock/install.ts b/Timeline/ClientApp/src/app/http/mock/install.ts deleted file mode 100644 index 17b7cc13..00000000 --- a/Timeline/ClientApp/src/app/http/mock/install.ts +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 27addd61..00000000 --- a/Timeline/ClientApp/src/app/http/mock/timeline.ts +++ /dev/null @@ -1,660 +0,0 @@ -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) - ) as Promise; -} - -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) - ) as Promise; -} - -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 deleted file mode 100644 index 0a350894..00000000 --- a/Timeline/ClientApp/src/app/http/mock/token.ts +++ /dev/null @@ -1,53 +0,0 @@ -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 deleted file mode 100644 index 7948da11..00000000 --- a/Timeline/ClientApp/src/app/http/mock/user.ts +++ /dev/null @@ -1,139 +0,0 @@ -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/webpack.config.dev.js b/Timeline/ClientApp/webpack.config.dev.js index fea5a2c5..c88e1aaf 100644 --- a/Timeline/ClientApp/webpack.config.dev.js +++ b/Timeline/ClientApp/webpack.config.dev.js @@ -49,12 +49,4 @@ config.devServer config.plugin("hot").use(webpack.HotModuleReplacementPlugin); -module.exports = (env) => { - if (env && env.TIMELINE_USE_MOCK_BACKEND) { - config - .entry("index") - .add(path.join(__dirname, "src/app/http/mock/install.ts")); - } - - return config.toConfig(); -}; +module.exports = config.toConfig(); -- cgit v1.2.3 From ac769e656b122ff569c3f1534701b71e00fed586 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 27 Oct 2020 19:21:35 +0800 Subject: Split front and back end. --- .dockerignore | 44 +- .github/workflows/back-ci.yaml | 16 +- .github/workflows/front-ci.yaml | 12 +- .vscode/settings.json | 8 +- BackEnd/Directory.Build.props | 6 + BackEnd/Nuget.Config | 7 + .../Timeline.ErrorCodes.CodeGenerator/Program.cs | 77 + .../Timeline.ErrorCodes.CodeGenerator.csproj | 16 + .../packages.lock.json | 24 + BackEnd/Timeline.ErrorCodes/ErrorCodes.cs | 66 + .../Timeline.ErrorCodes/Timeline.ErrorCodes.csproj | 7 + BackEnd/Timeline.ErrorCodes/packages.lock.json | 6 + BackEnd/Timeline.Tests/ErrorCodeTest.cs | 53 + BackEnd/Timeline.Tests/GlobalSuppressions.cs | 16 + .../Helpers/AsyncFunctionAssertionsExtensions.cs | 16 + BackEnd/Timeline.Tests/Helpers/CacheTestHelper.cs | 64 + .../Timeline.Tests/Helpers/HttpClientExtensions.cs | 51 + .../Helpers/HttpResponseExtensions.cs | 35 + BackEnd/Timeline.Tests/Helpers/ImageHelper.cs | 26 + .../Helpers/ParameterInfoAssertions.cs | 60 + BackEnd/Timeline.Tests/Helpers/ReflectionHelper.cs | 13 + .../Timeline.Tests/Helpers/ResponseAssertions.cs | 172 ++ BackEnd/Timeline.Tests/Helpers/TestApplication.cs | 72 + BackEnd/Timeline.Tests/Helpers/TestClock.cs | 43 + BackEnd/Timeline.Tests/Helpers/TestDatabase.cs | 76 + .../IntegratedTests/AuthorizationTest.cs | 52 + .../Timeline.Tests/IntegratedTests/FrontEndTest.cs | 29 + .../IntegratedTests/IntegratedTestBase.cs | 164 ++ .../Timeline.Tests/IntegratedTests/TimelineTest.cs | 1523 +++++++++++++++ .../Timeline.Tests/IntegratedTests/TokenTest.cs | 165 ++ .../IntegratedTests/UnknownEndpointTest.cs | 21 + .../IntegratedTests/UserAvatarTest.cs | 251 +++ BackEnd/Timeline.Tests/IntegratedTests/UserTest.cs | 447 +++++ BackEnd/Timeline.Tests/PasswordGenerator.cs | 23 + .../Timeline.Tests/Properties/launchSettings.json | 2 + .../Timeline.Tests/Services/TimelineServiceTest.cs | 329 ++++ BackEnd/Timeline.Tests/Timeline.Tests.csproj | 34 + .../Timeline.Tests/UsernameValidatorUnitTest.cs | 78 + BackEnd/Timeline.Tests/coverletArgs.runsettings | 13 + BackEnd/Timeline.Tests/packages.lock.json | 2040 ++++++++++++++++++++ BackEnd/Timeline.sln | 42 + BackEnd/Timeline/Auth/Attribute.cs | 21 + BackEnd/Timeline/Auth/MyAuthenticationHandler.cs | 100 + BackEnd/Timeline/Auth/PrincipalExtensions.cs | 13 + .../Timeline/Configs/ApplicationConfiguration.cs | 13 + BackEnd/Timeline/Configs/JwtConfiguration.cs | 14 + .../Controllers/ControllerAuthExtensions.cs | 40 + .../Controllers/Testing/TestingAuthController.cs | 32 + BackEnd/Timeline/Controllers/TimelineController.cs | 491 +++++ BackEnd/Timeline/Controllers/TokenController.cs | 142 ++ .../Timeline/Controllers/UserAvatarController.cs | 174 ++ BackEnd/Timeline/Controllers/UserController.cs | 195 ++ BackEnd/Timeline/Entities/DataEntity.cs | 23 + BackEnd/Timeline/Entities/DatabaseContext.cs | 34 + BackEnd/Timeline/Entities/JwtTokenEntity.cs | 17 + BackEnd/Timeline/Entities/TimelineEntity.cs | 58 + BackEnd/Timeline/Entities/TimelineMemberEntity.cs | 24 + BackEnd/Timeline/Entities/TimelinePostEntity.cs | 43 + BackEnd/Timeline/Entities/UserAvatarEntity.cs | 29 + BackEnd/Timeline/Entities/UserEntity.cs | 56 + BackEnd/Timeline/Entities/UtcDateAnnotation.cs | 44 + BackEnd/Timeline/Filters/Header.cs | 63 + BackEnd/Timeline/Filters/Timeline.cs | 32 + BackEnd/Timeline/Formatters/BytesInputFormatter.cs | 79 + .../Timeline/Formatters/StringInputFormatter.cs | 26 + BackEnd/Timeline/GlobalSuppressions.cs | 14 + BackEnd/Timeline/Helpers/DataCacheHelper.cs | 125 ++ BackEnd/Timeline/Helpers/DateTimeExtensions.cs | 14 + .../Helpers/InvalidModelResponseFactory.cs | 25 + BackEnd/Timeline/Helpers/LanguageHelper.cs | 12 + BackEnd/Timeline/Helpers/Log.cs | 22 + .../20200105150407_Initialize.Designer.cs | 266 +++ .../Migrations/20200105150407_Initialize.cs | 217 +++ .../20200131100517_RefactorUser.Designer.cs | 240 +++ .../Migrations/20200131100517_RefactorUser.cs | 128 ++ .../20200221064341_AddJwtToken.Designer.cs | 257 +++ .../Migrations/20200221064341_AddJwtToken.cs | 45 + .../20200229103848_AddPostLocalId.Designer.cs | 265 +++ .../Migrations/20200229103848_AddPostLocalId.cs | 42 + .../20200306110049_AddDataTable.Designer.cs | 290 +++ .../Migrations/20200306110049_AddDataTable.cs | 87 + .../20200306111553_DropUserDetails.Designer.cs | 290 +++ .../Migrations/20200306111553_DropUserDetails.cs | 17 + .../20200312112552_AddImagePost.Designer.cs | 299 +++ .../Migrations/20200312112552_AddImagePost.cs | 38 + .../20200614061237_AddTimelineUniqueId.Designer.cs | 306 +++ .../20200614061237_AddTimelineUniqueId.cs | 50 + ...00618064936_TimelineAddModifiedTime.Designer.cs | 314 +++ .../20200618064936_TimelineAddModifiedTime.cs | 57 + .../20200808071611_UserAddUniqueId.Designer.cs | 321 +++ .../Migrations/20200808071611_UserAddUniqueId.cs | 55 + .../20200810155908_AddTimesToUser.Designer.cs | 339 ++++ .../Migrations/20200810155908_AddTimesToUser.cs | 67 + ...200810170533_MakePostAuthorOptional.Designer.cs | 337 ++++ .../20200810170533_MakePostAuthorOptional.cs | 78 + ...0808_ChangeDateTimeOffsetToDateTime.Designer.cs | 337 ++++ ...0200811080808_ChangeDateTimeOffsetToDateTime.cs | 17 + .../20200826164553_TimelineAddTitle.Designer.cs | 341 ++++ .../Migrations/20200826164553_TimelineAddTitle.cs | 22 + .../Migrations/DatabaseContextModelSnapshot.cs | 339 ++++ BackEnd/Timeline/MockClientApp/index.html | 10 + BackEnd/Timeline/Models/ByteData.cs | 33 + .../Models/Converters/JsonDateTimeConverter.cs | 23 + .../Models/Converters/MyDateTimeConverter.cs | 51 + .../Models/Http/ActionContextAccessorExtensions.cs | 14 + BackEnd/Timeline/Models/Http/Common.cs | 120 ++ BackEnd/Timeline/Models/Http/ErrorResponse.cs | 261 +++ BackEnd/Timeline/Models/Http/Timeline.cs | 219 +++ BackEnd/Timeline/Models/Http/TimelineController.cs | 93 + BackEnd/Timeline/Models/Http/TokenController.cs | 62 + BackEnd/Timeline/Models/Http/UserController.cs | 93 + BackEnd/Timeline/Models/Http/UserInfo.cs | 90 + BackEnd/Timeline/Models/Timeline.cs | 98 + BackEnd/Timeline/Models/User.cs | 21 + .../Validation/GeneralTimelineNameValidator.cs | 33 + .../Timeline/Models/Validation/NameValidator.cs | 42 + .../Models/Validation/NicknameValidator.cs | 25 + .../Models/Validation/TimelineNameValidator.cs | 19 + .../Models/Validation/UsernameValidator.cs | 19 + BackEnd/Timeline/Models/Validation/Validator.cs | 127 ++ BackEnd/Timeline/Program.cs | 43 + BackEnd/Timeline/Properties/launchSettings.json | 27 + .../Authentication/AuthHandler.Designer.cs | 99 + .../Resources/Authentication/AuthHandler.resx | 132 ++ .../ControllerAuthExtensions.Designer.cs | 81 + .../Controllers/ControllerAuthExtensions.resx | 126 ++ .../Controllers/TimelineController.Designer.cs | 81 + .../Resources/Controllers/TimelineController.resx | 126 ++ .../Controllers/TokenController.Designer.cs | 153 ++ .../Resources/Controllers/TokenController.resx | 150 ++ .../Controllers/UserAvatarController.Designer.cs | 144 ++ .../Controllers/UserAvatarController.resx | 147 ++ .../Controllers/UserController.Designer.cs | 117 ++ .../Resources/Controllers/UserController.resx | 138 ++ BackEnd/Timeline/Resources/Entities.Designer.cs | 72 + BackEnd/Timeline/Resources/Entities.resx | 123 ++ BackEnd/Timeline/Resources/Filters.Designer.cs | 90 + BackEnd/Timeline/Resources/Filters.resx | 129 ++ .../Resources/Helper/DataCacheHelper.Designer.cs | 90 + .../Timeline/Resources/Helper/DataCacheHelper.resx | 129 ++ BackEnd/Timeline/Resources/Messages.Designer.cs | 396 ++++ BackEnd/Timeline/Resources/Messages.resx | 231 +++ .../Resources/Models/Http/Common.Designer.cs | 99 + BackEnd/Timeline/Resources/Models/Http/Common.resx | 132 ++ .../Resources/Models/Http/Exception.Designer.cs | 81 + .../Timeline/Resources/Models/Http/Exception.resx | 126 ++ .../Models/Validation/NameValidator.Designer.cs | 99 + .../Resources/Models/Validation/NameValidator.resx | 132 ++ .../Validation/NicknameValidator.Designer.cs | 72 + .../Models/Validation/NicknameValidator.resx | 123 ++ .../Models/Validation/Validator.Designer.cs | 108 ++ .../Resources/Models/Validation/Validator.resx | 135 ++ .../Resources/Services/DataManager.Designer.cs | 72 + .../Timeline/Resources/Services/DataManager.resx | 123 ++ .../Resources/Services/Exception.Designer.cs | 234 +++ BackEnd/Timeline/Resources/Services/Exception.resx | 177 ++ .../Resources/Services/Exceptions.Designer.cs | 189 ++ .../Timeline/Resources/Services/Exceptions.resx | 142 ++ .../Resources/Services/TimelineService.Designer.cs | 144 ++ .../Resources/Services/TimelineService.resx | 147 ++ .../Services/UserAvatarService.Designer.cs | 108 ++ .../Resources/Services/UserAvatarService.resx | 135 ++ .../Resources/Services/UserService.Designer.cs | 162 ++ .../Timeline/Resources/Services/UserService.resx | 153 ++ .../Services/UserTokenService.Designer.cs | 72 + .../Resources/Services/UserTokenService.resx | 123 ++ .../Timeline/Routes/ApiRoutePrefixConvention.cs | 46 + .../Timeline/Routes/UnknownEndpointMiddleware.cs | 39 + BackEnd/Timeline/Services/BadPasswordException.cs | 27 + BackEnd/Timeline/Services/Clock.cs | 29 + BackEnd/Timeline/Services/DataManager.cs | 122 ++ BackEnd/Timeline/Services/DatabaseBackupService.cs | 35 + .../Services/DatabaseCorruptedException.cs | 15 + BackEnd/Timeline/Services/ETagGenerator.cs | 45 + BackEnd/Timeline/Services/EntityNames.cs | 14 + .../Services/Exceptions/EntityAlreadyExistError.cs | 63 + .../Services/Exceptions/EntityNotExistError.cs | 55 + .../Services/Exceptions/ExceptionMessageHelper.cs | 13 + .../Timeline/Services/Exceptions/ImageException.cs | 57 + .../Exceptions/TimelineNotExistException.cs | 21 + .../Exceptions/TimelinePostNoDataException.cs | 15 + .../Exceptions/TimelinePostNotExistException.cs | 33 + .../Services/Exceptions/UserNotExistException.cs | 40 + BackEnd/Timeline/Services/ImageValidator.cs | 54 + .../Services/JwtUserTokenBadFormatException.cs | 48 + .../Services/PasswordBadFormatException.cs | 27 + BackEnd/Timeline/Services/PasswordService.cs | 224 +++ BackEnd/Timeline/Services/PathProvider.cs | 42 + BackEnd/Timeline/Services/TimelineService.cs | 1166 +++++++++++ BackEnd/Timeline/Services/UserAvatarService.cs | 265 +++ BackEnd/Timeline/Services/UserDeleteService.cs | 69 + BackEnd/Timeline/Services/UserRoleConvert.cs | 43 + BackEnd/Timeline/Services/UserService.cs | 437 +++++ BackEnd/Timeline/Services/UserTokenException.cs | 68 + BackEnd/Timeline/Services/UserTokenManager.cs | 97 + BackEnd/Timeline/Services/UserTokenService.cs | 149 ++ BackEnd/Timeline/Startup.cs | 185 ++ BackEnd/Timeline/Swagger/ApiConvention.cs | 15 + .../Swagger/ByteDataRequestOperationProcessor.cs | 27 + .../DefaultDescriptionOperationProcessor.cs | 39 + .../DocumentDescriptionDocumentProcessor.cs | 55 + BackEnd/Timeline/Timeline.csproj | 263 +++ BackEnd/Timeline/appsettings.Development.json | 9 + BackEnd/Timeline/appsettings.json | 11 + BackEnd/Timeline/default-avatar.png | Bin 0 -> 26442 bytes BackEnd/Timeline/packages.lock.json | 1563 +++++++++++++++ BackEnd/tools/convert-eol.py | 35 + Directory.Build.props | 6 - Dockerfile | 26 +- FrontEnd/.babelrc | 27 + FrontEnd/.editorconfig | 14 + FrontEnd/.eslintignore | 6 + FrontEnd/.eslintrc.js | 47 + FrontEnd/.gitattributes | 1 + FrontEnd/.gitignore | 32 + FrontEnd/.vscode/extensions.json | 9 + FrontEnd/.vscode/preview.yml | 10 + FrontEnd/.vscode/settings.json | 6 + FrontEnd/.yarnrc.yml | 5 + FrontEnd/LICENSE | 21 + FrontEnd/package.json | 111 ++ FrontEnd/postcss.config.js | 10 + FrontEnd/public/android-chrome-192x192.png | Bin 0 -> 8519 bytes FrontEnd/public/android-chrome-512x512.png | Bin 0 -> 23619 bytes FrontEnd/public/apple-touch-icon.png | Bin 0 -> 2088 bytes FrontEnd/public/browserconfig.xml | 9 + FrontEnd/public/favicon-16x16.png | Bin 0 -> 682 bytes FrontEnd/public/favicon-32x32.png | Bin 0 -> 821 bytes FrontEnd/public/favicon.ico | Bin 0 -> 15086 bytes FrontEnd/public/mstile-144x144.png | Bin 0 -> 2259 bytes FrontEnd/public/mstile-150x150.png | Bin 0 -> 2240 bytes FrontEnd/public/mstile-310x150.png | Bin 0 -> 2482 bytes FrontEnd/public/mstile-310x310.png | Bin 0 -> 4937 bytes FrontEnd/public/mstile-70x70.png | Bin 0 -> 1561 bytes FrontEnd/public/safari-pinned-tab.svg | 25 + FrontEnd/public/site.webmanifest | 22 + FrontEnd/sandbox.config.json | 11 + FrontEnd/src/app/App.tsx | 84 + FrontEnd/src/app/common.ts | 44 + FrontEnd/src/app/http/common.ts | 161 ++ FrontEnd/src/app/http/timeline.ts | 544 ++++++ FrontEnd/src/app/http/token.ts | 72 + FrontEnd/src/app/http/user.ts | 134 ++ FrontEnd/src/app/i18n.ts | 79 + FrontEnd/src/app/index.ejs | 29 + FrontEnd/src/app/index.sass | 66 + FrontEnd/src/app/index.tsx | 15 + FrontEnd/src/app/locales/en/translation.ts | 202 ++ FrontEnd/src/app/locales/scheme.ts | 182 ++ FrontEnd/src/app/locales/zh/translation.ts | 195 ++ FrontEnd/src/app/service-worker.tsx | 113 ++ FrontEnd/src/app/services/DataHub.ts | 225 +++ FrontEnd/src/app/services/alert.ts | 61 + FrontEnd/src/app/services/common.ts | 23 + FrontEnd/src/app/services/timeline.ts | 702 +++++++ FrontEnd/src/app/services/user.ts | 393 ++++ FrontEnd/src/app/tsconfig.json | 13 + FrontEnd/src/app/typings.d.ts | 24 + FrontEnd/src/app/utilities/rxjs.ts | 14 + FrontEnd/src/app/utilities/url.ts | 52 + FrontEnd/src/app/views/about/about.sass | 4 + FrontEnd/src/app/views/about/author-avatar.png | Bin 0 -> 12038 bytes FrontEnd/src/app/views/about/github.png | Bin 0 -> 4268 bytes FrontEnd/src/app/views/about/index.tsx | 164 ++ FrontEnd/src/app/views/admin/Admin.tsx | 75 + FrontEnd/src/app/views/admin/UserAdmin.tsx | 460 +++++ FrontEnd/src/app/views/common/AppBar.tsx | 64 + FrontEnd/src/app/views/common/BlobImage.tsx | 27 + FrontEnd/src/app/views/common/ImageCropper.tsx | 306 +++ FrontEnd/src/app/views/common/LoadingButton.tsx | 29 + FrontEnd/src/app/views/common/LoadingPage.tsx | 12 + FrontEnd/src/app/views/common/OperationDialog.tsx | 364 ++++ FrontEnd/src/app/views/common/SearchInput.tsx | 63 + FrontEnd/src/app/views/common/TimelineLogo.tsx | 26 + FrontEnd/src/app/views/common/UserTimelineLogo.tsx | 26 + FrontEnd/src/app/views/common/alert/AlertHost.tsx | 101 + FrontEnd/src/app/views/common/alert/alert.sass | 15 + FrontEnd/src/app/views/common/common.sass | 33 + FrontEnd/src/app/views/home/BoardWithUser.tsx | 101 + FrontEnd/src/app/views/home/BoardWithoutUser.tsx | 60 + FrontEnd/src/app/views/home/OfflineBoard.tsx | 61 + FrontEnd/src/app/views/home/TimelineBoard.tsx | 73 + .../src/app/views/home/TimelineCreateDialog.tsx | 53 + FrontEnd/src/app/views/home/home.sass | 13 + FrontEnd/src/app/views/home/index.tsx | 99 + FrontEnd/src/app/views/login/index.tsx | 151 ++ FrontEnd/src/app/views/login/login.sass | 2 + FrontEnd/src/app/views/settings/index.tsx | 209 ++ .../app/views/timeline-common/CollapseButton.tsx | 23 + .../app/views/timeline-common/InfoCardTemplate.tsx | 26 + .../app/views/timeline-common/SyncStatusBadge.tsx | 58 + .../src/app/views/timeline-common/Timeline.tsx | 84 + .../src/app/views/timeline-common/TimelineItem.tsx | 172 ++ .../app/views/timeline-common/TimelineMember.tsx | 211 ++ .../views/timeline-common/TimelinePageTemplate.tsx | 185 ++ .../timeline-common/TimelinePageTemplateUI.tsx | 243 +++ .../app/views/timeline-common/TimelinePostEdit.tsx | 241 +++ .../TimelinePropertyChangeDialog.tsx | 72 + .../src/app/views/timeline-common/TimelineTop.tsx | 21 + .../app/views/timeline-common/timeline-common.sass | 146 ++ .../app/views/timeline/TimelineDeleteDialog.tsx | 55 + .../src/app/views/timeline/TimelineInfoCard.tsx | 85 + FrontEnd/src/app/views/timeline/TimelinePageUI.tsx | 20 + FrontEnd/src/app/views/timeline/index.tsx | 37 + FrontEnd/src/app/views/timeline/timeline.sass | 0 FrontEnd/src/app/views/user/ChangeAvatarDialog.tsx | 302 +++ .../src/app/views/user/ChangeNicknameDialog.tsx | 28 + FrontEnd/src/app/views/user/UserInfoCard.tsx | 80 + FrontEnd/src/app/views/user/UserPageUI.tsx | 18 + FrontEnd/src/app/views/user/index.tsx | 72 + FrontEnd/src/app/views/user/user.sass | 7 + FrontEnd/src/sw/sw.ts | 28 + FrontEnd/src/sw/tsconfig.json | 12 + FrontEnd/src/tsconfig.json | 23 + FrontEnd/webpack.common.js | 86 + FrontEnd/webpack.config.dev.js | 52 + FrontEnd/webpack.config.prod.js | 53 + Nuget.Config | 7 - Timeline.ErrorCodes.CodeGenerator/Program.cs | 77 - .../Timeline.ErrorCodes.CodeGenerator.csproj | 16 - .../packages.lock.json | 24 - Timeline.ErrorCodes/ErrorCodes.cs | 66 - Timeline.ErrorCodes/Timeline.ErrorCodes.csproj | 7 - Timeline.ErrorCodes/packages.lock.json | 6 - Timeline.Tests/ErrorCodeTest.cs | 53 - Timeline.Tests/GlobalSuppressions.cs | 16 - .../Helpers/AsyncFunctionAssertionsExtensions.cs | 16 - Timeline.Tests/Helpers/CacheTestHelper.cs | 64 - Timeline.Tests/Helpers/HttpClientExtensions.cs | 51 - Timeline.Tests/Helpers/HttpResponseExtensions.cs | 35 - Timeline.Tests/Helpers/ImageHelper.cs | 26 - Timeline.Tests/Helpers/ParameterInfoAssertions.cs | 60 - Timeline.Tests/Helpers/ReflectionHelper.cs | 13 - Timeline.Tests/Helpers/ResponseAssertions.cs | 172 -- Timeline.Tests/Helpers/TestApplication.cs | 72 - Timeline.Tests/Helpers/TestClock.cs | 43 - Timeline.Tests/Helpers/TestDatabase.cs | 76 - .../IntegratedTests/AuthorizationTest.cs | 52 - Timeline.Tests/IntegratedTests/FrontEndTest.cs | 29 - .../IntegratedTests/IntegratedTestBase.cs | 164 -- Timeline.Tests/IntegratedTests/TimelineTest.cs | 1523 --------------- Timeline.Tests/IntegratedTests/TokenTest.cs | 165 -- .../IntegratedTests/UnknownEndpointTest.cs | 21 - Timeline.Tests/IntegratedTests/UserAvatarTest.cs | 251 --- Timeline.Tests/IntegratedTests/UserTest.cs | 447 ----- Timeline.Tests/PasswordGenerator.cs | 23 - Timeline.Tests/Properties/launchSettings.json | 2 - Timeline.Tests/Services/TimelineServiceTest.cs | 329 ---- Timeline.Tests/Timeline.Tests.csproj | 34 - Timeline.Tests/UsernameValidatorUnitTest.cs | 78 - Timeline.Tests/coverletArgs.runsettings | 13 - Timeline.Tests/packages.lock.json | 2040 -------------------- Timeline.sln | 42 - Timeline/Auth/Attribute.cs | 21 - Timeline/Auth/MyAuthenticationHandler.cs | 100 - Timeline/Auth/PrincipalExtensions.cs | 13 - Timeline/ClientApp/.babelrc | 27 - Timeline/ClientApp/.editorconfig | 14 - Timeline/ClientApp/.eslintignore | 6 - Timeline/ClientApp/.eslintrc.js | 47 - Timeline/ClientApp/.gitattributes | 1 - Timeline/ClientApp/.gitignore | 32 - Timeline/ClientApp/.vscode/extensions.json | 9 - Timeline/ClientApp/.vscode/preview.yml | 10 - Timeline/ClientApp/.vscode/settings.json | 6 - Timeline/ClientApp/.yarnrc.yml | 5 - Timeline/ClientApp/LICENSE | 21 - Timeline/ClientApp/package.json | 111 -- Timeline/ClientApp/postcss.config.js | 10 - .../ClientApp/public/android-chrome-192x192.png | Bin 8519 -> 0 bytes .../ClientApp/public/android-chrome-512x512.png | Bin 23619 -> 0 bytes Timeline/ClientApp/public/apple-touch-icon.png | Bin 2088 -> 0 bytes Timeline/ClientApp/public/browserconfig.xml | 9 - Timeline/ClientApp/public/favicon-16x16.png | Bin 682 -> 0 bytes Timeline/ClientApp/public/favicon-32x32.png | Bin 821 -> 0 bytes Timeline/ClientApp/public/favicon.ico | Bin 15086 -> 0 bytes Timeline/ClientApp/public/mstile-144x144.png | Bin 2259 -> 0 bytes Timeline/ClientApp/public/mstile-150x150.png | Bin 2240 -> 0 bytes Timeline/ClientApp/public/mstile-310x150.png | Bin 2482 -> 0 bytes Timeline/ClientApp/public/mstile-310x310.png | Bin 4937 -> 0 bytes Timeline/ClientApp/public/mstile-70x70.png | Bin 1561 -> 0 bytes Timeline/ClientApp/public/safari-pinned-tab.svg | 25 - Timeline/ClientApp/public/site.webmanifest | 22 - Timeline/ClientApp/sandbox.config.json | 11 - Timeline/ClientApp/src/app/App.tsx | 84 - Timeline/ClientApp/src/app/common.ts | 44 - Timeline/ClientApp/src/app/http/common.ts | 161 -- Timeline/ClientApp/src/app/http/timeline.ts | 544 ------ Timeline/ClientApp/src/app/http/token.ts | 72 - Timeline/ClientApp/src/app/http/user.ts | 134 -- Timeline/ClientApp/src/app/i18n.ts | 79 - Timeline/ClientApp/src/app/index.ejs | 29 - Timeline/ClientApp/src/app/index.sass | 66 - Timeline/ClientApp/src/app/index.tsx | 15 - .../ClientApp/src/app/locales/en/translation.ts | 202 -- Timeline/ClientApp/src/app/locales/scheme.ts | 182 -- .../ClientApp/src/app/locales/zh/translation.ts | 195 -- Timeline/ClientApp/src/app/service-worker.tsx | 113 -- Timeline/ClientApp/src/app/services/DataHub.ts | 225 --- Timeline/ClientApp/src/app/services/alert.ts | 61 - Timeline/ClientApp/src/app/services/common.ts | 23 - Timeline/ClientApp/src/app/services/timeline.ts | 702 ------- Timeline/ClientApp/src/app/services/user.ts | 393 ---- Timeline/ClientApp/src/app/tsconfig.json | 13 - Timeline/ClientApp/src/app/typings.d.ts | 24 - Timeline/ClientApp/src/app/utilities/rxjs.ts | 14 - Timeline/ClientApp/src/app/utilities/url.ts | 52 - Timeline/ClientApp/src/app/views/about/about.sass | 4 - .../src/app/views/about/author-avatar.png | Bin 12038 -> 0 bytes Timeline/ClientApp/src/app/views/about/github.png | Bin 4268 -> 0 bytes Timeline/ClientApp/src/app/views/about/index.tsx | 164 -- Timeline/ClientApp/src/app/views/admin/Admin.tsx | 75 - .../ClientApp/src/app/views/admin/UserAdmin.tsx | 460 ----- Timeline/ClientApp/src/app/views/common/AppBar.tsx | 64 - .../ClientApp/src/app/views/common/BlobImage.tsx | 27 - .../src/app/views/common/ImageCropper.tsx | 306 --- .../src/app/views/common/LoadingButton.tsx | 29 - .../ClientApp/src/app/views/common/LoadingPage.tsx | 12 - .../src/app/views/common/OperationDialog.tsx | 364 ---- .../ClientApp/src/app/views/common/SearchInput.tsx | 63 - .../src/app/views/common/TimelineLogo.tsx | 26 - .../src/app/views/common/UserTimelineLogo.tsx | 26 - .../src/app/views/common/alert/AlertHost.tsx | 101 - .../src/app/views/common/alert/alert.sass | 15 - .../ClientApp/src/app/views/common/common.sass | 33 - .../ClientApp/src/app/views/home/BoardWithUser.tsx | 101 - .../src/app/views/home/BoardWithoutUser.tsx | 60 - .../ClientApp/src/app/views/home/OfflineBoard.tsx | 61 - .../ClientApp/src/app/views/home/TimelineBoard.tsx | 73 - .../src/app/views/home/TimelineCreateDialog.tsx | 53 - Timeline/ClientApp/src/app/views/home/home.sass | 13 - Timeline/ClientApp/src/app/views/home/index.tsx | 99 - Timeline/ClientApp/src/app/views/login/index.tsx | 151 -- Timeline/ClientApp/src/app/views/login/login.sass | 2 - .../ClientApp/src/app/views/settings/index.tsx | 209 -- .../app/views/timeline-common/CollapseButton.tsx | 23 - .../app/views/timeline-common/InfoCardTemplate.tsx | 26 - .../app/views/timeline-common/SyncStatusBadge.tsx | 58 - .../src/app/views/timeline-common/Timeline.tsx | 84 - .../src/app/views/timeline-common/TimelineItem.tsx | 172 -- .../app/views/timeline-common/TimelineMember.tsx | 211 -- .../views/timeline-common/TimelinePageTemplate.tsx | 185 -- .../timeline-common/TimelinePageTemplateUI.tsx | 243 --- .../app/views/timeline-common/TimelinePostEdit.tsx | 241 --- .../TimelinePropertyChangeDialog.tsx | 72 - .../src/app/views/timeline-common/TimelineTop.tsx | 21 - .../app/views/timeline-common/timeline-common.sass | 146 -- .../app/views/timeline/TimelineDeleteDialog.tsx | 55 - .../src/app/views/timeline/TimelineInfoCard.tsx | 85 - .../src/app/views/timeline/TimelinePageUI.tsx | 20 - .../ClientApp/src/app/views/timeline/index.tsx | 37 - .../ClientApp/src/app/views/timeline/timeline.sass | 0 .../src/app/views/user/ChangeAvatarDialog.tsx | 302 --- .../src/app/views/user/ChangeNicknameDialog.tsx | 28 - .../ClientApp/src/app/views/user/UserInfoCard.tsx | 80 - .../ClientApp/src/app/views/user/UserPageUI.tsx | 18 - Timeline/ClientApp/src/app/views/user/index.tsx | 72 - Timeline/ClientApp/src/app/views/user/user.sass | 7 - Timeline/ClientApp/src/sw/sw.ts | 28 - Timeline/ClientApp/src/sw/tsconfig.json | 12 - Timeline/ClientApp/src/tsconfig.json | 23 - Timeline/ClientApp/webpack.common.js | 86 - Timeline/ClientApp/webpack.config.dev.js | 52 - Timeline/ClientApp/webpack.config.prod.js | 53 - Timeline/Configs/ApplicationConfiguration.cs | 13 - Timeline/Configs/JwtConfiguration.cs | 14 - Timeline/Controllers/ControllerAuthExtensions.cs | 40 - .../Controllers/Testing/TestingAuthController.cs | 32 - Timeline/Controllers/TimelineController.cs | 491 ----- Timeline/Controllers/TokenController.cs | 142 -- Timeline/Controllers/UserAvatarController.cs | 174 -- Timeline/Controllers/UserController.cs | 195 -- Timeline/Entities/DataEntity.cs | 23 - Timeline/Entities/DatabaseContext.cs | 34 - Timeline/Entities/JwtTokenEntity.cs | 17 - Timeline/Entities/TimelineEntity.cs | 58 - Timeline/Entities/TimelineMemberEntity.cs | 24 - Timeline/Entities/TimelinePostEntity.cs | 43 - Timeline/Entities/UserAvatarEntity.cs | 29 - Timeline/Entities/UserEntity.cs | 56 - Timeline/Entities/UtcDateAnnotation.cs | 44 - Timeline/Filters/Header.cs | 63 - Timeline/Filters/Timeline.cs | 32 - Timeline/Formatters/BytesInputFormatter.cs | 79 - Timeline/Formatters/StringInputFormatter.cs | 26 - Timeline/GlobalSuppressions.cs | 14 - Timeline/Helpers/DataCacheHelper.cs | 125 -- Timeline/Helpers/DateTimeExtensions.cs | 14 - Timeline/Helpers/InvalidModelResponseFactory.cs | 25 - Timeline/Helpers/LanguageHelper.cs | 12 - Timeline/Helpers/Log.cs | 22 - .../20200105150407_Initialize.Designer.cs | 266 --- Timeline/Migrations/20200105150407_Initialize.cs | 217 --- .../20200131100517_RefactorUser.Designer.cs | 240 --- Timeline/Migrations/20200131100517_RefactorUser.cs | 128 -- .../20200221064341_AddJwtToken.Designer.cs | 257 --- Timeline/Migrations/20200221064341_AddJwtToken.cs | 45 - .../20200229103848_AddPostLocalId.Designer.cs | 265 --- .../Migrations/20200229103848_AddPostLocalId.cs | 42 - .../20200306110049_AddDataTable.Designer.cs | 290 --- Timeline/Migrations/20200306110049_AddDataTable.cs | 87 - .../20200306111553_DropUserDetails.Designer.cs | 290 --- .../Migrations/20200306111553_DropUserDetails.cs | 17 - .../20200312112552_AddImagePost.Designer.cs | 299 --- Timeline/Migrations/20200312112552_AddImagePost.cs | 38 - .../20200614061237_AddTimelineUniqueId.Designer.cs | 306 --- .../20200614061237_AddTimelineUniqueId.cs | 50 - ...00618064936_TimelineAddModifiedTime.Designer.cs | 314 --- .../20200618064936_TimelineAddModifiedTime.cs | 57 - .../20200808071611_UserAddUniqueId.Designer.cs | 321 --- .../Migrations/20200808071611_UserAddUniqueId.cs | 55 - .../20200810155908_AddTimesToUser.Designer.cs | 339 ---- .../Migrations/20200810155908_AddTimesToUser.cs | 67 - ...200810170533_MakePostAuthorOptional.Designer.cs | 337 ---- .../20200810170533_MakePostAuthorOptional.cs | 78 - ...0808_ChangeDateTimeOffsetToDateTime.Designer.cs | 337 ---- ...0200811080808_ChangeDateTimeOffsetToDateTime.cs | 17 - .../20200826164553_TimelineAddTitle.Designer.cs | 341 ---- .../Migrations/20200826164553_TimelineAddTitle.cs | 22 - .../Migrations/DatabaseContextModelSnapshot.cs | 339 ---- Timeline/MockClientApp/index.html | 10 - Timeline/Models/ByteData.cs | 33 - .../Models/Converters/JsonDateTimeConverter.cs | 23 - Timeline/Models/Converters/MyDateTimeConverter.cs | 51 - .../Models/Http/ActionContextAccessorExtensions.cs | 14 - Timeline/Models/Http/Common.cs | 120 -- Timeline/Models/Http/ErrorResponse.cs | 261 --- Timeline/Models/Http/Timeline.cs | 219 --- Timeline/Models/Http/TimelineController.cs | 93 - Timeline/Models/Http/TokenController.cs | 62 - Timeline/Models/Http/UserController.cs | 93 - Timeline/Models/Http/UserInfo.cs | 90 - Timeline/Models/Timeline.cs | 98 - Timeline/Models/User.cs | 21 - .../Validation/GeneralTimelineNameValidator.cs | 33 - Timeline/Models/Validation/NameValidator.cs | 42 - Timeline/Models/Validation/NicknameValidator.cs | 25 - .../Models/Validation/TimelineNameValidator.cs | 19 - Timeline/Models/Validation/UsernameValidator.cs | 19 - Timeline/Models/Validation/Validator.cs | 127 -- Timeline/Program.cs | 43 - Timeline/Properties/launchSettings.json | 27 - .../Authentication/AuthHandler.Designer.cs | 99 - Timeline/Resources/Authentication/AuthHandler.resx | 132 -- .../ControllerAuthExtensions.Designer.cs | 81 - .../Controllers/ControllerAuthExtensions.resx | 126 -- .../Controllers/TimelineController.Designer.cs | 81 - .../Resources/Controllers/TimelineController.resx | 126 -- .../Controllers/TokenController.Designer.cs | 153 -- .../Resources/Controllers/TokenController.resx | 150 -- .../Controllers/UserAvatarController.Designer.cs | 144 -- .../Controllers/UserAvatarController.resx | 147 -- .../Controllers/UserController.Designer.cs | 117 -- Timeline/Resources/Controllers/UserController.resx | 138 -- Timeline/Resources/Entities.Designer.cs | 72 - Timeline/Resources/Entities.resx | 123 -- Timeline/Resources/Filters.Designer.cs | 90 - Timeline/Resources/Filters.resx | 129 -- .../Resources/Helper/DataCacheHelper.Designer.cs | 90 - Timeline/Resources/Helper/DataCacheHelper.resx | 129 -- Timeline/Resources/Messages.Designer.cs | 396 ---- Timeline/Resources/Messages.resx | 231 --- Timeline/Resources/Models/Http/Common.Designer.cs | 99 - Timeline/Resources/Models/Http/Common.resx | 132 -- .../Resources/Models/Http/Exception.Designer.cs | 81 - Timeline/Resources/Models/Http/Exception.resx | 126 -- .../Models/Validation/NameValidator.Designer.cs | 99 - .../Resources/Models/Validation/NameValidator.resx | 132 -- .../Validation/NicknameValidator.Designer.cs | 72 - .../Models/Validation/NicknameValidator.resx | 123 -- .../Models/Validation/Validator.Designer.cs | 108 -- .../Resources/Models/Validation/Validator.resx | 135 -- .../Resources/Services/DataManager.Designer.cs | 72 - Timeline/Resources/Services/DataManager.resx | 123 -- Timeline/Resources/Services/Exception.Designer.cs | 234 --- Timeline/Resources/Services/Exception.resx | 177 -- Timeline/Resources/Services/Exceptions.Designer.cs | 189 -- Timeline/Resources/Services/Exceptions.resx | 142 -- .../Resources/Services/TimelineService.Designer.cs | 144 -- Timeline/Resources/Services/TimelineService.resx | 147 -- .../Services/UserAvatarService.Designer.cs | 108 -- Timeline/Resources/Services/UserAvatarService.resx | 135 -- .../Resources/Services/UserService.Designer.cs | 162 -- Timeline/Resources/Services/UserService.resx | 153 -- .../Services/UserTokenService.Designer.cs | 72 - Timeline/Resources/Services/UserTokenService.resx | 123 -- Timeline/Routes/ApiRoutePrefixConvention.cs | 46 - Timeline/Routes/UnknownEndpointMiddleware.cs | 39 - Timeline/Services/BadPasswordException.cs | 27 - Timeline/Services/Clock.cs | 29 - Timeline/Services/DataManager.cs | 122 -- Timeline/Services/DatabaseBackupService.cs | 35 - Timeline/Services/DatabaseCorruptedException.cs | 15 - Timeline/Services/ETagGenerator.cs | 45 - Timeline/Services/EntityNames.cs | 14 - .../Services/Exceptions/EntityAlreadyExistError.cs | 63 - .../Services/Exceptions/EntityNotExistError.cs | 55 - .../Services/Exceptions/ExceptionMessageHelper.cs | 13 - Timeline/Services/Exceptions/ImageException.cs | 57 - .../Exceptions/TimelineNotExistException.cs | 21 - .../Exceptions/TimelinePostNoDataException.cs | 15 - .../Exceptions/TimelinePostNotExistException.cs | 33 - .../Services/Exceptions/UserNotExistException.cs | 40 - Timeline/Services/ImageValidator.cs | 54 - .../Services/JwtUserTokenBadFormatException.cs | 48 - Timeline/Services/PasswordBadFormatException.cs | 27 - Timeline/Services/PasswordService.cs | 224 --- Timeline/Services/PathProvider.cs | 42 - Timeline/Services/TimelineService.cs | 1166 ----------- Timeline/Services/UserAvatarService.cs | 265 --- Timeline/Services/UserDeleteService.cs | 69 - Timeline/Services/UserRoleConvert.cs | 43 - Timeline/Services/UserService.cs | 437 ----- Timeline/Services/UserTokenException.cs | 68 - Timeline/Services/UserTokenManager.cs | 97 - Timeline/Services/UserTokenService.cs | 149 -- Timeline/Startup.cs | 187 -- Timeline/Swagger/ApiConvention.cs | 15 - .../Swagger/ByteDataRequestOperationProcessor.cs | 27 - .../DefaultDescriptionOperationProcessor.cs | 39 - .../DocumentDescriptionDocumentProcessor.cs | 55 - Timeline/Timeline.csproj | 289 --- Timeline/appsettings.Development.json | 9 - Timeline/appsettings.json | 11 - Timeline/default-avatar.png | Bin 26442 -> 0 bytes Timeline/packages.lock.json | 1563 --------------- azure-pipelines.yml | 75 - tools/convert-eol.py | 35 - 628 files changed, 35363 insertions(+), 35454 deletions(-) create mode 100644 BackEnd/Directory.Build.props create mode 100644 BackEnd/Nuget.Config create mode 100644 BackEnd/Timeline.ErrorCodes.CodeGenerator/Program.cs create mode 100644 BackEnd/Timeline.ErrorCodes.CodeGenerator/Timeline.ErrorCodes.CodeGenerator.csproj create mode 100644 BackEnd/Timeline.ErrorCodes.CodeGenerator/packages.lock.json create mode 100644 BackEnd/Timeline.ErrorCodes/ErrorCodes.cs create mode 100644 BackEnd/Timeline.ErrorCodes/Timeline.ErrorCodes.csproj create mode 100644 BackEnd/Timeline.ErrorCodes/packages.lock.json create mode 100644 BackEnd/Timeline.Tests/ErrorCodeTest.cs create mode 100644 BackEnd/Timeline.Tests/GlobalSuppressions.cs create mode 100644 BackEnd/Timeline.Tests/Helpers/AsyncFunctionAssertionsExtensions.cs create mode 100644 BackEnd/Timeline.Tests/Helpers/CacheTestHelper.cs create mode 100644 BackEnd/Timeline.Tests/Helpers/HttpClientExtensions.cs create mode 100644 BackEnd/Timeline.Tests/Helpers/HttpResponseExtensions.cs create mode 100644 BackEnd/Timeline.Tests/Helpers/ImageHelper.cs create mode 100644 BackEnd/Timeline.Tests/Helpers/ParameterInfoAssertions.cs create mode 100644 BackEnd/Timeline.Tests/Helpers/ReflectionHelper.cs create mode 100644 BackEnd/Timeline.Tests/Helpers/ResponseAssertions.cs create mode 100644 BackEnd/Timeline.Tests/Helpers/TestApplication.cs create mode 100644 BackEnd/Timeline.Tests/Helpers/TestClock.cs create mode 100644 BackEnd/Timeline.Tests/Helpers/TestDatabase.cs create mode 100644 BackEnd/Timeline.Tests/IntegratedTests/AuthorizationTest.cs create mode 100644 BackEnd/Timeline.Tests/IntegratedTests/FrontEndTest.cs create mode 100644 BackEnd/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs create mode 100644 BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs create mode 100644 BackEnd/Timeline.Tests/IntegratedTests/TokenTest.cs create mode 100644 BackEnd/Timeline.Tests/IntegratedTests/UnknownEndpointTest.cs create mode 100644 BackEnd/Timeline.Tests/IntegratedTests/UserAvatarTest.cs create mode 100644 BackEnd/Timeline.Tests/IntegratedTests/UserTest.cs create mode 100644 BackEnd/Timeline.Tests/PasswordGenerator.cs create mode 100644 BackEnd/Timeline.Tests/Properties/launchSettings.json create mode 100644 BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs create mode 100644 BackEnd/Timeline.Tests/Timeline.Tests.csproj create mode 100644 BackEnd/Timeline.Tests/UsernameValidatorUnitTest.cs create mode 100644 BackEnd/Timeline.Tests/coverletArgs.runsettings create mode 100644 BackEnd/Timeline.Tests/packages.lock.json create mode 100644 BackEnd/Timeline.sln create mode 100644 BackEnd/Timeline/Auth/Attribute.cs create mode 100644 BackEnd/Timeline/Auth/MyAuthenticationHandler.cs create mode 100644 BackEnd/Timeline/Auth/PrincipalExtensions.cs create mode 100644 BackEnd/Timeline/Configs/ApplicationConfiguration.cs create mode 100644 BackEnd/Timeline/Configs/JwtConfiguration.cs create mode 100644 BackEnd/Timeline/Controllers/ControllerAuthExtensions.cs create mode 100644 BackEnd/Timeline/Controllers/Testing/TestingAuthController.cs create mode 100644 BackEnd/Timeline/Controllers/TimelineController.cs create mode 100644 BackEnd/Timeline/Controllers/TokenController.cs create mode 100644 BackEnd/Timeline/Controllers/UserAvatarController.cs create mode 100644 BackEnd/Timeline/Controllers/UserController.cs create mode 100644 BackEnd/Timeline/Entities/DataEntity.cs create mode 100644 BackEnd/Timeline/Entities/DatabaseContext.cs create mode 100644 BackEnd/Timeline/Entities/JwtTokenEntity.cs create mode 100644 BackEnd/Timeline/Entities/TimelineEntity.cs create mode 100644 BackEnd/Timeline/Entities/TimelineMemberEntity.cs create mode 100644 BackEnd/Timeline/Entities/TimelinePostEntity.cs create mode 100644 BackEnd/Timeline/Entities/UserAvatarEntity.cs create mode 100644 BackEnd/Timeline/Entities/UserEntity.cs create mode 100644 BackEnd/Timeline/Entities/UtcDateAnnotation.cs create mode 100644 BackEnd/Timeline/Filters/Header.cs create mode 100644 BackEnd/Timeline/Filters/Timeline.cs create mode 100644 BackEnd/Timeline/Formatters/BytesInputFormatter.cs create mode 100644 BackEnd/Timeline/Formatters/StringInputFormatter.cs create mode 100644 BackEnd/Timeline/GlobalSuppressions.cs create mode 100644 BackEnd/Timeline/Helpers/DataCacheHelper.cs create mode 100644 BackEnd/Timeline/Helpers/DateTimeExtensions.cs create mode 100644 BackEnd/Timeline/Helpers/InvalidModelResponseFactory.cs create mode 100644 BackEnd/Timeline/Helpers/LanguageHelper.cs create mode 100644 BackEnd/Timeline/Helpers/Log.cs create mode 100644 BackEnd/Timeline/Migrations/20200105150407_Initialize.Designer.cs create mode 100644 BackEnd/Timeline/Migrations/20200105150407_Initialize.cs create mode 100644 BackEnd/Timeline/Migrations/20200131100517_RefactorUser.Designer.cs create mode 100644 BackEnd/Timeline/Migrations/20200131100517_RefactorUser.cs create mode 100644 BackEnd/Timeline/Migrations/20200221064341_AddJwtToken.Designer.cs create mode 100644 BackEnd/Timeline/Migrations/20200221064341_AddJwtToken.cs create mode 100644 BackEnd/Timeline/Migrations/20200229103848_AddPostLocalId.Designer.cs create mode 100644 BackEnd/Timeline/Migrations/20200229103848_AddPostLocalId.cs create mode 100644 BackEnd/Timeline/Migrations/20200306110049_AddDataTable.Designer.cs create mode 100644 BackEnd/Timeline/Migrations/20200306110049_AddDataTable.cs create mode 100644 BackEnd/Timeline/Migrations/20200306111553_DropUserDetails.Designer.cs create mode 100644 BackEnd/Timeline/Migrations/20200306111553_DropUserDetails.cs create mode 100644 BackEnd/Timeline/Migrations/20200312112552_AddImagePost.Designer.cs create mode 100644 BackEnd/Timeline/Migrations/20200312112552_AddImagePost.cs create mode 100644 BackEnd/Timeline/Migrations/20200614061237_AddTimelineUniqueId.Designer.cs create mode 100644 BackEnd/Timeline/Migrations/20200614061237_AddTimelineUniqueId.cs create mode 100644 BackEnd/Timeline/Migrations/20200618064936_TimelineAddModifiedTime.Designer.cs create mode 100644 BackEnd/Timeline/Migrations/20200618064936_TimelineAddModifiedTime.cs create mode 100644 BackEnd/Timeline/Migrations/20200808071611_UserAddUniqueId.Designer.cs create mode 100644 BackEnd/Timeline/Migrations/20200808071611_UserAddUniqueId.cs create mode 100644 BackEnd/Timeline/Migrations/20200810155908_AddTimesToUser.Designer.cs create mode 100644 BackEnd/Timeline/Migrations/20200810155908_AddTimesToUser.cs create mode 100644 BackEnd/Timeline/Migrations/20200810170533_MakePostAuthorOptional.Designer.cs create mode 100644 BackEnd/Timeline/Migrations/20200810170533_MakePostAuthorOptional.cs create mode 100644 BackEnd/Timeline/Migrations/20200811080808_ChangeDateTimeOffsetToDateTime.Designer.cs create mode 100644 BackEnd/Timeline/Migrations/20200811080808_ChangeDateTimeOffsetToDateTime.cs create mode 100644 BackEnd/Timeline/Migrations/20200826164553_TimelineAddTitle.Designer.cs create mode 100644 BackEnd/Timeline/Migrations/20200826164553_TimelineAddTitle.cs create mode 100644 BackEnd/Timeline/Migrations/DatabaseContextModelSnapshot.cs create mode 100644 BackEnd/Timeline/MockClientApp/index.html create mode 100644 BackEnd/Timeline/Models/ByteData.cs create mode 100644 BackEnd/Timeline/Models/Converters/JsonDateTimeConverter.cs create mode 100644 BackEnd/Timeline/Models/Converters/MyDateTimeConverter.cs create mode 100644 BackEnd/Timeline/Models/Http/ActionContextAccessorExtensions.cs create mode 100644 BackEnd/Timeline/Models/Http/Common.cs create mode 100644 BackEnd/Timeline/Models/Http/ErrorResponse.cs create mode 100644 BackEnd/Timeline/Models/Http/Timeline.cs create mode 100644 BackEnd/Timeline/Models/Http/TimelineController.cs create mode 100644 BackEnd/Timeline/Models/Http/TokenController.cs create mode 100644 BackEnd/Timeline/Models/Http/UserController.cs create mode 100644 BackEnd/Timeline/Models/Http/UserInfo.cs create mode 100644 BackEnd/Timeline/Models/Timeline.cs create mode 100644 BackEnd/Timeline/Models/User.cs create mode 100644 BackEnd/Timeline/Models/Validation/GeneralTimelineNameValidator.cs create mode 100644 BackEnd/Timeline/Models/Validation/NameValidator.cs create mode 100644 BackEnd/Timeline/Models/Validation/NicknameValidator.cs create mode 100644 BackEnd/Timeline/Models/Validation/TimelineNameValidator.cs create mode 100644 BackEnd/Timeline/Models/Validation/UsernameValidator.cs create mode 100644 BackEnd/Timeline/Models/Validation/Validator.cs create mode 100644 BackEnd/Timeline/Program.cs create mode 100644 BackEnd/Timeline/Properties/launchSettings.json create mode 100644 BackEnd/Timeline/Resources/Authentication/AuthHandler.Designer.cs create mode 100644 BackEnd/Timeline/Resources/Authentication/AuthHandler.resx create mode 100644 BackEnd/Timeline/Resources/Controllers/ControllerAuthExtensions.Designer.cs create mode 100644 BackEnd/Timeline/Resources/Controllers/ControllerAuthExtensions.resx create mode 100644 BackEnd/Timeline/Resources/Controllers/TimelineController.Designer.cs create mode 100644 BackEnd/Timeline/Resources/Controllers/TimelineController.resx create mode 100644 BackEnd/Timeline/Resources/Controllers/TokenController.Designer.cs create mode 100644 BackEnd/Timeline/Resources/Controllers/TokenController.resx create mode 100644 BackEnd/Timeline/Resources/Controllers/UserAvatarController.Designer.cs create mode 100644 BackEnd/Timeline/Resources/Controllers/UserAvatarController.resx create mode 100644 BackEnd/Timeline/Resources/Controllers/UserController.Designer.cs create mode 100644 BackEnd/Timeline/Resources/Controllers/UserController.resx create mode 100644 BackEnd/Timeline/Resources/Entities.Designer.cs create mode 100644 BackEnd/Timeline/Resources/Entities.resx create mode 100644 BackEnd/Timeline/Resources/Filters.Designer.cs create mode 100644 BackEnd/Timeline/Resources/Filters.resx create mode 100644 BackEnd/Timeline/Resources/Helper/DataCacheHelper.Designer.cs create mode 100644 BackEnd/Timeline/Resources/Helper/DataCacheHelper.resx create mode 100644 BackEnd/Timeline/Resources/Messages.Designer.cs create mode 100644 BackEnd/Timeline/Resources/Messages.resx create mode 100644 BackEnd/Timeline/Resources/Models/Http/Common.Designer.cs create mode 100644 BackEnd/Timeline/Resources/Models/Http/Common.resx create mode 100644 BackEnd/Timeline/Resources/Models/Http/Exception.Designer.cs create mode 100644 BackEnd/Timeline/Resources/Models/Http/Exception.resx create mode 100644 BackEnd/Timeline/Resources/Models/Validation/NameValidator.Designer.cs create mode 100644 BackEnd/Timeline/Resources/Models/Validation/NameValidator.resx create mode 100644 BackEnd/Timeline/Resources/Models/Validation/NicknameValidator.Designer.cs create mode 100644 BackEnd/Timeline/Resources/Models/Validation/NicknameValidator.resx create mode 100644 BackEnd/Timeline/Resources/Models/Validation/Validator.Designer.cs create mode 100644 BackEnd/Timeline/Resources/Models/Validation/Validator.resx create mode 100644 BackEnd/Timeline/Resources/Services/DataManager.Designer.cs create mode 100644 BackEnd/Timeline/Resources/Services/DataManager.resx create mode 100644 BackEnd/Timeline/Resources/Services/Exception.Designer.cs create mode 100644 BackEnd/Timeline/Resources/Services/Exception.resx create mode 100644 BackEnd/Timeline/Resources/Services/Exceptions.Designer.cs create mode 100644 BackEnd/Timeline/Resources/Services/Exceptions.resx create mode 100644 BackEnd/Timeline/Resources/Services/TimelineService.Designer.cs create mode 100644 BackEnd/Timeline/Resources/Services/TimelineService.resx create mode 100644 BackEnd/Timeline/Resources/Services/UserAvatarService.Designer.cs create mode 100644 BackEnd/Timeline/Resources/Services/UserAvatarService.resx create mode 100644 BackEnd/Timeline/Resources/Services/UserService.Designer.cs create mode 100644 BackEnd/Timeline/Resources/Services/UserService.resx create mode 100644 BackEnd/Timeline/Resources/Services/UserTokenService.Designer.cs create mode 100644 BackEnd/Timeline/Resources/Services/UserTokenService.resx create mode 100644 BackEnd/Timeline/Routes/ApiRoutePrefixConvention.cs create mode 100644 BackEnd/Timeline/Routes/UnknownEndpointMiddleware.cs create mode 100644 BackEnd/Timeline/Services/BadPasswordException.cs create mode 100644 BackEnd/Timeline/Services/Clock.cs create mode 100644 BackEnd/Timeline/Services/DataManager.cs create mode 100644 BackEnd/Timeline/Services/DatabaseBackupService.cs create mode 100644 BackEnd/Timeline/Services/DatabaseCorruptedException.cs create mode 100644 BackEnd/Timeline/Services/ETagGenerator.cs create mode 100644 BackEnd/Timeline/Services/EntityNames.cs create mode 100644 BackEnd/Timeline/Services/Exceptions/EntityAlreadyExistError.cs create mode 100644 BackEnd/Timeline/Services/Exceptions/EntityNotExistError.cs create mode 100644 BackEnd/Timeline/Services/Exceptions/ExceptionMessageHelper.cs create mode 100644 BackEnd/Timeline/Services/Exceptions/ImageException.cs create mode 100644 BackEnd/Timeline/Services/Exceptions/TimelineNotExistException.cs create mode 100644 BackEnd/Timeline/Services/Exceptions/TimelinePostNoDataException.cs create mode 100644 BackEnd/Timeline/Services/Exceptions/TimelinePostNotExistException.cs create mode 100644 BackEnd/Timeline/Services/Exceptions/UserNotExistException.cs create mode 100644 BackEnd/Timeline/Services/ImageValidator.cs create mode 100644 BackEnd/Timeline/Services/JwtUserTokenBadFormatException.cs create mode 100644 BackEnd/Timeline/Services/PasswordBadFormatException.cs create mode 100644 BackEnd/Timeline/Services/PasswordService.cs create mode 100644 BackEnd/Timeline/Services/PathProvider.cs create mode 100644 BackEnd/Timeline/Services/TimelineService.cs create mode 100644 BackEnd/Timeline/Services/UserAvatarService.cs create mode 100644 BackEnd/Timeline/Services/UserDeleteService.cs create mode 100644 BackEnd/Timeline/Services/UserRoleConvert.cs create mode 100644 BackEnd/Timeline/Services/UserService.cs create mode 100644 BackEnd/Timeline/Services/UserTokenException.cs create mode 100644 BackEnd/Timeline/Services/UserTokenManager.cs create mode 100644 BackEnd/Timeline/Services/UserTokenService.cs create mode 100644 BackEnd/Timeline/Startup.cs create mode 100644 BackEnd/Timeline/Swagger/ApiConvention.cs create mode 100644 BackEnd/Timeline/Swagger/ByteDataRequestOperationProcessor.cs create mode 100644 BackEnd/Timeline/Swagger/DefaultDescriptionOperationProcessor.cs create mode 100644 BackEnd/Timeline/Swagger/DocumentDescriptionDocumentProcessor.cs create mode 100644 BackEnd/Timeline/Timeline.csproj create mode 100644 BackEnd/Timeline/appsettings.Development.json create mode 100644 BackEnd/Timeline/appsettings.json create mode 100644 BackEnd/Timeline/default-avatar.png create mode 100644 BackEnd/Timeline/packages.lock.json create mode 100644 BackEnd/tools/convert-eol.py delete mode 100644 Directory.Build.props create mode 100644 FrontEnd/.babelrc create mode 100644 FrontEnd/.editorconfig create mode 100644 FrontEnd/.eslintignore create mode 100644 FrontEnd/.eslintrc.js create mode 100644 FrontEnd/.gitattributes create mode 100644 FrontEnd/.gitignore create mode 100644 FrontEnd/.vscode/extensions.json create mode 100644 FrontEnd/.vscode/preview.yml create mode 100644 FrontEnd/.vscode/settings.json create mode 100644 FrontEnd/.yarnrc.yml create mode 100644 FrontEnd/LICENSE create mode 100644 FrontEnd/package.json create mode 100644 FrontEnd/postcss.config.js create mode 100644 FrontEnd/public/android-chrome-192x192.png create mode 100644 FrontEnd/public/android-chrome-512x512.png create mode 100644 FrontEnd/public/apple-touch-icon.png create mode 100644 FrontEnd/public/browserconfig.xml create mode 100644 FrontEnd/public/favicon-16x16.png create mode 100644 FrontEnd/public/favicon-32x32.png create mode 100644 FrontEnd/public/favicon.ico create mode 100644 FrontEnd/public/mstile-144x144.png create mode 100644 FrontEnd/public/mstile-150x150.png create mode 100644 FrontEnd/public/mstile-310x150.png create mode 100644 FrontEnd/public/mstile-310x310.png create mode 100644 FrontEnd/public/mstile-70x70.png create mode 100644 FrontEnd/public/safari-pinned-tab.svg create mode 100644 FrontEnd/public/site.webmanifest create mode 100644 FrontEnd/sandbox.config.json create mode 100644 FrontEnd/src/app/App.tsx create mode 100644 FrontEnd/src/app/common.ts create mode 100644 FrontEnd/src/app/http/common.ts create mode 100644 FrontEnd/src/app/http/timeline.ts create mode 100644 FrontEnd/src/app/http/token.ts create mode 100644 FrontEnd/src/app/http/user.ts create mode 100644 FrontEnd/src/app/i18n.ts create mode 100644 FrontEnd/src/app/index.ejs create mode 100644 FrontEnd/src/app/index.sass create mode 100644 FrontEnd/src/app/index.tsx create mode 100644 FrontEnd/src/app/locales/en/translation.ts create mode 100644 FrontEnd/src/app/locales/scheme.ts create mode 100644 FrontEnd/src/app/locales/zh/translation.ts create mode 100644 FrontEnd/src/app/service-worker.tsx create mode 100644 FrontEnd/src/app/services/DataHub.ts create mode 100644 FrontEnd/src/app/services/alert.ts create mode 100644 FrontEnd/src/app/services/common.ts create mode 100644 FrontEnd/src/app/services/timeline.ts create mode 100644 FrontEnd/src/app/services/user.ts create mode 100644 FrontEnd/src/app/tsconfig.json create mode 100644 FrontEnd/src/app/typings.d.ts create mode 100644 FrontEnd/src/app/utilities/rxjs.ts create mode 100644 FrontEnd/src/app/utilities/url.ts create mode 100644 FrontEnd/src/app/views/about/about.sass create mode 100644 FrontEnd/src/app/views/about/author-avatar.png create mode 100644 FrontEnd/src/app/views/about/github.png create mode 100644 FrontEnd/src/app/views/about/index.tsx create mode 100644 FrontEnd/src/app/views/admin/Admin.tsx create mode 100644 FrontEnd/src/app/views/admin/UserAdmin.tsx create mode 100644 FrontEnd/src/app/views/common/AppBar.tsx create mode 100644 FrontEnd/src/app/views/common/BlobImage.tsx create mode 100644 FrontEnd/src/app/views/common/ImageCropper.tsx create mode 100644 FrontEnd/src/app/views/common/LoadingButton.tsx create mode 100644 FrontEnd/src/app/views/common/LoadingPage.tsx create mode 100644 FrontEnd/src/app/views/common/OperationDialog.tsx create mode 100644 FrontEnd/src/app/views/common/SearchInput.tsx create mode 100644 FrontEnd/src/app/views/common/TimelineLogo.tsx create mode 100644 FrontEnd/src/app/views/common/UserTimelineLogo.tsx create mode 100644 FrontEnd/src/app/views/common/alert/AlertHost.tsx create mode 100644 FrontEnd/src/app/views/common/alert/alert.sass create mode 100644 FrontEnd/src/app/views/common/common.sass create mode 100644 FrontEnd/src/app/views/home/BoardWithUser.tsx create mode 100644 FrontEnd/src/app/views/home/BoardWithoutUser.tsx create mode 100644 FrontEnd/src/app/views/home/OfflineBoard.tsx create mode 100644 FrontEnd/src/app/views/home/TimelineBoard.tsx create mode 100644 FrontEnd/src/app/views/home/TimelineCreateDialog.tsx create mode 100644 FrontEnd/src/app/views/home/home.sass create mode 100644 FrontEnd/src/app/views/home/index.tsx create mode 100644 FrontEnd/src/app/views/login/index.tsx create mode 100644 FrontEnd/src/app/views/login/login.sass create mode 100644 FrontEnd/src/app/views/settings/index.tsx create mode 100644 FrontEnd/src/app/views/timeline-common/CollapseButton.tsx create mode 100644 FrontEnd/src/app/views/timeline-common/InfoCardTemplate.tsx create mode 100644 FrontEnd/src/app/views/timeline-common/SyncStatusBadge.tsx create mode 100644 FrontEnd/src/app/views/timeline-common/Timeline.tsx create mode 100644 FrontEnd/src/app/views/timeline-common/TimelineItem.tsx create mode 100644 FrontEnd/src/app/views/timeline-common/TimelineMember.tsx create mode 100644 FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx create mode 100644 FrontEnd/src/app/views/timeline-common/TimelinePageTemplateUI.tsx create mode 100644 FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx create mode 100644 FrontEnd/src/app/views/timeline-common/TimelinePropertyChangeDialog.tsx create mode 100644 FrontEnd/src/app/views/timeline-common/TimelineTop.tsx create mode 100644 FrontEnd/src/app/views/timeline-common/timeline-common.sass create mode 100644 FrontEnd/src/app/views/timeline/TimelineDeleteDialog.tsx create mode 100644 FrontEnd/src/app/views/timeline/TimelineInfoCard.tsx create mode 100644 FrontEnd/src/app/views/timeline/TimelinePageUI.tsx create mode 100644 FrontEnd/src/app/views/timeline/index.tsx create mode 100644 FrontEnd/src/app/views/timeline/timeline.sass create mode 100644 FrontEnd/src/app/views/user/ChangeAvatarDialog.tsx create mode 100644 FrontEnd/src/app/views/user/ChangeNicknameDialog.tsx create mode 100644 FrontEnd/src/app/views/user/UserInfoCard.tsx create mode 100644 FrontEnd/src/app/views/user/UserPageUI.tsx create mode 100644 FrontEnd/src/app/views/user/index.tsx create mode 100644 FrontEnd/src/app/views/user/user.sass create mode 100644 FrontEnd/src/sw/sw.ts create mode 100644 FrontEnd/src/sw/tsconfig.json create mode 100644 FrontEnd/src/tsconfig.json create mode 100644 FrontEnd/webpack.common.js create mode 100644 FrontEnd/webpack.config.dev.js create mode 100644 FrontEnd/webpack.config.prod.js delete mode 100644 Nuget.Config delete mode 100644 Timeline.ErrorCodes.CodeGenerator/Program.cs delete mode 100644 Timeline.ErrorCodes.CodeGenerator/Timeline.ErrorCodes.CodeGenerator.csproj delete mode 100644 Timeline.ErrorCodes.CodeGenerator/packages.lock.json delete mode 100644 Timeline.ErrorCodes/ErrorCodes.cs delete mode 100644 Timeline.ErrorCodes/Timeline.ErrorCodes.csproj delete mode 100644 Timeline.ErrorCodes/packages.lock.json delete mode 100644 Timeline.Tests/ErrorCodeTest.cs delete mode 100644 Timeline.Tests/GlobalSuppressions.cs delete mode 100644 Timeline.Tests/Helpers/AsyncFunctionAssertionsExtensions.cs delete mode 100644 Timeline.Tests/Helpers/CacheTestHelper.cs delete mode 100644 Timeline.Tests/Helpers/HttpClientExtensions.cs delete mode 100644 Timeline.Tests/Helpers/HttpResponseExtensions.cs delete mode 100644 Timeline.Tests/Helpers/ImageHelper.cs delete mode 100644 Timeline.Tests/Helpers/ParameterInfoAssertions.cs delete mode 100644 Timeline.Tests/Helpers/ReflectionHelper.cs delete mode 100644 Timeline.Tests/Helpers/ResponseAssertions.cs delete mode 100644 Timeline.Tests/Helpers/TestApplication.cs delete mode 100644 Timeline.Tests/Helpers/TestClock.cs delete mode 100644 Timeline.Tests/Helpers/TestDatabase.cs delete mode 100644 Timeline.Tests/IntegratedTests/AuthorizationTest.cs delete mode 100644 Timeline.Tests/IntegratedTests/FrontEndTest.cs delete mode 100644 Timeline.Tests/IntegratedTests/IntegratedTestBase.cs delete mode 100644 Timeline.Tests/IntegratedTests/TimelineTest.cs delete mode 100644 Timeline.Tests/IntegratedTests/TokenTest.cs delete mode 100644 Timeline.Tests/IntegratedTests/UnknownEndpointTest.cs delete mode 100644 Timeline.Tests/IntegratedTests/UserAvatarTest.cs delete mode 100644 Timeline.Tests/IntegratedTests/UserTest.cs delete mode 100644 Timeline.Tests/PasswordGenerator.cs delete mode 100644 Timeline.Tests/Properties/launchSettings.json delete mode 100644 Timeline.Tests/Services/TimelineServiceTest.cs delete mode 100644 Timeline.Tests/Timeline.Tests.csproj delete mode 100644 Timeline.Tests/UsernameValidatorUnitTest.cs delete mode 100644 Timeline.Tests/coverletArgs.runsettings delete mode 100644 Timeline.Tests/packages.lock.json delete mode 100644 Timeline.sln delete mode 100644 Timeline/Auth/Attribute.cs delete mode 100644 Timeline/Auth/MyAuthenticationHandler.cs delete mode 100644 Timeline/Auth/PrincipalExtensions.cs delete mode 100644 Timeline/ClientApp/.babelrc delete mode 100644 Timeline/ClientApp/.editorconfig delete mode 100644 Timeline/ClientApp/.eslintignore delete mode 100644 Timeline/ClientApp/.eslintrc.js delete mode 100644 Timeline/ClientApp/.gitattributes delete mode 100644 Timeline/ClientApp/.gitignore delete mode 100644 Timeline/ClientApp/.vscode/extensions.json delete mode 100644 Timeline/ClientApp/.vscode/preview.yml delete mode 100644 Timeline/ClientApp/.vscode/settings.json delete mode 100644 Timeline/ClientApp/.yarnrc.yml delete mode 100644 Timeline/ClientApp/LICENSE delete mode 100644 Timeline/ClientApp/package.json delete mode 100644 Timeline/ClientApp/postcss.config.js delete mode 100644 Timeline/ClientApp/public/android-chrome-192x192.png delete mode 100644 Timeline/ClientApp/public/android-chrome-512x512.png delete mode 100644 Timeline/ClientApp/public/apple-touch-icon.png delete mode 100644 Timeline/ClientApp/public/browserconfig.xml delete mode 100644 Timeline/ClientApp/public/favicon-16x16.png delete mode 100644 Timeline/ClientApp/public/favicon-32x32.png delete mode 100644 Timeline/ClientApp/public/favicon.ico delete mode 100644 Timeline/ClientApp/public/mstile-144x144.png delete mode 100644 Timeline/ClientApp/public/mstile-150x150.png delete mode 100644 Timeline/ClientApp/public/mstile-310x150.png delete mode 100644 Timeline/ClientApp/public/mstile-310x310.png delete mode 100644 Timeline/ClientApp/public/mstile-70x70.png delete mode 100644 Timeline/ClientApp/public/safari-pinned-tab.svg delete mode 100644 Timeline/ClientApp/public/site.webmanifest delete mode 100644 Timeline/ClientApp/sandbox.config.json delete mode 100644 Timeline/ClientApp/src/app/App.tsx delete mode 100644 Timeline/ClientApp/src/app/common.ts delete mode 100644 Timeline/ClientApp/src/app/http/common.ts delete mode 100644 Timeline/ClientApp/src/app/http/timeline.ts delete mode 100644 Timeline/ClientApp/src/app/http/token.ts delete mode 100644 Timeline/ClientApp/src/app/http/user.ts delete mode 100644 Timeline/ClientApp/src/app/i18n.ts delete mode 100644 Timeline/ClientApp/src/app/index.ejs delete mode 100644 Timeline/ClientApp/src/app/index.sass delete mode 100644 Timeline/ClientApp/src/app/index.tsx delete mode 100644 Timeline/ClientApp/src/app/locales/en/translation.ts delete mode 100644 Timeline/ClientApp/src/app/locales/scheme.ts delete mode 100644 Timeline/ClientApp/src/app/locales/zh/translation.ts delete mode 100644 Timeline/ClientApp/src/app/service-worker.tsx delete mode 100644 Timeline/ClientApp/src/app/services/DataHub.ts delete mode 100644 Timeline/ClientApp/src/app/services/alert.ts delete mode 100644 Timeline/ClientApp/src/app/services/common.ts delete mode 100644 Timeline/ClientApp/src/app/services/timeline.ts delete mode 100644 Timeline/ClientApp/src/app/services/user.ts delete mode 100644 Timeline/ClientApp/src/app/tsconfig.json delete mode 100644 Timeline/ClientApp/src/app/typings.d.ts delete mode 100644 Timeline/ClientApp/src/app/utilities/rxjs.ts delete mode 100644 Timeline/ClientApp/src/app/utilities/url.ts delete mode 100644 Timeline/ClientApp/src/app/views/about/about.sass delete mode 100644 Timeline/ClientApp/src/app/views/about/author-avatar.png delete mode 100644 Timeline/ClientApp/src/app/views/about/github.png delete mode 100644 Timeline/ClientApp/src/app/views/about/index.tsx delete mode 100644 Timeline/ClientApp/src/app/views/admin/Admin.tsx delete mode 100644 Timeline/ClientApp/src/app/views/admin/UserAdmin.tsx delete mode 100644 Timeline/ClientApp/src/app/views/common/AppBar.tsx delete mode 100644 Timeline/ClientApp/src/app/views/common/BlobImage.tsx delete mode 100644 Timeline/ClientApp/src/app/views/common/ImageCropper.tsx delete mode 100644 Timeline/ClientApp/src/app/views/common/LoadingButton.tsx delete mode 100644 Timeline/ClientApp/src/app/views/common/LoadingPage.tsx delete mode 100644 Timeline/ClientApp/src/app/views/common/OperationDialog.tsx delete mode 100644 Timeline/ClientApp/src/app/views/common/SearchInput.tsx delete mode 100644 Timeline/ClientApp/src/app/views/common/TimelineLogo.tsx delete mode 100644 Timeline/ClientApp/src/app/views/common/UserTimelineLogo.tsx delete mode 100644 Timeline/ClientApp/src/app/views/common/alert/AlertHost.tsx delete mode 100644 Timeline/ClientApp/src/app/views/common/alert/alert.sass delete mode 100644 Timeline/ClientApp/src/app/views/common/common.sass delete mode 100644 Timeline/ClientApp/src/app/views/home/BoardWithUser.tsx delete mode 100644 Timeline/ClientApp/src/app/views/home/BoardWithoutUser.tsx delete mode 100644 Timeline/ClientApp/src/app/views/home/OfflineBoard.tsx delete mode 100644 Timeline/ClientApp/src/app/views/home/TimelineBoard.tsx delete mode 100644 Timeline/ClientApp/src/app/views/home/TimelineCreateDialog.tsx delete mode 100644 Timeline/ClientApp/src/app/views/home/home.sass delete mode 100644 Timeline/ClientApp/src/app/views/home/index.tsx delete mode 100644 Timeline/ClientApp/src/app/views/login/index.tsx delete mode 100644 Timeline/ClientApp/src/app/views/login/login.sass delete mode 100644 Timeline/ClientApp/src/app/views/settings/index.tsx delete mode 100644 Timeline/ClientApp/src/app/views/timeline-common/CollapseButton.tsx delete mode 100644 Timeline/ClientApp/src/app/views/timeline-common/InfoCardTemplate.tsx delete mode 100644 Timeline/ClientApp/src/app/views/timeline-common/SyncStatusBadge.tsx delete mode 100644 Timeline/ClientApp/src/app/views/timeline-common/Timeline.tsx delete mode 100644 Timeline/ClientApp/src/app/views/timeline-common/TimelineItem.tsx delete mode 100644 Timeline/ClientApp/src/app/views/timeline-common/TimelineMember.tsx delete mode 100644 Timeline/ClientApp/src/app/views/timeline-common/TimelinePageTemplate.tsx delete mode 100644 Timeline/ClientApp/src/app/views/timeline-common/TimelinePageTemplateUI.tsx delete mode 100644 Timeline/ClientApp/src/app/views/timeline-common/TimelinePostEdit.tsx delete mode 100644 Timeline/ClientApp/src/app/views/timeline-common/TimelinePropertyChangeDialog.tsx delete mode 100644 Timeline/ClientApp/src/app/views/timeline-common/TimelineTop.tsx delete mode 100644 Timeline/ClientApp/src/app/views/timeline-common/timeline-common.sass delete mode 100644 Timeline/ClientApp/src/app/views/timeline/TimelineDeleteDialog.tsx delete mode 100644 Timeline/ClientApp/src/app/views/timeline/TimelineInfoCard.tsx delete mode 100644 Timeline/ClientApp/src/app/views/timeline/TimelinePageUI.tsx delete mode 100644 Timeline/ClientApp/src/app/views/timeline/index.tsx delete mode 100644 Timeline/ClientApp/src/app/views/timeline/timeline.sass delete mode 100644 Timeline/ClientApp/src/app/views/user/ChangeAvatarDialog.tsx delete mode 100644 Timeline/ClientApp/src/app/views/user/ChangeNicknameDialog.tsx delete mode 100644 Timeline/ClientApp/src/app/views/user/UserInfoCard.tsx delete mode 100644 Timeline/ClientApp/src/app/views/user/UserPageUI.tsx delete mode 100644 Timeline/ClientApp/src/app/views/user/index.tsx delete mode 100644 Timeline/ClientApp/src/app/views/user/user.sass delete mode 100644 Timeline/ClientApp/src/sw/sw.ts delete mode 100644 Timeline/ClientApp/src/sw/tsconfig.json delete mode 100644 Timeline/ClientApp/src/tsconfig.json delete mode 100644 Timeline/ClientApp/webpack.common.js delete mode 100644 Timeline/ClientApp/webpack.config.dev.js delete mode 100644 Timeline/ClientApp/webpack.config.prod.js delete mode 100644 Timeline/Configs/ApplicationConfiguration.cs delete mode 100644 Timeline/Configs/JwtConfiguration.cs delete mode 100644 Timeline/Controllers/ControllerAuthExtensions.cs delete mode 100644 Timeline/Controllers/Testing/TestingAuthController.cs delete mode 100644 Timeline/Controllers/TimelineController.cs delete mode 100644 Timeline/Controllers/TokenController.cs delete mode 100644 Timeline/Controllers/UserAvatarController.cs delete mode 100644 Timeline/Controllers/UserController.cs delete mode 100644 Timeline/Entities/DataEntity.cs delete mode 100644 Timeline/Entities/DatabaseContext.cs delete mode 100644 Timeline/Entities/JwtTokenEntity.cs delete mode 100644 Timeline/Entities/TimelineEntity.cs delete mode 100644 Timeline/Entities/TimelineMemberEntity.cs delete mode 100644 Timeline/Entities/TimelinePostEntity.cs delete mode 100644 Timeline/Entities/UserAvatarEntity.cs delete mode 100644 Timeline/Entities/UserEntity.cs delete mode 100644 Timeline/Entities/UtcDateAnnotation.cs delete mode 100644 Timeline/Filters/Header.cs delete mode 100644 Timeline/Filters/Timeline.cs delete mode 100644 Timeline/Formatters/BytesInputFormatter.cs delete mode 100644 Timeline/Formatters/StringInputFormatter.cs delete mode 100644 Timeline/GlobalSuppressions.cs delete mode 100644 Timeline/Helpers/DataCacheHelper.cs delete mode 100644 Timeline/Helpers/DateTimeExtensions.cs delete mode 100644 Timeline/Helpers/InvalidModelResponseFactory.cs delete mode 100644 Timeline/Helpers/LanguageHelper.cs delete mode 100644 Timeline/Helpers/Log.cs delete mode 100644 Timeline/Migrations/20200105150407_Initialize.Designer.cs delete mode 100644 Timeline/Migrations/20200105150407_Initialize.cs delete mode 100644 Timeline/Migrations/20200131100517_RefactorUser.Designer.cs delete mode 100644 Timeline/Migrations/20200131100517_RefactorUser.cs delete mode 100644 Timeline/Migrations/20200221064341_AddJwtToken.Designer.cs delete mode 100644 Timeline/Migrations/20200221064341_AddJwtToken.cs delete mode 100644 Timeline/Migrations/20200229103848_AddPostLocalId.Designer.cs delete mode 100644 Timeline/Migrations/20200229103848_AddPostLocalId.cs delete mode 100644 Timeline/Migrations/20200306110049_AddDataTable.Designer.cs delete mode 100644 Timeline/Migrations/20200306110049_AddDataTable.cs delete mode 100644 Timeline/Migrations/20200306111553_DropUserDetails.Designer.cs delete mode 100644 Timeline/Migrations/20200306111553_DropUserDetails.cs delete mode 100644 Timeline/Migrations/20200312112552_AddImagePost.Designer.cs delete mode 100644 Timeline/Migrations/20200312112552_AddImagePost.cs delete mode 100644 Timeline/Migrations/20200614061237_AddTimelineUniqueId.Designer.cs delete mode 100644 Timeline/Migrations/20200614061237_AddTimelineUniqueId.cs delete mode 100644 Timeline/Migrations/20200618064936_TimelineAddModifiedTime.Designer.cs delete mode 100644 Timeline/Migrations/20200618064936_TimelineAddModifiedTime.cs delete mode 100644 Timeline/Migrations/20200808071611_UserAddUniqueId.Designer.cs delete mode 100644 Timeline/Migrations/20200808071611_UserAddUniqueId.cs delete mode 100644 Timeline/Migrations/20200810155908_AddTimesToUser.Designer.cs delete mode 100644 Timeline/Migrations/20200810155908_AddTimesToUser.cs delete mode 100644 Timeline/Migrations/20200810170533_MakePostAuthorOptional.Designer.cs delete mode 100644 Timeline/Migrations/20200810170533_MakePostAuthorOptional.cs delete mode 100644 Timeline/Migrations/20200811080808_ChangeDateTimeOffsetToDateTime.Designer.cs delete mode 100644 Timeline/Migrations/20200811080808_ChangeDateTimeOffsetToDateTime.cs delete mode 100644 Timeline/Migrations/20200826164553_TimelineAddTitle.Designer.cs delete mode 100644 Timeline/Migrations/20200826164553_TimelineAddTitle.cs delete mode 100644 Timeline/Migrations/DatabaseContextModelSnapshot.cs delete mode 100644 Timeline/MockClientApp/index.html delete mode 100644 Timeline/Models/ByteData.cs delete mode 100644 Timeline/Models/Converters/JsonDateTimeConverter.cs delete mode 100644 Timeline/Models/Converters/MyDateTimeConverter.cs delete mode 100644 Timeline/Models/Http/ActionContextAccessorExtensions.cs delete mode 100644 Timeline/Models/Http/Common.cs delete mode 100644 Timeline/Models/Http/ErrorResponse.cs delete mode 100644 Timeline/Models/Http/Timeline.cs delete mode 100644 Timeline/Models/Http/TimelineController.cs delete mode 100644 Timeline/Models/Http/TokenController.cs delete mode 100644 Timeline/Models/Http/UserController.cs delete mode 100644 Timeline/Models/Http/UserInfo.cs delete mode 100644 Timeline/Models/Timeline.cs delete mode 100644 Timeline/Models/User.cs delete mode 100644 Timeline/Models/Validation/GeneralTimelineNameValidator.cs delete mode 100644 Timeline/Models/Validation/NameValidator.cs delete mode 100644 Timeline/Models/Validation/NicknameValidator.cs delete mode 100644 Timeline/Models/Validation/TimelineNameValidator.cs delete mode 100644 Timeline/Models/Validation/UsernameValidator.cs delete mode 100644 Timeline/Models/Validation/Validator.cs delete mode 100644 Timeline/Program.cs delete mode 100644 Timeline/Properties/launchSettings.json delete mode 100644 Timeline/Resources/Authentication/AuthHandler.Designer.cs delete mode 100644 Timeline/Resources/Authentication/AuthHandler.resx delete mode 100644 Timeline/Resources/Controllers/ControllerAuthExtensions.Designer.cs delete mode 100644 Timeline/Resources/Controllers/ControllerAuthExtensions.resx delete mode 100644 Timeline/Resources/Controllers/TimelineController.Designer.cs delete mode 100644 Timeline/Resources/Controllers/TimelineController.resx delete mode 100644 Timeline/Resources/Controllers/TokenController.Designer.cs delete mode 100644 Timeline/Resources/Controllers/TokenController.resx delete mode 100644 Timeline/Resources/Controllers/UserAvatarController.Designer.cs delete mode 100644 Timeline/Resources/Controllers/UserAvatarController.resx delete mode 100644 Timeline/Resources/Controllers/UserController.Designer.cs delete mode 100644 Timeline/Resources/Controllers/UserController.resx delete mode 100644 Timeline/Resources/Entities.Designer.cs delete mode 100644 Timeline/Resources/Entities.resx delete mode 100644 Timeline/Resources/Filters.Designer.cs delete mode 100644 Timeline/Resources/Filters.resx delete mode 100644 Timeline/Resources/Helper/DataCacheHelper.Designer.cs delete mode 100644 Timeline/Resources/Helper/DataCacheHelper.resx delete mode 100644 Timeline/Resources/Messages.Designer.cs delete mode 100644 Timeline/Resources/Messages.resx delete mode 100644 Timeline/Resources/Models/Http/Common.Designer.cs delete mode 100644 Timeline/Resources/Models/Http/Common.resx delete mode 100644 Timeline/Resources/Models/Http/Exception.Designer.cs delete mode 100644 Timeline/Resources/Models/Http/Exception.resx delete mode 100644 Timeline/Resources/Models/Validation/NameValidator.Designer.cs delete mode 100644 Timeline/Resources/Models/Validation/NameValidator.resx delete mode 100644 Timeline/Resources/Models/Validation/NicknameValidator.Designer.cs delete mode 100644 Timeline/Resources/Models/Validation/NicknameValidator.resx delete mode 100644 Timeline/Resources/Models/Validation/Validator.Designer.cs delete mode 100644 Timeline/Resources/Models/Validation/Validator.resx delete mode 100644 Timeline/Resources/Services/DataManager.Designer.cs delete mode 100644 Timeline/Resources/Services/DataManager.resx delete mode 100644 Timeline/Resources/Services/Exception.Designer.cs delete mode 100644 Timeline/Resources/Services/Exception.resx delete mode 100644 Timeline/Resources/Services/Exceptions.Designer.cs delete mode 100644 Timeline/Resources/Services/Exceptions.resx delete mode 100644 Timeline/Resources/Services/TimelineService.Designer.cs delete mode 100644 Timeline/Resources/Services/TimelineService.resx delete mode 100644 Timeline/Resources/Services/UserAvatarService.Designer.cs delete mode 100644 Timeline/Resources/Services/UserAvatarService.resx delete mode 100644 Timeline/Resources/Services/UserService.Designer.cs delete mode 100644 Timeline/Resources/Services/UserService.resx delete mode 100644 Timeline/Resources/Services/UserTokenService.Designer.cs delete mode 100644 Timeline/Resources/Services/UserTokenService.resx delete mode 100644 Timeline/Routes/ApiRoutePrefixConvention.cs delete mode 100644 Timeline/Routes/UnknownEndpointMiddleware.cs delete mode 100644 Timeline/Services/BadPasswordException.cs delete mode 100644 Timeline/Services/Clock.cs delete mode 100644 Timeline/Services/DataManager.cs delete mode 100644 Timeline/Services/DatabaseBackupService.cs delete mode 100644 Timeline/Services/DatabaseCorruptedException.cs delete mode 100644 Timeline/Services/ETagGenerator.cs delete mode 100644 Timeline/Services/EntityNames.cs delete mode 100644 Timeline/Services/Exceptions/EntityAlreadyExistError.cs delete mode 100644 Timeline/Services/Exceptions/EntityNotExistError.cs delete mode 100644 Timeline/Services/Exceptions/ExceptionMessageHelper.cs delete mode 100644 Timeline/Services/Exceptions/ImageException.cs delete mode 100644 Timeline/Services/Exceptions/TimelineNotExistException.cs delete mode 100644 Timeline/Services/Exceptions/TimelinePostNoDataException.cs delete mode 100644 Timeline/Services/Exceptions/TimelinePostNotExistException.cs delete mode 100644 Timeline/Services/Exceptions/UserNotExistException.cs delete mode 100644 Timeline/Services/ImageValidator.cs delete mode 100644 Timeline/Services/JwtUserTokenBadFormatException.cs delete mode 100644 Timeline/Services/PasswordBadFormatException.cs delete mode 100644 Timeline/Services/PasswordService.cs delete mode 100644 Timeline/Services/PathProvider.cs delete mode 100644 Timeline/Services/TimelineService.cs delete mode 100644 Timeline/Services/UserAvatarService.cs delete mode 100644 Timeline/Services/UserDeleteService.cs delete mode 100644 Timeline/Services/UserRoleConvert.cs delete mode 100644 Timeline/Services/UserService.cs delete mode 100644 Timeline/Services/UserTokenException.cs delete mode 100644 Timeline/Services/UserTokenManager.cs delete mode 100644 Timeline/Services/UserTokenService.cs delete mode 100644 Timeline/Startup.cs delete mode 100644 Timeline/Swagger/ApiConvention.cs delete mode 100644 Timeline/Swagger/ByteDataRequestOperationProcessor.cs delete mode 100644 Timeline/Swagger/DefaultDescriptionOperationProcessor.cs delete mode 100644 Timeline/Swagger/DocumentDescriptionDocumentProcessor.cs delete mode 100644 Timeline/Timeline.csproj delete mode 100644 Timeline/appsettings.Development.json delete mode 100644 Timeline/appsettings.json delete mode 100644 Timeline/default-avatar.png delete mode 100644 Timeline/packages.lock.json delete mode 100644 azure-pipelines.yml delete mode 100644 tools/convert-eol.py (limited to 'Timeline/ClientApp/src') diff --git a/.dockerignore b/.dockerignore index c0e737c7..8a68a387 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,21 +1,23 @@ -.git -Timeline/ClientApp/.git - -ErrorResponseCodeGenerator/bin -ErrorResponseCodeGenerator/obj -Timeline/bin -Timeline/obj -Timeline.ErrorCodes/bin -Timeline.ErrorCodes/obj -Timeline.Tests/bin -Timeline.Tests/obj -Timeline/publish - -Timeline/ClientApp/dist -Timeline/ClientApp/node_modules -Timeline/ClientApp/.yarn/* -!Timeline/ClientApp/.yarn/releases -!Timeline/ClientApp/.yarn/plugins -!Timeline/ClientApp/.yarn/cache -!Timeline/ClientApp/.yarn/sdks -!Timeline/ClientApp/.yarn/versions +.git + +art-src + +BackEnd/.vs +BackEnd/Timeline/bin +BackEnd/Timeline/obj +BackEnd/Timeline.ErrorCodes/bin +BackEnd/Timeline.ErrorCodes/obj +BackEnd/Timeline.ErrorCodes.CodeGenerator/bin +BackEnd/Timeline.ErrorCodes.CodeGenerator/obj +BackEnd/Timeline.Tests/bin +BackEnd/Timeline.Tests/obj +BackEnd/Timeline/publish + +FrontEnd/dist +FrontEnd/node_modules +FrontEnd/.yarn/* +!FrontEnd/.yarn/releases +!FrontEnd/.yarn/plugins +!FrontEnd/.yarn/cache +!FrontEnd/.yarn/sdks +!FrontEnd/.yarn/versions diff --git a/.github/workflows/back-ci.yaml b/.github/workflows/back-ci.yaml index a7f4f2d0..5b4609f7 100644 --- a/.github/workflows/back-ci.yaml +++ b/.github/workflows/back-ci.yaml @@ -3,17 +3,21 @@ name: Back End CI on: push: branches: [master] - paths-ignore: - - "Timeline/ClientApp/**" + paths: + - "BackEnd/**" pull_request: branches: [master] - paths-ignore: - - "Timeline/ClientApp/**" + paths: + - "BackEnd/**" jobs: build: name: Build runs-on: ubuntu-latest + defaults: + run: + shell: bash + working-directory: BackEnd env: ASPNETCORE_ENVIRONMENT: "Development" NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages @@ -25,7 +29,7 @@ jobs: uses: actions/cache@v2 with: path: ${{ env.NUGET_PACKAGES }} - key: nuget-${{ runner.os }}-${{ hashFiles('**/packages.lock.json') }} + key: nuget-${{ runner.os }}-${{ hashFiles('BackEnd/**/packages.lock.json') }} restore-keys: | nuget-${{ runner.os }}- @@ -42,4 +46,4 @@ jobs: if: ${{ success() || failure() }} with: name: test-result - path: Timeline.Tests/TestResults/ + path: BackEnd/Timeline.Tests/TestResults/ diff --git a/.github/workflows/front-ci.yaml b/.github/workflows/front-ci.yaml index b549479a..93581dad 100644 --- a/.github/workflows/front-ci.yaml +++ b/.github/workflows/front-ci.yaml @@ -4,11 +4,11 @@ on: push: branches: [master] paths: - - "Timeline/ClientApp/**" + - "FrontEnd/**" pull_request: branches: [master] paths: - - "Timeline/ClientApp/**" + - "FrontEnd/**" jobs: build: @@ -17,15 +17,15 @@ jobs: defaults: run: shell: bash - working-directory: Timeline/ClientApp + working-directory: FrontEnd steps: - uses: actions/checkout@v2 - - name: Use Node.js 14 + - name: Use Node.js 15 uses: actions/setup-node@v1 with: - node-version: "14" + node-version: "15" - name: Restore Packages run: yarn @@ -40,4 +40,4 @@ jobs: uses: actions/upload-artifact@v2 with: name: dist - path: Timeline/ClientApp/dist/ + path: FrontEnd/dist/ diff --git a/.vscode/settings.json b/.vscode/settings.json index f65e4358..7550c6fa 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ -{ - "yaml.schemas": { - "https://json.schemastore.org/github-workflow": "/.github/workflows" - } +{ + "yaml.schemas": { + "https://json.schemastore.org/github-workflow": "/.github/workflows" + } } \ No newline at end of file diff --git a/BackEnd/Directory.Build.props b/BackEnd/Directory.Build.props new file mode 100644 index 00000000..5030ba17 --- /dev/null +++ b/BackEnd/Directory.Build.props @@ -0,0 +1,6 @@ + + + true + true + + diff --git a/BackEnd/Nuget.Config b/BackEnd/Nuget.Config new file mode 100644 index 00000000..e219de2b --- /dev/null +++ b/BackEnd/Nuget.Config @@ -0,0 +1,7 @@ + + + + + + + diff --git a/BackEnd/Timeline.ErrorCodes.CodeGenerator/Program.cs b/BackEnd/Timeline.ErrorCodes.CodeGenerator/Program.cs new file mode 100644 index 00000000..84ab5908 --- /dev/null +++ b/BackEnd/Timeline.ErrorCodes.CodeGenerator/Program.cs @@ -0,0 +1,77 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace Timeline.ErrorCodes.CodeGenerator +{ + class Program + { + static void Main(string[] args) + { + string Indent(int n) + { + const string indent = " "; + return string.Concat(Enumerable.Repeat(indent, n)); + } + + StringBuilder code = new StringBuilder(); + + code.AppendLine("using static Timeline.Resources.Messages;"); + code.AppendLine(); + code.AppendLine("namespace Timeline.Models.Http"); + code.AppendLine("{"); + + int depth = 1; + + void RecursiveAddErrorCode(Type type, bool root) + { + code.AppendLine($"{Indent(depth)}public static class {(root ? "ErrorResponse" : type.Name)}"); + code.AppendLine($"{Indent(depth)}{{"); + + foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy) + .Where(fi => fi.IsLiteral && !fi.IsInitOnly && fi.FieldType == typeof(int))) + { + var path = type.FullName.Replace("+", ".").Replace("Timeline.Models.Http.ErrorCodes.", "") + "." + field.Name; + + code.AppendLine($"{Indent(depth + 1)}public static CommonResponse {field.Name}(params object?[] formatArgs)"); + code.AppendLine($"{Indent(depth + 1)}{{"); + code.AppendLine($"{Indent(depth + 2)}return new CommonResponse({"ErrorCodes." + path}, string.Format({path.Replace(".", "_")}, formatArgs));"); + code.AppendLine($"{Indent(depth + 1)}}}"); + code.AppendLine(); + code.AppendLine($"{Indent(depth + 1)}public static CommonResponse CustomMessage_{field.Name}(string message, params object?[] formatArgs)"); + code.AppendLine($"{Indent(depth + 1)}{{"); + code.AppendLine($"{Indent(depth + 2)}return new CommonResponse({"ErrorCodes." + path}, string.Format(message, formatArgs));"); + code.AppendLine($"{Indent(depth + 1)}}}"); + code.AppendLine(); + } + + depth += 1; + + foreach (var nestedType in type.GetNestedTypes()) + { + RecursiveAddErrorCode(nestedType, false); + } + + depth -= 1; + + code.AppendLine($"{Indent(depth)}}}"); + code.AppendLine(); + } + + RecursiveAddErrorCode(typeof(Timeline.Models.Http.ErrorCodes), true); + + code.AppendLine("}"); + + var generatedCode = code.ToString(); + + Console.WriteLine(generatedCode); + + TextCopy.ClipboardService.SetText(generatedCode); + var oldColor = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine("Code has copied to clipboard!"); + Console.ForegroundColor = oldColor; + } + } +} diff --git a/BackEnd/Timeline.ErrorCodes.CodeGenerator/Timeline.ErrorCodes.CodeGenerator.csproj b/BackEnd/Timeline.ErrorCodes.CodeGenerator/Timeline.ErrorCodes.CodeGenerator.csproj new file mode 100644 index 00000000..c8eb97f3 --- /dev/null +++ b/BackEnd/Timeline.ErrorCodes.CodeGenerator/Timeline.ErrorCodes.CodeGenerator.csproj @@ -0,0 +1,16 @@ + + + + Exe + netcoreapp3.1 + + + + + + + + + + + diff --git a/BackEnd/Timeline.ErrorCodes.CodeGenerator/packages.lock.json b/BackEnd/Timeline.ErrorCodes.CodeGenerator/packages.lock.json new file mode 100644 index 00000000..69cfee1e --- /dev/null +++ b/BackEnd/Timeline.ErrorCodes.CodeGenerator/packages.lock.json @@ -0,0 +1,24 @@ +{ + "version": 1, + "dependencies": { + ".NETCoreApp,Version=v3.1": { + "TextCopy": { + "type": "Direct", + "requested": "[4.2.0, )", + "resolved": "4.2.0", + "contentHash": "NY2UAFIjBJj+3aABP5tyO6ooEdkJxIGtwRNqvMQKLmyIeZiyGvM4XYbkKNntyQlhyFhhfBww05C3D/0DdimfaQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.4" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "3.1.4", + "contentHash": "AceHamXNKDMDwIoZqEoApLp8s3935wSC3VXrPaRWa0wWOaEcYdDlo1nWQ1zLiezoDmpJzV7FqDm53E0Ty/hEMg==" + }, + "timeline.errorcodes": { + "type": "Project" + } + } + } +} \ No newline at end of file diff --git a/BackEnd/Timeline.ErrorCodes/ErrorCodes.cs b/BackEnd/Timeline.ErrorCodes/ErrorCodes.cs new file mode 100644 index 00000000..91e0c1fd --- /dev/null +++ b/BackEnd/Timeline.ErrorCodes/ErrorCodes.cs @@ -0,0 +1,66 @@ +namespace Timeline.Models.Http +{ + /// + /// All error code constants. + /// + /// + /// Format: 1bbbccdd + /// + public static class ErrorCodes + { + public static class Common + { + public const int InvalidModel = 1_000_0001; + public const int Forbid = 1_000_0002; + public const int UnknownEndpoint = 1_000_0003; + + public static class Header + { + public const int IfNonMatch_BadFormat = 1_000_01_01; + } + + public static class Content + { + public const int TooBig = 1_000_11_01; + } + } + + public static class UserCommon + { + public const int NotExist = 1_001_0001; + } + + public static class TokenController + { + public const int Create_BadCredential = 1_101_01_01; + public const int Verify_BadFormat = 1_101_02_01; + public const int Verify_UserNotExist = 1_101_02_02; + public const int Verify_OldVersion = 1_101_02_03; + public const int Verify_TimeExpired = 1_101_02_04; + } + + public static class UserController + { + public const int UsernameConflict = 1_102_01_01; + public const int ChangePassword_BadOldPassword = 1_102_02_01; + } + + public static class UserAvatar + { + public const int BadFormat_CantDecode = 1_103_00_01; + public const int BadFormat_UnmatchedFormat = 1_103_00_02; + public const int BadFormat_BadSize = 1_103_00_03; + } + + public static class TimelineController + { + public const int NameConflict = 1_104_01_01; + public const int NotExist = 1_104_02_01; + public const int MemberPut_NotExist = 1_104_03_01; + public const int QueryRelateNotExist = 1_104_04_01; + public const int PostNotExist = 1_104_05_01; + public const int PostNoData = 1_104_05_02; + } + } +} + diff --git a/BackEnd/Timeline.ErrorCodes/Timeline.ErrorCodes.csproj b/BackEnd/Timeline.ErrorCodes/Timeline.ErrorCodes.csproj new file mode 100644 index 00000000..01ca2568 --- /dev/null +++ b/BackEnd/Timeline.ErrorCodes/Timeline.ErrorCodes.csproj @@ -0,0 +1,7 @@ + + + + netcoreapp3.1 + + + diff --git a/BackEnd/Timeline.ErrorCodes/packages.lock.json b/BackEnd/Timeline.ErrorCodes/packages.lock.json new file mode 100644 index 00000000..dabf86bc --- /dev/null +++ b/BackEnd/Timeline.ErrorCodes/packages.lock.json @@ -0,0 +1,6 @@ +{ + "version": 1, + "dependencies": { + ".NETCoreApp,Version=v3.1": {} + } +} \ No newline at end of file diff --git a/BackEnd/Timeline.Tests/ErrorCodeTest.cs b/BackEnd/Timeline.Tests/ErrorCodeTest.cs new file mode 100644 index 00000000..258ebf4e --- /dev/null +++ b/BackEnd/Timeline.Tests/ErrorCodeTest.cs @@ -0,0 +1,53 @@ +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Timeline.Models.Http; +using Xunit; +using Xunit.Abstractions; + +namespace Timeline.Tests +{ + public class ErrorCodeTest + { + private readonly ITestOutputHelper _output; + + public ErrorCodeTest(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void ShouldWork() + { + var errorCodes = new Dictionary(); + + void RecursiveCheckErrorCode(Type type) + { + foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy) + .Where(fi => fi.IsLiteral && !fi.IsInitOnly && fi.FieldType == typeof(int))) + { + var name = type.FullName + "." + field.Name; + var value = (int)field.GetRawConstantValue(); + _output.WriteLine($"Find error code {name} , value is {value}."); + + value.Should().BeInRange(1000_0000, 9999_9999, "Error code should have exactly 8 digits."); + + errorCodes.Should().NotContainKey(value, + "identical error codes are found and conflict paths are {0} and {1}", + name, errorCodes.GetValueOrDefault(value)); + + errorCodes.Add(value, name); + } + + foreach (var nestedType in type.GetNestedTypes()) + { + RecursiveCheckErrorCode(nestedType); + } + } + + RecursiveCheckErrorCode(typeof(ErrorCodes)); + } + } +} diff --git a/BackEnd/Timeline.Tests/GlobalSuppressions.cs b/BackEnd/Timeline.Tests/GlobalSuppressions.cs new file mode 100644 index 00000000..0f873033 --- /dev/null +++ b/BackEnd/Timeline.Tests/GlobalSuppressions.cs @@ -0,0 +1,16 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "This is not a UI application.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Tests name have underscores.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Test may catch all exceptions.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "Test classes can be nested.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "This is redundant.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1063:Implement IDisposable Correctly", Justification = "Test classes do not need to implement it that way.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize", Justification = "Test classes do not need to implement it that way.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2234:Pass system uri objects instead of strings", Justification = "I really don't understand this rule.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "Tests do not need make strings resources.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1054:Uri parameters should not be strings", Justification = "That's unnecessary.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1056:Uri properties should not be strings", Justification = "That's unnecessary.")] diff --git a/BackEnd/Timeline.Tests/Helpers/AsyncFunctionAssertionsExtensions.cs b/BackEnd/Timeline.Tests/Helpers/AsyncFunctionAssertionsExtensions.cs new file mode 100644 index 00000000..b78309c0 --- /dev/null +++ b/BackEnd/Timeline.Tests/Helpers/AsyncFunctionAssertionsExtensions.cs @@ -0,0 +1,16 @@ +using FluentAssertions; +using FluentAssertions.Primitives; +using FluentAssertions.Specialized; +using System; +using System.Threading.Tasks; + +namespace Timeline.Tests.Helpers +{ + public static class AsyncFunctionAssertionsExtensions + { + public static async Task> ThrowAsync(this AsyncFunctionAssertions assertions, Type exceptionType, string because = "", params object[] becauseArgs) + { + return (await assertions.ThrowAsync(because, becauseArgs)).Which.Should().BeAssignableTo(exceptionType); + } + } +} diff --git a/BackEnd/Timeline.Tests/Helpers/CacheTestHelper.cs b/BackEnd/Timeline.Tests/Helpers/CacheTestHelper.cs new file mode 100644 index 00000000..b3709a28 --- /dev/null +++ b/BackEnd/Timeline.Tests/Helpers/CacheTestHelper.cs @@ -0,0 +1,64 @@ +using FluentAssertions; +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Timeline.Models.Http; + +namespace Timeline.Tests.Helpers +{ + public static class CacheTestHelper + { + public static async Task TestCache(HttpClient client, string getUrl) + { + EntityTagHeaderValue eTag; + { + var res = await client.GetAsync(getUrl); + res.Should().HaveStatusCode(200); + var cacheControlHeader = res.Headers.CacheControl; + cacheControlHeader.NoCache.Should().BeTrue(); + cacheControlHeader.NoStore.Should().BeFalse(); + cacheControlHeader.Private.Should().BeTrue(); + cacheControlHeader.Public.Should().BeFalse(); + cacheControlHeader.MustRevalidate.Should().BeTrue(); + cacheControlHeader.MaxAge.Should().NotBeNull().And.Be(TimeSpan.FromDays(14)); + eTag = res.Headers.ETag; + } + + { + using var request = new HttpRequestMessage() + { + RequestUri = new Uri(client.BaseAddress, getUrl), + Method = HttpMethod.Get, + }; + request.Headers.TryAddWithoutValidation("If-None-Match", "\"dsdfd"); + var res = await client.SendAsync(request); + res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.HaveCommonBody(ErrorCodes.Common.Header.IfNonMatch_BadFormat); + } + + { + using var request = new HttpRequestMessage() + { + RequestUri = new Uri(client.BaseAddress, getUrl), + Method = HttpMethod.Get, + }; + request.Headers.TryAddWithoutValidation("If-None-Match", "\"aaa\""); + var res = await client.SendAsync(request); + res.Should().HaveStatusCode(HttpStatusCode.OK); + } + + { + using var request = new HttpRequestMessage() + { + RequestUri = new Uri(client.BaseAddress, getUrl), + Method = HttpMethod.Get, + }; + request.Headers.Add("If-None-Match", eTag.ToString()); + var res = await client.SendAsync(request); + res.Should().HaveStatusCode(HttpStatusCode.NotModified); + } + } + } +} diff --git a/BackEnd/Timeline.Tests/Helpers/HttpClientExtensions.cs b/BackEnd/Timeline.Tests/Helpers/HttpClientExtensions.cs new file mode 100644 index 00000000..6513bbe7 --- /dev/null +++ b/BackEnd/Timeline.Tests/Helpers/HttpClientExtensions.cs @@ -0,0 +1,51 @@ +using Newtonsoft.Json; +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Mime; +using System.Text; +using System.Threading.Tasks; + +namespace Timeline.Tests.Helpers +{ + public static class HttpClientExtensions + { + public static Task PatchAsJsonAsync(this HttpClient client, string url, T body) + { + return client.PatchAsJsonAsync(new Uri(url, UriKind.RelativeOrAbsolute), body); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")] + public static Task PatchAsJsonAsync(this HttpClient client, Uri url, T body) + { + return client.PatchAsync(url, new StringContent( + JsonConvert.SerializeObject(body), Encoding.UTF8, MediaTypeNames.Application.Json)); + } + + public static Task PutByteArrayAsync(this HttpClient client, string url, byte[] body, string mimeType) + { + return client.PutByteArrayAsync(new Uri(url, UriKind.RelativeOrAbsolute), body, mimeType); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")] + public static Task PutByteArrayAsync(this HttpClient client, Uri url, byte[] body, string mimeType) + { + var content = new ByteArrayContent(body); + content.Headers.ContentLength = body.Length; + content.Headers.ContentType = new MediaTypeHeaderValue(mimeType); + return client.PutAsync(url, content); + } + + public static Task PutStringAsync(this HttpClient client, string url, string body, string mimeType = null) + { + return client.PutStringAsync(new Uri(url, UriKind.RelativeOrAbsolute), body, mimeType); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")] + public static Task PutStringAsync(this HttpClient client, Uri url, string body, string mimeType = null) + { + var content = new StringContent(body, Encoding.UTF8, mimeType ?? MediaTypeNames.Text.Plain); + return client.PutAsync(url, content); + } + } +} diff --git a/BackEnd/Timeline.Tests/Helpers/HttpResponseExtensions.cs b/BackEnd/Timeline.Tests/Helpers/HttpResponseExtensions.cs new file mode 100644 index 00000000..2bd497f1 --- /dev/null +++ b/BackEnd/Timeline.Tests/Helpers/HttpResponseExtensions.cs @@ -0,0 +1,35 @@ +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Timeline.Models.Converters; +using Timeline.Models.Http; + +namespace Timeline.Tests.Helpers +{ + public static class HttpResponseExtensions + { + public static JsonSerializerOptions JsonSerializerOptions { get; } + + static HttpResponseExtensions() + { + JsonSerializerOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + JsonSerializerOptions.Converters.Add(new JsonDateTimeConverter()); + } + + public static async Task ReadBodyAsJsonAsync(this HttpResponseMessage response) + { + var stream = await response.Content.ReadAsStreamAsync(); + return await JsonSerializer.DeserializeAsync(stream, JsonSerializerOptions); + } + + public static Task ReadBodyAsCommonResponseAsync(this HttpResponseMessage response) + { + return response.ReadBodyAsJsonAsync(); + } + } +} diff --git a/BackEnd/Timeline.Tests/Helpers/ImageHelper.cs b/BackEnd/Timeline.Tests/Helpers/ImageHelper.cs new file mode 100644 index 00000000..9bed0917 --- /dev/null +++ b/BackEnd/Timeline.Tests/Helpers/ImageHelper.cs @@ -0,0 +1,26 @@ +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.PixelFormats; +using System.IO; + +namespace Timeline.Tests.Helpers +{ + public static class ImageHelper + { + public static byte[] CreatePngWithSize(int width, int height) + { + using var image = new Image(width, height); + using var stream = new MemoryStream(); + image.SaveAsPng(stream); + return stream.ToArray(); + } + + public static byte[] CreateImageWithSize(int width, int height, IImageFormat format) + { + using var image = new Image(width, height); + using var stream = new MemoryStream(); + image.Save(stream, format); + return stream.ToArray(); + } + } +} diff --git a/BackEnd/Timeline.Tests/Helpers/ParameterInfoAssertions.cs b/BackEnd/Timeline.Tests/Helpers/ParameterInfoAssertions.cs new file mode 100644 index 00000000..d3e5a41e --- /dev/null +++ b/BackEnd/Timeline.Tests/Helpers/ParameterInfoAssertions.cs @@ -0,0 +1,60 @@ +using FluentAssertions; +using FluentAssertions.Execution; +using FluentAssertions.Formatting; +using FluentAssertions.Primitives; +using System; +using System.Reflection; + +namespace Timeline.Tests.Helpers +{ + public class ParameterInfoValueFormatter : IValueFormatter + { + public bool CanHandle(object value) + { + return value is ParameterInfo; + } + + public string Format(object value, FormattingContext context, FormatChild formatChild) + { + var param = (ParameterInfo)value; + return $"{param.Member.DeclaringType.FullName}.{param.Member.Name}#{param.Name}"; + } + } + + public class ParameterInfoAssertions : ReferenceTypeAssertions + { + static ParameterInfoAssertions() + { + Formatter.AddFormatter(new ParameterInfoValueFormatter()); + } + + public ParameterInfoAssertions(ParameterInfo parameterInfo) + { + Subject = parameterInfo; + } + + protected override string Identifier => "parameter"; + + public AndWhichConstraint BeDecoratedWith(string because = "", params object[] becauseArgs) + where TAttribute : Attribute + { + var attribute = Subject.GetCustomAttribute(false); + + Execute.Assertion + .BecauseOf(because, becauseArgs) + .ForCondition(attribute != null) + .FailWith("Expected {0} {1} to be decorated with {2}{reason}, but that attribute was not found.", + Identifier, Subject, typeof(TAttribute).FullName); + + return new AndWhichConstraint(this, attribute); + } + } + + public static class ParameterInfoAssertionExtensions + { + public static ParameterInfoAssertions Should(this ParameterInfo parameterInfo) + { + return new ParameterInfoAssertions(parameterInfo); + } + } +} diff --git a/BackEnd/Timeline.Tests/Helpers/ReflectionHelper.cs b/BackEnd/Timeline.Tests/Helpers/ReflectionHelper.cs new file mode 100644 index 00000000..3f6036e3 --- /dev/null +++ b/BackEnd/Timeline.Tests/Helpers/ReflectionHelper.cs @@ -0,0 +1,13 @@ +using System.Linq; +using System.Reflection; + +namespace Timeline.Tests.Helpers +{ + public static class ReflectionHelper + { + public static ParameterInfo GetParameter(this MethodInfo methodInfo, string name) + { + return methodInfo.GetParameters().Where(p => p.Name == name).Single(); + } + } +} diff --git a/BackEnd/Timeline.Tests/Helpers/ResponseAssertions.cs b/BackEnd/Timeline.Tests/Helpers/ResponseAssertions.cs new file mode 100644 index 00000000..024732f5 --- /dev/null +++ b/BackEnd/Timeline.Tests/Helpers/ResponseAssertions.cs @@ -0,0 +1,172 @@ +using FluentAssertions; +using FluentAssertions.Execution; +using FluentAssertions.Formatting; +using FluentAssertions.Primitives; +using System; +using System.Globalization; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Timeline.Models.Converters; +using Timeline.Models.Http; + +namespace Timeline.Tests.Helpers +{ + public class HttpResponseMessageValueFormatter : IValueFormatter + { + public bool CanHandle(object value) + { + return value is HttpResponseMessage; + } + + public string Format(object value, FormattingContext context, FormatChild formatChild) + { + string newline = context.UseLineBreaks ? Environment.NewLine : ""; + string padding = new string('\t', context.Depth); + + var res = (HttpResponseMessage)value; + + var builder = new StringBuilder(); + builder.Append($"{newline}{padding} Status Code: {res.StatusCode} ; Body: "); + + try + { + var task = res.Content.ReadAsStringAsync(); + task.Wait(); + var body = task.Result; + if (body.Length > 40) + { + body = body[0..40] + " ..."; + } + builder.Append(body); + } + catch (AggregateException) + { + builder.Append("NOT A STRING."); + } + + return builder.ToString(); + } + } + + public class HttpResponseMessageAssertions + : ReferenceTypeAssertions + { + static HttpResponseMessageAssertions() + { + Formatter.AddFormatter(new HttpResponseMessageValueFormatter()); + } + + public HttpResponseMessageAssertions(HttpResponseMessage instance) + { + Subject = instance; + } + + protected override string Identifier => "HttpResponseMessage"; + + public AndConstraint HaveStatusCode(int expected, string because = "", params object[] becauseArgs) + { + return HaveStatusCode((HttpStatusCode)expected, because, becauseArgs); + } + + public AndConstraint HaveStatusCode(HttpStatusCode expected, string because = "", params object[] becauseArgs) + { + Execute.Assertion.BecauseOf(because, becauseArgs) + .ForCondition(Subject.StatusCode == expected) + .FailWith("Expected status code of {context:HttpResponseMessage} to be {0}{reason}, but found {1}.", expected, Subject.StatusCode); + return new AndConstraint(this); + } + + public AndWhichConstraint HaveJsonBody(string because = "", params object[] becauseArgs) + { + var a = Execute.Assertion.BecauseOf(because, becauseArgs); + string body; + try + { + var task = Subject.Content.ReadAsStringAsync(); + task.Wait(); + body = task.Result; + } + catch (AggregateException e) + { + a.FailWith("Expected response body of {context:HttpResponseMessage} to be json string{reason}, but failed to read it or it was not a string. Exception is {0}.", e.InnerExceptions); + return new AndWhichConstraint(this, null); + } + + + try + { + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + options.Converters.Add(new JsonStringEnumConverter()); + options.Converters.Add(new JsonDateTimeConverter()); + + var result = JsonSerializer.Deserialize(body, options); + + return new AndWhichConstraint(this, result); + } + catch (JsonException e) + { + a.FailWith("Expected response body of {context:HttpResponseMessage} to be json string{reason}, but failed to deserialize it. Exception is {0}.", e); + return new AndWhichConstraint(this, null); + } + } + } + + public static class AssertionResponseExtensions + { + public static HttpResponseMessageAssertions Should(this HttpResponseMessage instance) + { + return new HttpResponseMessageAssertions(instance); + } + + public static AndWhichConstraint HaveCommonBody(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs) + { + return assertions.HaveJsonBody(because, becauseArgs); + } + + public static void HaveCommonBody(this HttpResponseMessageAssertions assertions, int code, string message = null, params object[] messageArgs) + { + message = string.IsNullOrEmpty(message) ? "" : ", " + string.Format(CultureInfo.CurrentCulture, message, messageArgs); + var body = assertions.HaveCommonBody("Response body should be CommonResponse{0}", message).Which; + body.Code.Should().Be(code, "Response body code is not the specified one{0}", message); + } + + public static AndWhichConstraint> HaveCommonDataBody(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs) + { + return assertions.HaveJsonBody>(because, becauseArgs); + } + + public static void BePut(this HttpResponseMessageAssertions assertions, bool create, string because = "", params object[] becauseArgs) + { + var body = assertions.HaveStatusCode(create ? 201 : 200, because, becauseArgs) + .And.HaveJsonBody(because, becauseArgs) + .Which; + body.Code.Should().Be(0); + body.Data.Create.Should().Be(create); + } + + public static void BeDelete(this HttpResponseMessageAssertions assertions, bool delete, string because = "", params object[] becauseArgs) + { + var body = assertions.HaveStatusCode(200, because, becauseArgs) + .And.HaveJsonBody(because, becauseArgs) + .Which; + body.Code.Should().Be(0); + body.Data.Delete.Should().Be(delete); + } + + public static void BeInvalidModel(this HttpResponseMessageAssertions assertions, string message = null) + { + message = string.IsNullOrEmpty(message) ? "" : ", " + message; + assertions.HaveStatusCode(400, "Invalid Model Error must have 400 status code{0}", message) + .And.HaveCommonBody("Invalid Model Error must have CommonResponse body{0}", message) + .Which.Code.Should().Be(ErrorCodes.Common.InvalidModel, + "Invalid Model Error must have code {0} in body{1}", + ErrorCodes.Common.InvalidModel, message); + } + } +} diff --git a/BackEnd/Timeline.Tests/Helpers/TestApplication.cs b/BackEnd/Timeline.Tests/Helpers/TestApplication.cs new file mode 100644 index 00000000..684ffe2c --- /dev/null +++ b/BackEnd/Timeline.Tests/Helpers/TestApplication.cs @@ -0,0 +1,72 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Timeline.Configs; +using Timeline.Entities; +using Xunit; + +namespace Timeline.Tests.Helpers +{ + public class TestApplication : IAsyncLifetime + { + public TestDatabase Database { get; } + + public IHost Host { get; private set; } + + public string WorkDir { get; private set; } + + public TestApplication() + { + Database = new TestDatabase(false); + } + + public async Task InitializeAsync() + { + await Database.InitializeAsync(); + + WorkDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(WorkDir); + + Host = await Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder() + .ConfigureAppConfiguration((context, config) => + { + config.AddInMemoryCollection(new Dictionary + { + [ApplicationConfiguration.UseMockFrontEndKey] = "true", + ["WorkDir"] = WorkDir + }); + }) + .ConfigureServices(services => + { + services.AddDbContext(options => + { + options.UseSqlite(Database.Connection); + }); + }) + .ConfigureWebHost(webBuilder => + { + webBuilder + .UseTestServer() + .UseStartup(); + }) + .StartAsync(); + } + + public async Task DisposeAsync() + { + await Host.StopAsync(); + Host.Dispose(); + + Directory.Delete(WorkDir, true); + + await Database.DisposeAsync(); + } + } +} diff --git a/BackEnd/Timeline.Tests/Helpers/TestClock.cs b/BackEnd/Timeline.Tests/Helpers/TestClock.cs new file mode 100644 index 00000000..34adb245 --- /dev/null +++ b/BackEnd/Timeline.Tests/Helpers/TestClock.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Services; + +namespace Timeline.Tests.Helpers +{ + public class TestClock : IClock + { + private DateTime? _currentTime; + + public DateTime GetCurrentTime() + { + return _currentTime ?? DateTime.UtcNow; + } + + public void SetCurrentTime(DateTime? mockTime) + { + _currentTime = mockTime; + } + + public DateTime SetMockCurrentTime() + { + var time = new DateTime(3000, 1, 1, 1, 1, 1, DateTimeKind.Utc); + _currentTime = time; + return time; + } + + public DateTime ForwardCurrentTime() + { + return ForwardCurrentTime(TimeSpan.FromDays(1)); + } + + public DateTime ForwardCurrentTime(TimeSpan timeSpan) + { + if (_currentTime == null) + return SetMockCurrentTime(); + _currentTime += timeSpan; + return _currentTime.Value; + } + } +} diff --git a/BackEnd/Timeline.Tests/Helpers/TestDatabase.cs b/BackEnd/Timeline.Tests/Helpers/TestDatabase.cs new file mode 100644 index 00000000..f0c26180 --- /dev/null +++ b/BackEnd/Timeline.Tests/Helpers/TestDatabase.cs @@ -0,0 +1,76 @@ +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging.Abstractions; +using System.Threading.Tasks; +using Timeline.Entities; +using Timeline.Migrations; +using Timeline.Models; +using Timeline.Services; +using Xunit; + +namespace Timeline.Tests.Helpers +{ + public class TestDatabase : IAsyncLifetime + { + private readonly bool _createUser; + + public TestDatabase(bool createUser = true) + { + _createUser = createUser; + Connection = new SqliteConnection("Data Source=:memory:;"); + } + + public async Task InitializeAsync() + { + await Connection.OpenAsync(); + + using (var context = CreateContext()) + { + await context.Database.EnsureCreatedAsync(); + context.JwtToken.Add(new JwtTokenEntity + { + Key = JwtTokenGenerateHelper.GenerateKey() + }); + await context.SaveChangesAsync(); + + if (_createUser) + { + var passwordService = new PasswordService(); + var userService = new UserService(NullLogger.Instance, context, passwordService, new Clock()); + + await userService.CreateUser(new User + { + Username = "admin", + Password = "adminpw", + Administrator = true, + Nickname = "administrator" + }); + + await userService.CreateUser(new User + { + Username = "user", + Password = "userpw", + Administrator = false, + Nickname = "imuser" + }); + } + } + } + + public async Task DisposeAsync() + { + await Connection.CloseAsync(); + await Connection.DisposeAsync(); + } + + public SqliteConnection Connection { get; } + + public DatabaseContext CreateContext() + { + var options = new DbContextOptionsBuilder() + .UseSqlite(Connection).Options; + + return new DatabaseContext(options); + } + } +} diff --git a/BackEnd/Timeline.Tests/IntegratedTests/AuthorizationTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/AuthorizationTest.cs new file mode 100644 index 00000000..38071394 --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/AuthorizationTest.cs @@ -0,0 +1,52 @@ +using FluentAssertions; +using System.Net; +using System.Threading.Tasks; +using Timeline.Tests.Helpers; +using Xunit; + +namespace Timeline.Tests.IntegratedTests +{ + public class AuthorizationTest : IntegratedTestBase + { + private const string BaseUrl = "testing/auth/"; + private const string AuthorizeUrl = BaseUrl + "Authorize"; + private const string UserUrl = BaseUrl + "User"; + private const string AdminUrl = BaseUrl + "Admin"; + + [Fact] + public async Task UnauthenticationTest() + { + using var client = await CreateDefaultClient(); + var response = await client.GetAsync(AuthorizeUrl); + response.Should().HaveStatusCode(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task AuthenticationTest() + { + using var client = await CreateClientAsUser(); + var response = await client.GetAsync(AuthorizeUrl); + response.Should().HaveStatusCode(HttpStatusCode.OK); + } + + [Fact] + public async Task UserAuthorizationTest() + { + using var client = await CreateClientAsUser(); + var response1 = await client.GetAsync(UserUrl); + response1.Should().HaveStatusCode(HttpStatusCode.OK); + var response2 = await client.GetAsync(AdminUrl); + response2.Should().HaveStatusCode(HttpStatusCode.Forbidden); + } + + [Fact] + public async Task AdminAuthorizationTest() + { + using var client = await CreateClientAsAdministrator(); + var response1 = await client.GetAsync(UserUrl); + response1.Should().HaveStatusCode(HttpStatusCode.OK); + var response2 = await client.GetAsync(AdminUrl); + response2.Should().HaveStatusCode(HttpStatusCode.OK); + } + } +} diff --git a/BackEnd/Timeline.Tests/IntegratedTests/FrontEndTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/FrontEndTest.cs new file mode 100644 index 00000000..39a6e545 --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/FrontEndTest.cs @@ -0,0 +1,29 @@ +using FluentAssertions; +using System.Net.Mime; +using System.Threading.Tasks; +using Timeline.Tests.Helpers; +using Xunit; + +namespace Timeline.Tests.IntegratedTests +{ + public class FrontEndTest : IntegratedTestBase + { + [Fact] + public async Task Index() + { + using var client = await CreateDefaultClient(false); + var res = await client.GetAsync("index.html"); + res.Should().HaveStatusCode(200); + res.Content.Headers.ContentType.MediaType.Should().Be(MediaTypeNames.Text.Html); + } + + [Fact] + public async Task Fallback() + { + using var client = await CreateDefaultClient(false); + var res = await client.GetAsync("aaaaaaaaaaaaaaa"); + res.Should().HaveStatusCode(200); + res.Content.Headers.ContentType.MediaType.Should().Be(MediaTypeNames.Text.Html); + } + } +} diff --git a/BackEnd/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs b/BackEnd/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs new file mode 100644 index 00000000..7cf27297 --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs @@ -0,0 +1,164 @@ +using FluentAssertions; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Timeline.Models; +using Timeline.Models.Converters; +using Timeline.Models.Http; +using Timeline.Services; +using Timeline.Tests.Helpers; +using Xunit; + +namespace Timeline.Tests.IntegratedTests +{ + public abstract class IntegratedTestBase : IAsyncLifetime + { + protected TestApplication TestApp { get; } + + public IReadOnlyList UserInfos { get; private set; } + + private readonly int _userCount; + + public IntegratedTestBase() : this(1) + { + + } + + public IntegratedTestBase(int userCount) + { + if (userCount < 0) + throw new ArgumentOutOfRangeException(nameof(userCount), userCount, "User count can't be negative."); + + _userCount = userCount; + + TestApp = new TestApplication(); + } + + protected virtual Task OnInitializeAsync() + { + return Task.CompletedTask; + } + + protected virtual Task OnDisposeAsync() + { + return Task.CompletedTask; + } + + protected virtual void OnDispose() + { + + } + + public async Task InitializeAsync() + { + await TestApp.InitializeAsync(); + + using (var scope = TestApp.Host.Services.CreateScope()) + { + var users = new List() + { + new User + { + Username = "admin", + Password = "adminpw", + Administrator = true, + Nickname = "administrator" + } + }; + + for (int i = 1; i <= _userCount; i++) + { + users.Add(new User + { + Username = $"user{i}", + Password = $"user{i}pw", + Administrator = false, + Nickname = $"imuser{i}" + }); + } + + var userInfoList = new List(); + + var userService = scope.ServiceProvider.GetRequiredService(); + foreach (var user in users) + { + await userService.CreateUser(user); + } + + using var client = await CreateDefaultClient(); + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + options.Converters.Add(new JsonStringEnumConverter()); + options.Converters.Add(new JsonDateTimeConverter()); + foreach (var user in users) + { + var s = await client.GetStringAsync($"users/{user.Username}"); + userInfoList.Add(JsonSerializer.Deserialize(s, options)); + } + + UserInfos = userInfoList; + } + + await OnInitializeAsync(); + } + + public async Task DisposeAsync() + { + await OnDisposeAsync(); + OnDispose(); + await TestApp.DisposeAsync(); + } + + public Task CreateDefaultClient(bool setApiBase = true) + { + var client = TestApp.Host.GetTestServer().CreateClient(); + if (setApiBase) + { + client.BaseAddress = new Uri(client.BaseAddress, "api/"); + } + return Task.FromResult(client); + } + + public async Task CreateClientWithCredential(string username, string password, bool setApiBase = true) + { + var client = TestApp.Host.GetTestServer().CreateClient(); + if (setApiBase) + { + client.BaseAddress = new Uri(client.BaseAddress, "api/"); + } + var response = await client.PostAsJsonAsync("token/create", + new CreateTokenRequest { Username = username, Password = password }); + var token = response.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which.Token; + client.DefaultRequestHeaders.Add("Authorization", "Bearer " + token); + return client; + } + + public Task CreateClientAs(int userNumber, bool setApiBase = true) + { + if (userNumber < 0) + return CreateDefaultClient(setApiBase); + if (userNumber == 0) + return CreateClientWithCredential("admin", "adminpw", setApiBase); + else + return CreateClientWithCredential($"user{userNumber}", $"user{userNumber}pw", setApiBase); + } + + public Task CreateClientAsAdministrator(bool setApiBase = true) + { + return CreateClientAs(0, setApiBase); + } + + public Task CreateClientAsUser(bool setApiBase = true) + { + return CreateClientAs(1, setApiBase); + } + } +} diff --git a/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs new file mode 100644 index 00000000..ec46b96a --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs @@ -0,0 +1,1523 @@ +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Png; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Timeline.Entities; +using Timeline.Models; +using Timeline.Models.Http; +using Timeline.Tests.Helpers; +using Xunit; + +namespace Timeline.Tests.IntegratedTests +{ + public static class TimelineHelper + { + public static TimelinePostContentInfo TextPostContent(string text) + { + return new TimelinePostContentInfo + { + Type = "text", + Text = text + }; + } + + public static TimelinePostCreateRequest TextPostCreateRequest(string text, DateTime? time = null) + { + return new TimelinePostCreateRequest + { + Content = new TimelinePostCreateRequestContent + { + Type = "text", + Text = text + }, + Time = time + }; + } + } + + public class TimelineTest : IntegratedTestBase + { + public TimelineTest() : base(3) + { + } + + protected override async Task OnInitializeAsync() + { + await CreateTestTimelines(); + } + + private List _testTimelines; + + private async Task CreateTestTimelines() + { + _testTimelines = new List(); + for (int i = 0; i <= 3; i++) + { + var client = await CreateClientAs(i); + var res = await client.PostAsJsonAsync("timelines", new TimelineCreateRequest { Name = $"t{i}" }); + var timelineInfo = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + _testTimelines.Add(timelineInfo); + } + } + + private static string CalculateUrlTail(string subpath, ICollection> query) + { + StringBuilder result = new StringBuilder(); + if (subpath != null) + { + if (!subpath.StartsWith("/", StringComparison.OrdinalIgnoreCase)) + result.Append('/'); + result.Append(subpath); + } + + if (query != null && query.Count != 0) + { + result.Append('?'); + foreach (var (key, value, index) in query.Select((pair, index) => (pair.Key, pair.Value, index))) + { + result.Append(WebUtility.UrlEncode(key)); + result.Append('='); + result.Append(WebUtility.UrlEncode(value)); + if (index != query.Count - 1) + result.Append('&'); + } + } + + return result.ToString(); + } + + private static string GeneratePersonalTimelineUrl(int id, string subpath = null, ICollection> query = null) + { + return $"timelines/@{(id == 0 ? "admin" : ("user" + id))}{CalculateUrlTail(subpath, query)}"; + } + + private static string GenerateOrdinaryTimelineUrl(int id, string subpath = null, ICollection> query = null) + { + return $"timelines/t{id}{CalculateUrlTail(subpath, query)}"; + } + + public delegate string TimelineUrlGenerator(int userId, string subpath = null, ICollection> query = null); + + public static IEnumerable TimelineUrlGeneratorData() + { + yield return new[] { new TimelineUrlGenerator(GeneratePersonalTimelineUrl) }; + yield return new[] { new TimelineUrlGenerator(GenerateOrdinaryTimelineUrl) }; + } + + private static string GeneratePersonalTimelineUrlByName(string name, string subpath = null) + { + return $"timelines/@{name}{(subpath == null ? "" : "/" + subpath)}"; + } + + private static string GenerateOrdinaryTimelineUrlByName(string name, string subpath = null) + { + return $"timelines/{name}{(subpath == null ? "" : "/" + subpath)}"; + } + + public static IEnumerable TimelineUrlByNameGeneratorData() + { + yield return new[] { new Func(GeneratePersonalTimelineUrlByName) }; + yield return new[] { new Func(GenerateOrdinaryTimelineUrlByName) }; + } + + [Fact] + public async Task TimelineGet_Should_Work() + { + using var client = await CreateDefaultClient(); + { + var res = await client.GetAsync("timelines/@user1"); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + body.Owner.Should().BeEquivalentTo(UserInfos[1]); + body.Visibility.Should().Be(TimelineVisibility.Register); + body.Description.Should().Be(""); + body.Members.Should().NotBeNull().And.BeEmpty(); + var links = body._links; + links.Should().NotBeNull(); + links.Self.Should().EndWith("timelines/@user1"); + links.Posts.Should().EndWith("timelines/@user1/posts"); + } + + { + var res = await client.GetAsync("timelines/t1"); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + body.Owner.Should().BeEquivalentTo(UserInfos[1]); + body.Visibility.Should().Be(TimelineVisibility.Register); + body.Description.Should().Be(""); + body.Members.Should().NotBeNull().And.BeEmpty(); + var links = body._links; + links.Should().NotBeNull(); + links.Self.Should().EndWith("timelines/t1"); + links.Posts.Should().EndWith("timelines/t1/posts"); + } + } + + [Fact] + public async Task TimelineList() + { + TimelineInfo user1Timeline; + + var client = await CreateDefaultClient(); + + { + var res = await client.GetAsync("timelines/@user1"); + user1Timeline = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + } + + { + var testResult = new List(); + testResult.Add(user1Timeline); + testResult.AddRange(_testTimelines); + + var res = await client.GetAsync("timelines"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody>() + .Which.Should().BeEquivalentTo(testResult); + } + } + + [Fact] + public async Task TimelineList_WithQuery() + { + var testResultRelate = new List(); + var testResultOwn = new List(); + var testResultJoin = new List(); + var testResultOwnPrivate = new List(); + var testResultRelatePublic = new List(); + var testResultRelateRegister = new List(); + var testResultJoinPrivate = new List(); + var testResultPublic = new List(); + + { + var client = await CreateClientAsUser(); + + { + var res = await client.PutAsync("timelines/@user1/members/user3", null); + res.Should().HaveStatusCode(200); + } + + { + var res = await client.PutAsync("timelines/t1/members/user3", null); + res.Should().HaveStatusCode(200); + } + + { + var res = await client.PatchAsJsonAsync("timelines/@user1", new TimelinePatchRequest { Visibility = TimelineVisibility.Public }); + res.Should().HaveStatusCode(200); + } + + { + var res = await client.PatchAsJsonAsync("timelines/t1", new TimelinePatchRequest { Visibility = TimelineVisibility.Register }); + res.Should().HaveStatusCode(200); + } + + { + var res = await client.GetAsync("timelines/@user1"); + var timeline = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + testResultRelate.Add(timeline); + testResultJoin.Add(timeline); + testResultRelatePublic.Add(timeline); + testResultPublic.Add(timeline); + } + + { + var res = await client.GetAsync("timelines/t1"); + var timeline = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + testResultRelate.Add(timeline); + testResultJoin.Add(timeline); + testResultRelateRegister.Add(timeline); + } + } + + { + var client = await CreateClientAs(2); + + { + var res = await client.PutAsync("timelines/@user2/members/user3", null); + res.Should().HaveStatusCode(200); + } + + { + var res = await client.PutAsync("timelines/t2/members/user3", null); + res.Should().HaveStatusCode(200); + } + + { + var res = await client.PatchAsJsonAsync("timelines/@user2", new TimelinePatchRequest { Visibility = TimelineVisibility.Register }); + res.Should().HaveStatusCode(200); + } + + { + var res = await client.PatchAsJsonAsync("timelines/t2", new TimelinePatchRequest { Visibility = TimelineVisibility.Private }); + res.Should().HaveStatusCode(200); + } + + { + var res = await client.GetAsync("timelines/@user2"); + var timeline = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + testResultRelate.Add(timeline); + testResultJoin.Add(timeline); + testResultRelateRegister.Add(timeline); + } + + { + var res = await client.GetAsync("timelines/t2"); + var timeline = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + testResultRelate.Add(timeline); + testResultJoin.Add(timeline); + testResultJoinPrivate.Add(timeline); + } + } + + { + var client = await CreateClientAs(3); + + { + var res = await client.PatchAsJsonAsync("timelines/@user3", new TimelinePatchRequest { Visibility = TimelineVisibility.Private }); + res.Should().HaveStatusCode(200); + } + + { + var res = await client.PatchAsJsonAsync("timelines/t3", new TimelinePatchRequest { Visibility = TimelineVisibility.Register }); + res.Should().HaveStatusCode(200); + } + + { + var res = await client.GetAsync("timelines/@user3"); + var timeline = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + testResultRelate.Add(timeline); + testResultOwn.Add(timeline); + testResultOwnPrivate.Add(timeline); + } + + { + var res = await client.GetAsync("timelines/t3"); + var timeline = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + testResultRelate.Add(timeline); + testResultOwn.Add(timeline); + testResultRelateRegister.Add(timeline); + } + } + + { + var client = await CreateClientAs(3); + { + var res = await client.GetAsync("timelines?relate=user3"); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody>() + .Which; + body.Should().BeEquivalentTo(testResultRelate); + } + + { + var res = await client.GetAsync("timelines?relate=user3&relateType=own"); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody>() + .Which; + body.Should().BeEquivalentTo(testResultOwn); + } + + { + var res = await client.GetAsync("timelines?relate=user3&visibility=public"); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody>() + .Which; + body.Should().BeEquivalentTo(testResultRelatePublic); + } + + { + var res = await client.GetAsync("timelines?relate=user3&visibility=register"); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody>() + .Which; + body.Should().BeEquivalentTo(testResultRelateRegister); + } + + { + var res = await client.GetAsync("timelines?relate=user3&relateType=join&visibility=private"); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody>() + .Which; + body.Should().BeEquivalentTo(testResultJoinPrivate); + } + + { + var res = await client.GetAsync("timelines?relate=user3&relateType=own&visibility=private"); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody>() + .Which; + body.Should().BeEquivalentTo(testResultOwnPrivate); + } + } + + { + var client = await CreateDefaultClient(); + { + var res = await client.GetAsync("timelines?visibility=public"); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody>() + .Which; + body.Should().BeEquivalentTo(testResultPublic); + } + } + } + + [Fact] + public async Task TimelineList_InvalidModel() + { + var client = await CreateClientAsUser(); + + { + var res = await client.GetAsync("timelines?relate=us!!"); + res.Should().BeInvalidModel(); + } + + { + var res = await client.GetAsync("timelines?relateType=aaa"); + res.Should().BeInvalidModel(); + } + + { + var res = await client.GetAsync("timelines?visibility=aaa"); + res.Should().BeInvalidModel(); + } + } + + [Fact] + public async Task TimelineCreate_Should_Work() + { + { + using var client = await CreateDefaultClient(); + var res = await client.PostAsJsonAsync("timelines", new TimelineCreateRequest { Name = "aaa" }); + res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); + } + + using (var client = await CreateClientAsUser()) + { + { + var res = await client.PostAsJsonAsync("timelines", new TimelineCreateRequest { Name = "!!!" }); + res.Should().BeInvalidModel(); + } + + TimelineInfo timelineInfo; + { + var res = await client.PostAsJsonAsync("timelines", new TimelineCreateRequest { Name = "aaa" }); + timelineInfo = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + } + + { + var res = await client.GetAsync("timelines/aaa"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().BeEquivalentTo(timelineInfo); + } + + { + var res = await client.PostAsJsonAsync("timelines", new TimelineCreateRequest { Name = "aaa" }); + res.Should().HaveStatusCode(400) + .And.HaveCommonBody(ErrorCodes.TimelineController.NameConflict); + } + } + } + + [Fact] + public async Task TimelineDelete_Should_Work() + { + { + using var client = await CreateDefaultClient(); + var res = await client.DeleteAsync("timelines/t1"); + res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); + } + + { + using var client = await CreateClientAs(2); + var res = await client.DeleteAsync("timelines/t1"); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden); + } + + { + using var client = await CreateClientAsAdministrator(); + + { + var res = await client.DeleteAsync("timelines/!!!"); + res.Should().BeInvalidModel(); + } + + { + var res = await client.DeleteAsync("timelines/t2"); + res.Should().BeDelete(true); + } + + { + var res = await client.DeleteAsync("timelines/t2"); + res.Should().BeDelete(false); + } + } + + { + using var client = await CreateClientAs(1); + + { + var res = await client.DeleteAsync("timelines/!!!"); + res.Should().BeInvalidModel(); + } + + { + var res = await client.DeleteAsync("timelines/t1"); + res.Should().BeDelete(true); + } + + { + var res = await client.DeleteAsync("timelines/t1"); + res.Should().HaveStatusCode(400); + } + } + } + + [Theory] + [MemberData(nameof(TimelineUrlByNameGeneratorData))] + public async Task InvalidModel_BadName(Func generator) + { + using var client = await CreateClientAsAdministrator(); + { + var res = await client.GetAsync(generator("aaa!!!", null)); + res.Should().BeInvalidModel(); + } + { + var res = await client.PatchAsJsonAsync(generator("aaa!!!", null), new TimelinePatchRequest { }); + res.Should().BeInvalidModel(); + } + { + var res = await client.PutAsync(generator("aaa!!!", "members/user1"), null); + res.Should().BeInvalidModel(); + } + { + var res = await client.DeleteAsync(generator("aaa!!!", "members/user1")); + res.Should().BeInvalidModel(); + } + { + var res = await client.GetAsync(generator("aaa!!!", "posts")); + res.Should().BeInvalidModel(); + } + { + var res = await client.PostAsJsonAsync(generator("aaa!!!", "posts"), TimelineHelper.TextPostCreateRequest("aaa")); + res.Should().BeInvalidModel(); + } + { + var res = await client.DeleteAsync(generator("aaa!!!", "posts/123")); + res.Should().BeInvalidModel(); + } + { + var res = await client.GetAsync(generator("aaa!!!", "posts/123/data")); + res.Should().BeInvalidModel(); + } + } + + [Theory] + [MemberData(nameof(TimelineUrlByNameGeneratorData))] + public async Task Ordinary_NotFound(Func generator) + { + var errorCode = generator == GenerateOrdinaryTimelineUrlByName ? ErrorCodes.TimelineController.NotExist : ErrorCodes.UserCommon.NotExist; + + using var client = await CreateClientAsAdministrator(); + { + var res = await client.GetAsync(generator("notexist", null)); + res.Should().HaveStatusCode(404).And.HaveCommonBody(errorCode); + } + { + var res = await client.PatchAsJsonAsync(generator("notexist", null), new TimelinePatchRequest { }); + res.Should().HaveStatusCode(400).And.HaveCommonBody(errorCode); + } + { + var res = await client.PutAsync(generator("notexist", "members/user1"), null); + res.Should().HaveStatusCode(400).And.HaveCommonBody(errorCode); + } + { + var res = await client.DeleteAsync(generator("notexist", "members/user1")); + res.Should().HaveStatusCode(400).And.HaveCommonBody(errorCode); + } + { + var res = await client.GetAsync(generator("notexist", "posts")); + res.Should().HaveStatusCode(404).And.HaveCommonBody(errorCode); + } + { + var res = await client.PostAsJsonAsync(generator("notexist", "posts"), TimelineHelper.TextPostCreateRequest("aaa")); + res.Should().HaveStatusCode(400).And.HaveCommonBody(errorCode); + } + { + var res = await client.DeleteAsync(generator("notexist", "posts/123")); + res.Should().HaveStatusCode(400).And.HaveCommonBody(errorCode); + } + { + var res = await client.GetAsync(generator("notexist", "posts/123/data")); + res.Should().HaveStatusCode(404).And.HaveCommonBody(errorCode); + } + } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task Description_Should_Work(TimelineUrlGenerator generator) + { + using var client = await CreateClientAsUser(); + + async Task AssertDescription(string description) + { + var res = await client.GetAsync(generator(1, null)); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Description.Should().Be(description); + } + + const string mockDescription = "haha"; + + await AssertDescription(""); + { + var res = await client.PatchAsJsonAsync(generator(1, null), + new TimelinePatchRequest { Description = mockDescription }); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which.Description.Should().Be(mockDescription); + await AssertDescription(mockDescription); + } + { + var res = await client.PatchAsJsonAsync(generator(1, null), + new TimelinePatchRequest { Description = null }); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which.Description.Should().Be(mockDescription); + await AssertDescription(mockDescription); + } + { + var res = await client.PatchAsJsonAsync(generator(1, null), + new TimelinePatchRequest { Description = "" }); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which.Description.Should().Be(""); + await AssertDescription(""); + } + } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task Member_Should_Work(TimelineUrlGenerator generator) + { + var getUrl = generator(1, null); + using var client = await CreateClientAsUser(); + + async Task AssertMembers(IList members) + { + var res = await client.GetAsync(getUrl); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Members.Should().NotBeNull().And.BeEquivalentTo(members); + } + + async Task AssertEmptyMembers() + { + var res = await client.GetAsync(getUrl); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Members.Should().NotBeNull().And.BeEmpty(); + } + + await AssertEmptyMembers(); + { + var res = await client.PutAsync(generator(1, "members/usernotexist"), null); + res.Should().HaveStatusCode(400) + .And.HaveCommonBody(ErrorCodes.TimelineController.MemberPut_NotExist); + } + await AssertEmptyMembers(); + { + var res = await client.PutAsync(generator(1, "members/user2"), null); + res.Should().HaveStatusCode(200); + } + await AssertMembers(new List { UserInfos[2] }); + { + var res = await client.DeleteAsync(generator(1, "members/user2")); + res.Should().BeDelete(true); + } + await AssertEmptyMembers(); + { + var res = await client.DeleteAsync(generator(1, "members/aaa")); + res.Should().BeDelete(false); + } + await AssertEmptyMembers(); + } + + public static IEnumerable Permission_Timeline_Data() + { + yield return new object[] { new TimelineUrlGenerator(GenerateOrdinaryTimelineUrl), -1, 200, 401, 401, 401, 401 }; + yield return new object[] { new TimelineUrlGenerator(GenerateOrdinaryTimelineUrl), 1, 200, 200, 403, 200, 403 }; + yield return new object[] { new TimelineUrlGenerator(GenerateOrdinaryTimelineUrl), 0, 200, 200, 200, 200, 200 }; + yield return new object[] { new TimelineUrlGenerator(GeneratePersonalTimelineUrl), -1, 200, 401, 401, 401, 401 }; + yield return new object[] { new TimelineUrlGenerator(GeneratePersonalTimelineUrl), 1, 200, 200, 403, 200, 403 }; + yield return new object[] { new TimelineUrlGenerator(GeneratePersonalTimelineUrl), 0, 200, 200, 200, 200, 200 }; + } + + [Theory] + [MemberData(nameof(Permission_Timeline_Data))] + public async Task Permission_Timeline(TimelineUrlGenerator generator, int userNumber, int get, int opPatchUser, int opPatchAdmin, int opMemberUser, int opMemberAdmin) + { + using var client = await CreateClientAs(userNumber); + { + var res = await client.GetAsync("timelines/t1"); + res.Should().HaveStatusCode(get); + } + + { + var res = await client.PatchAsJsonAsync(generator(1, null), new TimelinePatchRequest { Description = "hahaha" }); + res.Should().HaveStatusCode(opPatchUser); + } + + { + var res = await client.PatchAsJsonAsync(generator(0, null), new TimelinePatchRequest { Description = "hahaha" }); + res.Should().HaveStatusCode(opPatchAdmin); + } + + { + var res = await client.PutAsync(generator(1, "members/user2"), null); + res.Should().HaveStatusCode(opMemberUser); + } + + { + var res = await client.DeleteAsync(generator(1, "members/user2")); + res.Should().HaveStatusCode(opMemberUser); + } + + { + var res = await client.PutAsync(generator(0, "members/user2"), null); + res.Should().HaveStatusCode(opMemberAdmin); + } + + { + var res = await client.DeleteAsync(generator(0, "members/user2")); + res.Should().HaveStatusCode(opMemberAdmin); + } + } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task Visibility_Test(TimelineUrlGenerator generator) + { + var userUrl = generator(1, "posts"); + var adminUrl = generator(0, "posts"); + { + + using var client = await CreateClientAsUser(); + using var content = new StringContent(@"{""visibility"":""abcdefg""}", System.Text.Encoding.UTF8, System.Net.Mime.MediaTypeNames.Application.Json); + var res = await client.PatchAsync(generator(1, null), content); + res.Should().BeInvalidModel(); + } + { // default visibility is registered + { + using var client = await CreateDefaultClient(); + var res = await client.GetAsync(userUrl); + res.Should().HaveStatusCode(403); + } + + { + using var client = await CreateClientAsUser(); + var res = await client.GetAsync(adminUrl); + res.Should().HaveStatusCode(200); + } + } + + { // change visibility to public + { + using var client = await CreateClientAsUser(); + var res = await client.PatchAsJsonAsync(generator(1, null), + new TimelinePatchRequest { Visibility = TimelineVisibility.Public }); + res.Should().HaveStatusCode(200); + } + { + using var client = await CreateDefaultClient(); + var res = await client.GetAsync(userUrl); + res.Should().HaveStatusCode(200); + } + } + + { // change visibility to private + { + using var client = await CreateClientAsAdministrator(); + { + var res = await client.PatchAsJsonAsync(generator(1, null), + new TimelinePatchRequest { Visibility = TimelineVisibility.Private }); + res.Should().HaveStatusCode(200); + } + { + var res = await client.PatchAsJsonAsync(generator(0, null), + new TimelinePatchRequest { Visibility = TimelineVisibility.Private }); + res.Should().HaveStatusCode(200); + } + } + { + using var client = await CreateDefaultClient(); + var res = await client.GetAsync(userUrl); + res.Should().HaveStatusCode(403); + } + { // user can't read admin's + using var client = await CreateClientAsUser(); + var res = await client.GetAsync(adminUrl); + res.Should().HaveStatusCode(403); + } + { // admin can read user's + using var client = await CreateClientAsAdministrator(); + var res = await client.GetAsync(userUrl); + res.Should().HaveStatusCode(200); + } + { // add member + using var client = await CreateClientAsAdministrator(); + var res = await client.PutAsync(generator(0, "members/user1"), null); + res.Should().HaveStatusCode(200); + } + { // now user can read admin's + using var client = await CreateClientAsUser(); + var res = await client.GetAsync(adminUrl); + res.Should().HaveStatusCode(200); + } + } + } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task Permission_Post_Create(TimelineUrlGenerator generator) + { + using (var client = await CreateClientAsUser()) + { + var res = await client.PutAsync(generator(1, "members/user2"), null); + res.Should().HaveStatusCode(200); + } + + using (var client = await CreateDefaultClient()) + { + { // no auth should get 401 + var res = await client.PostAsJsonAsync(generator(1, "posts"), + TimelineHelper.TextPostCreateRequest("aaa")); + res.Should().HaveStatusCode(401); + } + } + + using (var client = await CreateClientAsUser()) + { + { // post self's + var res = await client.PostAsJsonAsync(generator(1, "posts"), + TimelineHelper.TextPostCreateRequest("aaa")); + res.Should().HaveStatusCode(200); + } + { // post other not as a member should get 403 + var res = await client.PostAsJsonAsync(generator(0, "posts"), + TimelineHelper.TextPostCreateRequest("aaa")); + res.Should().HaveStatusCode(403); + } + } + + using (var client = await CreateClientAsAdministrator()) + { + { // post as admin + var res = await client.PostAsJsonAsync(generator(1, "posts"), + TimelineHelper.TextPostCreateRequest("aaa")); + res.Should().HaveStatusCode(200); + } + } + + using (var client = await CreateClientAs(2)) + { + { // post as member + var res = await client.PostAsJsonAsync(generator(1, "posts"), + TimelineHelper.TextPostCreateRequest("aaa")); + res.Should().HaveStatusCode(200); + } + } + } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task Permission_Post_Delete(TimelineUrlGenerator generator) + { + async Task CreatePost(int userNumber) + { + using var client = await CreateClientAs(userNumber); + var res = await client.PostAsJsonAsync(generator(1, "posts"), + TimelineHelper.TextPostCreateRequest("aaa")); + return res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Id; + } + + using (var client = await CreateClientAsUser()) + { + { + var res = await client.PutAsync(generator(1, "members/user2"), null); + res.Should().HaveStatusCode(200); + } + { + var res = await client.PutAsync(generator(1, "members/user3"), null); + res.Should().HaveStatusCode(200); + } + } + + { // no auth should get 401 + using var client = await CreateDefaultClient(); + var res = await client.DeleteAsync(generator(1, "posts/12")); + res.Should().HaveStatusCode(401); + } + + { // self can delete self + var postId = await CreatePost(1); + using var client = await CreateClientAsUser(); + var res = await client.DeleteAsync(generator(1, $"posts/{postId}")); + res.Should().HaveStatusCode(200); + } + + { // admin can delete any + var postId = await CreatePost(1); + using var client = await CreateClientAsAdministrator(); + var res = await client.DeleteAsync(generator(1, $"posts/{postId}")); + res.Should().HaveStatusCode(200); + } + + { // owner can delete other + var postId = await CreatePost(2); + using var client = await CreateClientAsUser(); + var res = await client.DeleteAsync(generator(1, $"posts/{postId}")); + res.Should().HaveStatusCode(200); + } + + { // author can delete self + var postId = await CreatePost(2); + using var client = await CreateClientAs(2); + var res = await client.DeleteAsync(generator(1, $"posts/{postId}")); + res.Should().HaveStatusCode(200); + } + + { // otherwise is forbidden + var postId = await CreatePost(2); + using var client = await CreateClientAs(3); + var res = await client.DeleteAsync(generator(1, $"posts/{postId}")); + res.Should().HaveStatusCode(403); + } + } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task TextPost_ShouldWork(TimelineUrlGenerator generator) + { + { + using var client = await CreateClientAsUser(); + { + var res = await client.GetAsync(generator(1, "posts")); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().NotBeNull().And.BeEmpty(); + } + { + var res = await client.PostAsJsonAsync(generator(1, "posts"), + TimelineHelper.TextPostCreateRequest(null)); + res.Should().BeInvalidModel(); + } + const string mockContent = "aaa"; + TimelinePostInfo createRes; + { + var res = await client.PostAsJsonAsync(generator(1, "posts"), + TimelineHelper.TextPostCreateRequest(mockContent)); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which; + body.Should().NotBeNull(); + body.Content.Should().BeEquivalentTo(TimelineHelper.TextPostContent(mockContent)); + body.Author.Should().BeEquivalentTo(UserInfos[1]); + body.Deleted.Should().BeFalse(); + createRes = body; + } + { + var res = await client.GetAsync(generator(1, "posts")); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().NotBeNull().And.BeEquivalentTo(createRes); + } + const string mockContent2 = "bbb"; + var mockTime2 = DateTime.UtcNow.AddDays(-1); + TimelinePostInfo createRes2; + { + var res = await client.PostAsJsonAsync(generator(1, "posts"), + TimelineHelper.TextPostCreateRequest(mockContent2, mockTime2)); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which; + body.Should().NotBeNull(); + body.Content.Should().BeEquivalentTo(TimelineHelper.TextPostContent(mockContent2)); + body.Author.Should().BeEquivalentTo(UserInfos[1]); + body.Time.Should().BeCloseTo(mockTime2, 1000); + body.Deleted.Should().BeFalse(); + createRes2 = body; + } + { + var res = await client.GetAsync(generator(1, "posts")); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().NotBeNull().And.BeEquivalentTo(createRes, createRes2); + } + { + var res = await client.DeleteAsync(generator(1, $"posts/{createRes.Id}")); + res.Should().BeDelete(true); + } + { + var res = await client.DeleteAsync(generator(1, $"posts/{createRes.Id}")); + res.Should().BeDelete(false); + } + { + var res = await client.DeleteAsync(generator(1, "posts/30000")); + res.Should().BeDelete(false); + } + { + var res = await client.GetAsync(generator(1, "posts")); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().NotBeNull().And.BeEquivalentTo(createRes2); + } + } + } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task GetPost_Should_Ordered(TimelineUrlGenerator generator) + { + using var client = await CreateClientAsUser(); + + async Task CreatePost(DateTime time) + { + var res = await client.PostAsJsonAsync(generator(1, "posts"), + TimelineHelper.TextPostCreateRequest("aaa", time)); + return res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Id; + } + + var now = DateTime.UtcNow; + var id0 = await CreatePost(now.AddDays(1)); + var id1 = await CreatePost(now.AddDays(-1)); + var id2 = await CreatePost(now); + + { + var res = await client.GetAsync(generator(1, "posts")); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Select(p => p.Id).Should().Equal(id1, id2, id0); + } + } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task CreatePost_InvalidModel(TimelineUrlGenerator generator) + { + using var client = await CreateClientAsUser(); + + { + var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = null }); + res.Should().BeInvalidModel(); + } + + { + var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = null } }); + res.Should().BeInvalidModel(); + } + + { + var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "hahaha" } }); + res.Should().BeInvalidModel(); + } + + { + var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "text", Text = null } }); + res.Should().BeInvalidModel(); + } + + { + var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "image", Data = null } }); + res.Should().BeInvalidModel(); + } + + { + // image not base64 + var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "image", Data = "!!!" } }); + res.Should().BeInvalidModel(); + } + + { + // image base64 not image + var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "image", Data = Convert.ToBase64String(new byte[] { 0x01, 0x02, 0x03 }) } }); + res.Should().BeInvalidModel(); + } + } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task ImagePost_ShouldWork(TimelineUrlGenerator generator) + { + var imageData = ImageHelper.CreatePngWithSize(100, 200); + + long postId; + string postImageUrl; + + void AssertPostContent(TimelinePostContentInfo content) + { + content.Type.Should().Be(TimelinePostContentTypes.Image); + content.Url.Should().EndWith(generator(1, $"posts/{postId}/data")); + content.Text.Should().Be(null); + } + + using var client = await CreateClientAsUser(); + + { + var res = await client.PostAsJsonAsync(generator(1, "posts"), + new TimelinePostCreateRequest + { + Content = new TimelinePostCreateRequestContent + { + Type = TimelinePostContentTypes.Image, + Data = Convert.ToBase64String(imageData) + } + }); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + postId = body.Id; + postImageUrl = body.Content.Url; + AssertPostContent(body.Content); + } + + { + var res = await client.GetAsync(generator(1, "posts")); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + body.Should().HaveCount(1); + var post = body[0]; + post.Id.Should().Be(postId); + AssertPostContent(post.Content); + } + + { + var res = await client.GetAsync(generator(1, $"posts/{postId}/data")); + res.Content.Headers.ContentType.MediaType.Should().Be("image/png"); + var data = await res.Content.ReadAsByteArrayAsync(); + var image = Image.Load(data, out var format); + image.Width.Should().Be(100); + image.Height.Should().Be(200); + format.Name.Should().Be(PngFormat.Instance.Name); + } + + { + await CacheTestHelper.TestCache(client, generator(1, $"posts/{postId}/data")); + } + + { + var res = await client.DeleteAsync(generator(1, $"posts/{postId}")); + res.Should().BeDelete(true); + } + + { + var res = await client.DeleteAsync(generator(1, $"posts/{postId}")); + res.Should().BeDelete(false); + } + + { + var res = await client.GetAsync(generator(1, "posts")); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().BeEmpty(); + } + + { + using var scope = TestApp.Host.Services.CreateScope(); + var database = scope.ServiceProvider.GetRequiredService(); + var count = await database.Data.CountAsync(); + count.Should().Be(0); + } + } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task ImagePost_400(TimelineUrlGenerator generator) + { + using var client = await CreateClientAsUser(); + + { + var res = await client.GetAsync(generator(1, "posts/11234/data")); + res.Should().HaveStatusCode(404) + .And.HaveCommonBody(ErrorCodes.TimelineController.PostNotExist); + } + + long postId; + { + var res = await client.PostAsJsonAsync(generator(1, "posts"), + TimelineHelper.TextPostCreateRequest("aaa")); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which; + postId = body.Id; + } + + { + var res = await client.GetAsync(generator(1, $"posts/{postId}/data")); + res.Should().HaveStatusCode(400) + .And.HaveCommonBody(ErrorCodes.TimelineController.PostNoData); + } + } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task Timeline_LastModified(TimelineUrlGenerator generator) + { + using var client = await CreateClientAsUser(); + + DateTime lastModified; + + { + var res = await client.GetAsync(generator(1)); + lastModified = res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.LastModified; + } + + await Task.Delay(1000); + + { + var res = await client.PatchAsJsonAsync(generator(1), new TimelinePatchRequest { Description = "123" }); + lastModified = res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.LastModified.Should().BeAfter(lastModified).And.Subject.Value; + } + + { + var res = await client.GetAsync(generator(1)); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.LastModified.Should().Be(lastModified); + } + + await Task.Delay(1000); + + { + var res = await client.PutAsync(generator(1, "members/user2"), null); + res.Should().HaveStatusCode(200); + } + + { + var res = await client.GetAsync(generator(1)); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.LastModified.Should().BeAfter(lastModified); + } + } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task Post_ModifiedSince(TimelineUrlGenerator generator) + { + using var client = await CreateClientAsUser(); + + var postContentList = new List { "a", "b", "c", "d" }; + var posts = new List(); + + foreach (var content in postContentList) + { + var res = await client.PostAsJsonAsync(generator(1, "posts"), + new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } }); + var post = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + posts.Add(post); + await Task.Delay(1000); + } + + { + var res = await client.DeleteAsync(generator(1, $"posts/{posts[2].Id}")); + res.Should().BeDelete(true); + } + + { + var res = await client.GetAsync(generator(1, "posts", + new Dictionary { { "modifiedSince", posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture) } })); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody>() + .Which.Should().HaveCount(2) + .And.Subject.Select(p => p.Content.Text).Should().Equal("b", "d"); + } + } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task PostList_IncludeDeleted(TimelineUrlGenerator urlGenerator) + { + using var client = await CreateClientAsUser(); + + var postContentList = new List { "a", "b", "c", "d" }; + var posts = new List(); + + foreach (var content in postContentList) + { + var res = await client.PostAsJsonAsync(urlGenerator(1, "posts"), + new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } }); + posts.Add(res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which); + } + + foreach (var id in new long[] { posts[0].Id, posts[2].Id }) + { + var res = await client.DeleteAsync(urlGenerator(1, $"posts/{id}")); + res.Should().BeDelete(true); + } + + { + var res = await client.GetAsync(urlGenerator(1, "posts", new Dictionary { ["includeDeleted"] = "true" })); + posts = res.Should().HaveStatusCode(200) + .And.HaveJsonBody>() + .Which; + posts.Should().HaveCount(4); + posts.Select(p => p.Deleted).Should().Equal(true, false, true, false); + posts.Select(p => p.Content == null).Should().Equal(true, false, true, false); + } + } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task Post_ModifiedSince_And_IncludeDeleted(TimelineUrlGenerator urlGenerator) + { + using var client = await CreateClientAsUser(); + + var postContentList = new List { "a", "b", "c", "d" }; + var posts = new List(); + + foreach (var (content, index) in postContentList.Select((v, i) => (v, i))) + { + var res = await client.PostAsJsonAsync(urlGenerator(1, "posts"), + new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } }); + var post = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + posts.Add(post); + await Task.Delay(1000); + } + + { + var res = await client.DeleteAsync(urlGenerator(1, $"posts/{posts[2].Id}")); + res.Should().BeDelete(true); + } + + { + + var res = await client.GetAsync(urlGenerator(1, "posts", + new Dictionary { + { "modifiedSince", posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture) }, + { "includeDeleted", "true" } + })); + posts = res.Should().HaveStatusCode(200) + .And.HaveJsonBody>() + .Which; + posts.Should().HaveCount(3); + posts.Select(p => p.Deleted).Should().Equal(false, true, false); + posts.Select(p => p.Content == null).Should().Equal(false, true, false); + } + } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task Timeline_Get_IfModifiedSince_And_CheckUniqueId(TimelineUrlGenerator urlGenerator) + { + using var client = await CreateClientAsUser(); + + DateTime lastModifiedTime; + TimelineInfo timeline; + string uniqueId; + + { + var res = await client.GetAsync(urlGenerator(1)); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + timeline = body; + lastModifiedTime = body.LastModified; + uniqueId = body.UniqueId; + } + + { + using var req = new HttpRequestMessage + { + RequestUri = new Uri(client.BaseAddress, urlGenerator(1)), + Method = HttpMethod.Get, + }; + req.Headers.IfModifiedSince = lastModifiedTime.AddSeconds(1); + var res = await client.SendAsync(req); + res.Should().HaveStatusCode(304); + } + + { + using var req = new HttpRequestMessage + { + RequestUri = new Uri(client.BaseAddress, urlGenerator(1)), + Method = HttpMethod.Get, + }; + req.Headers.IfModifiedSince = lastModifiedTime.AddSeconds(-1); + var res = await client.SendAsync(req); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().BeEquivalentTo(timeline); + } + + { + var res = await client.GetAsync(urlGenerator(1, null, + new Dictionary { { "ifModifiedSince", lastModifiedTime.AddSeconds(1).ToString("s", CultureInfo.InvariantCulture) } })); + res.Should().HaveStatusCode(304); + } + + { + var res = await client.GetAsync(urlGenerator(1, null, + new Dictionary { { "ifModifiedSince", lastModifiedTime.AddSeconds(-1).ToString("s", CultureInfo.InvariantCulture) } })); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().BeEquivalentTo(timeline); + } + + { + var res = await client.GetAsync(urlGenerator(1, null, + new Dictionary { { "ifModifiedSince", lastModifiedTime.AddSeconds(1).ToString("s", CultureInfo.InvariantCulture) }, + {"checkUniqueId", uniqueId } })); + res.Should().HaveStatusCode(304); + } + + { + var testUniqueId = (uniqueId[0] == 'a' ? "b" : "a") + uniqueId[1..]; + var res = await client.GetAsync(urlGenerator(1, null, + new Dictionary { { "ifModifiedSince", lastModifiedTime.AddSeconds(1).ToString("s", CultureInfo.InvariantCulture) }, + {"checkUniqueId", testUniqueId } })); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().BeEquivalentTo(timeline); + } + } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task Title(TimelineUrlGenerator urlGenerator) + { + using var client = await CreateClientAsUser(); + + { + var res = await client.GetAsync(urlGenerator(1)); + var timeline = res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which; + timeline.Title.Should().Be(timeline.Name); + } + + { + var res = await client.PatchAsJsonAsync(urlGenerator(1), new TimelinePatchRequest { Title = "atitle" }); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Title.Should().Be("atitle"); + } + + { + var res = await client.GetAsync(urlGenerator(1)); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Title.Should().Be("atitle"); + } + } + + [Fact] + public async Task ChangeName() + { + { + using var client = await CreateDefaultClient(); + var res = await client.PostAsJsonAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "t1", NewName = "tttttttt" }); + res.Should().HaveStatusCode(401); + } + + { + using var client = await CreateClientAs(2); + var res = await client.PostAsJsonAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "t1", NewName = "tttttttt" }); + res.Should().HaveStatusCode(403); + } + + using (var client = await CreateClientAsUser()) + { + { + var res = await client.PostAsJsonAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "!!!", NewName = "tttttttt" }); + res.Should().BeInvalidModel(); + } + + { + var res = await client.PostAsJsonAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "ttt", NewName = "!!!!" }); + res.Should().BeInvalidModel(); + } + + { + var res = await client.PostAsJsonAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "ttttt", NewName = "tttttttt" }); + res.Should().HaveStatusCode(400).And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.TimelineController.NotExist); + } + + { + var res = await client.PostAsJsonAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "t1", NewName = "newt" }); + res.Should().HaveStatusCode(200).And.HaveJsonBody().Which.Name.Should().Be("newt"); + } + + { + var res = await client.GetAsync("timelines/t1"); + res.Should().HaveStatusCode(404); + } + + { + var res = await client.GetAsync("timelines/newt"); + res.Should().HaveStatusCode(200).And.HaveJsonBody().Which.Name.Should().Be("newt"); + } + } + } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task PostDataETag(TimelineUrlGenerator urlGenerator) + { + using var client = await CreateClientAsUser(); + + long id; + string etag; + + { + var res = await client.PostAsJsonAsync(urlGenerator(1, "posts"), new TimelinePostCreateRequest + { + Content = new TimelinePostCreateRequestContent + { + Type = TimelinePostContentTypes.Image, + Data = Convert.ToBase64String(ImageHelper.CreatePngWithSize(100, 50)) + } + }); + res.Should().HaveStatusCode(200); + var body = await res.ReadBodyAsJsonAsync(); + body.Content.ETag.Should().NotBeNullOrEmpty(); + + id = body.Id; + etag = body.Content.ETag; + } + + { + var res = await client.GetAsync(urlGenerator(1, $"posts/{id}/data")); + res.Should().HaveStatusCode(200); + res.Headers.ETag.Should().NotBeNull(); + res.Headers.ETag.ToString().Should().Be(etag); + } + } + } +} diff --git a/BackEnd/Timeline.Tests/IntegratedTests/TokenTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/TokenTest.cs new file mode 100644 index 00000000..480d66cd --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/TokenTest.cs @@ -0,0 +1,165 @@ +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Timeline.Models; +using Timeline.Models.Http; +using Timeline.Services; +using Timeline.Tests.Helpers; +using Xunit; + +namespace Timeline.Tests.IntegratedTests +{ + public class TokenTest : IntegratedTestBase + { + private const string CreateTokenUrl = "token/create"; + private const string VerifyTokenUrl = "token/verify"; + + private static async Task CreateUserTokenAsync(HttpClient client, string username, string password, int? expireOffset = null) + { + var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = username, Password = password, Expire = expireOffset }); + return response.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + } + + public static IEnumerable CreateToken_InvalidModel_Data() + { + yield return new[] { null, "p", null }; + yield return new[] { "u", null, null }; + yield return new object[] { "u", "p", 2000 }; + yield return new object[] { "u", "p", -1 }; + } + + [Theory] + [MemberData(nameof(CreateToken_InvalidModel_Data))] + public async Task CreateToken_InvalidModel(string username, string password, int expire) + { + using var client = await CreateDefaultClient(); + (await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest + { + Username = username, + Password = password, + Expire = expire + })).Should().BeInvalidModel(); + } + + public static IEnumerable CreateToken_UserCredential_Data() + { + yield return new[] { "usernotexist", "p" }; + yield return new[] { "user1", "???" }; + } + + [Theory] + [MemberData(nameof(CreateToken_UserCredential_Data))] + public async void CreateToken_UserCredential(string username, string password) + { + using var client = await CreateDefaultClient(); + var response = await client.PostAsJsonAsync(CreateTokenUrl, + new CreateTokenRequest { Username = username, Password = password }); + response.Should().HaveStatusCode(400) + .And.HaveCommonBody() + .Which.Code.Should().Be(ErrorCodes.TokenController.Create_BadCredential); + } + + [Fact] + public async Task CreateToken_Success() + { + using var client = await CreateDefaultClient(); + var response = await client.PostAsJsonAsync(CreateTokenUrl, + new CreateTokenRequest { Username = "user1", Password = "user1pw" }); + var body = response.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + body.Token.Should().NotBeNullOrWhiteSpace(); + body.User.Should().BeEquivalentTo(UserInfos[1]); + } + + [Fact] + public async Task VerifyToken_InvalidModel() + { + using var client = await CreateDefaultClient(); + (await client.PostAsJsonAsync(VerifyTokenUrl, + new VerifyTokenRequest { Token = null })).Should().BeInvalidModel(); + } + + [Fact] + public async Task VerifyToken_BadFormat() + { + using var client = await CreateDefaultClient(); + var response = await client.PostAsJsonAsync(VerifyTokenUrl, + new VerifyTokenRequest { Token = "bad token hahaha" }); + response.Should().HaveStatusCode(400) + .And.HaveCommonBody() + .Which.Code.Should().Be(ErrorCodes.TokenController.Verify_BadFormat); + } + + [Fact] + public async Task VerifyToken_OldVersion() + { + using var client = await CreateDefaultClient(); + var token = (await CreateUserTokenAsync(client, "user1", "user1pw")).Token; + + using (var scope = TestApp.Host.Services.CreateScope()) // UserService is scoped. + { + // create a user for test + var userService = scope.ServiceProvider.GetRequiredService(); + await userService.ModifyUser("user1", new User { Password = "user1pw" }); + } + + (await client.PostAsJsonAsync(VerifyTokenUrl, + new VerifyTokenRequest { Token = token })) + .Should().HaveStatusCode(400) + .And.HaveCommonBody() + .Which.Code.Should().Be(ErrorCodes.TokenController.Verify_OldVersion); + } + + [Fact] + public async Task VerifyToken_UserNotExist() + { + using var client = await CreateDefaultClient(); + var token = (await CreateUserTokenAsync(client, "user1", "user1pw")).Token; + + using (var scope = TestApp.Host.Services.CreateScope()) // UserDeleteService is scoped. + { + var userService = scope.ServiceProvider.GetRequiredService(); + await userService.DeleteUser("user1"); + } + + (await client.PostAsJsonAsync(VerifyTokenUrl, + new VerifyTokenRequest { Token = token })) + .Should().HaveStatusCode(400) + .And.HaveCommonBody() + .Which.Code.Should().Be(ErrorCodes.TokenController.Verify_UserNotExist); + } + + //[Fact] + //public async Task VerifyToken_Expired() + //{ + // using (var client = await CreateClientWithNoAuth()) + // { + // // I can only control the token expired time but not current time + // // because verify logic is encapsuled in other library. + // var mockClock = _factory.GetTestClock(); + // mockClock.MockCurrentTime = DateTime.Now - TimeSpan.FromDays(2); + // var token = (await client.CreateUserTokenAsync(MockUsers.UserUsername, MockUsers.UserPassword, 1)).Token; + // var response = await client.PostAsJsonAsync(VerifyTokenUrl, + // new VerifyTokenRequest { Token = token }); + // response.Should().HaveStatusCodeBadRequest() + // .And.Should().HaveBodyAsCommonResponseWithCode(TokenController.ErrorCodes.Verify_Expired); + // mockClock.MockCurrentTime = null; + // } + //} + + [Fact] + public async Task VerifyToken_Success() + { + using var client = await CreateDefaultClient(); + var createTokenResult = await CreateUserTokenAsync(client, "user1", "user1pw"); + var response = await client.PostAsJsonAsync(VerifyTokenUrl, + new VerifyTokenRequest { Token = createTokenResult.Token }); + response.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.User.Should().BeEquivalentTo(UserInfos[1]); + } + } +} diff --git a/BackEnd/Timeline.Tests/IntegratedTests/UnknownEndpointTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/UnknownEndpointTest.cs new file mode 100644 index 00000000..732232e2 --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/UnknownEndpointTest.cs @@ -0,0 +1,21 @@ +using FluentAssertions; +using System.Threading.Tasks; +using Timeline.Models.Http; +using Timeline.Tests.Helpers; +using Xunit; + +namespace Timeline.Tests.IntegratedTests +{ + public class UnknownEndpointTest : IntegratedTestBase + { + [Fact] + public async Task UnknownEndpoint() + { + using var client = await CreateDefaultClient(); + var res = await client.GetAsync("unknownEndpoint"); + res.Should().HaveStatusCode(400) + .And.HaveCommonBody() + .Which.Code.Should().Be(ErrorCodes.Common.UnknownEndpoint); + } + } +} diff --git a/BackEnd/Timeline.Tests/IntegratedTests/UserAvatarTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/UserAvatarTest.cs new file mode 100644 index 00000000..f2796005 --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/UserAvatarTest.cs @@ -0,0 +1,251 @@ +using FluentAssertions; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Png; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Mime; +using System.Threading.Tasks; +using Timeline.Models.Http; +using Timeline.Services; +using Timeline.Tests.Helpers; +using Xunit; + +namespace Timeline.Tests.IntegratedTests +{ + public class UserAvatarTest : IntegratedTestBase + { + [Fact] + public async Task Test() + { + Avatar mockAvatar = new Avatar + { + Data = ImageHelper.CreatePngWithSize(100, 100), + Type = PngFormat.Instance.DefaultMimeType + }; + + using (var client = await CreateClientAsUser()) + { + { + var res = await client.GetAsync("users/usernotexist/avatar"); + res.Should().HaveStatusCode(404) + .And.HaveCommonBody() + .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); + } + + var env = TestApp.Host.Services.GetRequiredService(); + var defaultAvatarData = await File.ReadAllBytesAsync(Path.Combine(env.ContentRootPath, "default-avatar.png")); + + async Task GetReturnDefault(string username = "user1") + { + var res = await client.GetAsync($"users/{username}/avatar"); + res.Should().HaveStatusCode(200); + res.Content.Headers.ContentType.MediaType.Should().Be("image/png"); + var body = await res.Content.ReadAsByteArrayAsync(); + body.Should().Equal(defaultAvatarData); + } + + { + var res = await client.GetAsync("users/user1/avatar"); + res.Should().HaveStatusCode(200); + res.Content.Headers.ContentType.MediaType.Should().Be("image/png"); + var body = await res.Content.ReadAsByteArrayAsync(); + body.Should().Equal(defaultAvatarData); + } + + await CacheTestHelper.TestCache(client, "users/user1/avatar"); + + await GetReturnDefault("admin"); + + { + using var content = new ByteArrayContent(new[] { (byte)0x00 }); + content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); + var res = await client.PutAsync("users/user1/avatar", content); + res.Should().BeInvalidModel(); + } + + { + using var content = new ByteArrayContent(new[] { (byte)0x00 }); + content.Headers.ContentLength = 1; + var res = await client.PutAsync("users/user1/avatar", content); + res.Should().BeInvalidModel(); + } + + { + using var content = new ByteArrayContent(new[] { (byte)0x00 }); + content.Headers.ContentLength = 0; + content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); + var res = await client.PutAsync("users/user1/avatar", content); + res.Should().BeInvalidModel(); + } + + { + var res = await client.PutByteArrayAsync("users/user1/avatar", new[] { (byte)0x00 }, "image/notaccept"); + res.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); + } + + { + using var content = new ByteArrayContent(new[] { (byte)0x00 }); + content.Headers.ContentLength = 1000 * 1000 * 11; + content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); + var res = await client.PutAsync("users/user1/avatar", content); + res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Content.TooBig); + } + + { + using var content = new ByteArrayContent(new[] { (byte)0x00 }); + content.Headers.ContentLength = 2; + content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); + var res = await client.PutAsync("users/user1/avatar", content); + res.Should().BeInvalidModel(); + } + + { + using var content = new ByteArrayContent(new[] { (byte)0x00, (byte)0x01 }); + content.Headers.ContentLength = 1; + content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); + var res = await client.PutAsync("users/user1/avatar", content); + res.Should().BeInvalidModel(); + } + + { + var res = await client.PutByteArrayAsync("users/user1/avatar", new[] { (byte)0x00 }, "image/png"); + res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.UserAvatar.BadFormat_CantDecode); + } + + { + var res = await client.PutByteArrayAsync("users/user1/avatar", mockAvatar.Data, "image/jpeg"); + res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.UserAvatar.BadFormat_UnmatchedFormat); + } + + { + var res = await client.PutByteArrayAsync("users/user1/avatar", ImageHelper.CreatePngWithSize(100, 200), "image/png"); + res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.UserAvatar.BadFormat_BadSize); + } + + { + var res = await client.PutByteArrayAsync("users/user1/avatar", mockAvatar.Data, mockAvatar.Type); + res.Should().HaveStatusCode(HttpStatusCode.OK); + + var res2 = await client.GetAsync("users/user1/avatar"); + res2.Should().HaveStatusCode(200); + res2.Content.Headers.ContentType.MediaType.Should().Be(mockAvatar.Type); + var body = await res2.Content.ReadAsByteArrayAsync(); + body.Should().Equal(mockAvatar.Data); + } + + IEnumerable<(string, IImageFormat)> formats = new (string, IImageFormat)[] + { + ("image/jpeg", JpegFormat.Instance), + ("image/gif", GifFormat.Instance), + ("image/png", PngFormat.Instance), + }; + + foreach ((var mimeType, var format) in formats) + { + var res = await client.PutByteArrayAsync("users/user1/avatar", ImageHelper.CreateImageWithSize(100, 100, format), mimeType); + res.Should().HaveStatusCode(HttpStatusCode.OK); + } + + { + var res = await client.PutByteArrayAsync("users/admin/avatar", new[] { (byte)0x00 }, "image/png"); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden) + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Forbid); + } + + { + var res = await client.DeleteAsync("users/admin/avatar"); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden) + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Forbid); + } + + for (int i = 0; i < 2; i++) // double delete should work. + { + var res = await client.DeleteAsync("users/user1/avatar"); + res.Should().HaveStatusCode(200); + await GetReturnDefault(); + } + } + + // Authorization check. + using (var client = await CreateClientAsAdministrator()) + { + { + var res = await client.PutByteArrayAsync("users/user1/avatar", mockAvatar.Data, mockAvatar.Type); + res.Should().HaveStatusCode(HttpStatusCode.OK); + } + + { + var res = await client.DeleteAsync("users/user1/avatar"); + res.Should().HaveStatusCode(HttpStatusCode.OK); + } + + { + var res = await client.PutByteArrayAsync("users/usernotexist/avatar", new[] { (byte)0x00 }, "image/png"); + res.Should().HaveStatusCode(400) + .And.HaveCommonBody() + .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); + } + + { + var res = await client.DeleteAsync("users/usernotexist/avatar"); + res.Should().HaveStatusCode(400) + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); + } + } + + // bad username check + using (var client = await CreateClientAsAdministrator()) + { + { + var res = await client.GetAsync("users/u!ser/avatar"); + res.Should().BeInvalidModel(); + } + + { + var res = await client.PutByteArrayAsync("users/u!ser/avatar", ImageHelper.CreatePngWithSize(100, 100), "image/png"); + res.Should().BeInvalidModel(); + } + + { + var res = await client.DeleteAsync("users/u!ser/avatar"); + res.Should().BeInvalidModel(); + } + } + } + + [Fact] + public async Task AvatarPutReturnETag() + { + using var client = await CreateClientAsUser(); + + EntityTagHeaderValue etag; + + { + var image = ImageHelper.CreatePngWithSize(100, 100); + var res = await client.PutByteArrayAsync("users/user1/avatar", image, PngFormat.Instance.DefaultMimeType); + res.Should().HaveStatusCode(200); + etag = res.Headers.ETag; + etag.Should().NotBeNull(); + etag.Tag.Should().NotBeNullOrEmpty(); + } + + { + var res = await client.GetAsync("users/user1/avatar"); + res.Should().HaveStatusCode(200); + res.Headers.ETag.Should().Be(etag); + res.Headers.ETag.Tag.Should().Be(etag.Tag); + } + } + } +} \ No newline at end of file diff --git a/BackEnd/Timeline.Tests/IntegratedTests/UserTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/UserTest.cs new file mode 100644 index 00000000..9dfcc6a5 --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/UserTest.cs @@ -0,0 +1,447 @@ +using FluentAssertions; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Timeline.Models.Http; +using Timeline.Tests.Helpers; +using Xunit; + +namespace Timeline.Tests.IntegratedTests +{ + public class UserTest : IntegratedTestBase + { + [Fact] + public void UserListShouldHaveUniqueId() + { + foreach (var user in UserInfos) + { + user.UniqueId.Should().NotBeNullOrWhiteSpace(); + } + } + + [Fact] + public async Task GetList_NoAuth() + { + using var client = await CreateDefaultClient(); + var res = await client.GetAsync("users"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().BeEquivalentTo(UserInfos); + } + + [Fact] + public async Task GetList_User() + { + using var client = await CreateClientAsUser(); + var res = await client.GetAsync("users"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().BeEquivalentTo(UserInfos); + } + + [Fact] + public async Task GetList_Admin() + { + using var client = await CreateClientAsAdministrator(); + var res = await client.GetAsync("users"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().BeEquivalentTo(UserInfos); + } + + [Fact] + public async Task Get_NoAuth() + { + using var client = await CreateDefaultClient(); + var res = await client.GetAsync($"users/admin"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().BeEquivalentTo(UserInfos[0]); + } + + [Fact] + public async Task Get_User() + { + using var client = await CreateClientAsUser(); + var res = await client.GetAsync($"users/admin"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().BeEquivalentTo(UserInfos[0]); + } + + [Fact] + public async Task Get_Admin() + { + using var client = await CreateClientAsAdministrator(); + var res = await client.GetAsync($"users/user1"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().BeEquivalentTo(UserInfos[1]); + } + + [Fact] + public async Task Get_InvalidModel() + { + using var client = await CreateClientAsUser(); + var res = await client.GetAsync("users/aaa!a"); + res.Should().BeInvalidModel(); + } + + [Fact] + public async Task Get_404() + { + using var client = await CreateClientAsUser(); + var res = await client.GetAsync("users/usernotexist"); + res.Should().HaveStatusCode(404) + .And.HaveCommonBody(ErrorCodes.UserCommon.NotExist); + } + + [Fact] + public async Task Patch_User() + { + using var client = await CreateClientAsUser(); + { + var res = await client.PatchAsJsonAsync("users/user1", + new UserPatchRequest { Nickname = "aaa" }); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Nickname.Should().Be("aaa"); + } + + { + var res = await client.GetAsync("users/user1"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Nickname.Should().Be("aaa"); + } + } + + [Fact] + public async Task Patch_Admin() + { + using var client = await CreateClientAsAdministrator(); + using var userClient = await CreateClientAsUser(); + + { + var res = await client.PatchAsJsonAsync("users/user1", + new UserPatchRequest + { + Username = "newuser", + Password = "newpw", + Administrator = true, + Nickname = "aaa" + }); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which; + body.Administrator.Should().Be(true); + body.Nickname.Should().Be("aaa"); + } + + { + var res = await client.GetAsync("users/newuser"); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which; + body.Administrator.Should().Be(true); + body.Nickname.Should().Be("aaa"); + } + + { + // Token should expire. + var res = await userClient.GetAsync("testing/auth/Authorize"); + res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); + } + + { + // Check password. + (await CreateClientWithCredential("newuser", "newpw")).Dispose(); + } + } + + [Fact] + public async Task Patch_NotExist() + { + using var client = await CreateClientAsAdministrator(); + var res = await client.PatchAsJsonAsync("users/usernotexist", new UserPatchRequest { }); + res.Should().HaveStatusCode(404) + .And.HaveCommonBody() + .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); + } + + [Fact] + public async Task Patch_InvalidModel() + { + using var client = await CreateClientAsAdministrator(); + var res = await client.PatchAsJsonAsync("users/aaa!a", new UserPatchRequest { }); + res.Should().BeInvalidModel(); + } + + public static IEnumerable Patch_InvalidModel_Body_Data() + { + yield return new[] { new UserPatchRequest { Username = "aaa!a" } }; + yield return new[] { new UserPatchRequest { Password = "" } }; + yield return new[] { new UserPatchRequest { Nickname = new string('a', 50) } }; + } + + [Theory] + [MemberData(nameof(Patch_InvalidModel_Body_Data))] + public async Task Patch_InvalidModel_Body(UserPatchRequest body) + { + using var client = await CreateClientAsAdministrator(); + var res = await client.PatchAsJsonAsync("users/user1", body); + res.Should().BeInvalidModel(); + } + + [Fact] + public async Task Patch_UsernameConflict() + { + using var client = await CreateClientAsAdministrator(); + var res = await client.PatchAsJsonAsync("users/user1", new UserPatchRequest { Username = "admin" }); + res.Should().HaveStatusCode(400) + .And.HaveCommonBody(ErrorCodes.UserController.UsernameConflict); + } + + [Fact] + public async Task Patch_NoAuth_Unauthorized() + { + using var client = await CreateDefaultClient(); + var res = await client.PatchAsJsonAsync("users/user1", new UserPatchRequest { Nickname = "aaa" }); + res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task Patch_User_Forbid() + { + using var client = await CreateClientAsUser(); + var res = await client.PatchAsJsonAsync("users/admin", new UserPatchRequest { Nickname = "aaa" }); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden); + } + + [Fact] + public async Task Patch_Username_Forbid() + { + using var client = await CreateClientAsUser(); + var res = await client.PatchAsJsonAsync("users/user1", new UserPatchRequest { Username = "aaa" }); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden); + } + + [Fact] + public async Task Patch_Password_Forbid() + { + using var client = await CreateClientAsUser(); + var res = await client.PatchAsJsonAsync("users/user1", new UserPatchRequest { Password = "aaa" }); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden); + } + + [Fact] + public async Task Patch_Administrator_Forbid() + { + using var client = await CreateClientAsUser(); + var res = await client.PatchAsJsonAsync("users/user1", new UserPatchRequest { Administrator = true }); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden); + } + + [Fact] + public async Task Delete_Deleted() + { + using var client = await CreateClientAsAdministrator(); + { + var res = await client.DeleteAsync("users/user1"); + res.Should().BeDelete(true); + } + + { + var res = await client.GetAsync("users/user1"); + res.Should().HaveStatusCode(404); + } + } + + [Fact] + public async Task Delete_NotExist() + { + using var client = await CreateClientAsAdministrator(); + var res = await client.DeleteAsync("users/usernotexist"); + res.Should().BeDelete(false); + } + + [Fact] + public async Task Delete_InvalidModel() + { + using var client = await CreateClientAsAdministrator(); + var res = await client.DeleteAsync("users/aaa!a"); + res.Should().BeInvalidModel(); + } + + [Fact] + public async Task Delete_NoAuth_Unauthorized() + { + using var client = await CreateDefaultClient(); + var res = await client.DeleteAsync("users/aaa!a"); + res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task Delete_User_Forbid() + { + using var client = await CreateClientAsUser(); + var res = await client.DeleteAsync("users/aaa!a"); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden); + } + + private const string createUserUrl = "userop/createuser"; + + [Fact] + public async Task Op_CreateUser() + { + using var client = await CreateClientAsAdministrator(); + { + var res = await client.PostAsJsonAsync(createUserUrl, new CreateUserRequest + { + Username = "aaa", + Password = "bbb", + Administrator = true, + Nickname = "ccc" + }); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + body.Username.Should().Be("aaa"); + body.Nickname.Should().Be("ccc"); + body.Administrator.Should().BeTrue(); + } + { + var res = await client.GetAsync("users/aaa"); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + body.Username.Should().Be("aaa"); + body.Nickname.Should().Be("ccc"); + body.Administrator.Should().BeTrue(); + } + { + // Test password. + (await CreateClientWithCredential("aaa", "bbb")).Dispose(); + } + } + + public static IEnumerable Op_CreateUser_InvalidModel_Data() + { + yield return new[] { new CreateUserRequest { Username = "aaa", Password = "bbb" } }; + yield return new[] { new CreateUserRequest { Username = "aaa", Administrator = true } }; + yield return new[] { new CreateUserRequest { Password = "bbb", Administrator = true } }; + yield return new[] { new CreateUserRequest { Username = "a!a", Password = "bbb", Administrator = true } }; + yield return new[] { new CreateUserRequest { Username = "aaa", Password = "", Administrator = true } }; + yield return new[] { new CreateUserRequest { Username = "aaa", Password = "bbb", Administrator = true, Nickname = new string('a', 40) } }; + } + + [Theory] + [MemberData(nameof(Op_CreateUser_InvalidModel_Data))] + public async Task Op_CreateUser_InvalidModel(CreateUserRequest body) + { + using var client = await CreateClientAsAdministrator(); + { + var res = await client.PostAsJsonAsync(createUserUrl, body); + res.Should().BeInvalidModel(); + } + } + + [Fact] + public async Task Op_CreateUser_UsernameConflict() + { + using var client = await CreateClientAsAdministrator(); + { + var res = await client.PostAsJsonAsync(createUserUrl, new CreateUserRequest + { + Username = "user1", + Password = "bbb", + Administrator = false + }); + res.Should().HaveStatusCode(400) + .And.HaveCommonBody(ErrorCodes.UserController.UsernameConflict); + } + } + + [Fact] + public async Task Op_CreateUser_NoAuth_Unauthorized() + { + using var client = await CreateDefaultClient(); + { + var res = await client.PostAsJsonAsync(createUserUrl, new CreateUserRequest + { + Username = "aaa", + Password = "bbb", + Administrator = false + }); + res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); + } + } + + [Fact] + public async Task Op_CreateUser_User_Forbid() + { + using var client = await CreateClientAsUser(); + { + var res = await client.PostAsJsonAsync(createUserUrl, new CreateUserRequest + { + Username = "aaa", + Password = "bbb", + Administrator = false + }); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden); + } + } + + private const string changePasswordUrl = "userop/changepassword"; + + [Fact] + public async Task Op_ChangePassword() + { + using var client = await CreateClientAsUser(); + { + var res = await client.PostAsJsonAsync(changePasswordUrl, + new ChangePasswordRequest { OldPassword = "user1pw", NewPassword = "newpw" }); + res.Should().HaveStatusCode(200); + } + { + var res = await client.PatchAsJsonAsync("users/user1", new UserPatchRequest { }); + res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); + } + { + (await CreateClientWithCredential("user1", "newpw")).Dispose(); + } + } + + public static IEnumerable Op_ChangePassword_InvalidModel_Data() + { + yield return new[] { null, "ppp" }; + yield return new[] { "ppp", null }; + } + + [Theory] + [MemberData(nameof(Op_ChangePassword_InvalidModel_Data))] + public async Task Op_ChangePassword_InvalidModel(string oldPassword, string newPassword) + { + using var client = await CreateClientAsUser(); + var res = await client.PostAsJsonAsync(changePasswordUrl, + new ChangePasswordRequest { OldPassword = oldPassword, NewPassword = newPassword }); + res.Should().BeInvalidModel(); + } + + [Fact] + public async Task Op_ChangePassword_BadOldPassword() + { + using var client = await CreateClientAsUser(); + var res = await client.PostAsJsonAsync(changePasswordUrl, new ChangePasswordRequest { OldPassword = "???", NewPassword = "???" }); + res.Should().HaveStatusCode(400) + .And.HaveCommonBody(ErrorCodes.UserController.ChangePassword_BadOldPassword); + } + + [Fact] + public async Task Op_ChangePassword_NoAuth_Unauthorized() + { + using var client = await CreateDefaultClient(); + var res = await client.PostAsJsonAsync(changePasswordUrl, new ChangePasswordRequest { OldPassword = "???", NewPassword = "???" }); + res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); + } + } +} diff --git a/BackEnd/Timeline.Tests/PasswordGenerator.cs b/BackEnd/Timeline.Tests/PasswordGenerator.cs new file mode 100644 index 00000000..863439b5 --- /dev/null +++ b/BackEnd/Timeline.Tests/PasswordGenerator.cs @@ -0,0 +1,23 @@ +using Timeline.Services; +using Xunit; +using Xunit.Abstractions; + +namespace Timeline.Tests +{ + public class PasswordGenerator + { + private readonly ITestOutputHelper _output; + + public PasswordGenerator(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void Generate() + { + var service = new PasswordService(); + _output.WriteLine(service.HashPassword("crupest")); + } + } +} diff --git a/BackEnd/Timeline.Tests/Properties/launchSettings.json b/BackEnd/Timeline.Tests/Properties/launchSettings.json new file mode 100644 index 00000000..f3ee419d --- /dev/null +++ b/BackEnd/Timeline.Tests/Properties/launchSettings.json @@ -0,0 +1,2 @@ +{ +} diff --git a/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs b/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs new file mode 100644 index 00000000..5a774b78 --- /dev/null +++ b/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs @@ -0,0 +1,329 @@ +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Entities; +using Timeline.Models; +using Timeline.Services; +using Timeline.Services.Exceptions; +using Timeline.Tests.Helpers; +using Xunit; + +namespace Timeline.Tests.Services +{ + public class TimelineServiceTest : IAsyncLifetime, IDisposable + { + private readonly TestDatabase _testDatabase = new TestDatabase(); + + private DatabaseContext _databaseContext; + + private readonly PasswordService _passwordService = new PasswordService(); + + private readonly ETagGenerator _eTagGenerator = new ETagGenerator(); + + private readonly ImageValidator _imageValidator = new ImageValidator(); + + private readonly TestClock _clock = new TestClock(); + + private DataManager _dataManager; + + private UserService _userService; + + private TimelineService _timelineService; + + private UserDeleteService _userDeleteService; + + public TimelineServiceTest() + { + } + + public async Task InitializeAsync() + { + await _testDatabase.InitializeAsync(); + _databaseContext = _testDatabase.CreateContext(); + _dataManager = new DataManager(_databaseContext, _eTagGenerator); + _userService = new UserService(NullLogger.Instance, _databaseContext, _passwordService, _clock); + _timelineService = new TimelineService(NullLogger.Instance, _databaseContext, _dataManager, _userService, _imageValidator, _clock); + _userDeleteService = new UserDeleteService(NullLogger.Instance, _databaseContext, _timelineService); + } + + public async Task DisposeAsync() + { + await _testDatabase.DisposeAsync(); + await _databaseContext.DisposeAsync(); + } + + public void Dispose() + { + _eTagGenerator.Dispose(); + } + + [Theory] + [InlineData("@user")] + [InlineData("tl")] + public async Task Timeline_GetLastModified(string timelineName) + { + var time = _clock.ForwardCurrentTime(); + + var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); + if (!isPersonal) + await _timelineService.CreateTimeline(timelineName, await _userService.GetUserIdByUsername("user")); + + var t = await _timelineService.GetTimelineLastModifiedTime(timelineName); + + t.Should().Be(time); + } + + [Theory] + [InlineData("@user")] + [InlineData("tl")] + public async Task Timeline_GetUnqiueId(string timelineName) + { + var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); + if (!isPersonal) + await _timelineService.CreateTimeline(timelineName, await _userService.GetUserIdByUsername("user")); + + var uniqueId = await _timelineService.GetTimelineUniqueId(timelineName); + + uniqueId.Should().NotBeNullOrEmpty(); + } + + [Theory] + [InlineData("@user")] + [InlineData("tl")] + public async Task Timeline_LastModified(string timelineName) + { + var initTime = _clock.ForwardCurrentTime(); + + void Check(Models.Timeline timeline) + { + timeline.NameLastModified.Should().Be(initTime); + timeline.LastModified.Should().Be(_clock.GetCurrentTime()); + } + + async Task GetAndCheck() + { + Check(await _timelineService.GetTimeline(timelineName)); + } + + var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); + if (!isPersonal) + Check(await _timelineService.CreateTimeline(timelineName, await _userService.GetUserIdByUsername("user"))); + + await GetAndCheck(); + + _clock.ForwardCurrentTime(); + await _timelineService.ChangeProperty(timelineName, new TimelineChangePropertyRequest { Visibility = TimelineVisibility.Public }); + await GetAndCheck(); + + _clock.ForwardCurrentTime(); + await _timelineService.ChangeMember(timelineName, new List { "admin" }, null); + await GetAndCheck(); + } + + [Theory] + [InlineData("@user")] + [InlineData("tl")] + public async Task GetPosts_ModifiedSince(string timelineName) + { + _clock.ForwardCurrentTime(); + + var userId = await _userService.GetUserIdByUsername("user"); + + var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); + if (!isPersonal) + await _timelineService.CreateTimeline(timelineName, userId); + + var postContentList = new string[] { "a", "b", "c", "d" }; + + DateTime testPoint = new DateTime(); + + foreach (var (content, index) in postContentList.Select((v, i) => (v, i))) + { + var t = _clock.ForwardCurrentTime(); + if (index == 1) + testPoint = t; + await _timelineService.CreateTextPost(timelineName, userId, content, null); + } + + var posts = await _timelineService.GetPosts(timelineName, testPoint); + posts.Should().HaveCount(3) + .And.Subject.Select(p => (p.Content as TextTimelinePostContent).Text).Should().Equal(postContentList.Skip(1)); + } + + [Theory] + [InlineData("@user")] + [InlineData("tl")] + public async Task GetPosts_IncludeDeleted(string timelineName) + { + var userId = await _userService.GetUserIdByUsername("user"); + + var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); + if (!isPersonal) + await _timelineService.CreateTimeline(timelineName, userId); + + var postContentList = new string[] { "a", "b", "c", "d" }; + + foreach (var content in postContentList) + { + await _timelineService.CreateTextPost(timelineName, userId, content, null); + } + + var posts = await _timelineService.GetPosts(timelineName); + posts.Should().HaveCount(4); + posts.Select(p => p.Deleted).Should().Equal(Enumerable.Repeat(false, posts.Count)); + posts.Select(p => ((TextTimelinePostContent)p.Content).Text).Should().Equal(postContentList); + + foreach (var id in new long[] { posts[0].Id, posts[2].Id }) + { + await _timelineService.DeletePost(timelineName, id); + } + + posts = await _timelineService.GetPosts(timelineName); + posts.Should().HaveCount(2); + posts.Select(p => p.Deleted).Should().Equal(Enumerable.Repeat(false, posts.Count)); + posts.Select(p => ((TextTimelinePostContent)p.Content).Text).Should().Equal(new string[] { "b", "d" }); + + posts = await _timelineService.GetPosts(timelineName, includeDeleted: true); + posts.Should().HaveCount(4); + posts.Select(p => p.Deleted).Should().Equal(new bool[] { true, false, true, false }); + posts.Where(p => !p.Deleted).Select(p => ((TextTimelinePostContent)p.Content).Text).Should().Equal(new string[] { "b", "d" }); + } + + [Theory] + [InlineData("@admin")] + [InlineData("tl")] + public async Task GetPosts_ModifiedSince_UsernameChange(string timelineName) + { + var time1 = _clock.ForwardCurrentTime(); + + var userId = await _userService.GetUserIdByUsername("user"); + + var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); + if (!isPersonal) + await _timelineService.CreateTimeline(timelineName, userId); + + var postContentList = new string[] { "a", "b", "c", "d" }; + + foreach (var (content, index) in postContentList.Select((v, i) => (v, i))) + { + await _timelineService.CreateTextPost(timelineName, userId, content, null); + } + + var time2 = _clock.ForwardCurrentTime(); + + { + var posts = await _timelineService.GetPosts(timelineName, time2); + posts.Should().HaveCount(0); + } + + { + await _userService.ModifyUser(userId, new User { Nickname = "haha" }); + var posts = await _timelineService.GetPosts(timelineName, time2); + posts.Should().HaveCount(0); + } + + { + await _userService.ModifyUser(userId, new User { Username = "haha" }); + var posts = await _timelineService.GetPosts(timelineName, time2); + posts.Should().HaveCount(4); + } + } + + [Theory] + [InlineData("@admin")] + [InlineData("tl")] + public async Task GetPosts_ModifiedSince_UserDelete(string timelineName) + { + var time1 = _clock.ForwardCurrentTime(); + + var userId = await _userService.GetUserIdByUsername("user"); + var adminId = await _userService.GetUserIdByUsername("admin"); + + var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); + if (!isPersonal) + await _timelineService.CreateTimeline(timelineName, adminId); + + var postContentList = new string[] { "a", "b", "c", "d" }; + + foreach (var (content, index) in postContentList.Select((v, i) => (v, i))) + { + await _timelineService.CreateTextPost(timelineName, userId, content, null); + } + + var time2 = _clock.ForwardCurrentTime(); + + { + var posts = await _timelineService.GetPosts(timelineName, time2); + posts.Should().HaveCount(0); + } + + await _userDeleteService.DeleteUser("user"); + + { + var posts = await _timelineService.GetPosts(timelineName, time2); + posts.Should().HaveCount(0); + } + + { + var posts = await _timelineService.GetPosts(timelineName, time2, true); + posts.Should().HaveCount(4); + } + } + + [Theory] + [InlineData("@admin")] + [InlineData("tl")] + public async Task Title(string timelineName) + { + var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); + if (!isPersonal) + await _timelineService.CreateTimeline(timelineName, await _userService.GetUserIdByUsername("user")); + + { + var timeline = await _timelineService.GetTimeline(timelineName); + timeline.Title.Should().Be(timelineName); + } + + { + await _timelineService.ChangeProperty(timelineName, new TimelineChangePropertyRequest { Title = null }); + var timeline = await _timelineService.GetTimeline(timelineName); + timeline.Title.Should().Be(timelineName); + } + + { + await _timelineService.ChangeProperty(timelineName, new TimelineChangePropertyRequest { Title = "atitle" }); + var timeline = await _timelineService.GetTimeline(timelineName); + timeline.Title.Should().Be("atitle"); + } + } + + [Fact] + public async Task ChangeName() + { + _clock.ForwardCurrentTime(); + + await _timelineService.Awaiting(s => s.ChangeTimelineName("!!!", "newtl")).Should().ThrowAsync(); + await _timelineService.Awaiting(s => s.ChangeTimelineName("tl", "!!!")).Should().ThrowAsync(); + await _timelineService.Awaiting(s => s.ChangeTimelineName("tl", "newtl")).Should().ThrowAsync(); + + await _timelineService.CreateTimeline("tl", await _userService.GetUserIdByUsername("user")); + await _timelineService.CreateTimeline("tl2", await _userService.GetUserIdByUsername("user")); + + await _timelineService.Awaiting(s => s.ChangeTimelineName("tl", "tl2")).Should().ThrowAsync(); + + var time = _clock.ForwardCurrentTime(); + + await _timelineService.ChangeTimelineName("tl", "newtl"); + + { + var timeline = await _timelineService.GetTimeline("newtl"); + timeline.Name.Should().Be("newtl"); + timeline.LastModified.Should().Be(time); + timeline.NameLastModified.Should().Be(time); + } + } + } +} diff --git a/BackEnd/Timeline.Tests/Timeline.Tests.csproj b/BackEnd/Timeline.Tests/Timeline.Tests.csproj new file mode 100644 index 00000000..973e0fc0 --- /dev/null +++ b/BackEnd/Timeline.Tests/Timeline.Tests.csproj @@ -0,0 +1,34 @@ + + + + netcoreapp3.1 + + 8.0 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + diff --git a/BackEnd/Timeline.Tests/UsernameValidatorUnitTest.cs b/BackEnd/Timeline.Tests/UsernameValidatorUnitTest.cs new file mode 100644 index 00000000..5b568adf --- /dev/null +++ b/BackEnd/Timeline.Tests/UsernameValidatorUnitTest.cs @@ -0,0 +1,78 @@ +using FluentAssertions; +using Timeline.Models.Validation; +using Timeline.Tests.Helpers; +using Xunit; + +namespace Timeline.Tests +{ + public class UsernameValidatorUnitTest : IClassFixture + { + private readonly UsernameValidator _validator; + + public UsernameValidatorUnitTest(UsernameValidator validator) + { + _validator = validator; + } + + private string FailAndMessage(string username) + { + var (result, message) = _validator.Validate(username); + result.Should().BeFalse(); + return message; + } + + [Fact] + public void NotString() + { + var (result, message) = _validator.Validate(123); + result.Should().BeFalse(); + message.Should().ContainEquivalentOf("type"); + } + + [Fact] + public void Empty() + { + FailAndMessage("").Should().ContainEquivalentOf("empty"); + } + + [Theory] + [InlineData("!")] + [InlineData("!abc")] + [InlineData("ab c")] + [InlineData("ab c!")] // This is a chinese ! . + public void BadCharactor(string value) + { + FailAndMessage(value).Should().ContainEquivalentOf("invalid") + .And.ContainEquivalentOf("character"); + } + + [Fact] + public void TooLong() + { + FailAndMessage(new string('a', 40)).Should().ContainEquivalentOf("long"); + } + + [Fact(Skip = "Currently name can't be longer than 26. So this will print message of too long.")] + public void UniqueId() + { + FailAndMessage("e4c80127d092d9b2fc19c5e04612d4c0").Should().ContainEquivalentOf("unique id"); + } + + [Theory] + [InlineData(null)] + [InlineData("abc")] + [InlineData("-abc")] + [InlineData("_abc")] + [InlineData("abc-")] + [InlineData("abc_")] + [InlineData("a-bc")] + [InlineData("a-b-c")] + [InlineData("a-b_c")] + [InlineData("a-你好_c")] + public void Success(string value) + { + var (result, _) = _validator.Validate(value); + result.Should().BeTrue(); + } + } +} diff --git a/BackEnd/Timeline.Tests/coverletArgs.runsettings b/BackEnd/Timeline.Tests/coverletArgs.runsettings new file mode 100644 index 00000000..24cd1822 --- /dev/null +++ b/BackEnd/Timeline.Tests/coverletArgs.runsettings @@ -0,0 +1,13 @@ + + + + + + + + [xunit.*]*,[Timeline]Timeline.Migrations.* + + + + + diff --git a/BackEnd/Timeline.Tests/packages.lock.json b/BackEnd/Timeline.Tests/packages.lock.json new file mode 100644 index 00000000..7150a222 --- /dev/null +++ b/BackEnd/Timeline.Tests/packages.lock.json @@ -0,0 +1,2040 @@ +{ + "version": 1, + "dependencies": { + ".NETCoreApp,Version=v3.1": { + "coverlet.collector": { + "type": "Direct", + "requested": "[1.3.0, )", + "resolved": "1.3.0", + "contentHash": "t8pnf5SX2ya0RX4vjoxsbhDMQCZJcpPun2neHKJ4FouMmObylo25FvoOydvf3Bl+l+IzWw7u2vjEeCBHnleB9g==" + }, + "FluentAssertions": { + "type": "Direct", + "requested": "[5.10.3, )", + "resolved": "5.10.3", + "contentHash": "gVPEVp1hLVqcv+7Q2wiDf7kqCNn7+bQcQ0jbJ2mcRT6CeRoZl1tNkqvzSIhvekyldDptk77j1b03MXTTRIqqpg==", + "dependencies": { + "System.Configuration.ConfigurationManager": "4.4.0" + } + }, + "JunitXml.TestLogger": { + "type": "Direct", + "requested": "[2.1.78, )", + "resolved": "2.1.78", + "contentHash": "4y4FSfKWxlked8ilQdqBBSeRMf5jD/Hkvyp744hc54yQcABLt4rR2Q+4hNqAqrSo+mhwAlusj2rpXpN/5TICCA==" + }, + "Microsoft.AspNet.WebApi.Client": { + "type": "Direct", + "requested": "[5.2.7, )", + "resolved": "5.2.7", + "contentHash": "/76fAHknzvFqbznS6Uj2sOyE9rJB3PltY+f53TH8dX9RiGhk02EhuFCWljSj5nnqKaTsmma8DFR50OGyQ4yJ1g==", + "dependencies": { + "Newtonsoft.Json": "10.0.1", + "Newtonsoft.Json.Bson": "1.0.1" + } + }, + "Microsoft.AspNetCore.TestHost": { + "type": "Direct", + "requested": "[3.1.9, )", + "resolved": "3.1.9", + "contentHash": "0DBtfgmM2yS4h0v+gS4JHRX4nuyQmW7Yi5/G4yB5KelA2dDXPsAiipw9z47B1jVEs9QZdOwSqPQm2R/owl2TnA==", + "dependencies": { + "System.IO.Pipelines": "4.7.3" + } + }, + "Microsoft.CodeAnalysis.FxCopAnalyzers": { + "type": "Direct", + "requested": "[3.3.0, )", + "resolved": "3.3.0", + "contentHash": "k3Icqx8kc+NrHImuiB8Jc/wd32Xeyd2B/7HOR5Qu9pyKzXQ4ikPeBAwzG2FSTuYhyIuNWvwL5k9yYBbbVz6w9w==", + "dependencies": { + "Microsoft.CodeAnalysis.VersionCheckAnalyzer": "[3.3.0]", + "Microsoft.CodeQuality.Analyzers": "[3.3.0]", + "Microsoft.NetCore.Analyzers": "[3.3.0]", + "Microsoft.NetFramework.Analyzers": "[3.3.0]" + } + }, + "Microsoft.NET.Test.Sdk": { + "type": "Direct", + "requested": "[16.7.1, )", + "resolved": "16.7.1", + "contentHash": "7T3XYuLT2CRMZXwlp8p4cEEf6y7VifxTdKwYNzCYp31CN4iyrcDKneIJvNTo0YVnTxJn+CSlGVlUnZHUlAwt9A==", + "dependencies": { + "Microsoft.CodeCoverage": "16.7.1", + "Microsoft.TestPlatform.TestHost": "16.7.1" + } + }, + "Moq": { + "type": "Direct", + "requested": "[4.14.7, )", + "resolved": "4.14.7", + "contentHash": "z1jwY3lL3d4l+92cdSnhRDUUco68HiRNfLKB9r9/PLP5lrN+ZL1Qtt3brVGVB8iY+ioBXhlFue2JtycBczE8Pw==", + "dependencies": { + "Castle.Core": "4.4.0", + "System.Threading.Tasks.Extensions": "4.5.1" + } + }, + "xunit": { + "type": "Direct", + "requested": "[2.4.1, )", + "resolved": "2.4.1", + "contentHash": "XNR3Yz9QTtec16O0aKcO6+baVNpXmOnPUxDkCY97J+8krUYxPvXT1szYYEUdKk4sB8GOI2YbAjRIOm8ZnXRfzQ==", + "dependencies": { + "xunit.analyzers": "0.10.0", + "xunit.assert": "[2.4.1]", + "xunit.core": "[2.4.1]" + } + }, + "xunit.runner.visualstudio": { + "type": "Direct", + "requested": "[2.4.3, )", + "resolved": "2.4.3", + "contentHash": "kZZSmOmKA8OBlAJaquPXnJJLM9RwQ27H7BMVqfMLUcTi9xHinWGJiWksa3D4NEtz0wZ/nxd2mogObvBgJKCRhQ==" + }, + "AutoMapper": { + "type": "Transitive", + "resolved": "10.1.1", + "contentHash": "uMgbqOdu9ZG5cIOty0C85hzzayBH2i9BthnS5FlMqKtMSHDv4ts81a2jS1VFaDBVhlBeIqJ/kQKjQY95BZde9w==", + "dependencies": { + "Microsoft.CSharp": "4.7.0", + "System.Reflection.Emit": "4.7.0" + } + }, + "AutoMapper.Extensions.Microsoft.DependencyInjection": { + "type": "Transitive", + "resolved": "8.1.0", + "contentHash": "dQyGCAYcHbGuimVvCMu4Ea2S1oYOlgO9XfVdClmY5wgygJMZoS57emPzH0qNfknmtzMm4QbDO9i237W5IDjU1A==", + "dependencies": { + "AutoMapper": "[10.1.0, 11.0.0)", + "Microsoft.Extensions.DependencyInjection.Abstractions": "3.0.0", + "Microsoft.Extensions.Options": "3.0.0" + } + }, + "Castle.Core": { + "type": "Transitive", + "resolved": "4.4.0", + "contentHash": "b5rRL5zeaau1y/5hIbI+6mGw3cwun16YjkHZnV9RRT5UyUIFsgLmNXJ0YnIN9p8Hw7K7AbG1q1UclQVU3DinAQ==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "System.Collections.Specialized": "4.3.0", + "System.ComponentModel": "4.3.0", + "System.ComponentModel.TypeConverter": "4.3.0", + "System.Diagnostics.TraceSource": "4.3.0", + "System.Dynamic.Runtime": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Xml.XmlDocument": "4.3.0" + } + }, + "Microsoft.AspNetCore.Authorization": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "cN2KJkfHcKwh82c9WGx4Tqfd2h5HflU/Mu5vYLMHON8WahHU9hE32ciIXcEIoKLNpu+zs1u1cN/qxcKTdqu89w==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "1.0.2", + "Microsoft.Extensions.Options": "1.0.2", + "System.Security.Claims": "4.0.1" + } + }, + "Microsoft.AspNetCore.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "1.0.4", + "contentHash": "ybY8FOkdNfBPB5PLv1JO+It/94ftBzGUI1WqU4XySbIWyhw2TPmmKAUuO9uvJoR0qpsFup8FJz6trsBcBITg9w==", + "dependencies": { + "Microsoft.AspNetCore.Hosting.Server.Abstractions": "1.0.4", + "Microsoft.AspNetCore.Http.Abstractions": "1.0.3", + "Microsoft.Extensions.Configuration.Abstractions": "1.0.2", + "Microsoft.Extensions.DependencyInjection.Abstractions": "1.0.2", + "Microsoft.Extensions.FileProviders.Abstractions": "1.0.1", + "Microsoft.Extensions.Logging.Abstractions": "1.0.2" + } + }, + "Microsoft.AspNetCore.Hosting.Server.Abstractions": { + "type": "Transitive", + "resolved": "1.0.4", + "contentHash": "XUiQPe/CflK1i0Voo9S6/G1iQh00gQ6sMqi3LRtKeceBbO6AOostaAUdhjyME92MapI4VFNl+Z+/KXUlMAExJQ==", + "dependencies": { + "Microsoft.AspNetCore.Http.Features": "1.0.3", + "Microsoft.Extensions.Configuration.Abstractions": "1.0.2" + } + }, + "Microsoft.AspNetCore.Http": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "kfNOIGGgVtMzsSWZzXBqz5zsdo8ssBa90YHzZt95N8ARGXoolSaBHy6yBoMm/XcpbXM+m/x1fixTTMIWMgzJdQ==", + "dependencies": { + "Microsoft.AspNetCore.Http.Abstractions": "1.0.3", + "Microsoft.AspNetCore.WebUtilities": "1.0.3", + "Microsoft.Extensions.ObjectPool": "1.0.1", + "Microsoft.Extensions.Options": "1.0.2", + "Microsoft.Net.Http.Headers": "1.0.3", + "System.Buffers": "4.0.0", + "System.Threading": "4.0.11" + } + }, + "Microsoft.AspNetCore.Http.Abstractions": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "nnjvAf7ag6P0DyD/0nhRGjLpv+3DkPU0juF8aQh46X8uF4kzjJdrh65oL+4PVOu3K6BgSg6OVUs0QC0SE0FRtg==", + "dependencies": { + "Microsoft.AspNetCore.Http.Features": "1.0.3", + "System.Globalization.Extensions": "4.0.1", + "System.Linq.Expressions": "4.1.1", + "System.Reflection.TypeExtensions": "4.1.0", + "System.Runtime.InteropServices": "4.1.0", + "System.Text.Encodings.Web": "4.0.1" + } + }, + "Microsoft.AspNetCore.Http.Extensions": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "+7Sd+14nexIJqcB4S1Eur9kzeMZ5CBtrxkei+PNbD78fg8vO3+TcCgrl5SBNTsUB/VJAfD/s0fgs5t+hHRj2Pg==", + "dependencies": { + "Microsoft.AspNetCore.Http.Abstractions": "1.0.3", + "Microsoft.Extensions.FileProviders.Abstractions": "1.0.1", + "Microsoft.Net.Http.Headers": "1.0.3", + "System.Buffers": "4.0.0", + "System.IO.FileSystem": "4.0.1" + } + }, + "Microsoft.AspNetCore.Http.Features": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "Ihq57tseNyPbJTmFXY4jQ4JkxLP0lh45VRwocQci/sFx+qcJGvWB+sJJ2/YPLy/qTWFAEfNAcswuY3OsNH9Gwg==", + "dependencies": { + "Microsoft.Extensions.Primitives": "1.0.1", + "System.Collections": "4.0.11", + "System.ComponentModel": "4.0.1", + "System.Linq": "4.1.0", + "System.Net.Primitives": "4.0.11", + "System.Net.WebSockets": "4.0.0", + "System.Runtime.Extensions": "4.1.0", + "System.Security.Claims": "4.0.1", + "System.Security.Cryptography.X509Certificates": "4.1.0", + "System.Security.Principal": "4.0.1" + } + }, + "Microsoft.AspNetCore.JsonPatch": { + "type": "Transitive", + "resolved": "1.0.0", + "contentHash": "WVaSVS+dDlWCR/qerHnBxU9tIeJ9GMA3M5tg4cxH7/cJYZZLnr2zvaFHGB+cRRNCKKTJ0pFRxT7ES8knhgAAaA==", + "dependencies": { + "Microsoft.CSharp": "4.0.1", + "Newtonsoft.Json": "9.0.1", + "System.Collections.Concurrent": "4.0.12", + "System.ComponentModel.TypeConverter": "4.1.0", + "System.Diagnostics.Debug": "4.0.11", + "System.Globalization": "4.0.11", + "System.Linq": "4.1.0", + "System.Reflection.Extensions": "4.0.1", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime.Extensions": "4.1.0", + "System.Runtime.Serialization.Primitives": "4.1.1", + "System.Text.Encoding.Extensions": "4.0.11" + } + }, + "Microsoft.AspNetCore.Mvc.Abstractions": { + "type": "Transitive", + "resolved": "1.0.4", + "contentHash": "Isqgif1nuB+um86cEkpL8KnoxFCUCXBsbs9PuiuzElvlSiv4Ek3LvtrSUcbivekDDfys8CDbJhxwEI7WKJieAQ==", + "dependencies": { + "Microsoft.AspNetCore.Routing.Abstractions": "1.0.4", + "Microsoft.CSharp": "4.0.1", + "Microsoft.Net.Http.Headers": "1.0.3", + "System.ComponentModel.TypeConverter": "4.1.0", + "System.Reflection.Extensions": "4.0.1", + "System.Text.Encoding.Extensions": "4.0.11" + } + }, + "Microsoft.AspNetCore.Mvc.ApiExplorer": { + "type": "Transitive", + "resolved": "1.0.4", + "contentHash": "ujCFTM42U2WKUBhdaoLoiI+wVHgYhrmDrkl5+hWJ7EJW4fhp42w4cRZ97tjuveWr+M6JZjpS0q+7PVofQzFUiw==", + "dependencies": { + "Microsoft.AspNetCore.Mvc.Core": "1.0.4" + } + }, + "Microsoft.AspNetCore.Mvc.Core": { + "type": "Transitive", + "resolved": "1.0.4", + "contentHash": "1ukcttN1+T82hWXE8WS5kawkruolKI6LPVqVI4rTzN16kFszS/UqTrcwSUEnmTRpmWgFo665V3c2GpdQ9B6znw==", + "dependencies": { + "Microsoft.AspNetCore.Authorization": "1.0.3", + "Microsoft.AspNetCore.Hosting.Abstractions": "1.0.3", + "Microsoft.AspNetCore.Http": "1.0.3", + "Microsoft.AspNetCore.Mvc.Abstractions": "1.0.4", + "Microsoft.AspNetCore.Routing": "1.0.4", + "Microsoft.Extensions.DependencyModel": "1.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "1.0.1", + "Microsoft.Extensions.Logging.Abstractions": "1.0.2", + "Microsoft.Extensions.PlatformAbstractions": "1.0.0", + "System.Buffers": "4.0.0", + "System.Diagnostics.DiagnosticSource": "4.0.0", + "System.Text.Encoding": "4.0.11" + } + }, + "Microsoft.AspNetCore.Mvc.Formatters.Json": { + "type": "Transitive", + "resolved": "1.0.4", + "contentHash": "i8WWK2GwlBHfOL+d+kknJWPks6DS9tbN6nfJZU4yb+/wfUAYd311B2CIHzdat3IewubnK1TYONwrhQcs2FbLeA==", + "dependencies": { + "Microsoft.AspNetCore.JsonPatch": "1.0.0", + "Microsoft.AspNetCore.Mvc.Core": "1.0.4" + } + }, + "Microsoft.AspNetCore.NodeServices": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "bbd3FlSPWiRQrIcBLa5TaOvo4gjmmiNMkxA8VmZ6u0eIpS0Yj35/eTopaGdtzqwlqj5jXbdRoib1MruXuPaW8A==", + "dependencies": { + "Microsoft.Extensions.Logging.Console": "3.1.9", + "Newtonsoft.Json": "12.0.2" + } + }, + "Microsoft.AspNetCore.Routing": { + "type": "Transitive", + "resolved": "1.0.4", + "contentHash": "mdIF3ckRothHWuCSFkk6YXACj5zxi5qM+cEAHjcpP04/wCHUoV0gGVnW+HI+LyFXE6JUwu2zXn5tfsCpW0U+SA==", + "dependencies": { + "Microsoft.AspNetCore.Http.Extensions": "1.0.3", + "Microsoft.AspNetCore.Routing.Abstractions": "1.0.4", + "Microsoft.Extensions.Logging.Abstractions": "1.0.2", + "Microsoft.Extensions.ObjectPool": "1.0.1", + "Microsoft.Extensions.Options": "1.0.2", + "System.Collections": "4.0.11", + "System.Text.RegularExpressions": "4.1.0" + } + }, + "Microsoft.AspNetCore.Routing.Abstractions": { + "type": "Transitive", + "resolved": "1.0.4", + "contentHash": "GHxVt6LlXHFsCUd2Un+/vY1tBTXxnogfbDO0b8G5EGmkapSK+dOGOLJviscxQkp338Uabs081JEIdkRymI5GXA==", + "dependencies": { + "Microsoft.AspNetCore.Http.Abstractions": "1.0.3", + "System.Collections.Concurrent": "4.0.12", + "System.Reflection.Extensions": "4.0.1", + "System.Threading.Tasks": "4.0.11" + } + }, + "Microsoft.AspNetCore.SpaServices": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "Fb+N2ZyF1wNrGeWggT+Ovv6W8AAVxfi4V/SnuEsBOR+nmkFhty9zyh6IDRRS98GJK6OE3adqqPbWMtJqbxYnNA==", + "dependencies": { + "Microsoft.AspNetCore.NodeServices": "3.1.9" + } + }, + "Microsoft.AspNetCore.SpaServices.Extensions": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "ciy2GCvRnh9C22laArLsaItS+72U6Hqf4nDYShdvFgcen2ZV+NNSitb/B3vsmFfIPM8m4mf2x4T+vZ6OlI5XaA==", + "dependencies": { + "Microsoft.AspNetCore.SpaServices": "3.1.9", + "Microsoft.Extensions.FileProviders.Physical": "3.1.9" + } + }, + "Microsoft.AspNetCore.StaticFiles": { + "type": "Transitive", + "resolved": "1.0.4", + "contentHash": "2pNvwewAazhaaCdw2CGUvIcDrNQMlqP57JgBDf3v+pRj1rZ29HVnpvkX6a+TrmRYlJNmmxHOKEt468uE/gDcFw==", + "dependencies": { + "Microsoft.AspNetCore.Hosting.Abstractions": "1.0.4", + "Microsoft.AspNetCore.Http.Extensions": "1.0.3", + "Microsoft.Extensions.FileProviders.Abstractions": "1.0.1", + "Microsoft.Extensions.Logging.Abstractions": "1.0.2", + "Microsoft.Extensions.WebEncoders": "1.0.3" + } + }, + "Microsoft.AspNetCore.WebUtilities": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "snSGNs5EEisqivDjDiskFkFyu+DV2Ib9sMPOBQKtoFwI5H1W5YNB/rIVqDZQL16zj/uzdwwxrdE/5xhkVyf6gQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "1.0.1", + "System.Buffers": "4.0.0", + "System.Collections": "4.0.11", + "System.IO": "4.1.0", + "System.IO.FileSystem": "4.0.1", + "System.Text.Encodings.Web": "4.0.1" + } + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "1.1.1", + "contentHash": "yuvf07qFWFqtK3P/MRkEKLhn5r2UbSpVueRziSqj0yJQIKFwG1pq9mOayK3zE5qZCTs0CbrwL9M6R8VwqyGy2w==" + }, + "Microsoft.Bcl.HashCode": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "J2G1k+u5unBV+aYcwxo94ip16Rkp65pgWFb0R6zwJipzWNMgvqlWeuI7/+R+e8bob66LnSG+llLJ+z8wI94cHg==" + }, + "Microsoft.CodeAnalysis.VersionCheckAnalyzer": { + "type": "Transitive", + "resolved": "3.3.0", + "contentHash": "xjLM3DRFZMan3nQyBQEM1mBw6VqQybi4iMJhMFW6Ic1E1GCvqJR3ABOwEL7WtQjDUzxyrGld9bASnAos7G/Xyg==" + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "16.7.1", + "contentHash": "PhSppbk+kvAyD9yGJIcBRJ/XYwY+21YK88l22PGTtixaxNdjnx1idVKh88LCGwKaTL8HhlnQ41VmBiBdZJzIQw==" + }, + "Microsoft.CodeQuality.Analyzers": { + "type": "Transitive", + "resolved": "3.3.0", + "contentHash": "zZ3miq6u22UFQKhfJyLnVEJ+DgeOopLh3eKJnKAcOetPP2hiv3wa7kHZlBDeTvtqJQiAQhAVbttket8XxjN1zw==" + }, + "Microsoft.CSharp": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA==" + }, + "Microsoft.Data.Sqlite.Core": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "+u4PeT1npi2EzhxGc5r1Z2z73zuXw+TlKVZm44WQhNCUw4LtUVDaxGSpUhrjW+X4snBCBfr4kT/uJyKnL4R4og==", + "dependencies": { + "SQLitePCLRaw.core": "2.0.2" + } + }, + "Microsoft.DotNet.PlatformAbstractions": { + "type": "Transitive", + "resolved": "3.1.6", + "contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg==" + }, + "Microsoft.EntityFrameworkCore": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "u3A2W0BvAuAF2jgW+WX+C+Sh8sMGX5Kl1hdA0gu6A/XSrZQoW/BUP4a/q2n3iitDGndaorqjAKx+Spb9gBto+w==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "1.1.1", + "Microsoft.Bcl.HashCode": "1.1.0", + "Microsoft.EntityFrameworkCore.Abstractions": "3.1.9", + "Microsoft.EntityFrameworkCore.Analyzers": "3.1.9", + "Microsoft.Extensions.Caching.Memory": "3.1.9", + "Microsoft.Extensions.DependencyInjection": "3.1.9", + "Microsoft.Extensions.Logging": "3.1.9", + "System.Collections.Immutable": "1.7.1", + "System.ComponentModel.Annotations": "4.7.0", + "System.Diagnostics.DiagnosticSource": "4.7.1" + } + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "IR6Y4RJVlw0QXdWXjF3Kx9s1QLiicJus+BFBKr43lBtriV20j3yrWMoaZ9W1AUUgnicZXpXVcNfklqtmwb9Sxw==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "eXGyx/Lb1fiiKtnIStdxGrfBSSQg8oZytE10f1T/2xAx12W9dKB9U9fg05cwNCDC0S2CXILsmZHYaGqCSXVAqQ==" + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "7fhWuSfrCYlv/hvOX5OhbFJF/G9f8sifqTrJiYnAYLDOvNizwv7t9tFPD8JwaF3zM2S54O5/Vni2NxvwzSaW2w==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "3.1.9" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "sMFCWv/1UcsFQZeGQcbfPbEZKZ1oKZqWZXTbc7PEZVMIXu82nbavstdNQ84x5IBXJkxl8iW3zjChb/FRBr5uLQ==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Sqlite.Core": "3.1.9", + "SQLitePCLRaw.bundle_e_sqlite3": "2.0.2" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite.Core": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "Da6h8LdpJwKc1az9DMWt2Mt6gHXPRZqwiumV1Zx0AuM3EThyokVDzBGy2sti0AcBhcQMLJHPEr5R9xuiWvaYYQ==", + "dependencies": { + "Microsoft.Data.Sqlite.Core": "3.1.9", + "Microsoft.DotNet.PlatformAbstractions": "3.1.6", + "Microsoft.EntityFrameworkCore.Relational": "3.1.9", + "Microsoft.Extensions.DependencyModel": "3.1.6" + } + }, + "Microsoft.Extensions.ApiDescription.Server": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "LH4OE/76F6sOCslif7+Xh3fS/wUUrE5ryeXAMcoCnuwOQGT5Smw0p57IgDh/pHgHaGz/e+AmEQb7pRgb++wt0w==" + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "/2QsPAsUZD4qvftZkUKHRRRryPDXWh606/iNXPLrulwHLMr9JNsKBJWVqylT3qU92nJok5VoqSblkY9mSyxFyg==", + "dependencies": { + "Microsoft.Extensions.Primitives": "3.1.9" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "/JrVMVetX/kpJQUIlJ6NLQ3zbF0yyryXpo4+uFCqYIUZzgmWk8DS/zSKcyj1tQ3410+vhDEAPngxC+hg0IlJeg==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "3.1.9", + "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.9", + "Microsoft.Extensions.Logging.Abstractions": "3.1.9", + "Microsoft.Extensions.Options": "3.1.9" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "lqdkOGNeTMKG981Q7yWGlRiFbIlsRwTlMMiybT+WOzUCFBS/wc25tZgh7Wm/uRoBbWefgvokzmnea7ZjmFedmA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "3.1.9" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "vOJxPKczaHpXeZFrxARxYwsEulhEouXc5aZGgMdkhV/iEXX9/pfjqKk76rTG+4CsJjHV+G/4eMhvOIaQMHENNA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "3.1.9" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "BG6HcT7tARYakftqfQu+cLksgIWG1NdxMY+igI12hdZrUK+WjS973NiRyuao/U9yyTeM9NPwRnC61hCmG3G3jg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "3.1.9" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "ORqfrAACcvTInie1oGola5uky344/PiNfgayTPuZWV4WnSfIQZJQm/ZLpGshJE3h7TqwYaYElGazK/yaM2bFLA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.9" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "8PkcaPwiTPOhqshoY4+rQUbz86X6YpLDLUqXOezh7L2A3pgpBmeBBByYIffofBlvQxDdQ0zB2DkWjbZWyCxRWg==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "3.1.6", + "contentHash": "/UlDKULIVkLQYn1BaHcy/rc91ApDxJb7T75HcCbGdqwvxhnRQRKM2di1E70iCPMF9zsr6f4EgQTotBGxFIfXmw==", + "dependencies": { + "System.Text.Json": "4.7.2" + } + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "Q4SGwEFZKiZbpzPgdGbQUULxtcH1zXMOwCPKSm6QwVcOCGshf3QLfBh+O/GyFH4B0RfZ16nKyeW1mMONlRyjUw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "3.1.9" + } + }, + "Microsoft.Extensions.FileProviders.Embedded": { + "type": "Transitive", + "resolved": "1.0.1", + "contentHash": "nSEa8bH3fVdTYGqK4twOKLxxgKIW3cz9g9mrzhPh/CmdvGJWKRTIlBIZi7lz+lqNQpxean5vbAo84R/mU+JpGA==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "1.0.1", + "System.Runtime.Extensions": "4.1.0" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "HWDSsblTCQp7EEJJmnLzttIhFGzDu+DGqBbOvGCdFT0+pkCuBkn3EiWpEEcm5WMTO5njmsbLSK9ZuUUf2zPsFg==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "3.1.9", + "Microsoft.Extensions.FileSystemGlobbing": "3.1.9" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "5bnewG1aBiSESPNwcXGIxDDRN95uqdy+fqZZ8Z63Et5rRNlAwAfXHOrg+FTht7UjHobjvtjzquMCbAWhWEPHIw==" + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "+V3i0jCQCO6IIOf6e+fL0SqrZd2x/Krug9EEL1JHa9R03RsbEpltCtjVY5hxedyuyuQKwvLoR12sCfu/9XEUAw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Binder": "3.1.9", + "Microsoft.Extensions.DependencyInjection": "3.1.9", + "Microsoft.Extensions.Logging.Abstractions": "3.1.9", + "Microsoft.Extensions.Options": "3.1.9" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "W5fbF8qVR9SMVVJqDQLIR7meWbev6Pu/lbrm7LDNr4Sp7HOotr4k2UULTdFSXOi5aoDdkQZpWnq0ZSpjrR3tjg==" + }, + "Microsoft.Extensions.Logging.Configuration": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "hv6XsGgikrbkolBJdF1usl9R/nrliC5mifMqHMEY9zWcCLwNkXMJiS8p0lbosrnpVAMi4PbNx39DB51Dqscd0w==", + "dependencies": { + "Microsoft.Extensions.Logging": "3.1.9", + "Microsoft.Extensions.Options.ConfigurationExtensions": "3.1.9" + } + }, + "Microsoft.Extensions.Logging.Console": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "8Dusl1rkDivmvLrwj6QAo917xMHPiDBzG3IG3agiyDdtsC/fRp+1VN5iIN+O09PtEaMged2OLA6wCDwfSTSTZw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "3.1.9", + "Microsoft.Extensions.Logging": "3.1.9", + "Microsoft.Extensions.Logging.Configuration": "3.1.9" + } + }, + "Microsoft.Extensions.ObjectPool": { + "type": "Transitive", + "resolved": "1.0.1", + "contentHash": "pJMOnxuqmG37OjccfvtqVoo3bQGoN+0EJUzzp7+2uxSdioER82caAk6Yi/z5aysapn5XENNIIa7SaYnYKSS69A==", + "dependencies": { + "System.Diagnostics.Debug": "4.0.11", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime.Extensions": "4.1.0", + "System.Threading": "4.0.11" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "EIb3G1DL+Rl9MvJR7LjI1wCy2nfTN4y8MflbOftn1HLYQBj/Rwl8kUbGTrSFE01c99Wm4ETjWVsjqKcpFvhPng==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.9", + "Microsoft.Extensions.Primitives": "3.1.9" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "u5jh7RW+Ev81YqK1ZoBG0lftp2MA9xqXiTiRL46XzaPj2ScNUyiVbzcVY0fPbE27UOpT2hj+yPzRSOMIIo55UA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "3.1.9", + "Microsoft.Extensions.Configuration.Binder": "3.1.9", + "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.9", + "Microsoft.Extensions.Options": "3.1.9" + } + }, + "Microsoft.Extensions.PlatformAbstractions": { + "type": "Transitive", + "resolved": "1.0.0", + "contentHash": "zyjUzrOmuevOAJpIo3Mt5GmpALVYCVdLZ99keMbmCxxgQH7oxzU58kGHzE6hAgYEiWsdfMJLjVR7r+vSmaJmtg==", + "dependencies": { + "System.AppContext": "4.1.0", + "System.Reflection": "4.1.0", + "System.Reflection.Extensions": "4.0.1", + "System.Reflection.TypeExtensions": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime.Extensions": "4.1.0" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "IrHecH0eGG7/XoeEtv++oLg/sJHRNyeCqlA9RhAo6ig4GpOTjtDr32sBMYuuLtUq8ALahneWkrOzoBAwJ4L4iA==" + }, + "Microsoft.Extensions.WebEncoders": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "TClNvczWRxF6bVPhn5EK3Y3QNi5jTP68Qur+5Fk+MQLPeBI18WN7X145DDJ6bFeNOwgdCHl73lHs5uZp9ish1A==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "1.0.2", + "Microsoft.Extensions.Options": "1.0.2", + "System.Text.Encodings.Web": "4.0.1" + } + }, + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "Transitive", + "resolved": "6.8.0", + "contentHash": "+7JIww64PkMt7NWFxoe4Y/joeF7TAtA/fQ0b2GFGcagzB59sKkTt/sMZWR6aSZht5YC7SdHi3W6yM1yylRGJCQ==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "6.8.0" + } + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "6.8.0", + "contentHash": "Rfh/p4MaN4gkmhPxwbu8IjrmoDncGfHHPh1sTnc0AcM/Oc39/fzC9doKNWvUAjzFb8LqA6lgZyblTrIsX/wDXg==" + }, + "Microsoft.IdentityModel.Tokens": { + "type": "Transitive", + "resolved": "6.8.0", + "contentHash": "gTqzsGcmD13HgtNePPcuVHZ/NXWmyV+InJgalW/FhWpII1D7V1k0obIseGlWMeA4G+tZfeGMfXr0klnWbMR/mQ==", + "dependencies": { + "Microsoft.CSharp": "4.5.0", + "Microsoft.IdentityModel.Logging": "6.8.0", + "System.Security.Cryptography.Cng": "4.5.0" + } + }, + "Microsoft.Net.Http.Headers": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "2F8USh4hR5xppvaxtw2EStX74Ih+HhRj7aQD1uaB9JmTGy478F7t4VU+IdZXauEDrvS7LYAyyhmOExsUFK3PAw==", + "dependencies": { + "System.Buffers": "4.0.0", + "System.Collections": "4.0.11", + "System.Diagnostics.Contracts": "4.0.1", + "System.Globalization": "4.0.11", + "System.Linq": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime.Extensions": "4.1.0", + "System.Text.Encoding": "4.0.11" + } + }, + "Microsoft.NetCore.Analyzers": { + "type": "Transitive", + "resolved": "3.3.0", + "contentHash": "6qptTHUu1Wfszuf83NhU0IoAb4j7YWOpJs6oc6S4G/nI6aGGWKH/Xi5Vs9L/8lrI74ijEEzPcIwafSQW5ASHtA==" + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" + }, + "Microsoft.NETCore.Targets": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==" + }, + "Microsoft.NetFramework.Analyzers": { + "type": "Transitive", + "resolved": "3.3.0", + "contentHash": "JTfMic5fEFWICePbr7GXOGPranqS9Qxu2U/BZEcnnGbK1SFW8TxRyGp6O1L52xsbfOdqmzjc0t5ubhDrjj+Xpg==" + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "16.7.1", + "contentHash": "FL+VpAC/nCCzj80MwX6L8gJD06u2m1SKcQQLAymDLFqNtgtI9h3J5n0mVN+s18qcMzybsmO9GK7rMuHYx11KMg==", + "dependencies": { + "NuGet.Frameworks": "5.0.0" + } + }, + "Microsoft.TestPlatform.TestHost": { + "type": "Transitive", + "resolved": "16.7.1", + "contentHash": "mv7MnBDtqwQAjoH+AphE+Tu0dsF6x/c7Zs8umkb2McbvNALJdfBuWJQbiXGWqhNq7k8eMmnkNO6klJz4pkgekw==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "16.7.1", + "Newtonsoft.Json": "9.0.1" + } + }, + "Microsoft.Win32.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "Namotion.Reflection": { + "type": "Transitive", + "resolved": "1.0.14", + "contentHash": "wuJGiFvGfehH2w7jAhMbCJt0/rvUuHyqSZn0sMhNTviDfBZRyX8LFlR/ndQcofkGWulPDfH5nKYTeGXE8xBHPA==", + "dependencies": { + "Microsoft.CSharp": "4.3.0" + } + }, + "NETStandard.Library": { + "type": "Transitive", + "resolved": "1.6.1", + "contentHash": "WcSp3+vP+yHNgS8EV5J7pZ9IRpeDuARBPN28by8zqff1wJQXm26PVU8L3/fYLBJVU7BtDyqNVWq2KlCVvSSR4A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.Win32.Primitives": "4.3.0", + "System.AppContext": "4.3.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Console": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.Compression.ZipFile": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Linq": "4.3.0", + "System.Linq.Expressions": "4.3.0", + "System.Net.Http": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Net.Sockets": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.InteropServices.RuntimeInformation": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Timer": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0", + "System.Xml.XDocument": "4.3.0" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "12.0.2", + "contentHash": "rTK0s2EKlfHsQsH6Yx2smvcTCeyoDNgCW7FEYyV01drPlh2T243PR2DiDXqtC5N4GDm4Ma/lkxfW5a/4793vbA==" + }, + "Newtonsoft.Json.Bson": { + "type": "Transitive", + "resolved": "1.0.1", + "contentHash": "5PYT/IqQ+UK31AmZiSS102R6EsTo+LGTSI8bp7WAUqDKaF4wHXD8U9u4WxTI1vc64tYi++8p3dk3WWNqPFgldw==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "Newtonsoft.Json": "10.0.1" + } + }, + "NJsonSchema": { + "type": "Transitive", + "resolved": "10.2.1", + "contentHash": "/BtWbYTusyoSgQkCB4eYijMfZotB/rfASDsl1k9evlkm5vlOP4s4Y09TOzBChU77d/qUABVYL1Xf+TB8E0Wfpw==", + "dependencies": { + "Namotion.Reflection": "1.0.14", + "Newtonsoft.Json": "9.0.1" + } + }, + "NSwag.Annotations": { + "type": "Transitive", + "resolved": "13.8.2", + "contentHash": "/GO+35CjPYQTPS5/Q8udM5JAMEWVo8JsrkV2Uw3OW4/AJU9iOS7t6WJid6ZlkpLMjnW7oex9mvJ2EZNE4eOG/Q==" + }, + "NSwag.AspNetCore": { + "type": "Transitive", + "resolved": "13.8.2", + "contentHash": "SNGlVSZoMyywBWueZBxl3B/nfaIM0fAcuNhTD/cfMKUn3Cn/Oi8d45HZY5vAPqczvppTbk4cZXyVwWDOfgiPbA==", + "dependencies": { + "Microsoft.AspNetCore.Mvc.Core": "1.0.4", + "Microsoft.AspNetCore.Mvc.Formatters.Json": "1.0.4", + "Microsoft.AspNetCore.StaticFiles": "1.0.4", + "Microsoft.Extensions.ApiDescription.Server": "3.0.0", + "Microsoft.Extensions.FileProviders.Embedded": "1.0.1", + "NSwag.Annotations": "13.8.2", + "NSwag.Core": "13.8.2", + "NSwag.Generation": "13.8.2", + "NSwag.Generation.AspNetCore": "13.8.2", + "System.IO.FileSystem": "4.3.0", + "System.Xml.XPath.XDocument": "4.0.1" + } + }, + "NSwag.Core": { + "type": "Transitive", + "resolved": "13.8.2", + "contentHash": "Hm6pU9qFJuXLo3b27+JTXztfeuI/15Ob1sDsfUu4rchN0+bMogtn8Lia8KVbcalw/M+hXc0rWTFp5ueP23e+iA==", + "dependencies": { + "NJsonSchema": "10.2.1", + "Newtonsoft.Json": "9.0.1" + } + }, + "NSwag.Generation": { + "type": "Transitive", + "resolved": "13.8.2", + "contentHash": "LBIrpHFRZeMMbqL1hdyGb7r8v+T52aOCARxwfAmzE+MlOHVpjsIxyNSXht9EzBFMbSH0tj7CK2Ugo7bm+zUssg==", + "dependencies": { + "NJsonSchema": "10.2.1", + "NSwag.Core": "13.8.2", + "Newtonsoft.Json": "9.0.1" + } + }, + "NSwag.Generation.AspNetCore": { + "type": "Transitive", + "resolved": "13.8.2", + "contentHash": "0ydVv6OidspZ/MS6qmU8hswGtXwq5YZPg+2a2PHGD6jNp2Fef4j1wC3xa3hplDAq7cK+BgpyDKtvj9+X01+P5g==", + "dependencies": { + "Microsoft.AspNetCore.Mvc.ApiExplorer": "1.0.4", + "Microsoft.AspNetCore.Mvc.Core": "1.0.4", + "Microsoft.AspNetCore.Mvc.Formatters.Json": "1.0.4", + "NJsonSchema": "10.2.1", + "NSwag.Generation": "13.8.2" + } + }, + "NuGet.Frameworks": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "c5JVjuVAm4f7E9Vj+v09Z9s2ZsqFDjBpcsyS3M9xRo0bEdm/LVZSzLxxNvfvAwRiiE8nwe1h2G4OwiwlzFKXlA==" + }, + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "HdSSp5MnJSsg08KMfZThpuLPJpPwE5hBXvHwoKWosyHHfe8Mh5WKT0ylEOf6yNzX6Ngjxe4Whkafh5q7Ymac4Q==" + }, + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "+yH1a49wJMy8Zt4yx5RhJrxO/DBDByAiCzNwiETI+1S4mPdCu0OY4djdciC7Vssk0l22wQaDLrXxXkp+3+7bVA==" + }, + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c3YNH1GQJbfIPJeCnr4avseugSqPrxwIqzthYyZDN6EuOyNOzq+y2KSUfRcXauya1sF4foESTgwM5e1A8arAKw==" + }, + "runtime.native.System": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c/qWt2LieNZIj1jGnVNsE2Kl23Ya2aSTBuXMD6V7k9KWr6l16Tqdwq+hJScEpWER9753NWC8h96PaVNY5Ld7Jw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "INBPonS5QPEgn7naufQFXJEp3zX6L4bwHgJ/ZH78aBTpeNfQMtf7C6VrAFhlq2xxWBveIOWyFzQjJ8XzHMhdOQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZVuZJqnnegJhd2k/PtAbbIcZ3aZeITq3sj06oKfMBSfphW3HDmk/t4ObvbOk/JA/swGR0LNqMksAh/f7gpTROg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DloMk88juo0OuOWr56QG7MNchmafTLYWvABy36izkrLI5VledI0rq28KGs1i9wbpeT9NPQrx/wTf8U2vazqQ3Q==", + "dependencies": { + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": "4.3.0" + } + }, + "runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "NS1U+700m4KFRHR5o4vo9DSlTmlCKu/u7dtE5sUHVIPB+xpXxYQvgBgA6wEIeCz6Yfn0Z52/72WYsToCEPJnrw==", + "dependencies": { + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "b3pthNgxxFcD+Pc0WSEoC0+md3MyhRS6aCEeenvNE3Fdw1HyJ18ZhRFVJJzIeR/O/jpxPboB805Ho0T3Ul7w8A==" + }, + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KeLz4HClKf+nFS7p/6Fi/CqyLXh81FpiGzcmuS8DGi9lUqSnZ6Es23/gv2O+1XVGfrbNmviF7CckBpavkBoIFQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kVXCuMTrTlxq4XOOMAysuNwsXWpYeboGddNGpIgNSZmv1b6r/s/DPk0fYMB7Q5Qo4bY68o48jt4T4y5BVecbCQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X7IdhILzr4ROXd8mI1BUCQMSHSQwelUlBjF1JyTKCjXaOGn2fB4EKBxQbCK2VjO3WaWIdlXZL3W6TiIVnrhX4g==" + }, + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "nyFNiCk/r+VOiIqreLix8yN+q3Wga9+SE8BCgkf+2BwEKiNx6DyvFjCgkfV743/grxv8jHJ8gUK4XEQw7yzRYg==" + }, + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ytoewC6wGorL7KoCAvRfsgoJPJbNq+64k2SqW6JcOAebWsFUvCCYgfzQMrnpvPiEl4OrblUlhF2ji+Q1+SVLrQ==" + }, + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "I8bKw2I8k58Wx7fMKQJn2R8lamboCAiHfHeV/pS65ScKWMMI0+wJkLYlEKvgW1D/XvSl/221clBoR2q9QNNM7A==" + }, + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VB5cn/7OzUfzdnC8tqAIMQciVLiq2epm2NrAm1E9OjNRyG4lVhfR61SMcLizejzQP8R8Uf/0l5qOIbUEi+RdEg==" + }, + "SixLabors.ImageSharp": { + "type": "Transitive", + "resolved": "1.0.1", + "contentHash": "DjLoFNdUfsDP7RhPpr5hcUhl1XiejqBML9uDWuOUwCkc0Y+sG9IJLLbqSOi9XeoWqPviwdcDm1F8nKdF0qTYIQ==" + }, + "SQLitePCLRaw.bundle_e_sqlite3": { + "type": "Transitive", + "resolved": "2.0.2", + "contentHash": "OVPI/nh5AqfLCIKhAYqjCa6AHhc7oKApGcGM3UhMRSerFiBx58nSpGwxVFdMgjOCWZR+fA49nzsnKlWp5hFo8w==", + "dependencies": { + "SQLitePCLRaw.core": "2.0.2", + "SQLitePCLRaw.lib.e_sqlite3": "2.0.2", + "SQLitePCLRaw.provider.dynamic_cdecl": "2.0.2" + } + }, + "SQLitePCLRaw.core": { + "type": "Transitive", + "resolved": "2.0.2", + "contentHash": "TFSBX426OelS1tkaVC254NVVlrJIe9YLhWPkEvuqJj2104QpmDmEYOhfdfDJD1E/2SmqDhoRw1ek5cQHj8olcQ==", + "dependencies": { + "System.Memory": "4.5.3" + } + }, + "SQLitePCLRaw.lib.e_sqlite3": { + "type": "Transitive", + "resolved": "2.0.2", + "contentHash": "S+Tsqe/M7wsc+9HeediI6UHtBKf2X586aRwhi1aBVLGe0WxkAo52O9ZxwEy/v8XMLefcrEMupd2e9CDlIT6QCw==" + }, + "SQLitePCLRaw.provider.dynamic_cdecl": { + "type": "Transitive", + "resolved": "2.0.2", + "contentHash": "ZSwacbKJUsxJEZxwT23uZVrGbaIvXcADZDz5Sr66fikO5eehdcceDncjzwzTzWfW13di8gpTpstx3WJSt/Ci5Q==", + "dependencies": { + "SQLitePCLRaw.core": "2.0.2" + } + }, + "System.AppContext": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "fKC+rmaLfeIzUhagxY17Q9siv/sPrjjKcfNg1Ic8IlQkZLipo8ljcaZQu4VtI4Jqbzjc2VTjzGLF6WmsRXAEgA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ratu44uTIHgeBeI0dE8DWvmXVBSo4u7ozRZZHOMmK/JPpYyo0dAfgSiHlpiObMQ5lEtEyIXA40sKRYg5J6A8uQ==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Collections": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Collections.Concurrent": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ztl69Xp0Y/UXCL+3v3tEU+lIy+bvjKNUmopn1wep/a291pVPK7dxBd6T7WnlQqRog+d1a/hSsgRsmFnIBKTPLQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "1.7.1", + "contentHash": "B43Zsz5EfMwyEbnObwRxW5u85fzJma3lrDeGcSAV1qkhSRTNY5uXAByTn9h9ddNdhM+4/YoLc/CI43umjwIl9Q==" + }, + "System.Collections.NonGeneric": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "prtjIEMhGUnQq6RnPEYLpFt8AtLbp9yq2zxOSrY7KJJZrw25Fi97IzBqY7iqssbM61Ek5b8f3MG/sG1N2sN5KA==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Collections.Specialized": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Epx8PoVZR0iuOnJJDzp7pWvdfMMOAvpUo95pC4ScH2mJuXkKA2Y4aR3cG9qt2klHgSons1WFh4kcGW7cSXvrxg==", + "dependencies": { + "System.Collections.NonGeneric": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Extensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.ComponentModel": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VyGn1jGRZVfxnh8EdvDCi71v3bMXrsu8aYJOwoV7SNDLVhiEqwP86pPMyRGsDsxhXAm2b3o9OIqeETfN5qfezw==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.ComponentModel.Annotations": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "0YFqjhp/mYkDGpU0Ye1GjE53HMp9UVfGN7seGpAMttAC0C40v5gw598jCgpbBLMmCo0E5YRLBv5Z2doypO49ZQ==" + }, + "System.ComponentModel.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "j8GUkCpM8V4d4vhLIIoBLGey2Z5bCkMVNjEZseyAlm4n5arcsJOeI3zkUP+zvZgzsbLTYh4lYeP/ZD/gdIAPrw==", + "dependencies": { + "System.ComponentModel": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.ComponentModel.TypeConverter": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "16pQ6P+EdhcXzPiEK4kbA953Fu0MNG2ovxTZU81/qsCd1zPRsKc3uif5NgvllCY598k6bI0KUyKW8fanlfaDQg==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Collections.NonGeneric": "4.3.0", + "System.Collections.Specialized": "4.3.0", + "System.ComponentModel": "4.3.0", + "System.ComponentModel.Primitives": "4.3.0", + "System.Globalization": "4.3.0", + "System.Linq": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Configuration.ConfigurationManager": { + "type": "Transitive", + "resolved": "4.4.0", + "contentHash": "gWwQv/Ug1qWJmHCmN17nAbxJYmQBM/E94QxKLksvUiiKB1Ld3Sc/eK1lgmbSjDFxkQhVuayI/cGFZhpBSodLrg==", + "dependencies": { + "System.Security.Cryptography.ProtectedData": "4.4.0" + } + }, + "System.Console": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DHDrIxiqk1h03m6khKWV2X8p/uvN79rgSqpilL6uzpmSfxfU5ng8VcPtW4qsDsQDHiTv6IPV9TmD5M/vElPNLg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Diagnostics.Contracts": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "HvQQjy712vnlpPxaloZYkuE78Gn353L0SJLJVeLcNASeg9c4qla2a1Xq8I7B3jZoDzKPtHTkyVO7AZ5tpeQGuA==", + "dependencies": { + "System.Runtime": "4.1.0" + } + }, + "System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "4.7.1", + "contentHash": "j81Lovt90PDAq8kLpaJfJKV/rWdWuEk6jfV+MBkee33vzYLEUsy4gXK8laa9V2nZlLM9VM9yA/OOQxxPEJKAMw==" + }, + "System.Diagnostics.Tools": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "UUvkJfSYJMM6x527dJg2VyWPSRqIVB0Z7dbjHst1zmwTXz5CcXSYJFWRpuigfbO1Lf7yfZiIaEUesfnl/g5EyA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.TraceSource": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VnYp1NxGx8Ww731y2LJ1vpfb/DKVNKEZ8Jsh5SgQTZREL/YpWRArgh9pI8CDLmgHspZmLL697CaLvH85qQpRiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0" + } + }, + "System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rswfv0f/Cqkh78rA5S8eN8Neocz234+emGCtTF3lxPY96F+mmmUen6tbn0glN6PMvlKQb9bPAY5e9u7fgPTkKw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Dynamic.Runtime": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "SNVi1E/vfWUAs/WYKhE9+qlS6KqK0YVhnlT0HQtr8pMIA8YX3lwy3uPMownDwdYISBdmAF/2holEIldVp85Wag==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Linq": "4.3.0", + "System.Linq.Expressions": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Globalization": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Calendars": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GUlBtdOWT4LTV3I+9/PJW+56AnnChTaOqqTLFtdmype/L500M2LIyXgmtd9X2P2VOkmJd5c67H5SaC2QcL1bFA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "FhKmdR6MPG+pxow6wGtNAWdZh7noIOpdD5TwQ3CprzgIE1bBBoim0vbR1+AWsWjQmU7zXHgQo4TWSP6lCeiWcQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0" + } + }, + "System.IdentityModel.Tokens.Jwt": { + "type": "Transitive", + "resolved": "6.8.0", + "contentHash": "5tBCjAub2Bhd5qmcd0WhR5s354e4oLYa//kOWrkX+6/7ZbDDJjMTfwLSOiZ/MMpWdE4DWPLOfTLOq/juj9CKzA==", + "dependencies": { + "Microsoft.IdentityModel.JsonWebTokens": "6.8.0", + "Microsoft.IdentityModel.Tokens": "6.8.0" + } + }, + "System.IO": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YHndyoiV90iu4iKG115ibkhrG+S3jBm8Ap9OwoUAzO5oPDAWcr0SFwQFm0HjM8WkEZWo0zvLTyLmbvTkW1bXgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Buffers": "4.3.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.IO.Compression": "4.3.0" + } + }, + "System.IO.Compression.ZipFile": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "G4HwjEsgIwy3JFBduZ9quBkAu+eUwjIdJleuNSgmUojbH6O3mlvEIme+GHx/cLlTAPcrnnL7GqvB9pTlWRfhOg==", + "dependencies": { + "System.Buffers": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.IO.FileSystem": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3wEMARTnuio+ulnvi+hkRNROYwa1kylvYahhcLk4HSoVdl+xxTFVeVlYOfLwrDPImGls0mDqbMhrza8qnWPTdA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.FileSystem.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "6QOb2XFLch7bEc4lIcJH49nJN2HV+OC3fHDgsLVsBVBk3Y4hFAnOBGzJ2lUu7CyDDFo9IBWkSsnbkT6IBwwiMw==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "4.7.3", + "contentHash": "zykThu9scJyg2Yeg27GMZCbjzniIsmjtNP5x6kQCd/8rEeKXRy20fP2NOMS7xQ+0pS/E85LZQA+K1aoQLxiUdw==" + }, + "System.Linq": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5DbqIUpsDp0dFftytzuMmc0oeMdQwjcP/EWxsksIz/w1TcFRkZ3yKKz0PqiYFMmEwPSWw+qNVqD7PJ889JzHbw==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Linq.Expressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "PGKkrd2khG4CnlyJwxwwaWWiSiWFNBGlgXvJpeO0xCXrZ89ODrQ6tjEWS/kOqZ8GwEOUATtKtzp1eRgmYNfclg==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Linq": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Emit.Lightweight": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.3", + "contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA==" + }, + "System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "sYg+FtILtRQuYWSIAuNOELwVuVsxVyJGWQyOnlAzhV4xvhyFnON1bAzYYC+jjRW8JREM45R0R5Dgi8MTC5sEwA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.DiagnosticSource": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Extensions": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Net.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "qOu+hDwFwoZPbzPvwut2qATe3ygjeQBDQj91xlsaqGFQUI5i4ZnZb8yyQuLGpDGivEPIt8EJkd1BVzVoP31FXA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Net.Sockets": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "m6icV6TqQOAdgt5N/9I5KNpjom/5NFtkmGseEH+AK/hny8XrytLH3+b5M8zL/Ycg3fhIocFpUMyl/wpFnVRvdw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Net.WebSockets": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "2KJo8hir6Edi9jnMDAMhiJoI691xRBmKcbNpwjrvpIMOCTYOtBpSsSEGBxBDV7PKbasJNaFp1+PZz1D7xS41Hg==", + "dependencies": { + "Microsoft.Win32.Primitives": "4.0.1", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Threading.Tasks": "4.0.11" + } + }, + "System.ObjectModel": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "bdX+80eKv9bN6K4N+d77OankKHGn6CH711a6fcOpMQu2Fckp/Ft4L/kW9WznHpyR0NRAvJutzOMHNNlBGvxQzQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Reflection": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "VR4kk8XLKebQ4MZuKuIni/7oh+QGFmZW3qORd1GvBq/8026OpW501SzT/oypwiQl4TvT8ErnReh/NzY9u+C6wQ==" + }, + "System.Reflection.Emit.ILGeneration": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit.Lightweight": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "oadVHGSMsTmZsAF864QYN1t1QzZjIcuKU3l2S9cZOwDdDueNTrqq1yRj7koFfIGEnKpt6NjpL3rOzRhs4ryOgA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rJkrJD3kBI5B712aRu4DpSIiHRtr6QlfZSQsb0hYHrDCZORXCFjQfoipo2LaMUHoT9i1B7j7MnfaEKWDFmFQNQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.TypeExtensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7u6ulLcZbyxB5Gq0nMkQttcdBTx57ibzw+4IOXEfR+sXYQoHvjW5LTLyNr8O22UIMrqYbchJQJnos4eooYzYJA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.Handles": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "uv1ynXqiMK8mp1GM3jDqPCFN66eJ5w5XNomaK2XD+TuCroNTLFGeZ+WCmBMcBDyTFKou3P6cR6J/QsaqDp7fGQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Runtime.InteropServices.RuntimeInformation": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "cbz4YJMqRDR7oLeMRbdYv7mYzc++17lNhScCX0goO2XpGWdvAt60CGN+FHdePUEHCe/Jy9jUlvNAiNdM+7jsOw==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0" + } + }, + "System.Runtime.Numerics": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "yMH+MfdzHjy17l2KESnPiF2dwq7T+xLnSJar7slyimAkUh/gTrS9/UQOtv7xarskJ2/XDSNvfLGOBQPjL7PaHQ==", + "dependencies": { + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Runtime.Serialization.Primitives": { + "type": "Transitive", + "resolved": "4.1.1", + "contentHash": "HZ6Du5QrTG8MNJbf4e4qMO3JRAkIboGT5Fk804uZtg3Gq516S7hAqTm2UZKUHa7/6HUGdVy3AqMQKbns06G/cg==", + "dependencies": { + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.Security.Claims": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "4Jlp0OgJLS/Voj1kyFP6MJlIYp3crgfH8kNQk2p7+4JYfc1aAmh9PZyAMMbDhuoolGNtux9HqSOazsioRiDvCw==", + "dependencies": { + "System.Collections": "4.0.11", + "System.Globalization": "4.0.11", + "System.IO": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Security.Principal": "4.0.1" + } + }, + "System.Security.Cryptography.Algorithms": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.Apple": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Cng": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "WG3r7EyjUe9CMPFSs6bty5doUqT+q9pbI80hlNzo2SkPkZ4VTuZkGWjpp77JB8+uaL4DFPRdBsAY+DX3dBK92A==" + }, + "System.Security.Cryptography.Csp": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X4s/FCkEUnRGnwR3aSfVIkldBmtURMhmexALNTwpjklzxWU7yjMk7GHLKOZTNkgnWnE0q7+BCf9N2LVRWxewaA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Security.Cryptography.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Linq": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "h4CEgOgv5PKVF/HwaHzJRiVboL2THYCou97zpmhjghx5frc7fIvlkY1jL+lnIQyChrJDMNEXS6r7byGif8Cy4w==", + "dependencies": { + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Security.Cryptography.ProtectedData": { + "type": "Transitive", + "resolved": "4.4.0", + "contentHash": "cJV7ScGW7EhatRsjehfvvYVBvtiSMKgN8bOVI0bQhnF5bU7vnHVIsH49Kva7i7GWaWYvmEzkYVk1TC+gZYBEog==" + }, + "System.Security.Cryptography.X509Certificates": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Cng": "4.3.0", + "System.Security.Cryptography.Csp": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Principal": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "On+SKhXY5rzxh/S8wlH1Rm0ogBlu7zyHNxeNBiXauNrhHRXAe9EuX8Yl5IOzLPGU5Z4kLWHMvORDOCG8iu9hww==", + "dependencies": { + "System.Runtime": "4.1.0" + } + }, + "System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Text.Encoding.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YVMK0Bt/A43RmwizJoZ22ei2nmrhobgeiYwFzC4YAN+nue8RF6djXDMog0UCn+brerQoYVyaS+ghy9P/MUVcmw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "GgJDO6/1bW6kkttxIiPK2jsqllQ3ifaeeBAJJrcoJq0lAclIZsAZZdEqi6JHq+QLZXL2UsjyWb8K8EOH7nOSPw==", + "dependencies": { + "System.Diagnostics.Debug": "4.0.11", + "System.IO": "4.1.0", + "System.Reflection": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Threading": "4.0.11" + } + }, + "System.Text.Json": { + "type": "Transitive", + "resolved": "4.7.2", + "contentHash": "TcMd95wcrubm9nHvJEQs70rC0H/8omiSGGpU4FQ/ZA1URIqD4pjmFJh2Mfv1yH1eHgJDWTi2hMDXwTET+zOOyg==" + }, + "System.Text.RegularExpressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "RpT2DA+L660cBt1FssIE9CAGpLFdFPuheB7pLpKpn6ZXNby7jDERe8Ua/Ne2xGiwLVG2JOqziiaVCGDon5sKFA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Threading": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VkUS0kOBcUf3Wwm0TSbrevDDZ6BlM+b/HRiapRFWjM5O0NS0LviG0glKmFK+hhPDd1XFeSdU1GmlLhb2CoVpIw==", + "dependencies": { + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "WSKUTtLhPR8gllzIWO2x6l4lmAIfbyMAiTlyXAis4QBDonXK4b4S6F8zGARX4/P8wH3DH+sLdhamCiHn+fTU1A==" + }, + "System.Threading.Timer": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Z6YfyYTCg7lOZjJzBjONJTFKGN9/NIYKSxhU5GRd+DTwHSZyvWp1xuI5aR+dLg+ayyC5Xv57KiY4oJ0tMO89fQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Xml.ReaderWriter": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GrprA+Z0RUXaR4N7/eW71j1rgMnEnEVlgii49GZyAjTH7uliMnrOU3HNFBr6fEDBCJCIdlVNq9hHbaDR621XBA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Tasks.Extensions": "4.3.0" + } + }, + "System.Xml.XDocument": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5zJ0XDxAIg8iy+t4aMnQAu0MqVbqyvfoUVl1yDV61xdo3Vth45oA2FoY4pPkxYAH5f8ixpmTqXeEIya95x0aCQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0" + } + }, + "System.Xml.XmlDocument": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "lJ8AxvkX7GQxpC6GFCeBj8ThYVyQczx2+f/cWHJU8tjS7YfI6Cv6bon70jVEgs2CiFbmmM8b9j1oZVx0dSI2Ww==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0" + } + }, + "System.Xml.XPath": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "UWd1H+1IJ9Wlq5nognZ/XJdyj8qPE4XufBUkAW59ijsCPjZkZe0MUzKKJFBr+ZWBe5Wq1u1d5f2CYgE93uH7DA==", + "dependencies": { + "System.Collections": "4.0.11", + "System.Diagnostics.Debug": "4.0.11", + "System.Globalization": "4.0.11", + "System.IO": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Threading": "4.0.11", + "System.Xml.ReaderWriter": "4.0.11" + } + }, + "System.Xml.XPath.XDocument": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "FLhdYJx4331oGovQypQ8JIw2kEmNzCsjVOVYY/16kZTUoquZG85oVn7yUhBE2OZt1yGPSXAL0HTEfzjlbNpM7Q==", + "dependencies": { + "System.Diagnostics.Debug": "4.0.11", + "System.Linq": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Threading": "4.0.11", + "System.Xml.ReaderWriter": "4.0.11", + "System.Xml.XDocument": "4.0.11", + "System.Xml.XPath": "4.0.1" + } + }, + "xunit.abstractions": { + "type": "Transitive", + "resolved": "2.0.3", + "contentHash": "pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg==" + }, + "xunit.analyzers": { + "type": "Transitive", + "resolved": "0.10.0", + "contentHash": "4/IDFCJfIeg6bix9apmUtIMwvOsiwqdEexeO/R2D4GReIGPLIRODTpId/l4LRSrAJk9lEO3Zx1H0Zx6uohJDNg==" + }, + "xunit.assert": { + "type": "Transitive", + "resolved": "2.4.1", + "contentHash": "O/Oe0BS5RmSsM+LQOb041TzuPo5MdH2Rov+qXGS37X+KFG1Hxz7kopYklM5+1Y+tRGeXrOx5+Xne1RuqLFQoyQ==", + "dependencies": { + "NETStandard.Library": "1.6.1" + } + }, + "xunit.core": { + "type": "Transitive", + "resolved": "2.4.1", + "contentHash": "Zsj5OMU6JasNGERXZy8s72+pcheG6Q15atS5XpZXqAtULuyQiQ6XNnUsp1gyfC6WgqScqMvySiEHmHcOG6Eg0Q==", + "dependencies": { + "xunit.extensibility.core": "[2.4.1]", + "xunit.extensibility.execution": "[2.4.1]" + } + }, + "xunit.extensibility.core": { + "type": "Transitive", + "resolved": "2.4.1", + "contentHash": "yKZKm/8QNZnBnGZFD9SewkllHBiK0DThybQD/G4PiAmQjKtEZyHi6ET70QPU9KtSMJGRYS6Syk7EyR2EVDU4Kg==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "xunit.abstractions": "2.0.3" + } + }, + "xunit.extensibility.execution": { + "type": "Transitive", + "resolved": "2.4.1", + "contentHash": "7e/1jqBpcb7frLkB6XDrHCGXAbKN4Rtdb88epYxCSRQuZDRW8UtTfdTEVpdTl8s4T56e07hOBVd4G0OdCxIY2A==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "xunit.extensibility.core": "[2.4.1]" + } + }, + "timeline": { + "type": "Project", + "dependencies": { + "AutoMapper": "10.1.1", + "AutoMapper.Extensions.Microsoft.DependencyInjection": "8.1.0", + "Microsoft.AspNetCore.SpaServices.Extensions": "3.1.9", + "Microsoft.EntityFrameworkCore": "3.1.9", + "Microsoft.EntityFrameworkCore.Analyzers": "3.1.9", + "Microsoft.EntityFrameworkCore.Sqlite": "3.1.9", + "NSwag.AspNetCore": "13.8.2", + "SixLabors.ImageSharp": "1.0.1", + "System.IdentityModel.Tokens.Jwt": "6.8.0", + "Timeline.ErrorCodes": "1.0.0" + } + }, + "timeline.errorcodes": { + "type": "Project" + } + } + } +} \ No newline at end of file diff --git a/BackEnd/Timeline.sln b/BackEnd/Timeline.sln new file mode 100644 index 00000000..40a32ee9 --- /dev/null +++ b/BackEnd/Timeline.sln @@ -0,0 +1,42 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29709.97 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Timeline", "Timeline\Timeline.csproj", "{A34D323C-5233-4754-B14F-4819CE9C27CA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Timeline.Tests", "Timeline.Tests\Timeline.Tests.csproj", "{3D76D578-37BC-43C2-97BF-9C6DD3825F10}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Timeline.ErrorCodes", "Timeline.ErrorCodes\Timeline.ErrorCodes.csproj", "{1044E3B0-1010-47CA-956E-B6E8FE87055B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Timeline.ErrorCodes.CodeGenerator", "Timeline.ErrorCodes.CodeGenerator\Timeline.ErrorCodes.CodeGenerator.csproj", "{D0263FD3-DC6A-4676-A746-FDAFCDACC5F2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A34D323C-5233-4754-B14F-4819CE9C27CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A34D323C-5233-4754-B14F-4819CE9C27CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A34D323C-5233-4754-B14F-4819CE9C27CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A34D323C-5233-4754-B14F-4819CE9C27CA}.Release|Any CPU.Build.0 = Release|Any CPU + {3D76D578-37BC-43C2-97BF-9C6DD3825F10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3D76D578-37BC-43C2-97BF-9C6DD3825F10}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3D76D578-37BC-43C2-97BF-9C6DD3825F10}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3D76D578-37BC-43C2-97BF-9C6DD3825F10}.Release|Any CPU.Build.0 = Release|Any CPU + {1044E3B0-1010-47CA-956E-B6E8FE87055B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1044E3B0-1010-47CA-956E-B6E8FE87055B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1044E3B0-1010-47CA-956E-B6E8FE87055B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1044E3B0-1010-47CA-956E-B6E8FE87055B}.Release|Any CPU.Build.0 = Release|Any CPU + {D0263FD3-DC6A-4676-A746-FDAFCDACC5F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D0263FD3-DC6A-4676-A746-FDAFCDACC5F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D0263FD3-DC6A-4676-A746-FDAFCDACC5F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D0263FD3-DC6A-4676-A746-FDAFCDACC5F2}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {9A7526E5-E68F-465C-9E0F-88BF6E040F14} + EndGlobalSection +EndGlobal diff --git a/BackEnd/Timeline/Auth/Attribute.cs b/BackEnd/Timeline/Auth/Attribute.cs new file mode 100644 index 00000000..86d0109b --- /dev/null +++ b/BackEnd/Timeline/Auth/Attribute.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Authorization; +using Timeline.Entities; + +namespace Timeline.Auth +{ + public class AdminAuthorizeAttribute : AuthorizeAttribute + { + public AdminAuthorizeAttribute() + { + Roles = UserRoles.Admin; + } + } + + public class UserAuthorizeAttribute : AuthorizeAttribute + { + public UserAuthorizeAttribute() + { + Roles = UserRoles.User; + } + } +} diff --git a/BackEnd/Timeline/Auth/MyAuthenticationHandler.cs b/BackEnd/Timeline/Auth/MyAuthenticationHandler.cs new file mode 100644 index 00000000..3c97c329 --- /dev/null +++ b/BackEnd/Timeline/Auth/MyAuthenticationHandler.cs @@ -0,0 +1,100 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; +using System; +using System.Globalization; +using System.Linq; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Timeline.Services; +using static Timeline.Resources.Authentication.AuthHandler; + +namespace Timeline.Auth +{ + public static class AuthenticationConstants + { + public const string Scheme = "Bearer"; + public const string DisplayName = "My Jwt Auth Scheme"; + } + + public class MyAuthenticationOptions : AuthenticationSchemeOptions + { + /// + /// The query param key to search for token. If null then query params are not searched for token. Default to "token". + /// + public string TokenQueryParamKey { get; set; } = "token"; + } + + public class MyAuthenticationHandler : AuthenticationHandler + { + private readonly ILogger _logger; + private readonly IUserTokenManager _userTokenManager; + + public MyAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IUserTokenManager userTokenManager) + : base(options, logger, encoder, clock) + { + _logger = logger.CreateLogger(); + _userTokenManager = userTokenManager; + } + + // return null if no token is found + private string? ExtractToken() + { + // check the authorization header + string header = Request.Headers[HeaderNames.Authorization]; + if (!string.IsNullOrEmpty(header) && header.StartsWith("Bearer ", StringComparison.InvariantCultureIgnoreCase)) + { + var token = header.Substring("Bearer ".Length).Trim(); + _logger.LogInformation(LogTokenFoundInHeader, token); + return token; + } + + // check the query params + var paramQueryKey = Options.TokenQueryParamKey; + if (!string.IsNullOrEmpty(paramQueryKey)) + { + string token = Request.Query[paramQueryKey]; + if (!string.IsNullOrEmpty(token)) + { + _logger.LogInformation(LogTokenFoundInQuery, paramQueryKey, token); + return token; + } + } + + // not found anywhere then return null + return null; + } + + protected override async Task HandleAuthenticateAsync() + { + var token = ExtractToken(); + if (string.IsNullOrEmpty(token)) + { + _logger.LogInformation(LogTokenNotFound); + return AuthenticateResult.NoResult(); + } + + try + { + var userInfo = await _userTokenManager.VerifyToken(token); + + var identity = new ClaimsIdentity(AuthenticationConstants.Scheme); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userInfo.Id!.Value.ToString(CultureInfo.InvariantCulture), ClaimValueTypes.Integer64)); + identity.AddClaim(new Claim(identity.NameClaimType, userInfo.Username, ClaimValueTypes.String)); + identity.AddClaims(UserRoleConvert.ToArray(userInfo.Administrator!.Value).Select(role => new Claim(identity.RoleClaimType, role, ClaimValueTypes.String))); + + var principal = new ClaimsPrincipal(); + principal.AddIdentity(identity); + + return AuthenticateResult.Success(new AuthenticationTicket(principal, AuthenticationConstants.Scheme)); + } + catch (Exception e) when (!(e is ArgumentException)) + { + _logger.LogInformation(e, LogTokenValidationFail); + return AuthenticateResult.Fail(e); + } + } + } +} diff --git a/BackEnd/Timeline/Auth/PrincipalExtensions.cs b/BackEnd/Timeline/Auth/PrincipalExtensions.cs new file mode 100644 index 00000000..ad7a887f --- /dev/null +++ b/BackEnd/Timeline/Auth/PrincipalExtensions.cs @@ -0,0 +1,13 @@ +using System.Security.Principal; +using Timeline.Entities; + +namespace Timeline.Auth +{ + internal static class PrincipalExtensions + { + internal static bool IsAdministrator(this IPrincipal principal) + { + return principal.IsInRole(UserRoles.Admin); + } + } +} diff --git a/BackEnd/Timeline/Configs/ApplicationConfiguration.cs b/BackEnd/Timeline/Configs/ApplicationConfiguration.cs new file mode 100644 index 00000000..df281adb --- /dev/null +++ b/BackEnd/Timeline/Configs/ApplicationConfiguration.cs @@ -0,0 +1,13 @@ +namespace Timeline.Configs +{ + public static class ApplicationConfiguration + { + public const string WorkDirKey = "WorkDir"; + public const string DefaultWorkDir = "/timeline"; + public const string DatabaseFileName = "timeline.db"; + public const string DatabaseBackupDirectoryName = "backup"; + public const string DisableFrontEndKey = "DisableFrontEnd"; + public const string UseMockFrontEndKey = "UseMockFrontEnd"; + public const string UseProxyFrontEndKey = "UseProxyFrontEnd"; + } +} diff --git a/BackEnd/Timeline/Configs/JwtConfiguration.cs b/BackEnd/Timeline/Configs/JwtConfiguration.cs new file mode 100644 index 00000000..af8052de --- /dev/null +++ b/BackEnd/Timeline/Configs/JwtConfiguration.cs @@ -0,0 +1,14 @@ +namespace Timeline.Configs +{ + public class JwtConfiguration + { + public string Issuer { get; set; } = default!; + public string Audience { get; set; } = default!; + + /// + /// Set the default value of expire offset of jwt token. + /// Unit is second. Default is 3600 * 24 seconds, aka 1 day. + /// + public long DefaultExpireOffset { get; set; } = 3600 * 24; + } +} diff --git a/BackEnd/Timeline/Controllers/ControllerAuthExtensions.cs b/BackEnd/Timeline/Controllers/ControllerAuthExtensions.cs new file mode 100644 index 00000000..00a65454 --- /dev/null +++ b/BackEnd/Timeline/Controllers/ControllerAuthExtensions.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Security.Claims; +using Timeline.Auth; +using static Timeline.Resources.Controllers.ControllerAuthExtensions; + +namespace Timeline.Controllers +{ + public static class ControllerAuthExtensions + { + public static bool IsAdministrator(this ControllerBase controller) + { + return controller.User != null && controller.User.IsAdministrator(); + } + + public static long GetUserId(this ControllerBase controller) + { + var claim = controller.User.FindFirst(ClaimTypes.NameIdentifier); + if (claim == null) + throw new InvalidOperationException(ExceptionNoUserIdentifierClaim); + + if (long.TryParse(claim.Value, out var value)) + return value; + + throw new InvalidOperationException(ExceptionUserIdentifierClaimBadFormat); + } + + public static long? GetOptionalUserId(this ControllerBase controller) + { + var claim = controller.User.FindFirst(ClaimTypes.NameIdentifier); + if (claim == null) + return null; + + if (long.TryParse(claim.Value, out var value)) + return value; + + throw new InvalidOperationException(ExceptionUserIdentifierClaimBadFormat); + } + } +} diff --git a/BackEnd/Timeline/Controllers/Testing/TestingAuthController.cs b/BackEnd/Timeline/Controllers/Testing/TestingAuthController.cs new file mode 100644 index 00000000..4d3b3ec7 --- /dev/null +++ b/BackEnd/Timeline/Controllers/Testing/TestingAuthController.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Timeline.Auth; + +namespace Timeline.Controllers.Testing +{ + [Route("testing/auth")] + [ApiController] + public class TestingAuthController : Controller + { + [HttpGet("[action]")] + [Authorize] + public ActionResult Authorize() + { + return Ok(); + } + + [HttpGet("[action]")] + [UserAuthorize] + public new ActionResult User() + { + return Ok(); + } + + [HttpGet("[action]")] + [AdminAuthorize] + public ActionResult Admin() + { + return Ok(); + } + } +} diff --git a/BackEnd/Timeline/Controllers/TimelineController.cs b/BackEnd/Timeline/Controllers/TimelineController.cs new file mode 100644 index 00000000..9a3147ea --- /dev/null +++ b/BackEnd/Timeline/Controllers/TimelineController.cs @@ -0,0 +1,491 @@ +using AutoMapper; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using Timeline.Filters; +using Timeline.Helpers; +using Timeline.Models; +using Timeline.Models.Http; +using Timeline.Models.Validation; +using Timeline.Services; +using Timeline.Services.Exceptions; + +namespace Timeline.Controllers +{ + /// + /// Operations about timeline. + /// + [ApiController] + [CatchTimelineNotExistException] + [ProducesErrorResponseType(typeof(CommonResponse))] + public class TimelineController : Controller + { + private readonly ILogger _logger; + + private readonly IUserService _userService; + private readonly ITimelineService _service; + + private readonly IMapper _mapper; + + /// + /// + /// + public TimelineController(ILogger logger, IUserService userService, ITimelineService service, IMapper mapper) + { + _logger = logger; + _userService = userService; + _service = service; + _mapper = mapper; + } + + /// + /// List all timelines. + /// + /// A username. If set, only timelines related to the user will return. + /// Specify the relation type, may be 'own' or 'join'. If not set, both type will return. + /// "Private" or "Register" or "Public". If set, only timelines whose visibility is specified one will return. + /// The timeline list. + [HttpGet("timelines")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task>> TimelineList([FromQuery][Username] string? relate, [FromQuery][RegularExpression("(own)|(join)")] string? relateType, [FromQuery] string? visibility) + { + List? visibilityFilter = null; + if (visibility != null) + { + visibilityFilter = new List(); + var items = visibility.Split('|'); + foreach (var item in items) + { + if (item.Equals(nameof(TimelineVisibility.Private), StringComparison.OrdinalIgnoreCase)) + { + if (!visibilityFilter.Contains(TimelineVisibility.Private)) + visibilityFilter.Add(TimelineVisibility.Private); + } + else if (item.Equals(nameof(TimelineVisibility.Register), StringComparison.OrdinalIgnoreCase)) + { + if (!visibilityFilter.Contains(TimelineVisibility.Register)) + visibilityFilter.Add(TimelineVisibility.Register); + } + else if (item.Equals(nameof(TimelineVisibility.Public), StringComparison.OrdinalIgnoreCase)) + { + if (!visibilityFilter.Contains(TimelineVisibility.Public)) + visibilityFilter.Add(TimelineVisibility.Public); + } + else + { + return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_QueryVisibilityUnknown, item)); + } + } + } + + TimelineUserRelationship? relationship = null; + if (relate != null) + { + try + { + var relatedUserId = await _userService.GetUserIdByUsername(relate); + + relationship = new TimelineUserRelationship(relateType switch + { + "own" => TimelineUserRelationshipType.Own, + "join" => TimelineUserRelationshipType.Join, + _ => TimelineUserRelationshipType.Default + }, relatedUserId); + } + catch (UserNotExistException) + { + return BadRequest(ErrorResponse.TimelineController.QueryRelateNotExist()); + } + } + + var timelines = await _service.GetTimelines(relationship, visibilityFilter); + var result = _mapper.Map>(timelines); + return result; + } + + /// + /// Get info of a timeline. + /// + /// The timeline name. + /// A unique id. If specified and if-modified-since is also specified, the timeline info will return when unique id is not the specified one even if it is not modified. + /// Same effect as If-Modified-Since header and take precedence than it. + /// If specified, will return 304 if not modified. + /// The timeline info. + [HttpGet("timelines/{name}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status304NotModified)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> TimelineGet([FromRoute][GeneralTimelineName] string name, [FromQuery] string? checkUniqueId, [FromQuery(Name = "ifModifiedSince")] DateTime? queryIfModifiedSince, [FromHeader(Name = "If-Modified-Since")] DateTime? headerIfModifiedSince) + { + DateTime? ifModifiedSince = null; + if (queryIfModifiedSince.HasValue) + { + ifModifiedSince = queryIfModifiedSince.Value; + } + else if (headerIfModifiedSince != null) + { + ifModifiedSince = headerIfModifiedSince.Value; + } + + bool returnNotModified = false; + + if (ifModifiedSince.HasValue) + { + var lastModified = await _service.GetTimelineLastModifiedTime(name); + if (lastModified < ifModifiedSince.Value) + { + if (checkUniqueId != null) + { + var uniqueId = await _service.GetTimelineUniqueId(name); + if (uniqueId == checkUniqueId) + { + returnNotModified = true; + } + } + else + { + returnNotModified = true; + } + } + } + + if (returnNotModified) + { + return StatusCode(StatusCodes.Status304NotModified); + } + else + { + var timeline = await _service.GetTimeline(name); + var result = _mapper.Map(timeline); + return result; + } + } + + /// + /// Get posts of a timeline. + /// + /// The name of the timeline. + /// If set, only posts modified since the time will return. + /// If set to true, deleted post will also return. + /// The post list. + [HttpGet("timelines/{name}/posts")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> PostListGet([FromRoute][GeneralTimelineName] string name, [FromQuery] DateTime? modifiedSince, [FromQuery] bool? includeDeleted) + { + if (!this.IsAdministrator() && !await _service.HasReadPermission(name, this.GetOptionalUserId())) + { + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } + + List posts = await _service.GetPosts(name, modifiedSince, includeDeleted ?? false); + + var result = _mapper.Map>(posts); + return result; + } + + /// + /// Get the data of a post. Usually a image post. + /// + /// Timeline name. + /// The id of the post. + /// If-None-Match header. + /// The data. + [HttpGet("timelines/{name}/posts/{id}/data")] + [Produces("image/png", "image/jpeg", "image/gif", "image/webp", "application/json", "text/json")] + [ProducesResponseType(typeof(byte[]), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(void), StatusCodes.Status304NotModified)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task PostDataGet([FromRoute][GeneralTimelineName] string name, [FromRoute] long id, [FromHeader(Name = "If-None-Match")] string? ifNoneMatch) + { + _ = ifNoneMatch; + if (!this.IsAdministrator() && !await _service.HasReadPermission(name, this.GetOptionalUserId())) + { + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } + + try + { + return await DataCacheHelper.GenerateActionResult(this, () => _service.GetPostDataETag(name, id), async () => + { + var data = await _service.GetPostData(name, id); + return data; + }); + } + catch (TimelinePostNotExistException) + { + return NotFound(ErrorResponse.TimelineController.PostNotExist()); + } + catch (TimelinePostNoDataException) + { + return BadRequest(ErrorResponse.TimelineController.PostNoData()); + } + } + + /// + /// Create a new post. + /// + /// Timeline name. + /// + /// Info of new post. + [HttpPost("timelines/{name}/posts")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task> PostPost([FromRoute][GeneralTimelineName] string name, [FromBody] TimelinePostCreateRequest body) + { + var id = this.GetUserId(); + if (!this.IsAdministrator() && !await _service.IsMemberOf(name, id)) + { + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } + + var content = body.Content; + + TimelinePost post; + + if (content.Type == TimelinePostContentTypes.Text) + { + var text = content.Text; + if (text == null) + { + return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_TextContentTextRequired)); + } + post = await _service.CreateTextPost(name, id, text, body.Time); + } + else if (content.Type == TimelinePostContentTypes.Image) + { + var base64Data = content.Data; + if (base64Data == null) + { + return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataRequired)); + } + byte[] data; + try + { + data = Convert.FromBase64String(base64Data); + } + catch (FormatException) + { + return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataNotBase64)); + } + + try + { + post = await _service.CreateImagePost(name, id, data, body.Time); + } + catch (ImageException) + { + return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataNotImage)); + } + } + else + { + return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ContentUnknownType)); + } + + var result = _mapper.Map(post); + return result; + } + + /// + /// Delete a post. + /// + /// Timeline name. + /// Post id. + /// Info of deletion. + [HttpDelete("timelines/{name}/posts/{id}")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task> PostDelete([FromRoute][GeneralTimelineName] string name, [FromRoute] long id) + { + if (!this.IsAdministrator() && !await _service.HasPostModifyPermission(name, id, this.GetUserId())) + { + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } + try + { + await _service.DeletePost(name, id); + return CommonDeleteResponse.Delete(); + } + catch (TimelinePostNotExistException) + { + return CommonDeleteResponse.NotExist(); + } + } + + /// + /// Change properties of a timeline. + /// + /// Timeline name. + /// + /// The new info. + [HttpPatch("timelines/{name}")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task> TimelinePatch([FromRoute][GeneralTimelineName] string name, [FromBody] TimelinePatchRequest body) + { + if (!this.IsAdministrator() && !(await _service.HasManagePermission(name, this.GetUserId()))) + { + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } + await _service.ChangeProperty(name, _mapper.Map(body)); + var timeline = await _service.GetTimeline(name); + var result = _mapper.Map(timeline); + return result; + } + + /// + /// Add a member to timeline. + /// + /// Timeline name. + /// The new member's username. + [HttpPut("timelines/{name}/members/{member}")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task TimelineMemberPut([FromRoute][GeneralTimelineName] string name, [FromRoute][Username] string member) + { + if (!this.IsAdministrator() && !(await _service.HasManagePermission(name, this.GetUserId()))) + { + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } + + try + { + await _service.ChangeMember(name, new List { member }, null); + return Ok(); + } + catch (UserNotExistException) + { + return BadRequest(ErrorResponse.TimelineController.MemberPut_NotExist()); + } + } + + /// + /// Remove a member from timeline. + /// + /// Timeline name. + /// The member's username. + [HttpDelete("timelines/{name}/members/{member}")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task TimelineMemberDelete([FromRoute][GeneralTimelineName] string name, [FromRoute][Username] string member) + { + if (!this.IsAdministrator() && !(await _service.HasManagePermission(name, this.GetUserId()))) + { + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } + + try + { + await _service.ChangeMember(name, null, new List { member }); + return Ok(CommonDeleteResponse.Delete()); + } + catch (UserNotExistException) + { + return Ok(CommonDeleteResponse.NotExist()); + } + } + + /// + /// Create a timeline. + /// + /// + /// Info of new timeline. + [HttpPost("timelines")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task> TimelineCreate([FromBody] TimelineCreateRequest body) + { + var userId = this.GetUserId(); + + try + { + var timeline = await _service.CreateTimeline(body.Name, userId); + var result = _mapper.Map(timeline); + return result; + } + catch (EntityAlreadyExistException e) when (e.EntityName == EntityNames.Timeline) + { + return BadRequest(ErrorResponse.TimelineController.NameConflict()); + } + } + + /// + /// Delete a timeline. + /// + /// Timeline name. + /// Info of deletion. + [HttpDelete("timelines/{name}")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task> TimelineDelete([FromRoute][TimelineName] string name) + { + if (!this.IsAdministrator() && !(await _service.HasManagePermission(name, this.GetUserId()))) + { + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } + + try + { + await _service.DeleteTimeline(name); + return CommonDeleteResponse.Delete(); + } + catch (TimelineNotExistException) + { + return CommonDeleteResponse.NotExist(); + } + } + + [HttpPost("timelineop/changename")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task> TimelineOpChangeName([FromBody] TimelineChangeNameRequest body) + { + if (!this.IsAdministrator() && !(await _service.HasManagePermission(body.OldName, this.GetUserId()))) + { + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } + + try + { + var timeline = await _service.ChangeTimelineName(body.OldName, body.NewName); + return Ok(_mapper.Map(timeline)); + } + catch (EntityAlreadyExistException) + { + return BadRequest(ErrorResponse.TimelineController.NameConflict()); + } + } + } +} diff --git a/BackEnd/Timeline/Controllers/TokenController.cs b/BackEnd/Timeline/Controllers/TokenController.cs new file mode 100644 index 00000000..8f2ca600 --- /dev/null +++ b/BackEnd/Timeline/Controllers/TokenController.cs @@ -0,0 +1,142 @@ +using AutoMapper; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System; +using System.Globalization; +using System.Threading.Tasks; +using Timeline.Helpers; +using Timeline.Models.Http; +using Timeline.Services; +using Timeline.Services.Exceptions; +using static Timeline.Resources.Controllers.TokenController; + +namespace Timeline.Controllers +{ + /// + /// Operation about tokens. + /// + [Route("token")] + [ApiController] + [ProducesErrorResponseType(typeof(CommonResponse))] + public class TokenController : Controller + { + private readonly IUserTokenManager _userTokenManager; + private readonly ILogger _logger; + private readonly IClock _clock; + + private readonly IMapper _mapper; + + /// + public TokenController(IUserTokenManager userTokenManager, ILogger logger, IClock clock, IMapper mapper) + { + _userTokenManager = userTokenManager; + _logger = logger; + _clock = clock; + _mapper = mapper; + } + + /// + /// Create a new token for a user. + /// + /// Result of token creation. + [HttpPost("create")] + [AllowAnonymous] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task> Create([FromBody] CreateTokenRequest request) + { + void LogFailure(string reason, Exception? e = null) + { + _logger.LogInformation(e, Log.Format(LogCreateFailure, + ("Reason", reason), + ("Username", request.Username), + ("Password", request.Password), + ("Expire (in days)", request.Expire) + )); + } + + try + { + DateTime? expireTime = null; + if (request.Expire != null) + expireTime = _clock.GetCurrentTime().AddDays(request.Expire.Value); + + var result = await _userTokenManager.CreateToken(request.Username, request.Password, expireTime); + + _logger.LogInformation(Log.Format(LogCreateSuccess, + ("Username", request.Username), + ("Expire At", expireTime?.ToString(CultureInfo.CurrentCulture.DateTimeFormat) ?? "default") + )); + return Ok(new CreateTokenResponse + { + Token = result.Token, + User = _mapper.Map(result.User) + }); + } + catch (UserNotExistException e) + { + LogFailure(LogUserNotExist, e); + return BadRequest(ErrorResponse.TokenController.Create_BadCredential()); + } + catch (BadPasswordException e) + { + LogFailure(LogBadPassword, e); + return BadRequest(ErrorResponse.TokenController.Create_BadCredential()); + } + } + + /// + /// Verify a token. + /// + /// Result of token verification. + [HttpPost("verify")] + [AllowAnonymous] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task> Verify([FromBody] VerifyTokenRequest request) + { + void LogFailure(string reason, Exception? e = null, params (string, object?)[] otherProperties) + { + var properties = new (string, object?)[2 + otherProperties.Length]; + properties[0] = ("Reason", reason); + properties[1] = ("Token", request.Token); + otherProperties.CopyTo(properties, 2); + _logger.LogInformation(e, Log.Format(LogVerifyFailure, properties)); + } + + try + { + var result = await _userTokenManager.VerifyToken(request.Token); + _logger.LogInformation(Log.Format(LogVerifySuccess, + ("Username", result.Username), ("Token", request.Token))); + return Ok(new VerifyTokenResponse + { + User = _mapper.Map(result) + }); + } + catch (UserTokenTimeExpireException e) + { + LogFailure(LogVerifyExpire, e, ("Expire Time", e.ExpireTime), ("Verify Time", e.VerifyTime)); + return BadRequest(ErrorResponse.TokenController.Verify_TimeExpired()); + } + catch (UserTokenBadVersionException e) + { + LogFailure(LogVerifyOldVersion, e, ("Token Version", e.TokenVersion), ("Required Version", e.RequiredVersion)); + return BadRequest(ErrorResponse.TokenController.Verify_OldVersion()); + + } + catch (UserTokenBadFormatException e) + { + LogFailure(LogVerifyBadFormat, e); + return BadRequest(ErrorResponse.TokenController.Verify_BadFormat()); + } + catch (UserNotExistException e) + { + LogFailure(LogVerifyUserNotExist, e); + return BadRequest(ErrorResponse.TokenController.Verify_UserNotExist()); + } + } + } +} diff --git a/BackEnd/Timeline/Controllers/UserAvatarController.cs b/BackEnd/Timeline/Controllers/UserAvatarController.cs new file mode 100644 index 00000000..bc4afa30 --- /dev/null +++ b/BackEnd/Timeline/Controllers/UserAvatarController.cs @@ -0,0 +1,174 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; +using System; +using System.Threading.Tasks; +using Timeline.Auth; +using Timeline.Filters; +using Timeline.Helpers; +using Timeline.Models; +using Timeline.Models.Http; +using Timeline.Models.Validation; +using Timeline.Services; +using Timeline.Services.Exceptions; +using static Timeline.Resources.Controllers.UserAvatarController; + +namespace Timeline.Controllers +{ + /// + /// Operations about user avatar. + /// + [ApiController] + [ProducesErrorResponseType(typeof(CommonResponse))] + public class UserAvatarController : Controller + { + private readonly ILogger _logger; + + private readonly IUserService _userService; + private readonly IUserAvatarService _service; + + /// + /// + /// + public UserAvatarController(ILogger logger, IUserService userService, IUserAvatarService service) + { + _logger = logger; + _userService = userService; + _service = service; + } + + /// + /// Get avatar of a user. + /// + /// Username of the user to get avatar of. + /// If-None-Match header. + /// Avatar data. + [HttpGet("users/{username}/avatar")] + [Produces("image/png", "image/jpeg", "image/gif", "image/webp", "application/json", "text/json")] + [ProducesResponseType(typeof(byte[]), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(void), StatusCodes.Status304NotModified)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Get([FromRoute][Username] string username, [FromHeader(Name = "If-None-Match")] string? ifNoneMatch) + { + _ = ifNoneMatch; + long id; + try + { + id = await _userService.GetUserIdByUsername(username); + } + catch (UserNotExistException e) + { + _logger.LogInformation(e, Log.Format(LogGetUserNotExist, ("Username", username))); + return NotFound(ErrorResponse.UserCommon.NotExist()); + } + + return await DataCacheHelper.GenerateActionResult(this, () => _service.GetAvatarETag(id), async () => + { + var avatar = await _service.GetAvatar(id); + return avatar.ToCacheableData(); + }); + } + + /// + /// Set avatar of a user. You have to be administrator to change other's. + /// + /// Username of the user to set avatar of. + /// The avatar data. + [HttpPut("users/{username}/avatar")] + [Authorize] + [Consumes("image/png", "image/jpeg", "image/gif", "image/webp")] + [MaxContentLength(1000 * 1000 * 10)] + [ProducesResponseType(typeof(void), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task Put([FromRoute][Username] string username, [FromBody] ByteData body) + { + if (!User.IsAdministrator() && User.Identity.Name != username) + { + _logger.LogInformation(Log.Format(LogPutForbid, + ("Operator Username", User.Identity.Name), ("Username To Put Avatar", username))); + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } + + long id; + try + { + id = await _userService.GetUserIdByUsername(username); + } + catch (UserNotExistException e) + { + _logger.LogInformation(e, Log.Format(LogPutUserNotExist, ("Username", username))); + return BadRequest(ErrorResponse.UserCommon.NotExist()); + } + + try + { + var etag = await _service.SetAvatar(id, new Avatar + { + Data = body.Data, + Type = body.ContentType + }); + + _logger.LogInformation(Log.Format(LogPutSuccess, + ("Username", username), ("Mime Type", Request.ContentType))); + + Response.Headers.Append("ETag", new EntityTagHeaderValue($"\"{etag}\"").ToString()); + + return Ok(); + } + catch (ImageException e) + { + _logger.LogInformation(e, Log.Format(LogPutUserBadFormat, ("Username", username))); + return BadRequest(e.Error switch + { + ImageException.ErrorReason.CantDecode => ErrorResponse.UserAvatar.BadFormat_CantDecode(), + ImageException.ErrorReason.UnmatchedFormat => ErrorResponse.UserAvatar.BadFormat_UnmatchedFormat(), + ImageException.ErrorReason.NotSquare => ErrorResponse.UserAvatar.BadFormat_BadSize(), + _ => + throw new Exception(ExceptionUnknownAvatarFormatError) + }); + } + } + + /// + /// Reset the avatar to the default one. You have to be administrator to reset other's. + /// + /// Username of the user. + /// Succeeded to reset. + /// Error code is 10010001 if user does not exist. + /// You have not logged in. + /// You are not administrator. + [HttpDelete("users/{username}/avatar")] + [ProducesResponseType(typeof(void), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [Authorize] + public async Task Delete([FromRoute][Username] string username) + { + if (!User.IsAdministrator() && User.Identity.Name != username) + { + _logger.LogInformation(Log.Format(LogDeleteForbid, + ("Operator Username", User.Identity.Name), ("Username To Delete Avatar", username))); + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } + + long id; + try + { + id = await _userService.GetUserIdByUsername(username); + } + catch (UserNotExistException e) + { + _logger.LogInformation(e, Log.Format(LogDeleteNotExist, ("Username", username))); + return BadRequest(ErrorResponse.UserCommon.NotExist()); + } + + await _service.SetAvatar(id, null); + return Ok(); + } + } +} diff --git a/BackEnd/Timeline/Controllers/UserController.cs b/BackEnd/Timeline/Controllers/UserController.cs new file mode 100644 index 00000000..02c09aab --- /dev/null +++ b/BackEnd/Timeline/Controllers/UserController.cs @@ -0,0 +1,195 @@ +using AutoMapper; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Auth; +using Timeline.Helpers; +using Timeline.Models; +using Timeline.Models.Http; +using Timeline.Models.Validation; +using Timeline.Services; +using Timeline.Services.Exceptions; +using static Timeline.Resources.Controllers.UserController; +using static Timeline.Resources.Messages; + +namespace Timeline.Controllers +{ + /// + /// Operations about users. + /// + [ApiController] + [ProducesErrorResponseType(typeof(CommonResponse))] + public class UserController : Controller + { + private readonly ILogger _logger; + private readonly IUserService _userService; + private readonly IUserDeleteService _userDeleteService; + private readonly IMapper _mapper; + + /// + public UserController(ILogger logger, IUserService userService, IUserDeleteService userDeleteService, IMapper mapper) + { + _logger = logger; + _userService = userService; + _userDeleteService = userDeleteService; + _mapper = mapper; + } + + private UserInfo ConvertToUserInfo(User user) => _mapper.Map(user); + + /// + /// Get all users. + /// + /// All user list. + [HttpGet("users")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> List() + { + var users = await _userService.GetUsers(); + var result = users.Select(u => ConvertToUserInfo(u)).ToArray(); + return Ok(result); + } + + /// + /// Get a user's info. + /// + /// Username of the user. + /// User info. + [HttpGet("users/{username}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> Get([FromRoute][Username] string username) + { + try + { + var user = await _userService.GetUserByUsername(username); + return Ok(ConvertToUserInfo(user)); + } + catch (UserNotExistException e) + { + _logger.LogInformation(e, Log.Format(LogGetUserNotExist, ("Username", username))); + return NotFound(ErrorResponse.UserCommon.NotExist()); + } + } + + /// + /// Change a user's property. + /// + /// + /// Username of the user to change. + /// The new user info. + [HttpPatch("users/{username}"), Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> Patch([FromBody] UserPatchRequest body, [FromRoute][Username] string username) + { + if (this.IsAdministrator()) + { + try + { + var user = await _userService.ModifyUser(username, _mapper.Map(body)); + return Ok(ConvertToUserInfo(user)); + } + catch (UserNotExistException e) + { + _logger.LogInformation(e, Log.Format(LogPatchUserNotExist, ("Username", username))); + return NotFound(ErrorResponse.UserCommon.NotExist()); + } + catch (EntityAlreadyExistException e) when (e.EntityName == EntityNames.User) + { + return BadRequest(ErrorResponse.UserController.UsernameConflict()); + } + } + else + { + if (User.Identity.Name != username) + return StatusCode(StatusCodes.Status403Forbidden, + ErrorResponse.Common.CustomMessage_Forbid(Common_Forbid_NotSelf)); + + if (body.Username != null) + return StatusCode(StatusCodes.Status403Forbidden, + ErrorResponse.Common.CustomMessage_Forbid(UserController_Patch_Forbid_Username)); + + if (body.Password != null) + return StatusCode(StatusCodes.Status403Forbidden, + ErrorResponse.Common.CustomMessage_Forbid(UserController_Patch_Forbid_Password)); + + if (body.Administrator != null) + return StatusCode(StatusCodes.Status403Forbidden, + ErrorResponse.Common.CustomMessage_Forbid(UserController_Patch_Forbid_Administrator)); + + var user = await _userService.ModifyUser(this.GetUserId(), _mapper.Map(body)); + return Ok(ConvertToUserInfo(user)); + } + } + + /// + /// Delete a user and all his related data. You have to be administrator. + /// + /// Username of the user to delete. + /// Info of deletion. + [HttpDelete("users/{username}"), AdminAuthorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task> Delete([FromRoute][Username] string username) + { + var delete = await _userDeleteService.DeleteUser(username); + if (delete) + return Ok(CommonDeleteResponse.Delete()); + else + return Ok(CommonDeleteResponse.NotExist()); + } + + /// + /// Create a new user. You have to be administrator. + /// + /// The new user's info. + [HttpPost("userop/createuser"), AdminAuthorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task> CreateUser([FromBody] CreateUserRequest body) + { + try + { + var user = await _userService.CreateUser(_mapper.Map(body)); + return Ok(ConvertToUserInfo(user)); + } + catch (EntityAlreadyExistException e) when (e.EntityName == EntityNames.User) + { + return BadRequest(ErrorResponse.UserController.UsernameConflict()); + } + } + + /// + /// Change password with old password. + /// + [HttpPost("userop/changepassword"), Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task ChangePassword([FromBody] ChangePasswordRequest request) + { + try + { + await _userService.ChangePassword(this.GetUserId(), request.OldPassword, request.NewPassword); + return Ok(); + } + catch (BadPasswordException e) + { + _logger.LogInformation(e, Log.Format(LogChangePasswordBadPassword, + ("Username", User.Identity.Name), ("Old Password", request.OldPassword))); + return BadRequest(ErrorResponse.UserController.ChangePassword_BadOldPassword()); + } + // User can't be non-existent or the token is bad. + } + } +} diff --git a/BackEnd/Timeline/Entities/DataEntity.cs b/BackEnd/Timeline/Entities/DataEntity.cs new file mode 100644 index 00000000..b21e2dbf --- /dev/null +++ b/BackEnd/Timeline/Entities/DataEntity.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Timeline.Entities +{ + [Table("data")] + public class DataEntity + { + [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + [Column("tag"), Required] + public string Tag { get; set; } = default!; + + [Column("data"), Required] +#pragma warning disable CA1819 // Properties should not return arrays + public byte[] Data { get; set; } = default!; +#pragma warning restore CA1819 // Properties should not return arrays + + [Column("ref"), Required] + public int Ref { get; set; } + } +} diff --git a/BackEnd/Timeline/Entities/DatabaseContext.cs b/BackEnd/Timeline/Entities/DatabaseContext.cs new file mode 100644 index 00000000..ecadd703 --- /dev/null +++ b/BackEnd/Timeline/Entities/DatabaseContext.cs @@ -0,0 +1,34 @@ +using Microsoft.EntityFrameworkCore; + +namespace Timeline.Entities +{ + public class DatabaseContext : DbContext + { + public DatabaseContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().Property(e => e.Version).HasDefaultValue(0); + modelBuilder.Entity().HasIndex(e => e.Username).IsUnique(); + modelBuilder.Entity().Property(e => e.UniqueId).HasDefaultValueSql("lower(hex(randomblob(16)))"); + modelBuilder.Entity().Property(e => e.UsernameChangeTime).HasDefaultValueSql("datetime('now', 'utc')"); + modelBuilder.Entity().Property(e => e.CreateTime).HasDefaultValueSql("datetime('now', 'utc')"); + modelBuilder.Entity().Property(e => e.LastModified).HasDefaultValueSql("datetime('now', 'utc')"); + modelBuilder.Entity().HasIndex(e => e.Tag).IsUnique(); + modelBuilder.Entity().Property(e => e.UniqueId).HasDefaultValueSql("lower(hex(randomblob(16)))"); + + modelBuilder.ApplyUtcDateTimeConverter(); + } + + public DbSet Users { get; set; } = default!; + public DbSet UserAvatars { get; set; } = default!; + public DbSet Timelines { get; set; } = default!; + public DbSet TimelinePosts { get; set; } = default!; + public DbSet TimelineMembers { get; set; } = default!; + public DbSet JwtToken { get; set; } = default!; + public DbSet Data { get; set; } = default!; + } +} diff --git a/BackEnd/Timeline/Entities/JwtTokenEntity.cs b/BackEnd/Timeline/Entities/JwtTokenEntity.cs new file mode 100644 index 00000000..40cb230a --- /dev/null +++ b/BackEnd/Timeline/Entities/JwtTokenEntity.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Timeline.Entities +{ + [Table("jwt_token")] + public class JwtTokenEntity + { + [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + [Required, Column("key")] +#pragma warning disable CA1819 // Properties should not return arrays + public byte[] Key { get; set; } = default!; +#pragma warning restore CA1819 // Properties should not return arrays + } +} diff --git a/BackEnd/Timeline/Entities/TimelineEntity.cs b/BackEnd/Timeline/Entities/TimelineEntity.cs new file mode 100644 index 00000000..3e592673 --- /dev/null +++ b/BackEnd/Timeline/Entities/TimelineEntity.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Timeline.Models; + +namespace Timeline.Entities +{ +#pragma warning disable CA2227 // Collection properties should be read only + // TODO: Create index for this table. + [Table("timelines")] + public class TimelineEntity + { + [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + [Column("unique_id"), Required] + public string UniqueId { get; set; } = default!; + + /// + /// If null, then this timeline is a personal timeline. + /// + [Column("name")] + public string? Name { get; set; } + + [Column("title")] + public string? Title { get; set; } + + [Column("name_last_modified")] + public DateTime NameLastModified { get; set; } + + [Column("description")] + public string? Description { get; set; } + + [Column("owner")] + public long OwnerId { get; set; } + + [ForeignKey(nameof(OwnerId))] + public UserEntity Owner { get; set; } = default!; + + [Column("visibility")] + public TimelineVisibility Visibility { get; set; } + + [Column("create_time")] + public DateTime CreateTime { get; set; } + + [Column("last_modified")] + public DateTime LastModified { get; set; } + + [Column("current_post_local_id")] + public long CurrentPostLocalId { get; set; } + + public List Members { get; set; } = default!; + + public List Posts { get; set; } = default!; + } +#pragma warning restore CA2227 // Collection properties should be read only +} diff --git a/BackEnd/Timeline/Entities/TimelineMemberEntity.cs b/BackEnd/Timeline/Entities/TimelineMemberEntity.cs new file mode 100644 index 00000000..e76f2099 --- /dev/null +++ b/BackEnd/Timeline/Entities/TimelineMemberEntity.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Timeline.Entities +{ + [Table("timeline_members")] + public class TimelineMemberEntity + { + [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + [Column("user")] + public long UserId { get; set; } + + [ForeignKey(nameof(UserId))] + public UserEntity User { get; set; } = default!; + + [Column("timeline")] + public long TimelineId { get; set; } + + [ForeignKey(nameof(TimelineId))] + public TimelineEntity Timeline { get; set; } = default!; + } +} diff --git a/BackEnd/Timeline/Entities/TimelinePostEntity.cs b/BackEnd/Timeline/Entities/TimelinePostEntity.cs new file mode 100644 index 00000000..07367fba --- /dev/null +++ b/BackEnd/Timeline/Entities/TimelinePostEntity.cs @@ -0,0 +1,43 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Timeline.Entities +{ + [Table("timeline_posts")] + public class TimelinePostEntity + { + [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + [Column("local_id")] + public long LocalId { get; set; } + + [Column("timeline")] + public long TimelineId { get; set; } + + [ForeignKey(nameof(TimelineId))] + public TimelineEntity Timeline { get; set; } = default!; + + [Column("author")] + public long? AuthorId { get; set; } + + [ForeignKey(nameof(AuthorId))] + public UserEntity? Author { get; set; } = default!; + + [Column("content_type"), Required] + public string ContentType { get; set; } = default!; + + [Column("content")] + public string? Content { get; set; } + + [Column("extra_content")] + public string? ExtraContent { get; set; } + + [Column("time")] + public DateTime Time { get; set; } + + [Column("last_updated")] + public DateTime LastUpdated { get; set; } + } +} diff --git a/BackEnd/Timeline/Entities/UserAvatarEntity.cs b/BackEnd/Timeline/Entities/UserAvatarEntity.cs new file mode 100644 index 00000000..3c2720f7 --- /dev/null +++ b/BackEnd/Timeline/Entities/UserAvatarEntity.cs @@ -0,0 +1,29 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Timeline.Entities +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "This is data base entity.")] + [Table("user_avatars")] + public class UserAvatarEntity + { + [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + [Column("data_tag")] + public string? DataTag { get; set; } + + [Column("type")] + public string? Type { get; set; } + + [Column("last_modified"), Required] + public DateTime LastModified { get; set; } + + [Column("user"), Required] + public long UserId { get; set; } + + [ForeignKey(nameof(UserId))] + public UserEntity User { get; set; } = default!; + } +} diff --git a/BackEnd/Timeline/Entities/UserEntity.cs b/BackEnd/Timeline/Entities/UserEntity.cs new file mode 100644 index 00000000..0cfaa335 --- /dev/null +++ b/BackEnd/Timeline/Entities/UserEntity.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Timeline.Entities +{ + public static class UserRoles + { + public const string Admin = "admin"; + public const string User = "user"; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "This is an entity class.")] + [Table("users")] + public class UserEntity + { + [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + [Column("unique_id"), Required] + public string UniqueId { get; set; } = default!; + + [Column("username"), Required] + public string Username { get; set; } = default!; + + [Column("username_change_time")] + public DateTime UsernameChangeTime { get; set; } + + [Column("password"), Required] + public string Password { get; set; } = default!; + + [Column("roles"), Required] + public string Roles { get; set; } = default!; + + [Column("version"), Required] + public long Version { get; set; } + + [Column("nickname")] + public string? Nickname { get; set; } + + [Column("create_time")] + public DateTime CreateTime { get; set; } + + [Column("last_modified")] + public DateTime LastModified { get; set; } + + public UserAvatarEntity? Avatar { get; set; } + + public List Timelines { get; set; } = default!; + + public List TimelinePosts { get; set; } = default!; + + public List TimelinesJoined { get; set; } = default!; + } +} diff --git a/BackEnd/Timeline/Entities/UtcDateAnnotation.cs b/BackEnd/Timeline/Entities/UtcDateAnnotation.cs new file mode 100644 index 00000000..6600e701 --- /dev/null +++ b/BackEnd/Timeline/Entities/UtcDateAnnotation.cs @@ -0,0 +1,44 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using System; + +namespace Timeline.Entities +{ + // Copied from https://github.com/dotnet/efcore/issues/4711#issuecomment-589842988 + public static class UtcDateAnnotation + { + private const string IsUtcAnnotation = "IsUtc"; + private static readonly ValueConverter UtcConverter = + new ValueConverter(v => v, v => DateTime.SpecifyKind(v, DateTimeKind.Utc)); + + public static PropertyBuilder IsUtc(this PropertyBuilder builder, bool isUtc = true) => + builder.HasAnnotation(IsUtcAnnotation, isUtc); + + public static bool IsUtc(this IMutableProperty property) => + ((bool?)property.FindAnnotation(IsUtcAnnotation)?.Value) ?? true; + + /// + /// Make sure this is called after configuring all your entities. + /// + public static void ApplyUtcDateTimeConverter(this ModelBuilder builder) + { + foreach (var entityType in builder.Model.GetEntityTypes()) + { + foreach (var property in entityType.GetProperties()) + { + if (!property.IsUtc()) + { + continue; + } + + if (property.ClrType == typeof(DateTime)) + { + property.SetValueConverter(UtcConverter); + } + } + } + } + } +} diff --git a/BackEnd/Timeline/Filters/Header.cs b/BackEnd/Timeline/Filters/Header.cs new file mode 100644 index 00000000..cc5ddd9f --- /dev/null +++ b/BackEnd/Timeline/Filters/Header.cs @@ -0,0 +1,63 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Timeline.Models.Http; + +namespace Timeline.Filters +{ + /// + /// Restrict max content length. + /// + public class MaxContentLengthFilter : IResourceFilter + { + /// + /// + /// + /// Max length. + public MaxContentLengthFilter(long maxByteLength) + { + MaxByteLength = maxByteLength; + } + + /// + /// Max length. + /// + public long MaxByteLength { get; set; } + + /// + public void OnResourceExecuted(ResourceExecutedContext context) + { + } + + /// + public void OnResourceExecuting(ResourceExecutingContext context) + { + var contentLength = context.HttpContext.Request.ContentLength; + if (contentLength != null && contentLength > MaxByteLength) + { + context.Result = new BadRequestObjectResult(ErrorResponse.Common.Content.TooBig(MaxByteLength + "B")); + } + } + } + + /// + /// Restrict max content length. + /// + public class MaxContentLengthAttribute : TypeFilterAttribute + { + /// + /// + /// + /// Max length. + public MaxContentLengthAttribute(long maxByteLength) + : base(typeof(MaxContentLengthFilter)) + { + MaxByteLength = maxByteLength; + Arguments = new object[] { maxByteLength }; + } + + /// + /// Max length. + /// + public long MaxByteLength { get; } + } +} diff --git a/BackEnd/Timeline/Filters/Timeline.cs b/BackEnd/Timeline/Filters/Timeline.cs new file mode 100644 index 00000000..6a730ee7 --- /dev/null +++ b/BackEnd/Timeline/Filters/Timeline.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Timeline.Models.Http; +using Timeline.Services.Exceptions; + +namespace Timeline.Filters +{ + public class CatchTimelineNotExistExceptionAttribute : ExceptionFilterAttribute + { + public override void OnException(ExceptionContext context) + { + if (context.Exception is TimelineNotExistException e) + { + if (e.InnerException is UserNotExistException) + { + if (HttpMethods.IsGet(context.HttpContext.Request.Method)) + context.Result = new NotFoundObjectResult(ErrorResponse.UserCommon.NotExist()); + else + context.Result = new BadRequestObjectResult(ErrorResponse.UserCommon.NotExist()); + } + else + { + if (HttpMethods.IsGet(context.HttpContext.Request.Method)) + context.Result = new NotFoundObjectResult(ErrorResponse.TimelineController.NotExist()); + else + context.Result = new BadRequestObjectResult(ErrorResponse.TimelineController.NotExist()); + } + } + } + } +} diff --git a/BackEnd/Timeline/Formatters/BytesInputFormatter.cs b/BackEnd/Timeline/Formatters/BytesInputFormatter.cs new file mode 100644 index 00000000..ac6537c9 --- /dev/null +++ b/BackEnd/Timeline/Formatters/BytesInputFormatter.cs @@ -0,0 +1,79 @@ +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; +using System; +using System.Threading.Tasks; +using Timeline.Models; + +namespace Timeline.Formatters +{ + /// + /// Formatter that reads body as bytes. + /// + public class BytesInputFormatter : InputFormatter + { + /// + /// + /// + public BytesInputFormatter() + { + SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/png")); + SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/jpeg")); + SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/gif")); + SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/webp")); + } + + /// + public override bool CanRead(InputFormatterContext context) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + + if (context.ModelType == typeof(ByteData)) + return true; + + return false; + } + + /// + public override async Task ReadRequestBodyAsync(InputFormatterContext context) + { + var request = context.HttpContext.Request; + var contentLength = request.ContentLength; + + var logger = context.HttpContext.RequestServices.GetRequiredService>(); + + if (contentLength == null) + { + logger.LogInformation("Failed to read body as bytes. Content-Length is not set."); + return await InputFormatterResult.FailureAsync(); + } + + if (contentLength == 0) + { + logger.LogInformation("Failed to read body as bytes. Content-Length is 0."); + return await InputFormatterResult.FailureAsync(); + } + + var bodyStream = request.Body; + + var data = new byte[contentLength.Value]; + var bytesRead = await bodyStream.ReadAsync(data); + + if (bytesRead != contentLength) + { + logger.LogInformation("Failed to read body as bytes. Actual length of body is smaller than Content-Length."); + return await InputFormatterResult.FailureAsync(); + } + + var extraByte = new byte[1]; + if (await bodyStream.ReadAsync(extraByte) != 0) + { + logger.LogInformation("Failed to read body as bytes. Actual length of body is greater than Content-Length."); + return await InputFormatterResult.FailureAsync(); + } + + return await InputFormatterResult.SuccessAsync(new ByteData(data, request.ContentType)); + } + } +} diff --git a/BackEnd/Timeline/Formatters/StringInputFormatter.cs b/BackEnd/Timeline/Formatters/StringInputFormatter.cs new file mode 100644 index 00000000..b1924268 --- /dev/null +++ b/BackEnd/Timeline/Formatters/StringInputFormatter.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.Net.Http.Headers; +using System.IO; +using System.Net.Mime; +using System.Text; +using System.Threading.Tasks; + +namespace Timeline.Formatters +{ + public class StringInputFormatter : TextInputFormatter + { + public StringInputFormatter() + { + SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(MediaTypeNames.Text.Plain)); + SupportedEncodings.Add(Encoding.UTF8); + } + + public override async Task ReadRequestBodyAsync(InputFormatterContext context, Encoding effectiveEncoding) + { + var request = context.HttpContext.Request; + using var reader = new StreamReader(request.Body, effectiveEncoding); + var stringContent = await reader.ReadToEndAsync(); + return await InputFormatterResult.SuccessAsync(stringContent); + } + } +} diff --git a/BackEnd/Timeline/GlobalSuppressions.cs b/BackEnd/Timeline/GlobalSuppressions.cs new file mode 100644 index 00000000..2b0da576 --- /dev/null +++ b/BackEnd/Timeline/GlobalSuppressions.cs @@ -0,0 +1,14 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "This is not a UI application.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "This is not bad.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "No need to check the null because it's ASP.Net's duty.", Scope = "namespaceanddescendants", Target = "Timeline.Controllers")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Migrations code are auto generated.", Scope = "namespaceanddescendants", Target = "Timeline.Migrations")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Generated error response identifiers.", Scope = "type", Target = "Timeline.Models.Http.ErrorResponse")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1724:Type names should not match namespaces", Justification = "Generated error response identifiers.", Scope = "type", Target = "Timeline.Models.Http.ErrorResponse")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "Generated error response.", Scope = "type", Target = "Timeline.Models.Http.ErrorResponse")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1056:Uri properties should not be strings", Justification = "That's unnecessary.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Adundant")] diff --git a/BackEnd/Timeline/Helpers/DataCacheHelper.cs b/BackEnd/Timeline/Helpers/DataCacheHelper.cs new file mode 100644 index 00000000..1ad69708 --- /dev/null +++ b/BackEnd/Timeline/Helpers/DataCacheHelper.cs @@ -0,0 +1,125 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; +using System; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Models.Http; +using static Timeline.Resources.Helper.DataCacheHelper; + +namespace Timeline.Helpers +{ + public interface ICacheableData + { + string Type { get; } +#pragma warning disable CA1819 // Properties should not return arrays + byte[] Data { get; } +#pragma warning restore CA1819 // Properties should not return arrays + DateTime? LastModified { get; } + } + + public class CacheableData : ICacheableData + { + public CacheableData(string type, byte[] data, DateTime? lastModified) + { + Type = type; + Data = data; + LastModified = lastModified; + } + + public string Type { get; set; } +#pragma warning disable CA1819 // Properties should not return arrays + public byte[] Data { get; set; } +#pragma warning restore CA1819 // Properties should not return arrays + public DateTime? LastModified { get; set; } + } + + public interface ICacheableDataProvider + { + Task GetDataETag(); + Task GetData(); + } + + public class DelegateCacheableDataProvider : ICacheableDataProvider + { + private readonly Func> _getDataETagDelegate; + private readonly Func> _getDataDelegate; + + public DelegateCacheableDataProvider(Func> getDataETagDelegate, Func> getDataDelegate) + { + _getDataETagDelegate = getDataETagDelegate; + _getDataDelegate = getDataDelegate; + } + + public Task GetData() + { + return _getDataDelegate(); + } + + public Task GetDataETag() + { + return _getDataETagDelegate(); + } + } + + public static class DataCacheHelper + { + public static async Task GenerateActionResult(Controller controller, ICacheableDataProvider provider, TimeSpan? maxAge = null) + { + const string CacheControlHeaderKey = "Cache-Control"; + const string IfNonMatchHeaderKey = "If-None-Match"; + const string ETagHeaderKey = "ETag"; + + string GenerateCacheControlHeaderValue() + { + var cacheControlHeader = new CacheControlHeaderValue() + { + NoCache = true, + NoStore = false, + MaxAge = maxAge ?? TimeSpan.FromDays(14), + Private = true, + MustRevalidate = true + }; + return cacheControlHeader.ToString(); + } + + var loggerFactory = controller.HttpContext.RequestServices.GetRequiredService(); + var logger = loggerFactory.CreateLogger(typeof(DataCacheHelper)); + + var eTagValue = await provider.GetDataETag(); + eTagValue = '"' + eTagValue + '"'; + var eTag = new EntityTagHeaderValue(eTagValue); + + + if (controller.Request.Headers.TryGetValue(IfNonMatchHeaderKey, out var value)) + { + if (!EntityTagHeaderValue.TryParseStrictList(value, out var eTagList)) + { + logger.LogInformation(Log.Format(LogBadIfNoneMatch, ("Header Value", value))); + return controller.BadRequest(ErrorResponse.Common.Header.IfNonMatch_BadFormat()); + } + + if (eTagList.FirstOrDefault(e => e.Equals(eTag)) != null) + { + logger.LogInformation(LogResultNotModified); + controller.Response.Headers.Add(ETagHeaderKey, eTagValue); + controller.Response.Headers.Add(CacheControlHeaderKey, GenerateCacheControlHeaderValue()); + + return controller.StatusCode(StatusCodes.Status304NotModified, null); + } + } + + var data = await provider.GetData(); + logger.LogInformation(LogResultData); + controller.Response.Headers.Add(CacheControlHeaderKey, GenerateCacheControlHeaderValue()); + return controller.File(data.Data, data.Type, data.LastModified, eTag); + } + + public static Task GenerateActionResult(Controller controller, Func> getDataETagDelegate, Func> getDataDelegate, TimeSpan? maxAge = null) + { + return GenerateActionResult(controller, new DelegateCacheableDataProvider(getDataETagDelegate, getDataDelegate), maxAge); + } + } +} diff --git a/BackEnd/Timeline/Helpers/DateTimeExtensions.cs b/BackEnd/Timeline/Helpers/DateTimeExtensions.cs new file mode 100644 index 00000000..374f3bc9 --- /dev/null +++ b/BackEnd/Timeline/Helpers/DateTimeExtensions.cs @@ -0,0 +1,14 @@ +using System; + +namespace Timeline.Helpers +{ + public static class DateTimeExtensions + { + public static DateTime MyToUtc(this DateTime dateTime) + { + if (dateTime.Kind == DateTimeKind.Utc) return dateTime; + if (dateTime.Kind == DateTimeKind.Local) return dateTime.ToUniversalTime(); + return DateTime.SpecifyKind(dateTime, DateTimeKind.Utc); + } + } +} diff --git a/BackEnd/Timeline/Helpers/InvalidModelResponseFactory.cs b/BackEnd/Timeline/Helpers/InvalidModelResponseFactory.cs new file mode 100644 index 00000000..9b253e7d --- /dev/null +++ b/BackEnd/Timeline/Helpers/InvalidModelResponseFactory.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Mvc; +using System.Text; +using Timeline.Models.Http; + +namespace Timeline.Helpers +{ + public static class InvalidModelResponseFactory + { + public static IActionResult Factory(ActionContext context) + { + var modelState = context.ModelState; + + var messageBuilder = new StringBuilder(); + foreach (var model in modelState) + foreach (var error in model.Value.Errors) + { + messageBuilder.Append(model.Key); + messageBuilder.Append(" : "); + messageBuilder.AppendLine(error.ErrorMessage); + } + + return new BadRequestObjectResult(ErrorResponse.Common.CustomMessage_InvalidModel(messageBuilder.ToString())); + } + } +} diff --git a/BackEnd/Timeline/Helpers/LanguageHelper.cs b/BackEnd/Timeline/Helpers/LanguageHelper.cs new file mode 100644 index 00000000..b0156b8b --- /dev/null +++ b/BackEnd/Timeline/Helpers/LanguageHelper.cs @@ -0,0 +1,12 @@ +using System.Linq; + +namespace Timeline.Helpers +{ + public static class LanguageHelper + { + public static bool AreSame(this bool firstBool, params bool[] otherBools) + { + return otherBools.All(b => b == firstBool); + } + } +} diff --git a/BackEnd/Timeline/Helpers/Log.cs b/BackEnd/Timeline/Helpers/Log.cs new file mode 100644 index 00000000..af0b7e13 --- /dev/null +++ b/BackEnd/Timeline/Helpers/Log.cs @@ -0,0 +1,22 @@ +using System.Text; + +namespace Timeline.Helpers +{ + public static class Log + { + public static string Format(string summary, params (string, object?)[] properties) + { + var builder = new StringBuilder(); + builder.Append(summary); + foreach (var property in properties) + { + var (key, value) = property; + builder.AppendLine(); + builder.Append(key); + builder.Append(" : "); + builder.Append(value); + } + return builder.ToString(); + } + } +} diff --git a/BackEnd/Timeline/Migrations/20200105150407_Initialize.Designer.cs b/BackEnd/Timeline/Migrations/20200105150407_Initialize.Designer.cs new file mode 100644 index 00000000..99e4eaac --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200105150407_Initialize.Designer.cs @@ -0,0 +1,266 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Timeline.Entities; + +namespace Timeline.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20200105150407_Initialize")] + partial class Initialize + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.0"); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("CreateTime") + .HasColumnName("create_time") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnName("description") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnName("name") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnName("owner") + .HasColumnType("INTEGER"); + + b.Property("Visibility") + .HasColumnName("visibility") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("timelines"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TimelineId"); + + b.HasIndex("UserId"); + + b.ToTable("timeline_members"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnName("author") + .HasColumnType("INTEGER"); + + b.Property("Content") + .HasColumnName("content") + .HasColumnType("TEXT"); + + b.Property("LastUpdated") + .HasColumnName("last_updated") + .HasColumnType("TEXT"); + + b.Property("Time") + .HasColumnName("time") + .HasColumnType("TEXT"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("TimelineId"); + + b.ToTable("timeline_posts"); + }); + + modelBuilder.Entity("Timeline.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("EncryptedPassword") + .IsRequired() + .HasColumnName("password") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnName("name") + .HasColumnType("TEXT"); + + b.Property("RoleString") + .IsRequired() + .HasColumnName("roles") + .HasColumnType("TEXT"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnName("version") + .HasColumnType("INTEGER") + .HasDefaultValue(0L); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatar", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Data") + .HasColumnName("data") + .HasColumnType("BLOB"); + + b.Property("ETag") + .HasColumnName("etag") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnName("last_modified") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnName("type") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_avatars"); + }); + + modelBuilder.Entity("Timeline.Entities.UserDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Nickname") + .HasColumnName("nickname") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_details"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.HasOne("Timeline.Entities.User", "Owner") + .WithMany("Timelines") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Members") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.User", "User") + .WithMany("TimelinesJoined") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.HasOne("Timeline.Entities.User", "Author") + .WithMany("TimelinePosts") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Posts") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatar", b => + { + b.HasOne("Timeline.Entities.User", null) + .WithOne("Avatar") + .HasForeignKey("Timeline.Entities.UserAvatar", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.UserDetail", b => + { + b.HasOne("Timeline.Entities.User", null) + .WithOne("Detail") + .HasForeignKey("Timeline.Entities.UserDetail", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BackEnd/Timeline/Migrations/20200105150407_Initialize.cs b/BackEnd/Timeline/Migrations/20200105150407_Initialize.cs new file mode 100644 index 00000000..4e12ef83 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200105150407_Initialize.cs @@ -0,0 +1,217 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Timeline.Migrations +{ + public partial class Initialize : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "users", + columns: table => new + { + id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + name = table.Column(nullable: false), + password = table.Column(nullable: false), + roles = table.Column(nullable: false), + version = table.Column(nullable: false, defaultValue: 0L) + .Annotation("Sqlite:Autoincrement", true) + }, + constraints: table => + { + table.PrimaryKey("PK_users", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "timelines", + columns: table => new + { + id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + name = table.Column(nullable: true), + description = table.Column(nullable: true), + owner = table.Column(nullable: false), + visibility = table.Column(nullable: false), + create_time = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_timelines", x => x.id); + table.ForeignKey( + name: "FK_timelines_users_owner", + column: x => x.owner, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "user_avatars", + columns: table => new + { + id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + data = table.Column(nullable: true), + type = table.Column(nullable: true), + etag = table.Column(nullable: true), + last_modified = table.Column(nullable: false), + UserId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_user_avatars", x => x.id); + table.ForeignKey( + name: "FK_user_avatars_users_UserId", + column: x => x.UserId, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "user_details", + columns: table => new + { + id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + nickname = table.Column(nullable: true), + UserId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_user_details", x => x.id); + table.ForeignKey( + name: "FK_user_details_users_UserId", + column: x => x.UserId, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "timeline_members", + columns: table => new + { + id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + user = table.Column(nullable: false), + timeline = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_timeline_members", x => x.id); + table.ForeignKey( + name: "FK_timeline_members_timelines_timeline", + column: x => x.timeline, + principalTable: "timelines", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_timeline_members_users_user", + column: x => x.user, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "timeline_posts", + columns: table => new + { + id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + timeline = table.Column(nullable: false), + author = table.Column(nullable: false), + content = table.Column(nullable: true), + time = table.Column(nullable: false), + last_updated = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_timeline_posts", x => x.id); + table.ForeignKey( + name: "FK_timeline_posts_users_author", + column: x => x.author, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_timeline_posts_timelines_timeline", + column: x => x.timeline, + principalTable: "timelines", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_timeline_members_timeline", + table: "timeline_members", + column: "timeline"); + + migrationBuilder.CreateIndex( + name: "IX_timeline_members_user", + table: "timeline_members", + column: "user"); + + migrationBuilder.CreateIndex( + name: "IX_timeline_posts_author", + table: "timeline_posts", + column: "author"); + + migrationBuilder.CreateIndex( + name: "IX_timeline_posts_timeline", + table: "timeline_posts", + column: "timeline"); + + migrationBuilder.CreateIndex( + name: "IX_timelines_owner", + table: "timelines", + column: "owner"); + + migrationBuilder.CreateIndex( + name: "IX_user_avatars_UserId", + table: "user_avatars", + column: "UserId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_user_details_UserId", + table: "user_details", + column: "UserId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_users_name", + table: "users", + column: "name", + unique: true); + + // Add a init user. Username is "administrator". Password is "crupest". + migrationBuilder.InsertData("users", new string[] { "name", "password", "roles" }, + new object[] { "administrator", "AQAAAAEAACcQAAAAENsspZrk8Wo+UuMyg6QuWJsNvRg6gVu4K/TumVod3h9GVLX9zDVuQQds3o7V8QWJ2w==", "user,admin" }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "timeline_members"); + + migrationBuilder.DropTable( + name: "timeline_posts"); + + migrationBuilder.DropTable( + name: "user_avatars"); + + migrationBuilder.DropTable( + name: "user_details"); + + migrationBuilder.DropTable( + name: "timelines"); + + migrationBuilder.DropTable( + name: "users"); + } + } +} diff --git a/BackEnd/Timeline/Migrations/20200131100517_RefactorUser.Designer.cs b/BackEnd/Timeline/Migrations/20200131100517_RefactorUser.Designer.cs new file mode 100644 index 00000000..9b78eb15 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200131100517_RefactorUser.Designer.cs @@ -0,0 +1,240 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Timeline.Entities; + +namespace Timeline.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20200131100517_RefactorUser")] + partial class RefactorUser + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.1"); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("CreateTime") + .HasColumnName("create_time") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnName("description") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnName("name") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnName("owner") + .HasColumnType("INTEGER"); + + b.Property("Visibility") + .HasColumnName("visibility") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("timelines"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TimelineId"); + + b.HasIndex("UserId"); + + b.ToTable("timeline_members"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnName("author") + .HasColumnType("INTEGER"); + + b.Property("Content") + .HasColumnName("content") + .HasColumnType("TEXT"); + + b.Property("LastUpdated") + .HasColumnName("last_updated") + .HasColumnType("TEXT"); + + b.Property("Time") + .HasColumnName("time") + .HasColumnType("TEXT"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("TimelineId"); + + b.ToTable("timeline_posts"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Data") + .HasColumnName("data") + .HasColumnType("BLOB"); + + b.Property("ETag") + .HasColumnName("etag") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnName("last_modified") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnName("type") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_avatars"); + }); + + modelBuilder.Entity("Timeline.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Nickname") + .HasColumnName("nickname") + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnName("password") + .HasColumnType("TEXT"); + + b.Property("Roles") + .IsRequired() + .HasColumnName("roles") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnName("username") + .HasColumnType("TEXT"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnName("version") + .HasColumnType("INTEGER") + .HasDefaultValue(0L); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Owner") + .WithMany("Timelines") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Members") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithMany("TimelinesJoined") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Author") + .WithMany("TimelinePosts") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Posts") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithOne("Avatar") + .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BackEnd/Timeline/Migrations/20200131100517_RefactorUser.cs b/BackEnd/Timeline/Migrations/20200131100517_RefactorUser.cs new file mode 100644 index 00000000..8597ed50 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200131100517_RefactorUser.cs @@ -0,0 +1,128 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Timeline.Migrations +{ + public partial class RefactorUser : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn(name: "name", table: "users", newName: "username"); + migrationBuilder.RenameIndex(name: "IX_users_name", table: "users", newName: "IX_users_username"); + + migrationBuilder.AddColumn( + name: "nickname", + table: "users", + nullable: true); + + migrationBuilder.Sql(@" +UPDATE users + SET nickname = ( + SELECT nickname + FROM user_details + WHERE user_details.UserId = users.id + ); + "); + + /* + migrationBuilder.RenameColumn(name: "UserId", table: "user_avatars", newName: "user"); + + migrationBuilder.DropForeignKey( + name: "FK_user_avatars_users_UserId", + table: "user_avatars"); + + migrationBuilder.AddForeignKey( + name: "FK_user_avatars_users_user", + table: "user_avatars", + column: "user", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.RenameIndex( + name: "IX_user_avatars_UserId", + table: "user_avatars", + newName: "IX_user_avatars_user"); + */ + + migrationBuilder.Sql(@" +CREATE TABLE user_avatars_backup ( + id INTEGER NOT NULL + CONSTRAINT PK_user_avatars PRIMARY KEY AUTOINCREMENT, + data BLOB, + type TEXT, + etag TEXT, + last_modified TEXT NOT NULL, + user INTEGER NOT NULL, + CONSTRAINT FK_user_avatars_users_user FOREIGN KEY ( + user + ) + REFERENCES users (id) ON DELETE CASCADE +); + +INSERT INTO user_avatars_backup (id, data, type, etag, last_modified, user) + SELECT id, data, type, etag, last_modified, UserId FROM user_avatars; + +DROP TABLE user_avatars; + +ALTER TABLE user_avatars_backup + RENAME TO user_avatars; + +CREATE UNIQUE INDEX IX_user_avatars_user ON user_avatars (user); + "); + + // migrationBuilder.DropTable(name: "user_details"); + + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" +CREATE TABLE user_avatars_backup ( + id INTEGER NOT NULL + CONSTRAINT PK_user_avatars PRIMARY KEY AUTOINCREMENT, + data BLOB, + type TEXT, + etag TEXT, + last_modified TEXT NOT NULL, + UserId INTEGER NOT NULL, + CONSTRAINT FK_user_avatars_users_UserId FOREIGN KEY ( + user + ) + REFERENCES users (id) ON DELETE CASCADE +); + +INSERT INTO user_avatars_backup (id, data, type, etag, last_modified, UserId) + SELECT id, data, type, etag, last_modified, user FROM user_avatars; + +DROP TABLE user_avatars; + +ALTER TABLE user_avatars_backup + RENAME TO user_avatars; + +CREATE UNIQUE INDEX IX_user_avatars_UserId ON user_avatars (UserId); + "); + + migrationBuilder.Sql(@" +CREATE TABLE users_backup ( + id INTEGER NOT NULL + CONSTRAINT PK_users PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + password TEXT NOT NULL, + roles TEXT NOT NULL, + version INTEGER NOT NULL + DEFAULT 0 +); + +INSERT INTO users_backup (id, name, password, roles, version) + SELECT id, username, password, roles, version FROM users; + +DROP TABLE users; + +ALTER TABLE users_backup + RENAME TO users; + +CREATE UNIQUE INDEX IX_users_name ON users (name); + "); + } + } +} diff --git a/BackEnd/Timeline/Migrations/20200221064341_AddJwtToken.Designer.cs b/BackEnd/Timeline/Migrations/20200221064341_AddJwtToken.Designer.cs new file mode 100644 index 00000000..eb328b52 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200221064341_AddJwtToken.Designer.cs @@ -0,0 +1,257 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Timeline.Entities; + +namespace Timeline.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20200221064341_AddJwtToken")] + partial class AddJwtToken + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.2"); + + modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnName("key") + .HasColumnType("BLOB"); + + b.HasKey("Id"); + + b.ToTable("jwt_token"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("CreateTime") + .HasColumnName("create_time") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnName("description") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnName("name") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnName("owner") + .HasColumnType("INTEGER"); + + b.Property("Visibility") + .HasColumnName("visibility") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("timelines"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TimelineId"); + + b.HasIndex("UserId"); + + b.ToTable("timeline_members"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnName("author") + .HasColumnType("INTEGER"); + + b.Property("Content") + .HasColumnName("content") + .HasColumnType("TEXT"); + + b.Property("LastUpdated") + .HasColumnName("last_updated") + .HasColumnType("TEXT"); + + b.Property("Time") + .HasColumnName("time") + .HasColumnType("TEXT"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("TimelineId"); + + b.ToTable("timeline_posts"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Data") + .HasColumnName("data") + .HasColumnType("BLOB"); + + b.Property("ETag") + .HasColumnName("etag") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnName("last_modified") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnName("type") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_avatars"); + }); + + modelBuilder.Entity("Timeline.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Nickname") + .HasColumnName("nickname") + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnName("password") + .HasColumnType("TEXT"); + + b.Property("Roles") + .IsRequired() + .HasColumnName("roles") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnName("username") + .HasColumnType("TEXT"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnName("version") + .HasColumnType("INTEGER") + .HasDefaultValue(0L); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Owner") + .WithMany("Timelines") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Members") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithMany("TimelinesJoined") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Author") + .WithMany("TimelinePosts") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Posts") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithOne("Avatar") + .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BackEnd/Timeline/Migrations/20200221064341_AddJwtToken.cs b/BackEnd/Timeline/Migrations/20200221064341_AddJwtToken.cs new file mode 100644 index 00000000..628970c6 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200221064341_AddJwtToken.cs @@ -0,0 +1,45 @@ +using System; +using System.Security.Cryptography; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Timeline.Migrations +{ + public static class JwtTokenGenerateHelper + { + public static byte[] GenerateKey() + { + using var random = RandomNumberGenerator.Create(); + var key = new byte[16]; + random.GetBytes(key); + return key; + } + } + + public partial class AddJwtToken : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "jwt_token", + columns: table => new + { + id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + key = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_jwt_token", x => x.id); + }); + + + migrationBuilder.InsertData("jwt_token", "key", JwtTokenGenerateHelper.GenerateKey()); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "jwt_token"); + } + } +} diff --git a/BackEnd/Timeline/Migrations/20200229103848_AddPostLocalId.Designer.cs b/BackEnd/Timeline/Migrations/20200229103848_AddPostLocalId.Designer.cs new file mode 100644 index 00000000..cf6ae8a3 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200229103848_AddPostLocalId.Designer.cs @@ -0,0 +1,265 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Timeline.Entities; + +namespace Timeline.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20200229103848_AddPostLocalId")] + partial class AddPostLocalId + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.2"); + + modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnName("key") + .HasColumnType("BLOB"); + + b.HasKey("Id"); + + b.ToTable("jwt_token"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("CreateTime") + .HasColumnName("create_time") + .HasColumnType("TEXT"); + + b.Property("CurrentPostLocalId") + .HasColumnName("current_post_local_id") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnName("description") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnName("name") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnName("owner") + .HasColumnType("INTEGER"); + + b.Property("Visibility") + .HasColumnName("visibility") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("timelines"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TimelineId"); + + b.HasIndex("UserId"); + + b.ToTable("timeline_members"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnName("author") + .HasColumnType("INTEGER"); + + b.Property("Content") + .HasColumnName("content") + .HasColumnType("TEXT"); + + b.Property("LastUpdated") + .HasColumnName("last_updated") + .HasColumnType("TEXT"); + + b.Property("LocalId") + .HasColumnName("local_id") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnName("time") + .HasColumnType("TEXT"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("TimelineId"); + + b.ToTable("timeline_posts"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Data") + .HasColumnName("data") + .HasColumnType("BLOB"); + + b.Property("ETag") + .HasColumnName("etag") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnName("last_modified") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnName("type") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_avatars"); + }); + + modelBuilder.Entity("Timeline.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Nickname") + .HasColumnName("nickname") + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnName("password") + .HasColumnType("TEXT"); + + b.Property("Roles") + .IsRequired() + .HasColumnName("roles") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnName("username") + .HasColumnType("TEXT"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnName("version") + .HasColumnType("INTEGER") + .HasDefaultValue(0L); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Owner") + .WithMany("Timelines") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Members") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithMany("TimelinesJoined") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Author") + .WithMany("TimelinePosts") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Posts") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithOne("Avatar") + .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BackEnd/Timeline/Migrations/20200229103848_AddPostLocalId.cs b/BackEnd/Timeline/Migrations/20200229103848_AddPostLocalId.cs new file mode 100644 index 00000000..497b38a1 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200229103848_AddPostLocalId.cs @@ -0,0 +1,42 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Timeline.Migrations +{ + public partial class AddPostLocalId : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + + migrationBuilder.AddColumn( + name: "current_post_local_id", + table: "timelines", + nullable: false, + defaultValue: 0L); + + migrationBuilder.AddColumn( + name: "local_id", + table: "timeline_posts", + nullable: false, + defaultValue: 0L); + + migrationBuilder.Sql(@" +UPDATE timeline_posts +SET local_id = (SELECT COUNT (*) + FROM timeline_posts AS p + WHERE p.timeline = timeline_posts.timeline + AND p.id <= timeline_posts.id); + +UPDATE timelines +SET current_post_local_id = (SELECT COUNT (*) + FROM timeline_posts AS p + WHERE p.timeline = timelines.id); + "); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/BackEnd/Timeline/Migrations/20200306110049_AddDataTable.Designer.cs b/BackEnd/Timeline/Migrations/20200306110049_AddDataTable.Designer.cs new file mode 100644 index 00000000..336ffc18 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200306110049_AddDataTable.Designer.cs @@ -0,0 +1,290 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Timeline.Entities; + +namespace Timeline.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20200306110049_AddDataTable")] + partial class AddDataTable + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.2"); + + modelBuilder.Entity("Timeline.Entities.DataEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Data") + .IsRequired() + .HasColumnName("data") + .HasColumnType("BLOB"); + + b.Property("Ref") + .HasColumnName("ref") + .HasColumnType("INTEGER"); + + b.Property("Tag") + .IsRequired() + .HasColumnName("tag") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Tag") + .IsUnique(); + + b.ToTable("data"); + }); + + modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnName("key") + .HasColumnType("BLOB"); + + b.HasKey("Id"); + + b.ToTable("jwt_token"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("CreateTime") + .HasColumnName("create_time") + .HasColumnType("TEXT"); + + b.Property("CurrentPostLocalId") + .HasColumnName("current_post_local_id") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnName("description") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnName("name") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnName("owner") + .HasColumnType("INTEGER"); + + b.Property("Visibility") + .HasColumnName("visibility") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("timelines"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TimelineId"); + + b.HasIndex("UserId"); + + b.ToTable("timeline_members"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnName("author") + .HasColumnType("INTEGER"); + + b.Property("Content") + .HasColumnName("content") + .HasColumnType("TEXT"); + + b.Property("LastUpdated") + .HasColumnName("last_updated") + .HasColumnType("TEXT"); + + b.Property("LocalId") + .HasColumnName("local_id") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnName("time") + .HasColumnType("TEXT"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("TimelineId"); + + b.ToTable("timeline_posts"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("DataTag") + .HasColumnName("data_tag") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnName("last_modified") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnName("type") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_avatars"); + }); + + modelBuilder.Entity("Timeline.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Nickname") + .HasColumnName("nickname") + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnName("password") + .HasColumnType("TEXT"); + + b.Property("Roles") + .IsRequired() + .HasColumnName("roles") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnName("username") + .HasColumnType("TEXT"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnName("version") + .HasColumnType("INTEGER") + .HasDefaultValue(0L); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Owner") + .WithMany("Timelines") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Members") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithMany("TimelinesJoined") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Author") + .WithMany("TimelinePosts") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Posts") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithOne("Avatar") + .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BackEnd/Timeline/Migrations/20200306110049_AddDataTable.cs b/BackEnd/Timeline/Migrations/20200306110049_AddDataTable.cs new file mode 100644 index 00000000..e33bf4c9 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200306110049_AddDataTable.cs @@ -0,0 +1,87 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Timeline.Migrations +{ + public partial class AddDataTable : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "data", + columns: table => new + { + id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + tag = table.Column(nullable: false), + data = table.Column(nullable: false), + @ref = table.Column(name: "ref", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_data", x => x.id); + }); + + migrationBuilder.CreateIndex( + name: "IX_data_tag", + table: "data", + column: "tag", + unique: true); + + migrationBuilder.Sql(@" +ALTER TABLE user_avatars + RENAME TO user_avatars_backup; + +CREATE TABLE user_avatars ( + id INTEGER NOT NULL + CONSTRAINT PK_user_avatars PRIMARY KEY AUTOINCREMENT, + data_tag TEXT, + type TEXT, + last_modified TEXT NOT NULL, + user INTEGER NOT NULL, + CONSTRAINT FK_user_avatars_users_user FOREIGN KEY ( + user + ) + REFERENCES users (id) ON DELETE CASCADE +); + +INSERT INTO user_avatars (id, data_tag, type, last_modified, user) + SELECT id, etag, type, last_modified, user FROM user_avatars_backup; + +INSERT OR IGNORE INTO data (tag, data, ref) + SELECT etag, data, 0 FROM user_avatars_backup; + +UPDATE data +SET ref = (SELECT COUNT (*) + FROM user_avatars_backup AS a + WHERE a.etag == data.tag); + +DROP TABLE user_avatars_backup; + +CREATE UNIQUE INDEX IX_user_avatars_user ON user_avatars (user); + "); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "data"); + + migrationBuilder.DropColumn( + name: "data_tag", + table: "user_avatars"); + + migrationBuilder.AddColumn( + name: "data", + table: "user_avatars", + type: "BLOB", + nullable: true); + + migrationBuilder.AddColumn( + name: "etag", + table: "user_avatars", + type: "TEXT", + nullable: true); + } + } +} diff --git a/BackEnd/Timeline/Migrations/20200306111553_DropUserDetails.Designer.cs b/BackEnd/Timeline/Migrations/20200306111553_DropUserDetails.Designer.cs new file mode 100644 index 00000000..f0c4dc08 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200306111553_DropUserDetails.Designer.cs @@ -0,0 +1,290 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Timeline.Entities; + +namespace Timeline.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20200306111553_DropUserDetails")] + partial class DropUserDetails + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.2"); + + modelBuilder.Entity("Timeline.Entities.DataEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Data") + .IsRequired() + .HasColumnName("data") + .HasColumnType("BLOB"); + + b.Property("Ref") + .HasColumnName("ref") + .HasColumnType("INTEGER"); + + b.Property("Tag") + .IsRequired() + .HasColumnName("tag") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Tag") + .IsUnique(); + + b.ToTable("data"); + }); + + modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnName("key") + .HasColumnType("BLOB"); + + b.HasKey("Id"); + + b.ToTable("jwt_token"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("CreateTime") + .HasColumnName("create_time") + .HasColumnType("TEXT"); + + b.Property("CurrentPostLocalId") + .HasColumnName("current_post_local_id") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnName("description") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnName("name") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnName("owner") + .HasColumnType("INTEGER"); + + b.Property("Visibility") + .HasColumnName("visibility") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("timelines"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TimelineId"); + + b.HasIndex("UserId"); + + b.ToTable("timeline_members"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnName("author") + .HasColumnType("INTEGER"); + + b.Property("Content") + .HasColumnName("content") + .HasColumnType("TEXT"); + + b.Property("LastUpdated") + .HasColumnName("last_updated") + .HasColumnType("TEXT"); + + b.Property("LocalId") + .HasColumnName("local_id") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnName("time") + .HasColumnType("TEXT"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("TimelineId"); + + b.ToTable("timeline_posts"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("DataTag") + .HasColumnName("data_tag") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnName("last_modified") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnName("type") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_avatars"); + }); + + modelBuilder.Entity("Timeline.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Nickname") + .HasColumnName("nickname") + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnName("password") + .HasColumnType("TEXT"); + + b.Property("Roles") + .IsRequired() + .HasColumnName("roles") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnName("username") + .HasColumnType("TEXT"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnName("version") + .HasColumnType("INTEGER") + .HasDefaultValue(0L); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Owner") + .WithMany("Timelines") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Members") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithMany("TimelinesJoined") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Author") + .WithMany("TimelinePosts") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Posts") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithOne("Avatar") + .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BackEnd/Timeline/Migrations/20200306111553_DropUserDetails.cs b/BackEnd/Timeline/Migrations/20200306111553_DropUserDetails.cs new file mode 100644 index 00000000..0a176461 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200306111553_DropUserDetails.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Timeline.Migrations +{ + public partial class DropUserDetails : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable(name: "user_details"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/BackEnd/Timeline/Migrations/20200312112552_AddImagePost.Designer.cs b/BackEnd/Timeline/Migrations/20200312112552_AddImagePost.Designer.cs new file mode 100644 index 00000000..bd75a916 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200312112552_AddImagePost.Designer.cs @@ -0,0 +1,299 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Timeline.Entities; + +namespace Timeline.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20200312112552_AddImagePost")] + partial class AddImagePost + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.2"); + + modelBuilder.Entity("Timeline.Entities.DataEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Data") + .IsRequired() + .HasColumnName("data") + .HasColumnType("BLOB"); + + b.Property("Ref") + .HasColumnName("ref") + .HasColumnType("INTEGER"); + + b.Property("Tag") + .IsRequired() + .HasColumnName("tag") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Tag") + .IsUnique(); + + b.ToTable("data"); + }); + + modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnName("key") + .HasColumnType("BLOB"); + + b.HasKey("Id"); + + b.ToTable("jwt_token"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("CreateTime") + .HasColumnName("create_time") + .HasColumnType("TEXT"); + + b.Property("CurrentPostLocalId") + .HasColumnName("current_post_local_id") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnName("description") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnName("name") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnName("owner") + .HasColumnType("INTEGER"); + + b.Property("Visibility") + .HasColumnName("visibility") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("timelines"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TimelineId"); + + b.HasIndex("UserId"); + + b.ToTable("timeline_members"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnName("author") + .HasColumnType("INTEGER"); + + b.Property("Content") + .HasColumnName("content") + .HasColumnType("TEXT"); + + b.Property("ContentType") + .IsRequired() + .HasColumnName("content_type") + .HasColumnType("TEXT"); + + b.Property("ExtraContent") + .HasColumnName("extra_content") + .HasColumnType("TEXT"); + + b.Property("LastUpdated") + .HasColumnName("last_updated") + .HasColumnType("TEXT"); + + b.Property("LocalId") + .HasColumnName("local_id") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnName("time") + .HasColumnType("TEXT"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("TimelineId"); + + b.ToTable("timeline_posts"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("DataTag") + .HasColumnName("data_tag") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnName("last_modified") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnName("type") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_avatars"); + }); + + modelBuilder.Entity("Timeline.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Nickname") + .HasColumnName("nickname") + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnName("password") + .HasColumnType("TEXT"); + + b.Property("Roles") + .IsRequired() + .HasColumnName("roles") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnName("username") + .HasColumnType("TEXT"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnName("version") + .HasColumnType("INTEGER") + .HasDefaultValue(0L); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Owner") + .WithMany("Timelines") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Members") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithMany("TimelinesJoined") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Author") + .WithMany("TimelinePosts") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Posts") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithOne("Avatar") + .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BackEnd/Timeline/Migrations/20200312112552_AddImagePost.cs b/BackEnd/Timeline/Migrations/20200312112552_AddImagePost.cs new file mode 100644 index 00000000..d5098ce0 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200312112552_AddImagePost.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Timeline.Models; + +namespace Timeline.Migrations +{ + public partial class AddImagePost : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "content_type", + table: "timeline_posts", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "extra_content", + table: "timeline_posts", + nullable: true); + + migrationBuilder.Sql($@" +UPDATE timeline_posts +SET content_type = '{TimelinePostContentTypes.Text}'; + "); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "content_type", + table: "timeline_posts"); + + migrationBuilder.DropColumn( + name: "extra_content", + table: "timeline_posts"); + } + } +} diff --git a/BackEnd/Timeline/Migrations/20200614061237_AddTimelineUniqueId.Designer.cs b/BackEnd/Timeline/Migrations/20200614061237_AddTimelineUniqueId.Designer.cs new file mode 100644 index 00000000..adcc6308 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200614061237_AddTimelineUniqueId.Designer.cs @@ -0,0 +1,306 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Timeline.Entities; + +namespace Timeline.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20200614061237_AddTimelineUniqueId")] + partial class AddTimelineUniqueId + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.4"); + + modelBuilder.Entity("Timeline.Entities.DataEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Data") + .IsRequired() + .HasColumnName("data") + .HasColumnType("BLOB"); + + b.Property("Ref") + .HasColumnName("ref") + .HasColumnType("INTEGER"); + + b.Property("Tag") + .IsRequired() + .HasColumnName("tag") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Tag") + .IsUnique(); + + b.ToTable("data"); + }); + + modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnName("key") + .HasColumnType("BLOB"); + + b.HasKey("Id"); + + b.ToTable("jwt_token"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("CreateTime") + .HasColumnName("create_time") + .HasColumnType("TEXT"); + + b.Property("CurrentPostLocalId") + .HasColumnName("current_post_local_id") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnName("description") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnName("name") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnName("owner") + .HasColumnType("INTEGER"); + + b.Property("UniqueId") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnName("unique_id") + .HasColumnType("TEXT") + .HasDefaultValueSql("lower(hex(randomblob(16)))"); + + b.Property("Visibility") + .HasColumnName("visibility") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("timelines"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TimelineId"); + + b.HasIndex("UserId"); + + b.ToTable("timeline_members"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnName("author") + .HasColumnType("INTEGER"); + + b.Property("Content") + .HasColumnName("content") + .HasColumnType("TEXT"); + + b.Property("ContentType") + .IsRequired() + .HasColumnName("content_type") + .HasColumnType("TEXT"); + + b.Property("ExtraContent") + .HasColumnName("extra_content") + .HasColumnType("TEXT"); + + b.Property("LastUpdated") + .HasColumnName("last_updated") + .HasColumnType("TEXT"); + + b.Property("LocalId") + .HasColumnName("local_id") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnName("time") + .HasColumnType("TEXT"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("TimelineId"); + + b.ToTable("timeline_posts"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("DataTag") + .HasColumnName("data_tag") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnName("last_modified") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnName("type") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_avatars"); + }); + + modelBuilder.Entity("Timeline.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Nickname") + .HasColumnName("nickname") + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnName("password") + .HasColumnType("TEXT"); + + b.Property("Roles") + .IsRequired() + .HasColumnName("roles") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnName("username") + .HasColumnType("TEXT"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnName("version") + .HasColumnType("INTEGER") + .HasDefaultValue(0L); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Owner") + .WithMany("Timelines") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Members") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithMany("TimelinesJoined") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Author") + .WithMany("TimelinePosts") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Posts") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithOne("Avatar") + .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BackEnd/Timeline/Migrations/20200614061237_AddTimelineUniqueId.cs b/BackEnd/Timeline/Migrations/20200614061237_AddTimelineUniqueId.cs new file mode 100644 index 00000000..7abbed79 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200614061237_AddTimelineUniqueId.cs @@ -0,0 +1,50 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Timeline.Migrations +{ + public partial class AddTimelineUniqueId : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql( +@" +PRAGMA foreign_keys=OFF; + +BEGIN TRANSACTION; + +CREATE TABLE new_timelines ( + id INTEGER NOT NULL CONSTRAINT PK_timelines PRIMARY KEY AUTOINCREMENT, + unique_id TEXT NOT NULL DEFAULT (lower(hex(randomblob(16)))), + name TEXT NULL, + description TEXT NULL, + owner INTEGER NOT NULL, + visibility INTEGER NOT NULL, + create_time TEXT NOT NULL, + current_post_local_id INTEGER NOT NULL DEFAULT 0, + CONSTRAINT FK_timelines_users_owner FOREIGN KEY (owner) REFERENCES users (id) ON DELETE CASCADE +); + +INSERT INTO new_timelines (id, name, description, owner, visibility, create_time, current_post_local_id) + SELECT id, name, description, owner, visibility, create_time, current_post_local_id FROM timelines; + +DROP TABLE timelines; + +ALTER TABLE new_timelines + RENAME TO timelines; + +CREATE INDEX IX_timelines_owner ON timelines (owner); + +PRAGMA foreign_key_check; + +COMMIT TRANSACTION; + +PRAGMA foreign_keys=ON; +" + , true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + } + } +} diff --git a/BackEnd/Timeline/Migrations/20200618064936_TimelineAddModifiedTime.Designer.cs b/BackEnd/Timeline/Migrations/20200618064936_TimelineAddModifiedTime.Designer.cs new file mode 100644 index 00000000..fd10dfa9 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200618064936_TimelineAddModifiedTime.Designer.cs @@ -0,0 +1,314 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Timeline.Entities; + +namespace Timeline.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20200618064936_TimelineAddModifiedTime")] + partial class TimelineAddModifiedTime + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.5"); + + modelBuilder.Entity("Timeline.Entities.DataEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Data") + .IsRequired() + .HasColumnName("data") + .HasColumnType("BLOB"); + + b.Property("Ref") + .HasColumnName("ref") + .HasColumnType("INTEGER"); + + b.Property("Tag") + .IsRequired() + .HasColumnName("tag") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Tag") + .IsUnique(); + + b.ToTable("data"); + }); + + modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnName("key") + .HasColumnType("BLOB"); + + b.HasKey("Id"); + + b.ToTable("jwt_token"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("CreateTime") + .HasColumnName("create_time") + .HasColumnType("TEXT"); + + b.Property("CurrentPostLocalId") + .HasColumnName("current_post_local_id") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnName("description") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnName("last_modified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnName("name") + .HasColumnType("TEXT"); + + b.Property("NameLastModified") + .HasColumnName("name_last_modified") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnName("owner") + .HasColumnType("INTEGER"); + + b.Property("UniqueId") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnName("unique_id") + .HasColumnType("TEXT") + .HasDefaultValueSql("lower(hex(randomblob(16)))"); + + b.Property("Visibility") + .HasColumnName("visibility") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("timelines"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TimelineId"); + + b.HasIndex("UserId"); + + b.ToTable("timeline_members"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnName("author") + .HasColumnType("INTEGER"); + + b.Property("Content") + .HasColumnName("content") + .HasColumnType("TEXT"); + + b.Property("ContentType") + .IsRequired() + .HasColumnName("content_type") + .HasColumnType("TEXT"); + + b.Property("ExtraContent") + .HasColumnName("extra_content") + .HasColumnType("TEXT"); + + b.Property("LastUpdated") + .HasColumnName("last_updated") + .HasColumnType("TEXT"); + + b.Property("LocalId") + .HasColumnName("local_id") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnName("time") + .HasColumnType("TEXT"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("TimelineId"); + + b.ToTable("timeline_posts"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("DataTag") + .HasColumnName("data_tag") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnName("last_modified") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnName("type") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_avatars"); + }); + + modelBuilder.Entity("Timeline.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Nickname") + .HasColumnName("nickname") + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnName("password") + .HasColumnType("TEXT"); + + b.Property("Roles") + .IsRequired() + .HasColumnName("roles") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnName("username") + .HasColumnType("TEXT"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnName("version") + .HasColumnType("INTEGER") + .HasDefaultValue(0L); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Owner") + .WithMany("Timelines") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Members") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithMany("TimelinesJoined") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Author") + .WithMany("TimelinePosts") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Posts") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithOne("Avatar") + .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BackEnd/Timeline/Migrations/20200618064936_TimelineAddModifiedTime.cs b/BackEnd/Timeline/Migrations/20200618064936_TimelineAddModifiedTime.cs new file mode 100644 index 00000000..c277fe39 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200618064936_TimelineAddModifiedTime.cs @@ -0,0 +1,57 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Timeline.Migrations +{ + public partial class TimelineAddModifiedTime : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + var currentTime = new DateTimeToStringConverter().ConvertToProvider(DateTime.Now); + + migrationBuilder.Sql( +@$" +PRAGMA foreign_keys=OFF; + +BEGIN TRANSACTION; + +CREATE TABLE new_timelines ( + id INTEGER NOT NULL CONSTRAINT PK_timelines PRIMARY KEY AUTOINCREMENT, + unique_id TEXT NOT NULL DEFAULT (lower(hex(randomblob(16)))), + name TEXT NULL, + name_last_modified TEXT NOT NULL, + description TEXT NULL, + owner INTEGER NOT NULL, + visibility INTEGER NOT NULL, + create_time TEXT NOT NULL, + last_modified TEXT NOT NULL, + current_post_local_id INTEGER NOT NULL DEFAULT 0, + CONSTRAINT FK_timelines_users_owner FOREIGN KEY (owner) REFERENCES users (id) ON DELETE CASCADE +); + +INSERT INTO new_timelines (id, unique_id, name, name_last_modified, description, owner, visibility, create_time, last_modified, current_post_local_id) + SELECT id, unique_id, name, '{currentTime}', description, owner, visibility, create_time, '{currentTime}', current_post_local_id FROM timelines; + +DROP TABLE timelines; + +ALTER TABLE new_timelines + RENAME TO timelines; + +CREATE INDEX IX_timelines_owner ON timelines (owner); + +PRAGMA foreign_key_check; + +COMMIT TRANSACTION; + +PRAGMA foreign_keys=ON; +" + , true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/BackEnd/Timeline/Migrations/20200808071611_UserAddUniqueId.Designer.cs b/BackEnd/Timeline/Migrations/20200808071611_UserAddUniqueId.Designer.cs new file mode 100644 index 00000000..fe2329e4 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200808071611_UserAddUniqueId.Designer.cs @@ -0,0 +1,321 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Timeline.Entities; + +namespace Timeline.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20200808071611_UserAddUniqueId")] + partial class UserAddUniqueId + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.5"); + + modelBuilder.Entity("Timeline.Entities.DataEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Data") + .IsRequired() + .HasColumnName("data") + .HasColumnType("BLOB"); + + b.Property("Ref") + .HasColumnName("ref") + .HasColumnType("INTEGER"); + + b.Property("Tag") + .IsRequired() + .HasColumnName("tag") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Tag") + .IsUnique(); + + b.ToTable("data"); + }); + + modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnName("key") + .HasColumnType("BLOB"); + + b.HasKey("Id"); + + b.ToTable("jwt_token"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("CreateTime") + .HasColumnName("create_time") + .HasColumnType("TEXT"); + + b.Property("CurrentPostLocalId") + .HasColumnName("current_post_local_id") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnName("description") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnName("last_modified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnName("name") + .HasColumnType("TEXT"); + + b.Property("NameLastModified") + .HasColumnName("name_last_modified") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnName("owner") + .HasColumnType("INTEGER"); + + b.Property("UniqueId") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnName("unique_id") + .HasColumnType("TEXT") + .HasDefaultValueSql("lower(hex(randomblob(16)))"); + + b.Property("Visibility") + .HasColumnName("visibility") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("timelines"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TimelineId"); + + b.HasIndex("UserId"); + + b.ToTable("timeline_members"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnName("author") + .HasColumnType("INTEGER"); + + b.Property("Content") + .HasColumnName("content") + .HasColumnType("TEXT"); + + b.Property("ContentType") + .IsRequired() + .HasColumnName("content_type") + .HasColumnType("TEXT"); + + b.Property("ExtraContent") + .HasColumnName("extra_content") + .HasColumnType("TEXT"); + + b.Property("LastUpdated") + .HasColumnName("last_updated") + .HasColumnType("TEXT"); + + b.Property("LocalId") + .HasColumnName("local_id") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnName("time") + .HasColumnType("TEXT"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("TimelineId"); + + b.ToTable("timeline_posts"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("DataTag") + .HasColumnName("data_tag") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnName("last_modified") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnName("type") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_avatars"); + }); + + modelBuilder.Entity("Timeline.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Nickname") + .HasColumnName("nickname") + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnName("password") + .HasColumnType("TEXT"); + + b.Property("Roles") + .IsRequired() + .HasColumnName("roles") + .HasColumnType("TEXT"); + + b.Property("UniqueId") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnName("unique_id") + .HasColumnType("TEXT") + .HasDefaultValueSql("lower(hex(randomblob(16)))"); + + b.Property("Username") + .IsRequired() + .HasColumnName("username") + .HasColumnType("TEXT"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnName("version") + .HasColumnType("INTEGER") + .HasDefaultValue(0L); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Owner") + .WithMany("Timelines") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Members") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithMany("TimelinesJoined") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Author") + .WithMany("TimelinePosts") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Posts") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithOne("Avatar") + .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BackEnd/Timeline/Migrations/20200808071611_UserAddUniqueId.cs b/BackEnd/Timeline/Migrations/20200808071611_UserAddUniqueId.cs new file mode 100644 index 00000000..651a2b05 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200808071611_UserAddUniqueId.cs @@ -0,0 +1,55 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Timeline.Migrations +{ + public partial class UserAddUniqueId : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql( +@" +PRAGMA foreign_keys=OFF; + +BEGIN TRANSACTION; + +CREATE TABLE new_users ( + id INTEGER NOT NULL + CONSTRAINT PK_users PRIMARY KEY AUTOINCREMENT, + unique_id TEXT NOT NULL DEFAULT (lower(hex(randomblob(16)))), + username TEXT NOT NULL, + password TEXT NOT NULL, + roles TEXT NOT NULL, + version INTEGER NOT NULL + DEFAULT 0, + nickname TEXT +); + +INSERT INTO new_users (id, username, password, roles, version, nickname) + SELECT id, username, password, roles, version, nickname FROM users; + +DROP TABLE users; + +ALTER TABLE new_users + RENAME TO users; + +CREATE UNIQUE INDEX IX_users_username ON users ( + username +); + +PRAGMA foreign_key_check; + +COMMIT TRANSACTION; + +PRAGMA foreign_keys=ON; +" + , true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "unique_id", + table: "users"); + } + } +} diff --git a/BackEnd/Timeline/Migrations/20200810155908_AddTimesToUser.Designer.cs b/BackEnd/Timeline/Migrations/20200810155908_AddTimesToUser.Designer.cs new file mode 100644 index 00000000..71cc54dc --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200810155908_AddTimesToUser.Designer.cs @@ -0,0 +1,339 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Timeline.Entities; + +namespace Timeline.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20200810155908_AddTimesToUser")] + partial class AddTimesToUser + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.5"); + + modelBuilder.Entity("Timeline.Entities.DataEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Data") + .IsRequired() + .HasColumnName("data") + .HasColumnType("BLOB"); + + b.Property("Ref") + .HasColumnName("ref") + .HasColumnType("INTEGER"); + + b.Property("Tag") + .IsRequired() + .HasColumnName("tag") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Tag") + .IsUnique(); + + b.ToTable("data"); + }); + + modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnName("key") + .HasColumnType("BLOB"); + + b.HasKey("Id"); + + b.ToTable("jwt_token"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("CreateTime") + .HasColumnName("create_time") + .HasColumnType("TEXT"); + + b.Property("CurrentPostLocalId") + .HasColumnName("current_post_local_id") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnName("description") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnName("last_modified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnName("name") + .HasColumnType("TEXT"); + + b.Property("NameLastModified") + .HasColumnName("name_last_modified") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnName("owner") + .HasColumnType("INTEGER"); + + b.Property("UniqueId") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnName("unique_id") + .HasColumnType("TEXT") + .HasDefaultValueSql("lower(hex(randomblob(16)))"); + + b.Property("Visibility") + .HasColumnName("visibility") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("timelines"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TimelineId"); + + b.HasIndex("UserId"); + + b.ToTable("timeline_members"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnName("author") + .HasColumnType("INTEGER"); + + b.Property("Content") + .HasColumnName("content") + .HasColumnType("TEXT"); + + b.Property("ContentType") + .IsRequired() + .HasColumnName("content_type") + .HasColumnType("TEXT"); + + b.Property("ExtraContent") + .HasColumnName("extra_content") + .HasColumnType("TEXT"); + + b.Property("LastUpdated") + .HasColumnName("last_updated") + .HasColumnType("TEXT"); + + b.Property("LocalId") + .HasColumnName("local_id") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnName("time") + .HasColumnType("TEXT"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("TimelineId"); + + b.ToTable("timeline_posts"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("DataTag") + .HasColumnName("data_tag") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnName("last_modified") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnName("type") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_avatars"); + }); + + modelBuilder.Entity("Timeline.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("CreateTime") + .ValueGeneratedOnAdd() + .HasColumnName("create_time") + .HasColumnType("TEXT") + .HasDefaultValueSql("datetime('now', 'utc')"); + + b.Property("LastModified") + .ValueGeneratedOnAdd() + .HasColumnName("last_modified") + .HasColumnType("TEXT") + .HasDefaultValueSql("datetime('now', 'utc')"); + + b.Property("Nickname") + .HasColumnName("nickname") + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnName("password") + .HasColumnType("TEXT"); + + b.Property("Roles") + .IsRequired() + .HasColumnName("roles") + .HasColumnType("TEXT"); + + b.Property("UniqueId") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnName("unique_id") + .HasColumnType("TEXT") + .HasDefaultValueSql("lower(hex(randomblob(16)))"); + + b.Property("Username") + .IsRequired() + .HasColumnName("username") + .HasColumnType("TEXT"); + + b.Property("UsernameChangeTime") + .ValueGeneratedOnAdd() + .HasColumnName("username_change_time") + .HasColumnType("TEXT") + .HasDefaultValueSql("datetime('now', 'utc')"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnName("version") + .HasColumnType("INTEGER") + .HasDefaultValue(0L); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Owner") + .WithMany("Timelines") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Members") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithMany("TimelinesJoined") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Author") + .WithMany("TimelinePosts") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Posts") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithOne("Avatar") + .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BackEnd/Timeline/Migrations/20200810155908_AddTimesToUser.cs b/BackEnd/Timeline/Migrations/20200810155908_AddTimesToUser.cs new file mode 100644 index 00000000..369f85e6 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200810155908_AddTimesToUser.cs @@ -0,0 +1,67 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Timeline.Migrations +{ + public partial class AddTimesToUser : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql( +@" +PRAGMA foreign_keys=OFF; + +BEGIN TRANSACTION; + +CREATE TABLE new_users ( + id INTEGER NOT NULL + CONSTRAINT PK_users PRIMARY KEY AUTOINCREMENT, + unique_id TEXT NOT NULL DEFAULT (lower(hex(randomblob(16)))), + username TEXT NOT NULL, + password TEXT NOT NULL, + roles TEXT NOT NULL, + version INTEGER NOT NULL + DEFAULT 0, + nickname TEXT, + create_time TEXT NOT NULL DEFAULT (datetime('now', 'utc')), + last_modified TEXT NOT NULL DEFAULT (datetime('now', 'utc')), + username_change_time TEXT NOT NULL DEFAULT (datetime('now', 'utc')) +); + +INSERT INTO new_users (id, unique_id, username, password, roles, version, nickname) + SELECT id, unique_id, username, password, roles, version, nickname FROM users; + +DROP TABLE users; + +ALTER TABLE new_users + RENAME TO users; + +CREATE UNIQUE INDEX IX_users_username ON users ( + username +); + +PRAGMA foreign_key_check; + +COMMIT TRANSACTION; + +PRAGMA foreign_keys=ON; +" +, true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "create_time", + table: "users"); + + migrationBuilder.DropColumn( + name: "last_modified", + table: "users"); + + migrationBuilder.DropColumn( + name: "username_change_time", + table: "users"); + } + } +} diff --git a/BackEnd/Timeline/Migrations/20200810170533_MakePostAuthorOptional.Designer.cs b/BackEnd/Timeline/Migrations/20200810170533_MakePostAuthorOptional.Designer.cs new file mode 100644 index 00000000..80598fdf --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200810170533_MakePostAuthorOptional.Designer.cs @@ -0,0 +1,337 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Timeline.Entities; + +namespace Timeline.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20200810170533_MakePostAuthorOptional")] + partial class MakePostAuthorOptional + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.5"); + + modelBuilder.Entity("Timeline.Entities.DataEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Data") + .IsRequired() + .HasColumnName("data") + .HasColumnType("BLOB"); + + b.Property("Ref") + .HasColumnName("ref") + .HasColumnType("INTEGER"); + + b.Property("Tag") + .IsRequired() + .HasColumnName("tag") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Tag") + .IsUnique(); + + b.ToTable("data"); + }); + + modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnName("key") + .HasColumnType("BLOB"); + + b.HasKey("Id"); + + b.ToTable("jwt_token"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("CreateTime") + .HasColumnName("create_time") + .HasColumnType("TEXT"); + + b.Property("CurrentPostLocalId") + .HasColumnName("current_post_local_id") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnName("description") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnName("last_modified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnName("name") + .HasColumnType("TEXT"); + + b.Property("NameLastModified") + .HasColumnName("name_last_modified") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnName("owner") + .HasColumnType("INTEGER"); + + b.Property("UniqueId") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnName("unique_id") + .HasColumnType("TEXT") + .HasDefaultValueSql("lower(hex(randomblob(16)))"); + + b.Property("Visibility") + .HasColumnName("visibility") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("timelines"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TimelineId"); + + b.HasIndex("UserId"); + + b.ToTable("timeline_members"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnName("author") + .HasColumnType("INTEGER"); + + b.Property("Content") + .HasColumnName("content") + .HasColumnType("TEXT"); + + b.Property("ContentType") + .IsRequired() + .HasColumnName("content_type") + .HasColumnType("TEXT"); + + b.Property("ExtraContent") + .HasColumnName("extra_content") + .HasColumnType("TEXT"); + + b.Property("LastUpdated") + .HasColumnName("last_updated") + .HasColumnType("TEXT"); + + b.Property("LocalId") + .HasColumnName("local_id") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnName("time") + .HasColumnType("TEXT"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("TimelineId"); + + b.ToTable("timeline_posts"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("DataTag") + .HasColumnName("data_tag") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnName("last_modified") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnName("type") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_avatars"); + }); + + modelBuilder.Entity("Timeline.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("CreateTime") + .ValueGeneratedOnAdd() + .HasColumnName("create_time") + .HasColumnType("TEXT") + .HasDefaultValueSql("datetime('now', 'utc')"); + + b.Property("LastModified") + .ValueGeneratedOnAdd() + .HasColumnName("last_modified") + .HasColumnType("TEXT") + .HasDefaultValueSql("datetime('now', 'utc')"); + + b.Property("Nickname") + .HasColumnName("nickname") + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnName("password") + .HasColumnType("TEXT"); + + b.Property("Roles") + .IsRequired() + .HasColumnName("roles") + .HasColumnType("TEXT"); + + b.Property("UniqueId") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnName("unique_id") + .HasColumnType("TEXT") + .HasDefaultValueSql("lower(hex(randomblob(16)))"); + + b.Property("Username") + .IsRequired() + .HasColumnName("username") + .HasColumnType("TEXT"); + + b.Property("UsernameChangeTime") + .ValueGeneratedOnAdd() + .HasColumnName("username_change_time") + .HasColumnType("TEXT") + .HasDefaultValueSql("datetime('now', 'utc')"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnName("version") + .HasColumnType("INTEGER") + .HasDefaultValue(0L); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Owner") + .WithMany("Timelines") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Members") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithMany("TimelinesJoined") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Author") + .WithMany("TimelinePosts") + .HasForeignKey("AuthorId"); + + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Posts") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithOne("Avatar") + .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BackEnd/Timeline/Migrations/20200810170533_MakePostAuthorOptional.cs b/BackEnd/Timeline/Migrations/20200810170533_MakePostAuthorOptional.cs new file mode 100644 index 00000000..b0f0bca7 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200810170533_MakePostAuthorOptional.cs @@ -0,0 +1,78 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Timeline.Migrations +{ + public partial class MakePostAuthorOptional : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" +PRAGMA foreign_keys = 0; + +BEGIN TRANSACTION; + +CREATE TABLE new_timeline_posts ( + id INTEGER NOT NULL + CONSTRAINT PK_timeline_posts PRIMARY KEY AUTOINCREMENT, + timeline INTEGER NOT NULL, + author INTEGER, + content TEXT, + time TEXT NOT NULL, + last_updated TEXT NOT NULL, + local_id INTEGER NOT NULL + DEFAULT 0, + content_type TEXT NOT NULL + DEFAULT '', + extra_content TEXT, + CONSTRAINT FK_timeline_posts_users_author FOREIGN KEY ( + author + ) + REFERENCES users (id), + CONSTRAINT FK_timeline_posts_timelines_timeline FOREIGN KEY ( + timeline + ) + REFERENCES timelines (id) ON DELETE CASCADE +); + +INSERT INTO new_timeline_posts SELECT * FROM timeline_posts; + +DROP TABLE timeline_posts; + +ALTER TABLE new_timeline_posts RENAME TO timeline_posts; + +CREATE INDEX IX_timeline_posts_author ON timeline_posts (author); + +CREATE INDEX IX_timeline_posts_timeline ON timeline_posts(timeline); + +PRAGMA foreign_key_check; + +COMMIT TRANSACTION; + +PRAGMA foreign_keys = 1; + ", true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_timeline_posts_users_author", + table: "timeline_posts"); + + migrationBuilder.AlterColumn( + name: "author", + table: "timeline_posts", + type: "INTEGER", + nullable: false, + oldClrType: typeof(long), + oldNullable: true); + + migrationBuilder.AddForeignKey( + name: "FK_timeline_posts_users_author", + table: "timeline_posts", + column: "author", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/BackEnd/Timeline/Migrations/20200811080808_ChangeDateTimeOffsetToDateTime.Designer.cs b/BackEnd/Timeline/Migrations/20200811080808_ChangeDateTimeOffsetToDateTime.Designer.cs new file mode 100644 index 00000000..58238557 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200811080808_ChangeDateTimeOffsetToDateTime.Designer.cs @@ -0,0 +1,337 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Timeline.Entities; + +namespace Timeline.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20200811080808_ChangeDateTimeOffsetToDateTime")] + partial class ChangeDateTimeOffsetToDateTime + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.5"); + + modelBuilder.Entity("Timeline.Entities.DataEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Data") + .IsRequired() + .HasColumnName("data") + .HasColumnType("BLOB"); + + b.Property("Ref") + .HasColumnName("ref") + .HasColumnType("INTEGER"); + + b.Property("Tag") + .IsRequired() + .HasColumnName("tag") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Tag") + .IsUnique(); + + b.ToTable("data"); + }); + + modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnName("key") + .HasColumnType("BLOB"); + + b.HasKey("Id"); + + b.ToTable("jwt_token"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("CreateTime") + .HasColumnName("create_time") + .HasColumnType("TEXT"); + + b.Property("CurrentPostLocalId") + .HasColumnName("current_post_local_id") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnName("description") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnName("last_modified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnName("name") + .HasColumnType("TEXT"); + + b.Property("NameLastModified") + .HasColumnName("name_last_modified") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnName("owner") + .HasColumnType("INTEGER"); + + b.Property("UniqueId") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnName("unique_id") + .HasColumnType("TEXT") + .HasDefaultValueSql("lower(hex(randomblob(16)))"); + + b.Property("Visibility") + .HasColumnName("visibility") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("timelines"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TimelineId"); + + b.HasIndex("UserId"); + + b.ToTable("timeline_members"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnName("author") + .HasColumnType("INTEGER"); + + b.Property("Content") + .HasColumnName("content") + .HasColumnType("TEXT"); + + b.Property("ContentType") + .IsRequired() + .HasColumnName("content_type") + .HasColumnType("TEXT"); + + b.Property("ExtraContent") + .HasColumnName("extra_content") + .HasColumnType("TEXT"); + + b.Property("LastUpdated") + .HasColumnName("last_updated") + .HasColumnType("TEXT"); + + b.Property("LocalId") + .HasColumnName("local_id") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnName("time") + .HasColumnType("TEXT"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("TimelineId"); + + b.ToTable("timeline_posts"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("DataTag") + .HasColumnName("data_tag") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnName("last_modified") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnName("type") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_avatars"); + }); + + modelBuilder.Entity("Timeline.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("CreateTime") + .ValueGeneratedOnAdd() + .HasColumnName("create_time") + .HasColumnType("TEXT") + .HasDefaultValueSql("datetime('now', 'utc')"); + + b.Property("LastModified") + .ValueGeneratedOnAdd() + .HasColumnName("last_modified") + .HasColumnType("TEXT") + .HasDefaultValueSql("datetime('now', 'utc')"); + + b.Property("Nickname") + .HasColumnName("nickname") + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnName("password") + .HasColumnType("TEXT"); + + b.Property("Roles") + .IsRequired() + .HasColumnName("roles") + .HasColumnType("TEXT"); + + b.Property("UniqueId") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnName("unique_id") + .HasColumnType("TEXT") + .HasDefaultValueSql("lower(hex(randomblob(16)))"); + + b.Property("Username") + .IsRequired() + .HasColumnName("username") + .HasColumnType("TEXT"); + + b.Property("UsernameChangeTime") + .ValueGeneratedOnAdd() + .HasColumnName("username_change_time") + .HasColumnType("TEXT") + .HasDefaultValueSql("datetime('now', 'utc')"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnName("version") + .HasColumnType("INTEGER") + .HasDefaultValue(0L); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Owner") + .WithMany("Timelines") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Members") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithMany("TimelinesJoined") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Author") + .WithMany("TimelinePosts") + .HasForeignKey("AuthorId"); + + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Posts") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithOne("Avatar") + .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BackEnd/Timeline/Migrations/20200811080808_ChangeDateTimeOffsetToDateTime.cs b/BackEnd/Timeline/Migrations/20200811080808_ChangeDateTimeOffsetToDateTime.cs new file mode 100644 index 00000000..eb6b44f3 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200811080808_ChangeDateTimeOffsetToDateTime.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Timeline.Migrations +{ + public partial class ChangeDateTimeOffsetToDateTime : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/BackEnd/Timeline/Migrations/20200826164553_TimelineAddTitle.Designer.cs b/BackEnd/Timeline/Migrations/20200826164553_TimelineAddTitle.Designer.cs new file mode 100644 index 00000000..f2279f3b --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200826164553_TimelineAddTitle.Designer.cs @@ -0,0 +1,341 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Timeline.Entities; + +namespace Timeline.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20200826164553_TimelineAddTitle")] + partial class TimelineAddTitle + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.7"); + + modelBuilder.Entity("Timeline.Entities.DataEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Data") + .IsRequired() + .HasColumnName("data") + .HasColumnType("BLOB"); + + b.Property("Ref") + .HasColumnName("ref") + .HasColumnType("INTEGER"); + + b.Property("Tag") + .IsRequired() + .HasColumnName("tag") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Tag") + .IsUnique(); + + b.ToTable("data"); + }); + + modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnName("key") + .HasColumnType("BLOB"); + + b.HasKey("Id"); + + b.ToTable("jwt_token"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("CreateTime") + .HasColumnName("create_time") + .HasColumnType("TEXT"); + + b.Property("CurrentPostLocalId") + .HasColumnName("current_post_local_id") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnName("description") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnName("last_modified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnName("name") + .HasColumnType("TEXT"); + + b.Property("NameLastModified") + .HasColumnName("name_last_modified") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnName("owner") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnName("title") + .HasColumnType("TEXT"); + + b.Property("UniqueId") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnName("unique_id") + .HasColumnType("TEXT") + .HasDefaultValueSql("lower(hex(randomblob(16)))"); + + b.Property("Visibility") + .HasColumnName("visibility") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("timelines"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TimelineId"); + + b.HasIndex("UserId"); + + b.ToTable("timeline_members"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnName("author") + .HasColumnType("INTEGER"); + + b.Property("Content") + .HasColumnName("content") + .HasColumnType("TEXT"); + + b.Property("ContentType") + .IsRequired() + .HasColumnName("content_type") + .HasColumnType("TEXT"); + + b.Property("ExtraContent") + .HasColumnName("extra_content") + .HasColumnType("TEXT"); + + b.Property("LastUpdated") + .HasColumnName("last_updated") + .HasColumnType("TEXT"); + + b.Property("LocalId") + .HasColumnName("local_id") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnName("time") + .HasColumnType("TEXT"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("TimelineId"); + + b.ToTable("timeline_posts"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("DataTag") + .HasColumnName("data_tag") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnName("last_modified") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnName("type") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_avatars"); + }); + + modelBuilder.Entity("Timeline.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("CreateTime") + .ValueGeneratedOnAdd() + .HasColumnName("create_time") + .HasColumnType("TEXT") + .HasDefaultValueSql("datetime('now', 'utc')"); + + b.Property("LastModified") + .ValueGeneratedOnAdd() + .HasColumnName("last_modified") + .HasColumnType("TEXT") + .HasDefaultValueSql("datetime('now', 'utc')"); + + b.Property("Nickname") + .HasColumnName("nickname") + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnName("password") + .HasColumnType("TEXT"); + + b.Property("Roles") + .IsRequired() + .HasColumnName("roles") + .HasColumnType("TEXT"); + + b.Property("UniqueId") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnName("unique_id") + .HasColumnType("TEXT") + .HasDefaultValueSql("lower(hex(randomblob(16)))"); + + b.Property("Username") + .IsRequired() + .HasColumnName("username") + .HasColumnType("TEXT"); + + b.Property("UsernameChangeTime") + .ValueGeneratedOnAdd() + .HasColumnName("username_change_time") + .HasColumnType("TEXT") + .HasDefaultValueSql("datetime('now', 'utc')"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnName("version") + .HasColumnType("INTEGER") + .HasDefaultValue(0L); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Owner") + .WithMany("Timelines") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Members") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithMany("TimelinesJoined") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Author") + .WithMany("TimelinePosts") + .HasForeignKey("AuthorId"); + + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Posts") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithOne("Avatar") + .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BackEnd/Timeline/Migrations/20200826164553_TimelineAddTitle.cs b/BackEnd/Timeline/Migrations/20200826164553_TimelineAddTitle.cs new file mode 100644 index 00000000..7e8c498b --- /dev/null +++ b/BackEnd/Timeline/Migrations/20200826164553_TimelineAddTitle.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Timeline.Migrations +{ + public partial class TimelineAddTitle : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "title", + table: "timelines", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "title", + table: "timelines"); + } + } +} diff --git a/BackEnd/Timeline/Migrations/DatabaseContextModelSnapshot.cs b/BackEnd/Timeline/Migrations/DatabaseContextModelSnapshot.cs new file mode 100644 index 00000000..65ae6c9a --- /dev/null +++ b/BackEnd/Timeline/Migrations/DatabaseContextModelSnapshot.cs @@ -0,0 +1,339 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Timeline.Entities; + +namespace Timeline.Migrations +{ + [DbContext(typeof(DatabaseContext))] + partial class DatabaseContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.7"); + + modelBuilder.Entity("Timeline.Entities.DataEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Data") + .IsRequired() + .HasColumnName("data") + .HasColumnType("BLOB"); + + b.Property("Ref") + .HasColumnName("ref") + .HasColumnType("INTEGER"); + + b.Property("Tag") + .IsRequired() + .HasColumnName("tag") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Tag") + .IsUnique(); + + b.ToTable("data"); + }); + + modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnName("key") + .HasColumnType("BLOB"); + + b.HasKey("Id"); + + b.ToTable("jwt_token"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("CreateTime") + .HasColumnName("create_time") + .HasColumnType("TEXT"); + + b.Property("CurrentPostLocalId") + .HasColumnName("current_post_local_id") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnName("description") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnName("last_modified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnName("name") + .HasColumnType("TEXT"); + + b.Property("NameLastModified") + .HasColumnName("name_last_modified") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnName("owner") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnName("title") + .HasColumnType("TEXT"); + + b.Property("UniqueId") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnName("unique_id") + .HasColumnType("TEXT") + .HasDefaultValueSql("lower(hex(randomblob(16)))"); + + b.Property("Visibility") + .HasColumnName("visibility") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("timelines"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TimelineId"); + + b.HasIndex("UserId"); + + b.ToTable("timeline_members"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnName("author") + .HasColumnType("INTEGER"); + + b.Property("Content") + .HasColumnName("content") + .HasColumnType("TEXT"); + + b.Property("ContentType") + .IsRequired() + .HasColumnName("content_type") + .HasColumnType("TEXT"); + + b.Property("ExtraContent") + .HasColumnName("extra_content") + .HasColumnType("TEXT"); + + b.Property("LastUpdated") + .HasColumnName("last_updated") + .HasColumnType("TEXT"); + + b.Property("LocalId") + .HasColumnName("local_id") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnName("time") + .HasColumnType("TEXT"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("TimelineId"); + + b.ToTable("timeline_posts"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("DataTag") + .HasColumnName("data_tag") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnName("last_modified") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnName("type") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_avatars"); + }); + + modelBuilder.Entity("Timeline.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("CreateTime") + .ValueGeneratedOnAdd() + .HasColumnName("create_time") + .HasColumnType("TEXT") + .HasDefaultValueSql("datetime('now', 'utc')"); + + b.Property("LastModified") + .ValueGeneratedOnAdd() + .HasColumnName("last_modified") + .HasColumnType("TEXT") + .HasDefaultValueSql("datetime('now', 'utc')"); + + b.Property("Nickname") + .HasColumnName("nickname") + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnName("password") + .HasColumnType("TEXT"); + + b.Property("Roles") + .IsRequired() + .HasColumnName("roles") + .HasColumnType("TEXT"); + + b.Property("UniqueId") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnName("unique_id") + .HasColumnType("TEXT") + .HasDefaultValueSql("lower(hex(randomblob(16)))"); + + b.Property("Username") + .IsRequired() + .HasColumnName("username") + .HasColumnType("TEXT"); + + b.Property("UsernameChangeTime") + .ValueGeneratedOnAdd() + .HasColumnName("username_change_time") + .HasColumnType("TEXT") + .HasDefaultValueSql("datetime('now', 'utc')"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnName("version") + .HasColumnType("INTEGER") + .HasDefaultValue(0L); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Owner") + .WithMany("Timelines") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Members") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithMany("TimelinesJoined") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Author") + .WithMany("TimelinePosts") + .HasForeignKey("AuthorId"); + + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Posts") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithOne("Avatar") + .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BackEnd/Timeline/MockClientApp/index.html b/BackEnd/Timeline/MockClientApp/index.html new file mode 100644 index 00000000..03cf371e --- /dev/null +++ b/BackEnd/Timeline/MockClientApp/index.html @@ -0,0 +1,10 @@ + + + + + Mock Client App + + + This is a mock client app for testing. + + diff --git a/BackEnd/Timeline/Models/ByteData.cs b/BackEnd/Timeline/Models/ByteData.cs new file mode 100644 index 00000000..7b832eb5 --- /dev/null +++ b/BackEnd/Timeline/Models/ByteData.cs @@ -0,0 +1,33 @@ +using NSwag.Annotations; + +namespace Timeline.Models +{ + /// + /// Model for reading http body as bytes. + /// + [OpenApiFile] + public class ByteData + { + /// + /// + /// The data. + /// The content type. + public ByteData(byte[] data, string contentType) + { + Data = data; + ContentType = contentType; + } + + /// + /// Data. + /// +#pragma warning disable CA1819 // Properties should not return arrays + public byte[] Data { get; } +#pragma warning restore CA1819 // Properties should not return arrays + + /// + /// Content type. + /// + public string ContentType { get; } + } +} diff --git a/BackEnd/Timeline/Models/Converters/JsonDateTimeConverter.cs b/BackEnd/Timeline/Models/Converters/JsonDateTimeConverter.cs new file mode 100644 index 00000000..94b5cab0 --- /dev/null +++ b/BackEnd/Timeline/Models/Converters/JsonDateTimeConverter.cs @@ -0,0 +1,23 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; +using Timeline.Helpers; + +namespace Timeline.Models.Converters +{ + public class JsonDateTimeConverter : JsonConverter + { + public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + Debug.Assert(typeToConvert == typeof(DateTime)); + return DateTime.Parse(reader.GetString(), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); + } + + public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.MyToUtc().ToString("s", CultureInfo.InvariantCulture) + "Z"); + } + } +} diff --git a/BackEnd/Timeline/Models/Converters/MyDateTimeConverter.cs b/BackEnd/Timeline/Models/Converters/MyDateTimeConverter.cs new file mode 100644 index 00000000..f125cd5c --- /dev/null +++ b/BackEnd/Timeline/Models/Converters/MyDateTimeConverter.cs @@ -0,0 +1,51 @@ +using System; +using System.ComponentModel; +using System.Globalization; + +namespace Timeline.Models.Converters +{ + public class MyDateTimeConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); + } + + public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) + { + return base.CanConvertTo(context, destinationType); + } + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + if (value is string text) + { + text = text.Trim(); + if (text.Length == 0) + { + return DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc); + } + + return DateTime.Parse(text, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal); + } + + return base.ConvertFrom(context, culture, value); + } + + public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) + { + if (destinationType == typeof(string) && value is DateTime) + { + DateTime dt = (DateTime)value; + if (dt == DateTime.MinValue) + { + return string.Empty; + } + + return dt.ToString("s", CultureInfo.InvariantCulture) + "Z"; + } + + return base.ConvertTo(context, culture, value, destinationType); + } + } +} diff --git a/BackEnd/Timeline/Models/Http/ActionContextAccessorExtensions.cs b/BackEnd/Timeline/Models/Http/ActionContextAccessorExtensions.cs new file mode 100644 index 00000000..bcc55c5a --- /dev/null +++ b/BackEnd/Timeline/Models/Http/ActionContextAccessorExtensions.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using System; + +namespace Timeline.Models.Http +{ + public static class ActionContextAccessorExtensions + { + public static ActionContext AssertActionContextForUrlFill(this IActionContextAccessor accessor) + { + return accessor.ActionContext ?? throw new InvalidOperationException(Resources.Models.Http.Exception.ActionContextNull); + } + } +} diff --git a/BackEnd/Timeline/Models/Http/Common.cs b/BackEnd/Timeline/Models/Http/Common.cs new file mode 100644 index 00000000..5fa22c9e --- /dev/null +++ b/BackEnd/Timeline/Models/Http/Common.cs @@ -0,0 +1,120 @@ +using static Timeline.Resources.Models.Http.Common; + +namespace Timeline.Models.Http +{ + public class CommonResponse + { + public CommonResponse() + { + + } + + public CommonResponse(int code, string message) + { + Code = code; + Message = message; + } + + public int Code { get; set; } + public string? Message { get; set; } + } + + public class CommonDataResponse : CommonResponse + { + public CommonDataResponse() + { + + } + + public CommonDataResponse(int code, string message, T data) + : base(code, message) + { + Data = data; + } + + public T Data { get; set; } = default!; + } + + public class CommonPutResponse : CommonDataResponse + { + public class ResponseData + { + public ResponseData() { } + + public ResponseData(bool create) + { + Create = create; + } + + public bool Create { get; set; } + } + + public CommonPutResponse() + { + + } + + public CommonPutResponse(int code, string message, bool create) + : base(code, message, new ResponseData(create)) + { + + } + + internal static CommonPutResponse Create() + { + return new CommonPutResponse(0, MessagePutCreate, true); + } + + internal static CommonPutResponse Modify() + { + return new CommonPutResponse(0, MessagePutModify, false); + } + } + + /// + /// Common response for delete method. + /// + public class CommonDeleteResponse : CommonDataResponse + { + /// + public class ResponseData + { + /// + public ResponseData() { } + + /// + public ResponseData(bool delete) + { + Delete = delete; + } + + /// + /// True if the entry is deleted. False if the entry does not exist. + /// + public bool Delete { get; set; } + } + + /// + public CommonDeleteResponse() + { + + } + + /// + public CommonDeleteResponse(int code, string message, bool delete) + : base(code, message, new ResponseData(delete)) + { + + } + + internal static CommonDeleteResponse Delete() + { + return new CommonDeleteResponse(0, MessageDeleteDelete, true); + } + + internal static CommonDeleteResponse NotExist() + { + return new CommonDeleteResponse(0, MessageDeleteNotExist, false); + } + } +} diff --git a/BackEnd/Timeline/Models/Http/ErrorResponse.cs b/BackEnd/Timeline/Models/Http/ErrorResponse.cs new file mode 100644 index 00000000..ac86481f --- /dev/null +++ b/BackEnd/Timeline/Models/Http/ErrorResponse.cs @@ -0,0 +1,261 @@ +using static Timeline.Resources.Messages; + +namespace Timeline.Models.Http +{ + public static class ErrorResponse + { + public static class Common + { + public static CommonResponse InvalidModel(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.InvalidModel, string.Format(Common_InvalidModel, formatArgs)); + } + + public static CommonResponse CustomMessage_InvalidModel(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.InvalidModel, string.Format(message, formatArgs)); + } + + public static CommonResponse Forbid(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.Forbid, string.Format(Common_Forbid, formatArgs)); + } + + public static CommonResponse CustomMessage_Forbid(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.Forbid, string.Format(message, formatArgs)); + } + + public static CommonResponse UnknownEndpoint(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.UnknownEndpoint, string.Format(Common_UnknownEndpoint, formatArgs)); + } + + public static CommonResponse CustomMessage_UnknownEndpoint(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.UnknownEndpoint, string.Format(message, formatArgs)); + } + + public static class Header + { + public static CommonResponse IfNonMatch_BadFormat(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.Header.IfNonMatch_BadFormat, string.Format(Common_Header_IfNonMatch_BadFormat, formatArgs)); + } + + public static CommonResponse CustomMessage_IfNonMatch_BadFormat(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.Header.IfNonMatch_BadFormat, string.Format(message, formatArgs)); + } + + } + + public static class Content + { + public static CommonResponse TooBig(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.Content.TooBig, string.Format(Common_Content_TooBig, formatArgs)); + } + + public static CommonResponse CustomMessage_TooBig(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.Content.TooBig, string.Format(message, formatArgs)); + } + + } + + } + + public static class UserCommon + { + public static CommonResponse NotExist(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserCommon.NotExist, string.Format(UserCommon_NotExist, formatArgs)); + } + + public static CommonResponse CustomMessage_NotExist(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserCommon.NotExist, string.Format(message, formatArgs)); + } + + } + + public static class TokenController + { + public static CommonResponse Create_BadCredential(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TokenController.Create_BadCredential, string.Format(TokenController_Create_BadCredential, formatArgs)); + } + + public static CommonResponse CustomMessage_Create_BadCredential(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TokenController.Create_BadCredential, string.Format(message, formatArgs)); + } + + public static CommonResponse Verify_BadFormat(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TokenController.Verify_BadFormat, string.Format(TokenController_Verify_BadFormat, formatArgs)); + } + + public static CommonResponse CustomMessage_Verify_BadFormat(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TokenController.Verify_BadFormat, string.Format(message, formatArgs)); + } + + public static CommonResponse Verify_UserNotExist(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TokenController.Verify_UserNotExist, string.Format(TokenController_Verify_UserNotExist, formatArgs)); + } + + public static CommonResponse CustomMessage_Verify_UserNotExist(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TokenController.Verify_UserNotExist, string.Format(message, formatArgs)); + } + + public static CommonResponse Verify_OldVersion(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TokenController.Verify_OldVersion, string.Format(TokenController_Verify_OldVersion, formatArgs)); + } + + public static CommonResponse CustomMessage_Verify_OldVersion(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TokenController.Verify_OldVersion, string.Format(message, formatArgs)); + } + + public static CommonResponse Verify_TimeExpired(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TokenController.Verify_TimeExpired, string.Format(TokenController_Verify_TimeExpired, formatArgs)); + } + + public static CommonResponse CustomMessage_Verify_TimeExpired(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TokenController.Verify_TimeExpired, string.Format(message, formatArgs)); + } + + } + + public static class UserController + { + public static CommonResponse UsernameConflict(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserController.UsernameConflict, string.Format(UserController_UsernameConflict, formatArgs)); + } + + public static CommonResponse CustomMessage_UsernameConflict(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserController.UsernameConflict, string.Format(message, formatArgs)); + } + + public static CommonResponse ChangePassword_BadOldPassword(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserController.ChangePassword_BadOldPassword, string.Format(UserController_ChangePassword_BadOldPassword, formatArgs)); + } + + public static CommonResponse CustomMessage_ChangePassword_BadOldPassword(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserController.ChangePassword_BadOldPassword, string.Format(message, formatArgs)); + } + + } + + public static class UserAvatar + { + public static CommonResponse BadFormat_CantDecode(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserAvatar.BadFormat_CantDecode, string.Format(UserAvatar_BadFormat_CantDecode, formatArgs)); + } + + public static CommonResponse CustomMessage_BadFormat_CantDecode(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserAvatar.BadFormat_CantDecode, string.Format(message, formatArgs)); + } + + public static CommonResponse BadFormat_UnmatchedFormat(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserAvatar.BadFormat_UnmatchedFormat, string.Format(UserAvatar_BadFormat_UnmatchedFormat, formatArgs)); + } + + public static CommonResponse CustomMessage_BadFormat_UnmatchedFormat(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserAvatar.BadFormat_UnmatchedFormat, string.Format(message, formatArgs)); + } + + public static CommonResponse BadFormat_BadSize(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserAvatar.BadFormat_BadSize, string.Format(UserAvatar_BadFormat_BadSize, formatArgs)); + } + + public static CommonResponse CustomMessage_BadFormat_BadSize(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserAvatar.BadFormat_BadSize, string.Format(message, formatArgs)); + } + + } + + public static class TimelineController + { + public static CommonResponse NameConflict(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TimelineController.NameConflict, string.Format(TimelineController_NameConflict, formatArgs)); + } + + public static CommonResponse CustomMessage_NameConflict(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TimelineController.NameConflict, string.Format(message, formatArgs)); + } + + public static CommonResponse NotExist(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TimelineController.NotExist, string.Format(TimelineController_NotExist, formatArgs)); + } + + public static CommonResponse CustomMessage_NotExist(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TimelineController.NotExist, string.Format(message, formatArgs)); + } + + public static CommonResponse MemberPut_NotExist(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TimelineController.MemberPut_NotExist, string.Format(TimelineController_MemberPut_NotExist, formatArgs)); + } + + public static CommonResponse CustomMessage_MemberPut_NotExist(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TimelineController.MemberPut_NotExist, string.Format(message, formatArgs)); + } + + public static CommonResponse QueryRelateNotExist(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TimelineController.QueryRelateNotExist, string.Format(TimelineController_QueryRelateNotExist, formatArgs)); + } + + public static CommonResponse CustomMessage_QueryRelateNotExist(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TimelineController.QueryRelateNotExist, string.Format(message, formatArgs)); + } + + public static CommonResponse PostNotExist(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TimelineController.PostNotExist, string.Format(TimelineController_PostNotExist, formatArgs)); + } + + public static CommonResponse CustomMessage_PostNotExist(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TimelineController.PostNotExist, string.Format(message, formatArgs)); + } + + public static CommonResponse PostNoData(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TimelineController.PostNoData, string.Format(TimelineController_PostNoData, formatArgs)); + } + + public static CommonResponse CustomMessage_PostNoData(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TimelineController.PostNoData, string.Format(message, formatArgs)); + } + + } + + } + +} diff --git a/BackEnd/Timeline/Models/Http/Timeline.cs b/BackEnd/Timeline/Models/Http/Timeline.cs new file mode 100644 index 00000000..a81b33f5 --- /dev/null +++ b/BackEnd/Timeline/Models/Http/Timeline.cs @@ -0,0 +1,219 @@ +using AutoMapper; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using System; +using System.Collections.Generic; +using Timeline.Controllers; + +namespace Timeline.Models.Http +{ + /// + /// Info of post content. + /// + public class TimelinePostContentInfo + { + /// + /// Type of the post content. + /// + public string Type { get; set; } = default!; + /// + /// If post is of text type. This is the text. + /// + public string? Text { get; set; } + /// + /// If post is of image type. This is the image url. + /// + public string? Url { get; set; } + /// + /// If post has data (currently it means it's a image post), this is the data etag. + /// + public string? ETag { get; set; } + } + + /// + /// Info of a post. + /// + public class TimelinePostInfo + { + /// + /// Post id. + /// + public long Id { get; set; } + /// + /// Content of the post. May be null if post is deleted. + /// + public TimelinePostContentInfo? Content { get; set; } + /// + /// True if post is deleted. + /// + public bool Deleted { get; set; } + /// + /// Post time. + /// + public DateTime Time { get; set; } + /// + /// The author. May be null if the user has been deleted. + /// + public UserInfo? Author { get; set; } = default!; + /// + /// Last updated time. + /// + public DateTime LastUpdated { get; set; } = default!; + } + + /// + /// Info of a timeline. + /// + public class TimelineInfo + { + /// + /// Unique id. + /// + public string UniqueId { get; set; } = default!; + /// + /// Title. + /// + public string Title { get; set; } = default!; + /// + /// Name of timeline. + /// + public string Name { get; set; } = default!; + /// + /// Last modified time of timeline name. + /// + public DateTime NameLastModifed { get; set; } = default!; + /// + /// Timeline description. + /// + public string Description { get; set; } = default!; + /// + /// Owner of the timeline. + /// + public UserInfo Owner { get; set; } = default!; + /// + /// Visibility of the timeline. + /// + public TimelineVisibility Visibility { get; set; } +#pragma warning disable CA2227 // Collection properties should be read only + /// + /// Members of timeline. + /// + public List Members { get; set; } = default!; +#pragma warning restore CA2227 // Collection properties should be read only + /// + /// Create time of timeline. + /// + public DateTime CreateTime { get; set; } = default!; + /// + /// Last modified time of timeline. + /// + public DateTime LastModified { get; set; } = default!; + +#pragma warning disable CA1707 // Identifiers should not contain underscores + /// + /// Related links. + /// + public TimelineInfoLinks _links { get; set; } = default!; +#pragma warning restore CA1707 // Identifiers should not contain underscores + } + + /// + /// Related links for timeline. + /// + public class TimelineInfoLinks + { + /// + /// Self. + /// + public string Self { get; set; } = default!; + /// + /// Posts url. + /// + public string Posts { get; set; } = default!; + } + + public class TimelineInfoLinksValueResolver : IValueResolver + { + private readonly IActionContextAccessor _actionContextAccessor; + private readonly IUrlHelperFactory _urlHelperFactory; + + public TimelineInfoLinksValueResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) + { + _actionContextAccessor = actionContextAccessor; + _urlHelperFactory = urlHelperFactory; + } + + public TimelineInfoLinks Resolve(Timeline source, TimelineInfo destination, TimelineInfoLinks destMember, ResolutionContext context) + { + var actionContext = _actionContextAccessor.AssertActionContextForUrlFill(); + var urlHelper = _urlHelperFactory.GetUrlHelper(actionContext); + + return new TimelineInfoLinks + { + Self = urlHelper.ActionLink(nameof(TimelineController.TimelineGet), nameof(TimelineController)[0..^nameof(Controller).Length], new { source.Name }), + Posts = urlHelper.ActionLink(nameof(TimelineController.PostListGet), nameof(TimelineController)[0..^nameof(Controller).Length], new { source.Name }) + }; + } + } + + public class TimelinePostContentResolver : IValueResolver + { + private readonly IActionContextAccessor _actionContextAccessor; + private readonly IUrlHelperFactory _urlHelperFactory; + + public TimelinePostContentResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) + { + _actionContextAccessor = actionContextAccessor; + _urlHelperFactory = urlHelperFactory; + } + + public TimelinePostContentInfo? Resolve(TimelinePost source, TimelinePostInfo destination, TimelinePostContentInfo? destMember, ResolutionContext context) + { + var actionContext = _actionContextAccessor.AssertActionContextForUrlFill(); + var urlHelper = _urlHelperFactory.GetUrlHelper(actionContext); + + var sourceContent = source.Content; + + if (sourceContent == null) + { + return null; + } + + if (sourceContent is TextTimelinePostContent textContent) + { + return new TimelinePostContentInfo + { + Type = TimelinePostContentTypes.Text, + Text = textContent.Text + }; + } + else if (sourceContent is ImageTimelinePostContent imageContent) + { + return new TimelinePostContentInfo + { + Type = TimelinePostContentTypes.Image, + Url = urlHelper.ActionLink( + action: nameof(TimelineController.PostDataGet), + controller: nameof(TimelineController)[0..^nameof(Controller).Length], + values: new { Name = source.TimelineName, Id = source.Id }), + ETag = $"\"{imageContent.DataTag}\"" + }; + } + else + { + throw new InvalidOperationException(Resources.Models.Http.Exception.UnknownPostContentType); + } + } + } + + public class TimelineInfoAutoMapperProfile : Profile + { + public TimelineInfoAutoMapperProfile() + { + CreateMap().ForMember(u => u._links, opt => opt.MapFrom()); + CreateMap().ForMember(p => p.Content, opt => opt.MapFrom()); + CreateMap(); + } + } +} diff --git a/BackEnd/Timeline/Models/Http/TimelineController.cs b/BackEnd/Timeline/Models/Http/TimelineController.cs new file mode 100644 index 00000000..7bd141ed --- /dev/null +++ b/BackEnd/Timeline/Models/Http/TimelineController.cs @@ -0,0 +1,93 @@ +using System; +using System.ComponentModel.DataAnnotations; +using Timeline.Models.Validation; + +namespace Timeline.Models.Http +{ + /// + /// Content of post create request. + /// + public class TimelinePostCreateRequestContent + { + /// + /// Type of post content. + /// + [Required] + public string Type { get; set; } = default!; + /// + /// If post is of text type, this is the text. + /// + public string? Text { get; set; } + /// + /// If post is of image type, this is base64 of image data. + /// + public string? Data { get; set; } + } + + public class TimelinePostCreateRequest + { + /// + /// Content of the new post. + /// + [Required] + public TimelinePostCreateRequestContent Content { get; set; } = default!; + + /// + /// Time of the post. If not set, current time will be used. + /// + public DateTime? Time { get; set; } + } + + /// + /// Create timeline request model. + /// + public class TimelineCreateRequest + { + /// + /// Name of the new timeline. Must be a valid name. + /// + [Required] + [TimelineName] + public string Name { get; set; } = default!; + } + + /// + /// Patch timeline request model. + /// + public class TimelinePatchRequest + { + /// + /// New title. Null for not change. + /// + public string? Title { get; set; } + + /// + /// New description. Null for not change. + /// + public string? Description { get; set; } + + /// + /// New visibility. Null for not change. + /// + public TimelineVisibility? Visibility { get; set; } + } + + /// + /// Change timeline name request model. + /// + public class TimelineChangeNameRequest + { + /// + /// Old name of timeline. + /// + [Required] + [TimelineName] + public string OldName { get; set; } = default!; + /// + /// New name of timeline. + /// + [Required] + [TimelineName] + public string NewName { get; set; } = default!; + } +} diff --git a/BackEnd/Timeline/Models/Http/TokenController.cs b/BackEnd/Timeline/Models/Http/TokenController.cs new file mode 100644 index 00000000..a42c44e5 --- /dev/null +++ b/BackEnd/Timeline/Models/Http/TokenController.cs @@ -0,0 +1,62 @@ +using System.ComponentModel.DataAnnotations; +using Timeline.Controllers; + +namespace Timeline.Models.Http +{ + /// + /// Request model for . + /// + public class CreateTokenRequest + { + /// + /// The username. + /// + public string Username { get; set; } = default!; + /// + /// The password. + /// + public string Password { get; set; } = default!; + /// + /// Optional token validation period. In days. If not specified, server will use a default one. + /// + [Range(1, 365)] + public int? Expire { get; set; } + } + + /// + /// Response model for . + /// + public class CreateTokenResponse + { + /// + /// The token created. + /// + public string Token { get; set; } = default!; + /// + /// The user owning the token. + /// + public UserInfo User { get; set; } = default!; + } + + /// + /// Request model for . + /// + public class VerifyTokenRequest + { + /// + /// The token to verify. + /// + public string Token { get; set; } = default!; + } + + /// + /// Response model for . + /// + public class VerifyTokenResponse + { + /// + /// The user owning the token. + /// + public UserInfo User { get; set; } = default!; + } +} diff --git a/BackEnd/Timeline/Models/Http/UserController.cs b/BackEnd/Timeline/Models/Http/UserController.cs new file mode 100644 index 00000000..6bc5a66e --- /dev/null +++ b/BackEnd/Timeline/Models/Http/UserController.cs @@ -0,0 +1,93 @@ +using AutoMapper; +using System.ComponentModel.DataAnnotations; +using Timeline.Controllers; +using Timeline.Models.Validation; + +namespace Timeline.Models.Http +{ + /// + /// Request model for . + /// + public class UserPatchRequest + { + /// + /// New username. Null if not change. Need to be administrator. + /// + [Username] + public string? Username { get; set; } + + /// + /// New password. Null if not change. Need to be administrator. + /// + [MinLength(1)] + public string? Password { get; set; } + + /// + /// New nickname. Null if not change. Need to be administrator to change other's. + /// + [Nickname] + public string? Nickname { get; set; } + + /// + /// Whether to be administrator. Null if not change. Need to be administrator. + /// + public bool? Administrator { get; set; } + } + + /// + /// Request model for . + /// + public class CreateUserRequest + { + /// + /// Username of the new user. + /// + [Required, Username] + public string Username { get; set; } = default!; + + /// + /// Password of the new user. + /// + [Required, MinLength(1)] + public string Password { get; set; } = default!; + + /// + /// Whether the new user is administrator. + /// + [Required] + public bool? Administrator { get; set; } + + /// + /// Nickname of the new user. + /// + [Nickname] + public string? Nickname { get; set; } + } + + /// + /// Request model for . + /// + public class ChangePasswordRequest + { + /// + /// Old password. + /// + [Required(AllowEmptyStrings = false)] + public string OldPassword { get; set; } = default!; + + /// + /// New password. + /// + [Required(AllowEmptyStrings = false)] + public string NewPassword { get; set; } = default!; + } + + public class UserControllerAutoMapperProfile : Profile + { + public UserControllerAutoMapperProfile() + { + CreateMap(MemberList.Source); + CreateMap(MemberList.Source); + } + } +} diff --git a/BackEnd/Timeline/Models/Http/UserInfo.cs b/BackEnd/Timeline/Models/Http/UserInfo.cs new file mode 100644 index 00000000..d92a12c4 --- /dev/null +++ b/BackEnd/Timeline/Models/Http/UserInfo.cs @@ -0,0 +1,90 @@ +using AutoMapper; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Timeline.Controllers; + +namespace Timeline.Models.Http +{ + /// + /// Info of a user. + /// + public class UserInfo + { + /// + /// Unique id. + /// + public string UniqueId { get; set; } = default!; + /// + /// Username. + /// + public string Username { get; set; } = default!; + /// + /// Nickname. + /// + public string Nickname { get; set; } = default!; + /// + /// True if the user is a administrator. + /// + public bool? Administrator { get; set; } = default!; +#pragma warning disable CA1707 // Identifiers should not contain underscores + /// + /// Related links. + /// + public UserInfoLinks _links { get; set; } = default!; +#pragma warning restore CA1707 // Identifiers should not contain underscores + } + + /// + /// Related links for user. + /// + public class UserInfoLinks + { + /// + /// Self. + /// + public string Self { get; set; } = default!; + /// + /// Avatar url. + /// + public string Avatar { get; set; } = default!; + /// + /// Personal timeline url. + /// + public string Timeline { get; set; } = default!; + } + + public class UserInfoLinksValueResolver : IValueResolver + { + private readonly IActionContextAccessor _actionContextAccessor; + private readonly IUrlHelperFactory _urlHelperFactory; + + public UserInfoLinksValueResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) + { + _actionContextAccessor = actionContextAccessor; + _urlHelperFactory = urlHelperFactory; + } + + public UserInfoLinks Resolve(User source, UserInfo destination, UserInfoLinks destMember, ResolutionContext context) + { + var actionContext = _actionContextAccessor.AssertActionContextForUrlFill(); + var urlHelper = _urlHelperFactory.GetUrlHelper(actionContext); + + var result = new UserInfoLinks + { + Self = urlHelper.ActionLink(nameof(UserController.Get), nameof(UserController)[0..^nameof(Controller).Length], new { destination.Username }), + Avatar = urlHelper.ActionLink(nameof(UserAvatarController.Get), nameof(UserAvatarController)[0..^nameof(Controller).Length], new { destination.Username }), + Timeline = urlHelper.ActionLink(nameof(TimelineController.TimelineGet), nameof(TimelineController)[0..^nameof(Controller).Length], new { Name = "@" + destination.Username }) + }; + return result; + } + } + + public class UserInfoAutoMapperProfile : Profile + { + public UserInfoAutoMapperProfile() + { + CreateMap().ForMember(u => u._links, opt => opt.MapFrom()); + } + } +} diff --git a/BackEnd/Timeline/Models/Timeline.cs b/BackEnd/Timeline/Models/Timeline.cs new file mode 100644 index 00000000..a5987577 --- /dev/null +++ b/BackEnd/Timeline/Models/Timeline.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; + +namespace Timeline.Models +{ + public enum TimelineVisibility + { + /// + /// All people including those without accounts. + /// + Public, + /// + /// Only people signed in. + /// + Register, + /// + /// Only member. + /// + Private + } + + public static class TimelinePostContentTypes + { + public const string Text = "text"; + public const string Image = "image"; + } + + public interface ITimelinePostContent + { + public string Type { get; } + } + + public class TextTimelinePostContent : ITimelinePostContent + { + public TextTimelinePostContent(string text) { Text = text; } + + public string Type { get; } = TimelinePostContentTypes.Text; + public string Text { get; set; } + } + + public class ImageTimelinePostContent : ITimelinePostContent + { + public ImageTimelinePostContent(string dataTag) { DataTag = dataTag; } + + public string Type { get; } = TimelinePostContentTypes.Image; + + /// + /// The tag of the data. The tag of the entry in DataManager. Also the etag (not quoted). + /// + public string DataTag { get; set; } + } + + public class TimelinePost + { + public TimelinePost(long id, ITimelinePostContent? content, DateTime time, User? author, DateTime lastUpdated, string timelineName) + { + Id = id; + Content = content; + Time = time; + Author = author; + LastUpdated = lastUpdated; + TimelineName = timelineName; + } + + public long Id { get; set; } + public ITimelinePostContent? Content { get; set; } + public bool Deleted => Content == null; + public DateTime Time { get; set; } + public User? Author { get; set; } + public DateTime LastUpdated { get; set; } + public string TimelineName { get; set; } + } + +#pragma warning disable CA1724 // Type names should not match namespaces + public class Timeline +#pragma warning restore CA1724 // Type names should not match namespaces + { + public string UniqueID { get; set; } = default!; + public string Name { get; set; } = default!; + public DateTime NameLastModified { get; set; } = default!; + public string Title { get; set; } = default!; + public string Description { get; set; } = default!; + public User Owner { get; set; } = default!; + public TimelineVisibility Visibility { get; set; } +#pragma warning disable CA2227 // Collection properties should be read only + public List Members { get; set; } = default!; +#pragma warning restore CA2227 // Collection properties should be read only + public DateTime CreateTime { get; set; } = default!; + public DateTime LastModified { get; set; } = default!; + } + + public class TimelineChangePropertyRequest + { + public string? Title { get; set; } + public string? Description { get; set; } + public TimelineVisibility? Visibility { get; set; } + } +} diff --git a/BackEnd/Timeline/Models/User.cs b/BackEnd/Timeline/Models/User.cs new file mode 100644 index 00000000..f08a62db --- /dev/null +++ b/BackEnd/Timeline/Models/User.cs @@ -0,0 +1,21 @@ +using System; + +namespace Timeline.Models +{ + public class User + { + public string? UniqueId { get; set; } + public string? Username { get; set; } + public string? Nickname { get; set; } + public bool? Administrator { get; set; } + + #region secret + public long? Id { get; set; } + public string? Password { get; set; } + public long? Version { get; set; } + public DateTime? UsernameChangeTime { get; set; } + public DateTime? CreateTime { get; set; } + public DateTime? LastModified { get; set; } + #endregion secret + } +} diff --git a/BackEnd/Timeline/Models/Validation/GeneralTimelineNameValidator.cs b/BackEnd/Timeline/Models/Validation/GeneralTimelineNameValidator.cs new file mode 100644 index 00000000..e1c96fbd --- /dev/null +++ b/BackEnd/Timeline/Models/Validation/GeneralTimelineNameValidator.cs @@ -0,0 +1,33 @@ +using System; + +namespace Timeline.Models.Validation +{ + public class GeneralTimelineNameValidator : Validator + { + private readonly UsernameValidator _usernameValidator = new UsernameValidator(); + private readonly TimelineNameValidator _timelineNameValidator = new TimelineNameValidator(); + + protected override (bool, string) DoValidate(string value) + { + if (value.StartsWith('@')) + { + return _usernameValidator.Validate(value.Substring(1)); + } + else + { + return _timelineNameValidator.Validate(value); + } + } + } + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, + AllowMultiple = false)] + public class GeneralTimelineNameAttribute : ValidateWithAttribute + { + public GeneralTimelineNameAttribute() + : base(typeof(GeneralTimelineNameValidator)) + { + + } + } +} diff --git a/BackEnd/Timeline/Models/Validation/NameValidator.cs b/BackEnd/Timeline/Models/Validation/NameValidator.cs new file mode 100644 index 00000000..b74c40b7 --- /dev/null +++ b/BackEnd/Timeline/Models/Validation/NameValidator.cs @@ -0,0 +1,42 @@ +using System.Linq; +using System.Text.RegularExpressions; +using static Timeline.Resources.Models.Validation.NameValidator; + +namespace Timeline.Models.Validation +{ + public class NameValidator : Validator + { + private static Regex UniqueIdRegex { get; } = new Regex(@"^[a-zA-Z0-9]{32}$"); + + public const int MaxLength = 26; + + protected override (bool, string) DoValidate(string value) + { + if (value.Length == 0) + { + return (false, MessageEmptyString); + } + + if (value.Length > MaxLength) + { + return (false, MessageTooLong); + } + + foreach ((char c, int i) in value.Select((c, i) => (c, i))) + { + if (!(char.IsLetterOrDigit(c) || c == '-' || c == '_')) + { + return (false, MessageInvalidChar); + } + } + + // Currently name can't be longer than 26. So this is not needed. But reserve it for future use. + if (UniqueIdRegex.IsMatch(value)) + { + return (false, MessageUnqiueId); + } + + return (true, GetSuccessMessage()); + } + } +} diff --git a/BackEnd/Timeline/Models/Validation/NicknameValidator.cs b/BackEnd/Timeline/Models/Validation/NicknameValidator.cs new file mode 100644 index 00000000..1d6ab163 --- /dev/null +++ b/BackEnd/Timeline/Models/Validation/NicknameValidator.cs @@ -0,0 +1,25 @@ +using System; +using static Timeline.Resources.Models.Validation.NicknameValidator; + +namespace Timeline.Models.Validation +{ + public class NicknameValidator : Validator + { + protected override (bool, string) DoValidate(string value) + { + if (value.Length > 25) + return (false, MessageTooLong); + + return (true, GetSuccessMessage()); + } + } + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] + public class NicknameAttribute : ValidateWithAttribute + { + public NicknameAttribute() : base(typeof(NicknameValidator)) + { + + } + } +} diff --git a/BackEnd/Timeline/Models/Validation/TimelineNameValidator.cs b/BackEnd/Timeline/Models/Validation/TimelineNameValidator.cs new file mode 100644 index 00000000..f1ab54e8 --- /dev/null +++ b/BackEnd/Timeline/Models/Validation/TimelineNameValidator.cs @@ -0,0 +1,19 @@ +using System; + +namespace Timeline.Models.Validation +{ + public class TimelineNameValidator : NameValidator + { + } + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, + AllowMultiple = false)] + public class TimelineNameAttribute : ValidateWithAttribute + { + public TimelineNameAttribute() + : base(typeof(TimelineNameValidator)) + { + + } + } +} diff --git a/BackEnd/Timeline/Models/Validation/UsernameValidator.cs b/BackEnd/Timeline/Models/Validation/UsernameValidator.cs new file mode 100644 index 00000000..87bbf85f --- /dev/null +++ b/BackEnd/Timeline/Models/Validation/UsernameValidator.cs @@ -0,0 +1,19 @@ +using System; + +namespace Timeline.Models.Validation +{ + public class UsernameValidator : NameValidator + { + } + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, + AllowMultiple = false)] + public class UsernameAttribute : ValidateWithAttribute + { + public UsernameAttribute() + : base(typeof(UsernameValidator)) + { + + } + } +} diff --git a/BackEnd/Timeline/Models/Validation/Validator.cs b/BackEnd/Timeline/Models/Validation/Validator.cs new file mode 100644 index 00000000..aef7891c --- /dev/null +++ b/BackEnd/Timeline/Models/Validation/Validator.cs @@ -0,0 +1,127 @@ +using System; +using System.ComponentModel.DataAnnotations; +using static Timeline.Resources.Models.Validation.Validator; + +namespace Timeline.Models.Validation +{ + /// + /// A validator to validate value. + /// + public interface IValidator + { + /// + /// Validate given value. + /// + /// The value to validate. + /// Validation success or not and message. + (bool, string) Validate(object? value); + } + + public static class ValidatorExtensions + { + public static bool Validate(this IValidator validator, object? value, out string message) + { + if (validator == null) + throw new ArgumentNullException(nameof(validator)); + + var (r, m) = validator.Validate(value); + message = m; + return r; + } + } + + /// + /// Convenient base class for validator. + /// + /// The type of accepted value. + /// + /// Subclass should override to do the real validation. + /// This class will check the nullity and type of value. + /// If value is null, it will pass or fail depending on . + /// If value is not null and not of type + /// it will fail and not call . + /// + /// is true by default. + /// + /// If you want some other behaviours, write the validator from scratch. + /// + public abstract class Validator : IValidator + { + protected bool PermitNull { get; set; } = true; + + public (bool, string) Validate(object? value) + { + if (value == null) + { + if (PermitNull) + return (true, GetSuccessMessage()); + else + return (false, ValidatorMessageNull); + } + + if (value is T v) + { + return DoValidate(v); + } + else + { + return (false, ValidatorMessageBadType); + } + } + + protected static string GetSuccessMessage() => ValidatorMessageSuccess; + + protected abstract (bool, string) DoValidate(T value); + } + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, + AllowMultiple = false)] + public class ValidateWithAttribute : ValidationAttribute + { + private readonly IValidator _validator; + + /// + /// Create with a given validator. + /// + /// The validator used to validate. + public ValidateWithAttribute(IValidator validator) + { + _validator = validator ?? throw new ArgumentNullException(nameof(validator)); + } + + /// + /// Create the validator with default constructor. + /// + /// The type of the validator. + public ValidateWithAttribute(Type validatorType) + { + if (validatorType == null) + throw new ArgumentNullException(nameof(validatorType)); + + if (!typeof(IValidator).IsAssignableFrom(validatorType)) + throw new ArgumentException(ValidateWithAttributeExceptionNotValidator, nameof(validatorType)); + + try + { + _validator = (Activator.CreateInstance(validatorType) as IValidator)!; + } + catch (Exception e) + { + throw new ArgumentException(ValidateWithAttributeExceptionCreateFail, e); + } + } + + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + var (result, message) = _validator.Validate(value); + if (result) + { + return ValidationResult.Success; + } + else + { + return new ValidationResult(message); + } + } + } +} diff --git a/BackEnd/Timeline/Program.cs b/BackEnd/Timeline/Program.cs new file mode 100644 index 00000000..87e330a2 --- /dev/null +++ b/BackEnd/Timeline/Program.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System.Resources; +using Timeline.Entities; +using Timeline.Services; + +[assembly: NeutralResourcesLanguage("en")] + +namespace Timeline +{ + public static class Program + { + public static void Main(string[] args) + { + var host = CreateWebHostBuilder(args).Build(); + + var env = host.Services.GetRequiredService(); + + var databaseBackupService = host.Services.GetRequiredService(); + databaseBackupService.BackupNow(); + + if (env.IsProduction()) + { + using (var scope = host.Services.CreateScope()) + { + var databaseContext = scope.ServiceProvider.GetRequiredService(); + databaseContext.Database.Migrate(); + } + } + + host.Run(); + } + + public static IHostBuilder CreateWebHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/BackEnd/Timeline/Properties/launchSettings.json b/BackEnd/Timeline/Properties/launchSettings.json new file mode 100644 index 00000000..de8186db --- /dev/null +++ b/BackEnd/Timeline/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "profiles": { + "Development": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_USEPROXYFRONTEND": "true", + "ASPNETCORE_WORKDIR": "D:\\timeline-development" + } + }, + "Development-Mock": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_USEMOCKFRONTEND": "true", + "ASPNETCORE_WORKDIR": "D:\\timeline-development" + } + }, + "Staging": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Staging", + "ASPNETCORE_WORKDIR": "D:\\timeline-development" + } + } + } +} diff --git a/BackEnd/Timeline/Resources/Authentication/AuthHandler.Designer.cs b/BackEnd/Timeline/Resources/Authentication/AuthHandler.Designer.cs new file mode 100644 index 00000000..fd4540ea --- /dev/null +++ b/BackEnd/Timeline/Resources/Authentication/AuthHandler.Designer.cs @@ -0,0 +1,99 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Authentication { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class AuthHandler { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal AuthHandler() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Authentication.AuthHandler", typeof(AuthHandler).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Token is found in authorization header. Token is {0} .. + /// + internal static string LogTokenFoundInHeader { + get { + return ResourceManager.GetString("LogTokenFoundInHeader", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Token is found in query param with key "{0}". Token is {1} .. + /// + internal static string LogTokenFoundInQuery { + get { + return ResourceManager.GetString("LogTokenFoundInQuery", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No jwt token is found.. + /// + internal static string LogTokenNotFound { + get { + return ResourceManager.GetString("LogTokenNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A jwt token validation failed.. + /// + internal static string LogTokenValidationFail { + get { + return ResourceManager.GetString("LogTokenValidationFail", resourceCulture); + } + } + } +} diff --git a/BackEnd/Timeline/Resources/Authentication/AuthHandler.resx b/BackEnd/Timeline/Resources/Authentication/AuthHandler.resx new file mode 100644 index 00000000..4cddc8ce --- /dev/null +++ b/BackEnd/Timeline/Resources/Authentication/AuthHandler.resx @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Token is found in authorization header. Token is {0} . + + + Token is found in query param with key "{0}". Token is {1} . + + + No jwt token is found. + + + A jwt token validation failed. + + \ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Controllers/ControllerAuthExtensions.Designer.cs b/BackEnd/Timeline/Resources/Controllers/ControllerAuthExtensions.Designer.cs new file mode 100644 index 00000000..70a1d605 --- /dev/null +++ b/BackEnd/Timeline/Resources/Controllers/ControllerAuthExtensions.Designer.cs @@ -0,0 +1,81 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Controllers { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class ControllerAuthExtensions { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal ControllerAuthExtensions() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Controllers.ControllerAuthExtensions", typeof(ControllerAuthExtensions).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Failed to get user id because User has no NameIdentifier claim.. + /// + internal static string ExceptionNoUserIdentifierClaim { + get { + return ResourceManager.GetString("ExceptionNoUserIdentifierClaim", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to get user id because NameIdentifier claim is not a number.. + /// + internal static string ExceptionUserIdentifierClaimBadFormat { + get { + return ResourceManager.GetString("ExceptionUserIdentifierClaimBadFormat", resourceCulture); + } + } + } +} diff --git a/BackEnd/Timeline/Resources/Controllers/ControllerAuthExtensions.resx b/BackEnd/Timeline/Resources/Controllers/ControllerAuthExtensions.resx new file mode 100644 index 00000000..03e6d95a --- /dev/null +++ b/BackEnd/Timeline/Resources/Controllers/ControllerAuthExtensions.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Failed to get user id because User has no NameIdentifier claim. + + + Failed to get user id because NameIdentifier claim is not a number. + + \ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Controllers/TimelineController.Designer.cs b/BackEnd/Timeline/Resources/Controllers/TimelineController.Designer.cs new file mode 100644 index 00000000..ae6414e6 --- /dev/null +++ b/BackEnd/Timeline/Resources/Controllers/TimelineController.Designer.cs @@ -0,0 +1,81 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Controllers { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class TimelineController { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal TimelineController() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Controllers.TimelineController", typeof(TimelineController).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to An unknown timeline visibility value. Can't convert it.. + /// + internal static string ExceptionStringToVisibility { + get { + return ResourceManager.GetString("ExceptionStringToVisibility", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An unknown TimelineMemberOperationUserException is thrown. Can't recognize its inner exception. It is rethrown.. + /// + internal static string LogUnknownTimelineMemberOperationUserException { + get { + return ResourceManager.GetString("LogUnknownTimelineMemberOperationUserException", resourceCulture); + } + } + } +} diff --git a/BackEnd/Timeline/Resources/Controllers/TimelineController.resx b/BackEnd/Timeline/Resources/Controllers/TimelineController.resx new file mode 100644 index 00000000..4cf3d6fb --- /dev/null +++ b/BackEnd/Timeline/Resources/Controllers/TimelineController.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + An unknown timeline visibility value. Can't convert it. + + + An unknown TimelineMemberOperationUserException is thrown. Can't recognize its inner exception. It is rethrown. + + \ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Controllers/TokenController.Designer.cs b/BackEnd/Timeline/Resources/Controllers/TokenController.Designer.cs new file mode 100644 index 00000000..a7c2864b --- /dev/null +++ b/BackEnd/Timeline/Resources/Controllers/TokenController.Designer.cs @@ -0,0 +1,153 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Controllers { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class TokenController { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal TokenController() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Controllers.TokenController", typeof(TokenController).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The password is wrong.. + /// + internal static string LogBadPassword { + get { + return ResourceManager.GetString("LogBadPassword", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A user failed to create a token.. + /// + internal static string LogCreateFailure { + get { + return ResourceManager.GetString("LogCreateFailure", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A user succeeded to create a token.. + /// + internal static string LogCreateSuccess { + get { + return ResourceManager.GetString("LogCreateSuccess", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The user does not exist.. + /// + internal static string LogUserNotExist { + get { + return ResourceManager.GetString("LogUserNotExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The token is of bad format. It might not be created by the server.. + /// + internal static string LogVerifyBadFormat { + get { + return ResourceManager.GetString("LogVerifyBadFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The token is expired.. + /// + internal static string LogVerifyExpire { + get { + return ResourceManager.GetString("LogVerifyExpire", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A token failed to be verified.. + /// + internal static string LogVerifyFailure { + get { + return ResourceManager.GetString("LogVerifyFailure", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Token has an old version. User might have update some info.. + /// + internal static string LogVerifyOldVersion { + get { + return ResourceManager.GetString("LogVerifyOldVersion", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A token succeeded to be verified.. + /// + internal static string LogVerifySuccess { + get { + return ResourceManager.GetString("LogVerifySuccess", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to User does not exist. Administrator might have deleted this user.. + /// + internal static string LogVerifyUserNotExist { + get { + return ResourceManager.GetString("LogVerifyUserNotExist", resourceCulture); + } + } + } +} diff --git a/BackEnd/Timeline/Resources/Controllers/TokenController.resx b/BackEnd/Timeline/Resources/Controllers/TokenController.resx new file mode 100644 index 00000000..683d6cc9 --- /dev/null +++ b/BackEnd/Timeline/Resources/Controllers/TokenController.resx @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The password is wrong. + + + A user failed to create a token. + + + A user succeeded to create a token. + + + The user does not exist. + + + The token is of bad format. It might not be created by the server. + + + The token is expired. + + + A token failed to be verified. + + + Token has an old version. User might have update some info. + + + A token succeeded to be verified. + + + User does not exist. Administrator might have deleted this user. + + \ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Controllers/UserAvatarController.Designer.cs b/BackEnd/Timeline/Resources/Controllers/UserAvatarController.Designer.cs new file mode 100644 index 00000000..b0c35ff9 --- /dev/null +++ b/BackEnd/Timeline/Resources/Controllers/UserAvatarController.Designer.cs @@ -0,0 +1,144 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Controllers { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class UserAvatarController { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal UserAvatarController() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Controllers.UserAvatarController", typeof(UserAvatarController).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Unknown AvatarDataException.ErrorReason value.. + /// + internal static string ExceptionUnknownAvatarFormatError { + get { + return ResourceManager.GetString("ExceptionUnknownAvatarFormatError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attempt to delete a avatar of other user as a non-admin failed.. + /// + internal static string LogDeleteForbid { + get { + return ResourceManager.GetString("LogDeleteForbid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attempt to delete a avatar of a non-existent user failed.. + /// + internal static string LogDeleteNotExist { + get { + return ResourceManager.GetString("LogDeleteNotExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Succeed to delete a avatar of a user.. + /// + internal static string LogDeleteSuccess { + get { + return ResourceManager.GetString("LogDeleteSuccess", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attempt to get a avatar of a non-existent user failed.. + /// + internal static string LogGetUserNotExist { + get { + return ResourceManager.GetString("LogGetUserNotExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attempt to put a avatar of other user as a non-admin failed.. + /// + internal static string LogPutForbid { + get { + return ResourceManager.GetString("LogPutForbid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Succeed to put a avatar of a user.. + /// + internal static string LogPutSuccess { + get { + return ResourceManager.GetString("LogPutSuccess", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attempt to put a avatar of a bad format failed.. + /// + internal static string LogPutUserBadFormat { + get { + return ResourceManager.GetString("LogPutUserBadFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attempt to put a avatar of a non-existent user failed.. + /// + internal static string LogPutUserNotExist { + get { + return ResourceManager.GetString("LogPutUserNotExist", resourceCulture); + } + } + } +} diff --git a/BackEnd/Timeline/Resources/Controllers/UserAvatarController.resx b/BackEnd/Timeline/Resources/Controllers/UserAvatarController.resx new file mode 100644 index 00000000..864d96c0 --- /dev/null +++ b/BackEnd/Timeline/Resources/Controllers/UserAvatarController.resx @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Unknown AvatarDataException.ErrorReason value. + + + Attempt to delete a avatar of other user as a non-admin failed. + + + Attempt to delete a avatar of a non-existent user failed. + + + Succeed to delete a avatar of a user. + + + Attempt to get a avatar of a non-existent user failed. + + + Attempt to put a avatar of other user as a non-admin failed. + + + Succeed to put a avatar of a user. + + + Attempt to put a avatar of a bad format failed. + + + Attempt to put a avatar of a non-existent user failed. + + \ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Controllers/UserController.Designer.cs b/BackEnd/Timeline/Resources/Controllers/UserController.Designer.cs new file mode 100644 index 00000000..c8067614 --- /dev/null +++ b/BackEnd/Timeline/Resources/Controllers/UserController.Designer.cs @@ -0,0 +1,117 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Controllers { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class UserController { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal UserController() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Controllers.UserController", typeof(UserController).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Unknown PutResult.. + /// + internal static string ExceptionUnknownPutResult { + get { + return ResourceManager.GetString("ExceptionUnknownPutResult", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attempt to change password with wrong old password failed.. + /// + internal static string LogChangePasswordBadPassword { + get { + return ResourceManager.GetString("LogChangePasswordBadPassword", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attempt to change a user's username to a existent one failed.. + /// + internal static string LogChangeUsernameConflict { + get { + return ResourceManager.GetString("LogChangeUsernameConflict", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attempt to change a username of a user that does not exist failed.. + /// + internal static string LogChangeUsernameNotExist { + get { + return ResourceManager.GetString("LogChangeUsernameNotExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attempt to retrieve info of a user that does not exist failed.. + /// + internal static string LogGetUserNotExist { + get { + return ResourceManager.GetString("LogGetUserNotExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attempt to patch a user that does not exist failed.. + /// + internal static string LogPatchUserNotExist { + get { + return ResourceManager.GetString("LogPatchUserNotExist", resourceCulture); + } + } + } +} diff --git a/BackEnd/Timeline/Resources/Controllers/UserController.resx b/BackEnd/Timeline/Resources/Controllers/UserController.resx new file mode 100644 index 00000000..0bdf4845 --- /dev/null +++ b/BackEnd/Timeline/Resources/Controllers/UserController.resx @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Unknown PutResult. + + + Attempt to change password with wrong old password failed. + + + Attempt to change a user's username to a existent one failed. + + + Attempt to change a username of a user that does not exist failed. + + + Attempt to retrieve info of a user that does not exist failed. + + + Attempt to patch a user that does not exist failed. + + \ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Entities.Designer.cs b/BackEnd/Timeline/Resources/Entities.Designer.cs new file mode 100644 index 00000000..5f286f23 --- /dev/null +++ b/BackEnd/Timeline/Resources/Entities.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Entities { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Entities() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Entities", typeof(Entities).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Only sqlite is supported.. + /// + internal static string ExceptionOnlySqliteSupported { + get { + return ResourceManager.GetString("ExceptionOnlySqliteSupported", resourceCulture); + } + } + } +} diff --git a/BackEnd/Timeline/Resources/Entities.resx b/BackEnd/Timeline/Resources/Entities.resx new file mode 100644 index 00000000..1538b533 --- /dev/null +++ b/BackEnd/Timeline/Resources/Entities.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Only sqlite is supported. + + \ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Filters.Designer.cs b/BackEnd/Timeline/Resources/Filters.Designer.cs new file mode 100644 index 00000000..dedfe498 --- /dev/null +++ b/BackEnd/Timeline/Resources/Filters.Designer.cs @@ -0,0 +1,90 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Filters { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Filters() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Filters", typeof(Filters).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to You apply a SelfOrAdminAttribute on an action, but there is no user. Try add AuthorizeAttribute.. + /// + internal static string LogSelfOrAdminNoUser { + get { + return ResourceManager.GetString("LogSelfOrAdminNoUser", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You apply a SelfOrAdminAttribute on an action, but it does not have a model named username.. + /// + internal static string LogSelfOrAdminNoUsername { + get { + return ResourceManager.GetString("LogSelfOrAdminNoUsername", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You apply a SelfOrAdminAttribute on an action, found a model named username, but it is not string.. + /// + internal static string LogSelfOrAdminUsernameNotString { + get { + return ResourceManager.GetString("LogSelfOrAdminUsernameNotString", resourceCulture); + } + } + } +} diff --git a/BackEnd/Timeline/Resources/Filters.resx b/BackEnd/Timeline/Resources/Filters.resx new file mode 100644 index 00000000..22620889 --- /dev/null +++ b/BackEnd/Timeline/Resources/Filters.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + You apply a SelfOrAdminAttribute on an action, but there is no user. Try add AuthorizeAttribute. + + + You apply a SelfOrAdminAttribute on an action, but it does not have a model named username. + + + You apply a SelfOrAdminAttribute on an action, found a model named username, but it is not string. + + \ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Helper/DataCacheHelper.Designer.cs b/BackEnd/Timeline/Resources/Helper/DataCacheHelper.Designer.cs new file mode 100644 index 00000000..acf56d13 --- /dev/null +++ b/BackEnd/Timeline/Resources/Helper/DataCacheHelper.Designer.cs @@ -0,0 +1,90 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Helper { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class DataCacheHelper { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal DataCacheHelper() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Helper.DataCacheHelper", typeof(DataCacheHelper).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Header If-None-Match is of bad format.. + /// + internal static string LogBadIfNoneMatch { + get { + return ResourceManager.GetString("LogBadIfNoneMatch", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cache is invalid and data is returned.. + /// + internal static string LogResultData { + get { + return ResourceManager.GetString("LogResultData", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cache is valid and 304 Not Modified is returned.. + /// + internal static string LogResultNotModified { + get { + return ResourceManager.GetString("LogResultNotModified", resourceCulture); + } + } + } +} diff --git a/BackEnd/Timeline/Resources/Helper/DataCacheHelper.resx b/BackEnd/Timeline/Resources/Helper/DataCacheHelper.resx new file mode 100644 index 00000000..515cfa9b --- /dev/null +++ b/BackEnd/Timeline/Resources/Helper/DataCacheHelper.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Header If-None-Match is of bad format. + + + Cache is invalid and data is returned. + + + Cache is valid and 304 Not Modified is returned. + + \ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Messages.Designer.cs b/BackEnd/Timeline/Resources/Messages.Designer.cs new file mode 100644 index 00000000..bb654ce6 --- /dev/null +++ b/BackEnd/Timeline/Resources/Messages.Designer.cs @@ -0,0 +1,396 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Messages { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Messages() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Messages", typeof(Messages).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Body is too big. It can't be bigger than {0}.. + /// + internal static string Common_Content_TooBig { + get { + return ResourceManager.GetString("Common_Content_TooBig", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Actual body length is bigger than it in header.. + /// + internal static string Common_Content_UnmatchedLength_Bigger { + get { + return ResourceManager.GetString("Common_Content_UnmatchedLength_Bigger", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Actual body length is smaller than it in header.. + /// + internal static string Common_Content_UnmatchedLength_Smaller { + get { + return ResourceManager.GetString("Common_Content_UnmatchedLength_Smaller", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You have no permission to do the operation.. + /// + internal static string Common_Forbid { + get { + return ResourceManager.GetString("Common_Forbid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You are not the resource owner.. + /// + internal static string Common_Forbid_NotSelf { + get { + return ResourceManager.GetString("Common_Forbid_NotSelf", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Header Content-Length is missing or of bad format.. + /// + internal static string Common_Header_ContentLength_Missing { + get { + return ResourceManager.GetString("Common_Header_ContentLength_Missing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Header Content-Length must not be 0.. + /// + internal static string Common_Header_ContentLength_Zero { + get { + return ResourceManager.GetString("Common_Header_ContentLength_Zero", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Header Content-Type is missing.. + /// + internal static string Common_Header_ContentType_Missing { + get { + return ResourceManager.GetString("Common_Header_ContentType_Missing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Header If-Non-Match is of bad format.. + /// + internal static string Common_Header_IfNonMatch_BadFormat { + get { + return ResourceManager.GetString("Common_Header_IfNonMatch_BadFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Model is of bad format.. + /// + internal static string Common_InvalidModel { + get { + return ResourceManager.GetString("Common_InvalidModel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The api endpoint you request is unknown. You might get the wrong api entry.. + /// + internal static string Common_UnknownEndpoint { + get { + return ResourceManager.GetString("Common_UnknownEndpoint", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unknown type of post content.. + /// + internal static string TimelineController_ContentUnknownType { + get { + return ResourceManager.GetString("TimelineController_ContentUnknownType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Data field is not a valid base64 string in image content.. + /// + internal static string TimelineController_ImageContentDataNotBase64 { + get { + return ResourceManager.GetString("TimelineController_ImageContentDataNotBase64", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Data field is not a valid image after base64 decoding in image content.. + /// + internal static string TimelineController_ImageContentDataNotImage { + get { + return ResourceManager.GetString("TimelineController_ImageContentDataNotImage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Data field is required for image content.. + /// + internal static string TimelineController_ImageContentDataRequired { + get { + return ResourceManager.GetString("TimelineController_ImageContentDataRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The user to set as member does not exist.. + /// + internal static string TimelineController_MemberPut_NotExist { + get { + return ResourceManager.GetString("TimelineController_MemberPut_NotExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A timeline with given name already exists.. + /// + internal static string TimelineController_NameConflict { + get { + return ResourceManager.GetString("TimelineController_NameConflict", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The timeline with given name does not exist.. + /// + internal static string TimelineController_NotExist { + get { + return ResourceManager.GetString("TimelineController_NotExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The post of that type has no data.. + /// + internal static string TimelineController_PostNoData { + get { + return ResourceManager.GetString("TimelineController_PostNoData", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The post to operate on does not exist.. + /// + internal static string TimelineController_PostNotExist { + get { + return ResourceManager.GetString("TimelineController_PostNotExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The user specified by query param "relate" does not exist.. + /// + internal static string TimelineController_QueryRelateNotExist { + get { + return ResourceManager.GetString("TimelineController_QueryRelateNotExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to '{0}' is an unkown visibility in the query parameter 'visibility'. . + /// + internal static string TimelineController_QueryVisibilityUnknown { + get { + return ResourceManager.GetString("TimelineController_QueryVisibilityUnknown", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Text field is required for text content.. + /// + internal static string TimelineController_TextContentTextRequired { + get { + return ResourceManager.GetString("TimelineController_TextContentTextRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Username or password is invalid.. + /// + internal static string TokenController_Create_BadCredential { + get { + return ResourceManager.GetString("TokenController_Create_BadCredential", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The token is of bad format. It might not be created by the server.. + /// + internal static string TokenController_Verify_BadFormat { + get { + return ResourceManager.GetString("TokenController_Verify_BadFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Token has an old version. User might have update some info.. + /// + internal static string TokenController_Verify_OldVersion { + get { + return ResourceManager.GetString("TokenController_Verify_OldVersion", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The token is expired.. + /// + internal static string TokenController_Verify_TimeExpired { + get { + return ResourceManager.GetString("TokenController_Verify_TimeExpired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to User does not exist. Administrator might have deleted this user.. + /// + internal static string TokenController_Verify_UserNotExist { + get { + return ResourceManager.GetString("TokenController_Verify_UserNotExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Image is not a square.. + /// + internal static string UserAvatar_BadFormat_BadSize { + get { + return ResourceManager.GetString("UserAvatar_BadFormat_BadSize", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Image decode failed.. + /// + internal static string UserAvatar_BadFormat_CantDecode { + get { + return ResourceManager.GetString("UserAvatar_BadFormat_CantDecode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Image format does not match the one in header.. + /// + internal static string UserAvatar_BadFormat_UnmatchedFormat { + get { + return ResourceManager.GetString("UserAvatar_BadFormat_UnmatchedFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The user to operate on does not exist.. + /// + internal static string UserCommon_NotExist { + get { + return ResourceManager.GetString("UserCommon_NotExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Old password is wrong.. + /// + internal static string UserController_ChangePassword_BadOldPassword { + get { + return ResourceManager.GetString("UserController_ChangePassword_BadOldPassword", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You can't set permission unless you are administrator.. + /// + internal static string UserController_Patch_Forbid_Administrator { + get { + return ResourceManager.GetString("UserController_Patch_Forbid_Administrator", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You can't set password unless you are administrator. If you want to change password, use /userop/changepassword .. + /// + internal static string UserController_Patch_Forbid_Password { + get { + return ResourceManager.GetString("UserController_Patch_Forbid_Password", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You can't set username unless you are administrator.. + /// + internal static string UserController_Patch_Forbid_Username { + get { + return ResourceManager.GetString("UserController_Patch_Forbid_Username", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A user with given username already exists.. + /// + internal static string UserController_UsernameConflict { + get { + return ResourceManager.GetString("UserController_UsernameConflict", resourceCulture); + } + } + } +} diff --git a/BackEnd/Timeline/Resources/Messages.resx b/BackEnd/Timeline/Resources/Messages.resx new file mode 100644 index 00000000..2bbf494e --- /dev/null +++ b/BackEnd/Timeline/Resources/Messages.resx @@ -0,0 +1,231 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Body is too big. It can't be bigger than {0}. + + + Actual body length is bigger than it in header. + + + Actual body length is smaller than it in header. + + + You have no permission to do the operation. + + + You are not the resource owner. + + + Header Content-Length is missing or of bad format. + + + Header Content-Length must not be 0. + + + Header Content-Type is missing. + + + Header If-Non-Match is of bad format. + + + Model is of bad format. + + + The api endpoint you request is unknown. You might get the wrong api entry. + + + Unknown type of post content. + + + Data field is not a valid base64 string in image content. + + + Data field is not a valid image after base64 decoding in image content. + + + Data field is required for image content. + + + The user to set as member does not exist. + + + A timeline with given name already exists. + + + The timeline with given name does not exist. + + + The post of that type has no data. + + + The post to operate on does not exist. + + + The user specified by query param "relate" does not exist. + + + '{0}' is an unkown visibility in the query parameter 'visibility'. + + + Text field is required for text content. + + + Username or password is invalid. + + + The token is of bad format. It might not be created by the server. + + + Token has an old version. User might have update some info. + + + The token is expired. + + + User does not exist. Administrator might have deleted this user. + + + Image is not a square. + + + Image decode failed. + + + Image format does not match the one in header. + + + The user to operate on does not exist. + + + Old password is wrong. + + + You can't set permission unless you are administrator. + + + You can't set password unless you are administrator. If you want to change password, use /userop/changepassword . + + + You can't set username unless you are administrator. + + + A user with given username already exists. + + \ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Models/Http/Common.Designer.cs b/BackEnd/Timeline/Resources/Models/Http/Common.Designer.cs new file mode 100644 index 00000000..5165463e --- /dev/null +++ b/BackEnd/Timeline/Resources/Models/Http/Common.Designer.cs @@ -0,0 +1,99 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Models.Http { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Common { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Common() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Models.Http.Common", typeof(Common).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to An existent item is deleted.. + /// + internal static string MessageDeleteDelete { + get { + return ResourceManager.GetString("MessageDeleteDelete", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The item does not exist, so nothing is changed.. + /// + internal static string MessageDeleteNotExist { + get { + return ResourceManager.GetString("MessageDeleteNotExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A new item is created.. + /// + internal static string MessagePutCreate { + get { + return ResourceManager.GetString("MessagePutCreate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An existent item is modified.. + /// + internal static string MessagePutModify { + get { + return ResourceManager.GetString("MessagePutModify", resourceCulture); + } + } + } +} diff --git a/BackEnd/Timeline/Resources/Models/Http/Common.resx b/BackEnd/Timeline/Resources/Models/Http/Common.resx new file mode 100644 index 00000000..85ec4d32 --- /dev/null +++ b/BackEnd/Timeline/Resources/Models/Http/Common.resx @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + An existent item is deleted. + + + The item does not exist, so nothing is changed. + + + A new item is created. + + + An existent item is modified. + + \ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Models/Http/Exception.Designer.cs b/BackEnd/Timeline/Resources/Models/Http/Exception.Designer.cs new file mode 100644 index 00000000..19f42793 --- /dev/null +++ b/BackEnd/Timeline/Resources/Models/Http/Exception.Designer.cs @@ -0,0 +1,81 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Models.Http { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Exception { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Exception() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Models.Http.Exception", typeof(Exception).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to No action context currently, can't fill urls in value resolver.. + /// + internal static string ActionContextNull { + get { + return ResourceManager.GetString("ActionContextNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unknown post content type.. + /// + internal static string UnknownPostContentType { + get { + return ResourceManager.GetString("UnknownPostContentType", resourceCulture); + } + } + } +} diff --git a/BackEnd/Timeline/Resources/Models/Http/Exception.resx b/BackEnd/Timeline/Resources/Models/Http/Exception.resx new file mode 100644 index 00000000..3f7bddb6 --- /dev/null +++ b/BackEnd/Timeline/Resources/Models/Http/Exception.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + No action context currently, can't fill urls in value resolver. + + + Unknown post content type. + + \ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Models/Validation/NameValidator.Designer.cs b/BackEnd/Timeline/Resources/Models/Validation/NameValidator.Designer.cs new file mode 100644 index 00000000..3050049e --- /dev/null +++ b/BackEnd/Timeline/Resources/Models/Validation/NameValidator.Designer.cs @@ -0,0 +1,99 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Models.Validation { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class NameValidator { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal NameValidator() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Models.Validation.NameValidator", typeof(NameValidator).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to An empty string is not allowed.. + /// + internal static string MessageEmptyString { + get { + return ResourceManager.GetString("MessageEmptyString", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid character, only alphabet, digit, underscore and hyphen are allowed.. + /// + internal static string MessageInvalidChar { + get { + return ResourceManager.GetString("MessageInvalidChar", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Too long, more than 26 characters is not premitted.. + /// + internal static string MessageTooLong { + get { + return ResourceManager.GetString("MessageTooLong", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Name can't be of the same format of unique id.. + /// + internal static string MessageUnqiueId { + get { + return ResourceManager.GetString("MessageUnqiueId", resourceCulture); + } + } + } +} diff --git a/BackEnd/Timeline/Resources/Models/Validation/NameValidator.resx b/BackEnd/Timeline/Resources/Models/Validation/NameValidator.resx new file mode 100644 index 00000000..5e7e1745 --- /dev/null +++ b/BackEnd/Timeline/Resources/Models/Validation/NameValidator.resx @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + An empty string is not allowed. + + + Invalid character, only alphabet, digit, underscore and hyphen are allowed. + + + Too long, more than 26 characters is not premitted. + + + Name can't be of the same format of unique id. + + \ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Models/Validation/NicknameValidator.Designer.cs b/BackEnd/Timeline/Resources/Models/Validation/NicknameValidator.Designer.cs new file mode 100644 index 00000000..522f305a --- /dev/null +++ b/BackEnd/Timeline/Resources/Models/Validation/NicknameValidator.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Models.Validation { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class NicknameValidator { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal NicknameValidator() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Models.Validation.NicknameValidator", typeof(NicknameValidator).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Nickname is too long.. + /// + internal static string MessageTooLong { + get { + return ResourceManager.GetString("MessageTooLong", resourceCulture); + } + } + } +} diff --git a/BackEnd/Timeline/Resources/Models/Validation/NicknameValidator.resx b/BackEnd/Timeline/Resources/Models/Validation/NicknameValidator.resx new file mode 100644 index 00000000..b191b505 --- /dev/null +++ b/BackEnd/Timeline/Resources/Models/Validation/NicknameValidator.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Nickname is too long. + + \ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Models/Validation/Validator.Designer.cs b/BackEnd/Timeline/Resources/Models/Validation/Validator.Designer.cs new file mode 100644 index 00000000..74d4c169 --- /dev/null +++ b/BackEnd/Timeline/Resources/Models/Validation/Validator.Designer.cs @@ -0,0 +1,108 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Models.Validation { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Validator { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Validator() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Models.Validation.Validator", typeof(Validator).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Failed to create a validator instance from default constructor. See inner exception.. + /// + internal static string ValidateWithAttributeExceptionCreateFail { + get { + return ResourceManager.GetString("ValidateWithAttributeExceptionCreateFail", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Given type is not assignable to IValidator.. + /// + internal static string ValidateWithAttributeExceptionNotValidator { + get { + return ResourceManager.GetString("ValidateWithAttributeExceptionNotValidator", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Value is not of type {0}.. + /// + internal static string ValidatorMessageBadType { + get { + return ResourceManager.GetString("ValidatorMessageBadType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Value can't be null.. + /// + internal static string ValidatorMessageNull { + get { + return ResourceManager.GetString("ValidatorMessageNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Validation succeeded.. + /// + internal static string ValidatorMessageSuccess { + get { + return ResourceManager.GetString("ValidatorMessageSuccess", resourceCulture); + } + } + } +} diff --git a/BackEnd/Timeline/Resources/Models/Validation/Validator.resx b/BackEnd/Timeline/Resources/Models/Validation/Validator.resx new file mode 100644 index 00000000..8317e3eb --- /dev/null +++ b/BackEnd/Timeline/Resources/Models/Validation/Validator.resx @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Failed to create a validator instance from default constructor. See inner exception. + + + Given type is not assignable to IValidator. + + + Value is not of type {0}. + + + Value can't be null. + + + Validation succeeded. + + \ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Services/DataManager.Designer.cs b/BackEnd/Timeline/Resources/Services/DataManager.Designer.cs new file mode 100644 index 00000000..0872059a --- /dev/null +++ b/BackEnd/Timeline/Resources/Services/DataManager.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Services { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class DataManager { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal DataManager() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.DataManager", typeof(DataManager).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Entry with given tag does not exist.. + /// + internal static string ExceptionEntryNotExist { + get { + return ResourceManager.GetString("ExceptionEntryNotExist", resourceCulture); + } + } + } +} diff --git a/BackEnd/Timeline/Resources/Services/DataManager.resx b/BackEnd/Timeline/Resources/Services/DataManager.resx new file mode 100644 index 00000000..688e0e96 --- /dev/null +++ b/BackEnd/Timeline/Resources/Services/DataManager.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Entry with given tag does not exist. + + \ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Services/Exception.Designer.cs b/BackEnd/Timeline/Resources/Services/Exception.Designer.cs new file mode 100644 index 00000000..21ca7b86 --- /dev/null +++ b/BackEnd/Timeline/Resources/Services/Exception.Designer.cs @@ -0,0 +1,234 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Services { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Exception { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Exception() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.Exception", typeof(Exception).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The password is wrong.. + /// + internal static string BadPasswordException { + get { + return ResourceManager.GetString("BadPasswordException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The hashes password is of bad format. It might not be created by server.. + /// + internal static string HashedPasswordBadFromatException { + get { + return ResourceManager.GetString("HashedPasswordBadFromatException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Not of valid base64 format. See inner exception.. + /// + internal static string HashedPasswordBadFromatExceptionNotBase64 { + get { + return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotBase64", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Decoded hashed password is of length 0.. + /// + internal static string HashedPasswordBadFromatExceptionNotLength0 { + get { + return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotLength0", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to See inner exception.. + /// + internal static string HashedPasswordBadFromatExceptionNotOthers { + get { + return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotOthers", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Salt length < 128 bits.. + /// + internal static string HashedPasswordBadFromatExceptionNotSaltTooShort { + get { + return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotSaltTooShort", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Subkey length < 128 bits.. + /// + internal static string HashedPasswordBadFromatExceptionNotSubkeyTooShort { + get { + return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotSubkeyTooShort", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unknown format marker.. + /// + internal static string HashedPasswordBadFromatExceptionNotUnknownMarker { + get { + return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotUnknownMarker", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The token didn't pass verification because {0}.. + /// + internal static string JwtUserTokenBadFormatException { + get { + return ResourceManager.GetString("JwtUserTokenBadFormatException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to id claim is not a number. + /// + internal static string JwtUserTokenBadFormatExceptionIdBadFormat { + get { + return ResourceManager.GetString("JwtUserTokenBadFormatExceptionIdBadFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to id claim does not exist. + /// + internal static string JwtUserTokenBadFormatExceptionIdMissing { + get { + return ResourceManager.GetString("JwtUserTokenBadFormatExceptionIdMissing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to other error, see inner exception for information. + /// + internal static string JwtUserTokenBadFormatExceptionOthers { + get { + return ResourceManager.GetString("JwtUserTokenBadFormatExceptionOthers", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to unknown error. + /// + internal static string JwtUserTokenBadFormatExceptionUnknown { + get { + return ResourceManager.GetString("JwtUserTokenBadFormatExceptionUnknown", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to version claim is not a number.. + /// + internal static string JwtUserTokenBadFormatExceptionVersionBadFormat { + get { + return ResourceManager.GetString("JwtUserTokenBadFormatExceptionVersionBadFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to version claim does not exist.. + /// + internal static string JwtUserTokenBadFormatExceptionVersionMissing { + get { + return ResourceManager.GetString("JwtUserTokenBadFormatExceptionVersionMissing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Password is of bad format.. + /// + internal static string PasswordBadFormatException { + get { + return ResourceManager.GetString("PasswordBadFormatException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The token is of bad format, which means it may not be created by the server.. + /// + internal static string UserTokenBadFormatException { + get { + return ResourceManager.GetString("UserTokenBadFormatException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The token is of bad version.. + /// + internal static string UserTokenBadVersionException { + get { + return ResourceManager.GetString("UserTokenBadVersionException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The token is expired because its expiration time has passed.. + /// + internal static string UserTokenTimeExpireException { + get { + return ResourceManager.GetString("UserTokenTimeExpireException", resourceCulture); + } + } + } +} diff --git a/BackEnd/Timeline/Resources/Services/Exception.resx b/BackEnd/Timeline/Resources/Services/Exception.resx new file mode 100644 index 00000000..c31ed7c7 --- /dev/null +++ b/BackEnd/Timeline/Resources/Services/Exception.resx @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The password is wrong. + + + The hashes password is of bad format. It might not be created by server. + + + Not of valid base64 format. See inner exception. + + + Decoded hashed password is of length 0. + + + See inner exception. + + + Salt length < 128 bits. + + + Subkey length < 128 bits. + + + Unknown format marker. + + + The token didn't pass verification because {0}. + + + id claim is not a number + + + id claim does not exist + + + other error, see inner exception for information + + + unknown error + + + version claim is not a number. + + + version claim does not exist. + + + Password is of bad format. + + + The token is of bad format, which means it may not be created by the server. + + + The token is of bad version. + + + The token is expired because its expiration time has passed. + + \ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Services/Exceptions.Designer.cs b/BackEnd/Timeline/Resources/Services/Exceptions.Designer.cs new file mode 100644 index 00000000..1dbe11c9 --- /dev/null +++ b/BackEnd/Timeline/Resources/Services/Exceptions.Designer.cs @@ -0,0 +1,189 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Services { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Exceptions { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Exceptions() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.Exceptions", typeof(Exceptions).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to A entity of type "{0}" already exists.. + /// + internal static string EntityAlreadyExistError { + get { + return ResourceManager.GetString("EntityAlreadyExistError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The entity already exists.. + /// + internal static string EntityAlreadyExistErrorDefault { + get { + return ResourceManager.GetString("EntityAlreadyExistErrorDefault", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The required entity of type "{0}" does not exist.. + /// + internal static string EntityNotExistError { + get { + return ResourceManager.GetString("EntityNotExistError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The required entity does not exist.. + /// + internal static string EntityNotExistErrorDefault { + get { + return ResourceManager.GetString("EntityNotExistErrorDefault", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Image is in valid because {0}.. + /// + internal static string ImageException { + get { + return ResourceManager.GetString("ImageException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to image is not of required size. + /// + internal static string ImageExceptionBadSize { + get { + return ResourceManager.GetString("ImageExceptionBadSize", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to failed to decode image, see inner exception. + /// + internal static string ImageExceptionCantDecode { + get { + return ResourceManager.GetString("ImageExceptionCantDecode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to unknown error. + /// + internal static string ImageExceptionUnknownError { + get { + return ResourceManager.GetString("ImageExceptionUnknownError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to image's actual mime type is not the specified one. + /// + internal static string ImageExceptionUnmatchedFormat { + get { + return ResourceManager.GetString("ImageExceptionUnmatchedFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The timeline has no data.. + /// + internal static string TimelineNoDataException { + get { + return ResourceManager.GetString("TimelineNoDataException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Request timeline name is "{0}". If this is a personal timeline whose name starts with '@', it means the user does not exist and inner exception should be a UserNotExistException.. + /// + internal static string TimelineNotExistException { + get { + return ResourceManager.GetString("TimelineNotExistException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Request timeline name is "{0}". Request timeline post id is "{1}".. + /// + internal static string TimelinePostNotExistException { + get { + return ResourceManager.GetString("TimelinePostNotExistException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Request timeline name is "{0}". Request timeline post id is "{1}". The post does not exist because it is deleted.. + /// + internal static string TimelinePostNotExistExceptionDeleted { + get { + return ResourceManager.GetString("TimelinePostNotExistExceptionDeleted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Request username is "{0}". Request id is "{1}".. + /// + internal static string UserNotExistException { + get { + return ResourceManager.GetString("UserNotExistException", resourceCulture); + } + } + } +} diff --git a/BackEnd/Timeline/Resources/Services/Exceptions.resx b/BackEnd/Timeline/Resources/Services/Exceptions.resx new file mode 100644 index 00000000..e9595caa --- /dev/null +++ b/BackEnd/Timeline/Resources/Services/Exceptions.resx @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The required entity of type "{0}" does not exist. + + + The entity already exists. + + + A entity of type "{0}" already exists. + + + The required entity does not exist. + + + Request timeline name is "{0}". If this is a personal timeline whose name starts with '@', it means the user does not exist and inner exception should be a UserNotExistException. + + + Request timeline name is "{0}". Request timeline post id is "{1}". + + + Request username is "{0}". Request id is "{1}". + + + The timeline has no data. + + + Image is in valid because {0}. + + + image is not of required size + + + failed to decode image, see inner exception + + + unknown error + + + image's actual mime type is not the specified one + + + Request timeline name is "{0}". Request timeline post id is "{1}". The post does not exist because it is deleted. + + \ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Services/TimelineService.Designer.cs b/BackEnd/Timeline/Resources/Services/TimelineService.Designer.cs new file mode 100644 index 00000000..e16c1337 --- /dev/null +++ b/BackEnd/Timeline/Resources/Services/TimelineService.Designer.cs @@ -0,0 +1,144 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Services { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class TimelineService { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal TimelineService() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.TimelineService", typeof(TimelineService).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The number {0} username is invalid.. + /// + internal static string ExceptionChangeMemberUsernameBadFormat { + get { + return ResourceManager.GetString("ExceptionChangeMemberUsernameBadFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unknown post content type "{0}" is saved in database.. + /// + internal static string ExceptionDatabaseUnknownContentType { + get { + return ResourceManager.GetString("ExceptionDatabaseUnknownContentType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The owner username of personal timeline is of bad format.. + /// + internal static string ExceptionFindTimelineUsernameBadFormat { + get { + return ResourceManager.GetString("ExceptionFindTimelineUsernameBadFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The data entry of the tag of the image post does not exist.. + /// + internal static string ExceptionGetDataDataEntryNotExist { + get { + return ResourceManager.GetString("ExceptionGetDataDataEntryNotExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Can't get data of a non-image post.. + /// + internal static string ExceptionGetDataNonImagePost { + get { + return ResourceManager.GetString("ExceptionGetDataNonImagePost", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The post has been deleted because content of entity is null.. + /// + internal static string ExceptionPostDeleted { + get { + return ResourceManager.GetString("ExceptionPostDeleted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The timeline name is of bad format.. + /// + internal static string ExceptionTimelineNameBadFormat { + get { + return ResourceManager.GetString("ExceptionTimelineNameBadFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The timeline with given name already exists.. + /// + internal static string ExceptionTimelineNameConflict { + get { + return ResourceManager.GetString("ExceptionTimelineNameConflict", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Image format type of the post does not exist in column "extra_content". Normally this couldn't be possible because it should be saved when post was created. However, we now re-detect the format and save it.. + /// + internal static string LogGetDataNoFormat { + get { + return ResourceManager.GetString("LogGetDataNoFormat", resourceCulture); + } + } + } +} diff --git a/BackEnd/Timeline/Resources/Services/TimelineService.resx b/BackEnd/Timeline/Resources/Services/TimelineService.resx new file mode 100644 index 00000000..9314f51b --- /dev/null +++ b/BackEnd/Timeline/Resources/Services/TimelineService.resx @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The number {0} username is invalid. + + + Unknown post content type "{0}" is saved in database. + + + The owner username of personal timeline is of bad format. + + + The data entry of the tag of the image post does not exist. + + + Can't get data of a non-image post. + + + The timeline name is of bad format. + + + The timeline with given name already exists. + + + Image format type of the post does not exist in column "extra_content". Normally this couldn't be possible because it should be saved when post was created. However, we now re-detect the format and save it. + + + The post has been deleted because content of entity is null. + + \ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Services/UserAvatarService.Designer.cs b/BackEnd/Timeline/Resources/Services/UserAvatarService.Designer.cs new file mode 100644 index 00000000..c72d4215 --- /dev/null +++ b/BackEnd/Timeline/Resources/Services/UserAvatarService.Designer.cs @@ -0,0 +1,108 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Services { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class UserAvatarService { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal UserAvatarService() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.UserAvatarService", typeof(UserAvatarService).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Data of avatar is null.. + /// + internal static string ExceptionAvatarDataNull { + get { + return ResourceManager.GetString("ExceptionAvatarDataNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type of avatar is null or empty.. + /// + internal static string ExceptionAvatarTypeNullOrEmpty { + get { + return ResourceManager.GetString("ExceptionAvatarTypeNullOrEmpty", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Database corupted! One of type and data of a avatar is null but the other is not.. + /// + internal static string ExceptionDatabaseCorruptedDataAndTypeNotSame { + get { + return ResourceManager.GetString("ExceptionDatabaseCorruptedDataAndTypeNotSame", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Created an entry in user_avatars.. + /// + internal static string LogCreateEntity { + get { + return ResourceManager.GetString("LogCreateEntity", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Updated an entry in user_avatars.. + /// + internal static string LogUpdateEntity { + get { + return ResourceManager.GetString("LogUpdateEntity", resourceCulture); + } + } + } +} diff --git a/BackEnd/Timeline/Resources/Services/UserAvatarService.resx b/BackEnd/Timeline/Resources/Services/UserAvatarService.resx new file mode 100644 index 00000000..da9d7203 --- /dev/null +++ b/BackEnd/Timeline/Resources/Services/UserAvatarService.resx @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Data of avatar is null. + + + Type of avatar is null or empty. + + + Database corupted! One of type and data of a avatar is null but the other is not. + + + Created an entry in user_avatars. + + + Updated an entry in user_avatars. + + \ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Services/UserService.Designer.cs b/BackEnd/Timeline/Resources/Services/UserService.Designer.cs new file mode 100644 index 00000000..cdf7f390 --- /dev/null +++ b/BackEnd/Timeline/Resources/Services/UserService.Designer.cs @@ -0,0 +1,162 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Services { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class UserService { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal UserService() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.UserService", typeof(UserService).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to New username is of bad format.. + /// + internal static string ExceptionNewUsernameBadFormat { + get { + return ResourceManager.GetString("ExceptionNewUsernameBadFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Nickname is of bad format, because {}.. + /// + internal static string ExceptionNicknameBadFormat { + get { + return ResourceManager.GetString("ExceptionNicknameBadFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Old username is of bad format.. + /// + internal static string ExceptionOldUsernameBadFormat { + get { + return ResourceManager.GetString("ExceptionOldUsernameBadFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Password can't be empty.. + /// + internal static string ExceptionPasswordEmpty { + get { + return ResourceManager.GetString("ExceptionPasswordEmpty", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Password can't be null.. + /// + internal static string ExceptionPasswordNull { + get { + return ResourceManager.GetString("ExceptionPasswordNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Username is of bad format, because {}.. + /// + internal static string ExceptionUsernameBadFormat { + get { + return ResourceManager.GetString("ExceptionUsernameBadFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A user with given username already exists.. + /// + internal static string ExceptionUsernameConflict { + get { + return ResourceManager.GetString("ExceptionUsernameConflict", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Username can't be null.. + /// + internal static string ExceptionUsernameNull { + get { + return ResourceManager.GetString("ExceptionUsernameNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A new user entry is added to the database.. + /// + internal static string LogDatabaseCreate { + get { + return ResourceManager.GetString("LogDatabaseCreate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A user entry is removed from the database.. + /// + internal static string LogDatabaseRemove { + get { + return ResourceManager.GetString("LogDatabaseRemove", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A user entry is updated to the database.. + /// + internal static string LogDatabaseUpdate { + get { + return ResourceManager.GetString("LogDatabaseUpdate", resourceCulture); + } + } + } +} diff --git a/BackEnd/Timeline/Resources/Services/UserService.resx b/BackEnd/Timeline/Resources/Services/UserService.resx new file mode 100644 index 00000000..09bd4abb --- /dev/null +++ b/BackEnd/Timeline/Resources/Services/UserService.resx @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + New username is of bad format. + + + Nickname is of bad format, because {}. + + + Old username is of bad format. + + + Password can't be empty. + + + Password can't be null. + + + Username is of bad format, because {}. + + + A user with given username already exists. + + + Username can't be null. + + + A new user entry is added to the database. + + + A user entry is removed from the database. + + + A user entry is updated to the database. + + \ No newline at end of file diff --git a/BackEnd/Timeline/Resources/Services/UserTokenService.Designer.cs b/BackEnd/Timeline/Resources/Services/UserTokenService.Designer.cs new file mode 100644 index 00000000..3c3c7e41 --- /dev/null +++ b/BackEnd/Timeline/Resources/Services/UserTokenService.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Services { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class UserTokenService { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal UserTokenService() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.UserTokenService", typeof(UserTokenService).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Jwt token key is not set in database. Maybe you forget to migrate the database.. + /// + internal static string JwtKeyNotExist { + get { + return ResourceManager.GetString("JwtKeyNotExist", resourceCulture); + } + } + } +} diff --git a/BackEnd/Timeline/Resources/Services/UserTokenService.resx b/BackEnd/Timeline/Resources/Services/UserTokenService.resx new file mode 100644 index 00000000..1ce78427 --- /dev/null +++ b/BackEnd/Timeline/Resources/Services/UserTokenService.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Jwt token key is not set in database. Maybe you forget to migrate the database. + + \ No newline at end of file diff --git a/BackEnd/Timeline/Routes/ApiRoutePrefixConvention.cs b/BackEnd/Timeline/Routes/ApiRoutePrefixConvention.cs new file mode 100644 index 00000000..ca38a0d9 --- /dev/null +++ b/BackEnd/Timeline/Routes/ApiRoutePrefixConvention.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using System.Linq; + +namespace Timeline.Routes +{ + public static class MvcOptionsExtensions + { + public static void UseApiRoutePrefix(this MvcOptions opts, IRouteTemplateProvider routeAttribute) + { + opts.Conventions.Add(new ApiRoutePrefixConvention(routeAttribute)); + } + + public static void UseApiRoutePrefix(this MvcOptions opts, string prefix) + { + opts.UseApiRoutePrefix(new RouteAttribute(prefix)); + } + } + + public class ApiRoutePrefixConvention : IApplicationModelConvention + { + private readonly AttributeRouteModel _routePrefix; + + public ApiRoutePrefixConvention(IRouteTemplateProvider route) + { + _routePrefix = new AttributeRouteModel(route); + } + + public void Apply(ApplicationModel application) + { + foreach (var selector in application.Controllers.Where(c => c.Filters.Any(f => f is IApiBehaviorMetadata)).SelectMany(c => c.Selectors)) + { + if (selector.AttributeRouteModel != null) + { + selector.AttributeRouteModel = AttributeRouteModel.CombineAttributeRouteModel(_routePrefix, selector.AttributeRouteModel); + } + else + { + selector.AttributeRouteModel = _routePrefix; + } + } + } + } +} diff --git a/BackEnd/Timeline/Routes/UnknownEndpointMiddleware.cs b/BackEnd/Timeline/Routes/UnknownEndpointMiddleware.cs new file mode 100644 index 00000000..25ec563c --- /dev/null +++ b/BackEnd/Timeline/Routes/UnknownEndpointMiddleware.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using System; +using System.Net.Mime; +using System.Text.Json; +using Timeline.Models.Http; + +namespace Timeline.Routes +{ + public static class UnknownEndpointMiddleware + { + public static void Attach(IApplicationBuilder app) + { + app.Use(async (context, next) => + { + if (context.GetEndpoint() != null) + { + await next(); + return; + } + + if (context.Request.Path.StartsWithSegments("/api", StringComparison.OrdinalIgnoreCase)) + { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + context.Response.ContentType = MediaTypeNames.Application.Json; + + var body = JsonSerializer.SerializeToUtf8Bytes(ErrorResponse.Common.UnknownEndpoint(), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + + context.Response.ContentLength = body.Length; + await context.Response.Body.WriteAsync(body); + await context.Response.CompleteAsync(); + return; + } + + await next(); + }); + } + } +} diff --git a/BackEnd/Timeline/Services/BadPasswordException.cs b/BackEnd/Timeline/Services/BadPasswordException.cs new file mode 100644 index 00000000..f609371d --- /dev/null +++ b/BackEnd/Timeline/Services/BadPasswordException.cs @@ -0,0 +1,27 @@ +using System; +using Timeline.Helpers; + +namespace Timeline.Services +{ + [Serializable] + public class BadPasswordException : Exception + { + public BadPasswordException() : base(Resources.Services.Exception.BadPasswordException) { } + public BadPasswordException(string message, Exception inner) : base(message, inner) { } + + public BadPasswordException(string badPassword) + : base(Log.Format(Resources.Services.Exception.BadPasswordException, ("Bad Password", badPassword))) + { + Password = badPassword; + } + + protected BadPasswordException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + /// + /// The wrong password. + /// + public string? Password { get; set; } + } +} diff --git a/BackEnd/Timeline/Services/Clock.cs b/BackEnd/Timeline/Services/Clock.cs new file mode 100644 index 00000000..4395edcd --- /dev/null +++ b/BackEnd/Timeline/Services/Clock.cs @@ -0,0 +1,29 @@ +using System; + +namespace Timeline.Services +{ + /// + /// Convenient for unit test. + /// + public interface IClock + { + /// + /// Get current time. + /// + /// Current time. + DateTime GetCurrentTime(); + } + + public class Clock : IClock + { + public Clock() + { + + } + + public DateTime GetCurrentTime() + { + return DateTime.UtcNow; + } + } +} diff --git a/BackEnd/Timeline/Services/DataManager.cs b/BackEnd/Timeline/Services/DataManager.cs new file mode 100644 index 00000000..d447b0d5 --- /dev/null +++ b/BackEnd/Timeline/Services/DataManager.cs @@ -0,0 +1,122 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Entities; + +namespace Timeline.Services +{ + /// + /// A data manager controlling data. + /// + /// + /// Identical data will be saved as one copy and return the same tag. + /// Every data has a ref count. When data is retained, ref count increase. + /// When data is freed, ref count decease. If ref count is decreased + /// to 0, the data entry will be destroyed and no longer occupy space. + /// + public interface IDataManager + { + /// + /// Saves the data to a new entry if it does not exist, + /// increases its ref count and returns a tag to the entry. + /// + /// The data. Can't be null. + /// The tag of the created entry. + /// Thrown when is null. + public Task RetainEntry(byte[] data); + + /// + /// Decrease the the ref count of the entry. + /// Remove it if ref count is zero. + /// + /// The tag of the entry. + /// Thrown when is null. + /// + /// It's no-op if entry with tag does not exist. + /// + public Task FreeEntry(string tag); + + /// + /// Retrieve the entry with given tag. + /// + /// The tag of the entry. + /// The data of the entry. + /// Thrown when is null. + /// Thrown when entry with given tag does not exist. + public Task GetEntry(string tag); + } + + public class DataManager : IDataManager + { + private readonly DatabaseContext _database; + private readonly IETagGenerator _eTagGenerator; + + public DataManager(DatabaseContext database, IETagGenerator eTagGenerator) + { + _database = database; + _eTagGenerator = eTagGenerator; + } + + public async Task RetainEntry(byte[] data) + { + if (data == null) + throw new ArgumentNullException(nameof(data)); + + var tag = await _eTagGenerator.Generate(data); + + var entity = await _database.Data.Where(d => d.Tag == tag).SingleOrDefaultAsync(); + + if (entity == null) + { + entity = new DataEntity + { + Tag = tag, + Data = data, + Ref = 1 + }; + _database.Data.Add(entity); + } + else + { + entity.Ref += 1; + } + await _database.SaveChangesAsync(); + return tag; + } + + public async Task FreeEntry(string tag) + { + if (tag == null) + throw new ArgumentNullException(nameof(tag)); + + var entity = await _database.Data.Where(d => d.Tag == tag).SingleOrDefaultAsync(); + + if (entity != null) + { + if (entity.Ref == 1) + { + _database.Data.Remove(entity); + } + else + { + entity.Ref -= 1; + } + await _database.SaveChangesAsync(); + } + } + + public async Task GetEntry(string tag) + { + if (tag == null) + throw new ArgumentNullException(nameof(tag)); + + var entity = await _database.Data.Where(d => d.Tag == tag).Select(d => new { d.Data }).SingleOrDefaultAsync(); + + if (entity == null) + throw new InvalidOperationException(Resources.Services.DataManager.ExceptionEntryNotExist); + + return entity.Data; + } + } +} diff --git a/BackEnd/Timeline/Services/DatabaseBackupService.cs b/BackEnd/Timeline/Services/DatabaseBackupService.cs new file mode 100644 index 00000000..a76b2a0d --- /dev/null +++ b/BackEnd/Timeline/Services/DatabaseBackupService.cs @@ -0,0 +1,35 @@ +using System.Globalization; +using System.IO; + +namespace Timeline.Services +{ + public interface IDatabaseBackupService + { + void BackupNow(); + } + + public class DatabaseBackupService : IDatabaseBackupService + { + private readonly IPathProvider _pathProvider; + private readonly IClock _clock; + + public DatabaseBackupService(IPathProvider pathProvider, IClock clock) + { + _pathProvider = pathProvider; + _clock = clock; + } + + public void BackupNow() + { + var databasePath = _pathProvider.GetDatabaseFilePath(); + if (File.Exists(databasePath)) + { + var backupDirPath = _pathProvider.GetDatabaseBackupDirectory(); + Directory.CreateDirectory(backupDirPath); + var fileName = _clock.GetCurrentTime().ToString("yyyy-MM-ddTHH-mm-ss", CultureInfo.InvariantCulture); + var path = Path.Combine(backupDirPath, fileName); + File.Copy(databasePath, path); + } + } + } +} diff --git a/BackEnd/Timeline/Services/DatabaseCorruptedException.cs b/BackEnd/Timeline/Services/DatabaseCorruptedException.cs new file mode 100644 index 00000000..9988e0ad --- /dev/null +++ b/BackEnd/Timeline/Services/DatabaseCorruptedException.cs @@ -0,0 +1,15 @@ +using System; + +namespace Timeline.Services +{ + [Serializable] + public class DatabaseCorruptedException : Exception + { + public DatabaseCorruptedException() { } + public DatabaseCorruptedException(string message) : base(message) { } + public DatabaseCorruptedException(string message, Exception inner) : base(message, inner) { } + protected DatabaseCorruptedException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + } +} diff --git a/BackEnd/Timeline/Services/ETagGenerator.cs b/BackEnd/Timeline/Services/ETagGenerator.cs new file mode 100644 index 00000000..4493e903 --- /dev/null +++ b/BackEnd/Timeline/Services/ETagGenerator.cs @@ -0,0 +1,45 @@ +using System; +using System.Security.Cryptography; +using System.Threading.Tasks; + +namespace Timeline.Services +{ + public interface IETagGenerator + { + /// + /// Generate a etag for given source. + /// + /// The source data. + /// The generated etag. + /// Thrown if is null. + Task Generate(byte[] source); + } + + public sealed class ETagGenerator : IETagGenerator, IDisposable + { + private readonly SHA1 _sha1; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Security", "CA5350:Do Not Use Weak Cryptographic Algorithms", Justification = "Sha1 is enough ??? I don't know.")] + public ETagGenerator() + { + _sha1 = SHA1.Create(); + } + + public Task Generate(byte[] source) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + + return Task.Run(() => Convert.ToBase64String(_sha1.ComputeHash(source))); + } + + private bool _disposed; // To detect redundant calls + + public void Dispose() + { + if (_disposed) return; + _sha1.Dispose(); + _disposed = true; + } + } +} diff --git a/BackEnd/Timeline/Services/EntityNames.cs b/BackEnd/Timeline/Services/EntityNames.cs new file mode 100644 index 00000000..0ce1de3b --- /dev/null +++ b/BackEnd/Timeline/Services/EntityNames.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Timeline.Services +{ + public static class EntityNames + { + public const string User = "User"; + public const string Timeline = "Timeline"; + public const string TimelinePost = "TimelinePost"; + } +} diff --git a/BackEnd/Timeline/Services/Exceptions/EntityAlreadyExistError.cs b/BackEnd/Timeline/Services/Exceptions/EntityAlreadyExistError.cs new file mode 100644 index 00000000..7db2e860 --- /dev/null +++ b/BackEnd/Timeline/Services/Exceptions/EntityAlreadyExistError.cs @@ -0,0 +1,63 @@ +using System; +using System.Globalization; +using System.Text; + +namespace Timeline.Services.Exceptions +{ + /// + /// Thrown when an entity is already exists. + /// + /// + /// For example, want to create a timeline but a timeline with the same name already exists. + /// + [Serializable] + public class EntityAlreadyExistException : Exception + { + private readonly string? _entityName; + + public EntityAlreadyExistException() : this(null, null, null, null) { } + + public EntityAlreadyExistException(string? entityName) : this(entityName, null) { } + + public EntityAlreadyExistException(string? entityName, Exception? inner) : this(entityName, null, null, null, inner) { } + + public EntityAlreadyExistException(string? entityName, object? entity = null) : this(entityName, null, entity, null, null) { } + public EntityAlreadyExistException(Type? entityType, object? entity = null) : this(null, entityType, entity, null, null) { } + public EntityAlreadyExistException(string? entityName, Type? entityType, object? entity = null, string? message = null, Exception? inner = null) : base(MakeMessage(entityName, entityType, message), inner) + { + _entityName = entityName; + EntityType = entityType; + Entity = entity; + } + + private static string MakeMessage(string? entityName, Type? entityType, string? message) + { + string? name = entityName ?? (entityType?.Name); + + var result = new StringBuilder(); + + if (name == null) + result.Append(Resources.Services.Exceptions.EntityAlreadyExistErrorDefault); + else + result.AppendFormat(CultureInfo.InvariantCulture, Resources.Services.Exceptions.EntityAlreadyExistError, name); + + if (message != null) + { + result.Append(' '); + result.Append(message); + } + + return result.ToString(); + } + + protected EntityAlreadyExistException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + public string? EntityName => _entityName ?? (EntityType?.Name); + + public Type? EntityType { get; } + + public object? Entity { get; } + } +} diff --git a/BackEnd/Timeline/Services/Exceptions/EntityNotExistError.cs b/BackEnd/Timeline/Services/Exceptions/EntityNotExistError.cs new file mode 100644 index 00000000..e79496d3 --- /dev/null +++ b/BackEnd/Timeline/Services/Exceptions/EntityNotExistError.cs @@ -0,0 +1,55 @@ +using System; +using System.Globalization; +using System.Text; + +namespace Timeline.Services.Exceptions +{ + /// + /// Thrown when you want to get an entity that does not exist. + /// + /// + /// For example, you want to get a timeline with given name but it does not exist. + /// + [Serializable] + public class EntityNotExistException : Exception + { + public EntityNotExistException() : this(null, null, null, null) { } + public EntityNotExistException(string? entityName) : this(entityName, null, null, null) { } + public EntityNotExistException(Type? entityType) : this(null, entityType, null, null) { } + public EntityNotExistException(string? entityName, Exception? inner) : this(entityName, null, null, inner) { } + public EntityNotExistException(Type? entityType, Exception? inner) : this(null, entityType, null, inner) { } + public EntityNotExistException(string? entityName, Type? entityType, string? message = null, Exception? inner = null) : base(MakeMessage(entityName, entityType, message), inner) + { + EntityName = entityName; + EntityType = entityType; + } + + private static string MakeMessage(string? entityName, Type? entityType, string? message) + { + string? name = entityName ?? (entityType?.Name); + + var result = new StringBuilder(); + + if (name == null) + result.Append(Resources.Services.Exceptions.EntityNotExistErrorDefault); + else + result.AppendFormat(CultureInfo.InvariantCulture, Resources.Services.Exceptions.EntityNotExistError, name); + + if (message != null) + { + result.Append(' '); + result.Append(message); + } + + return result.ToString(); + } + + protected EntityNotExistException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + public string? EntityName { get; } + + public Type? EntityType { get; } + } +} diff --git a/BackEnd/Timeline/Services/Exceptions/ExceptionMessageHelper.cs b/BackEnd/Timeline/Services/Exceptions/ExceptionMessageHelper.cs new file mode 100644 index 00000000..be3c42a4 --- /dev/null +++ b/BackEnd/Timeline/Services/Exceptions/ExceptionMessageHelper.cs @@ -0,0 +1,13 @@ +namespace Timeline.Services.Exceptions +{ + public static class ExceptionMessageHelper + { + public static string AppendAdditionalMessage(this string origin, string? message) + { + if (message == null) + return origin; + else + return origin + " " + message; + } + } +} diff --git a/BackEnd/Timeline/Services/Exceptions/ImageException.cs b/BackEnd/Timeline/Services/Exceptions/ImageException.cs new file mode 100644 index 00000000..20dd48ae --- /dev/null +++ b/BackEnd/Timeline/Services/Exceptions/ImageException.cs @@ -0,0 +1,57 @@ +using System; +using System.Globalization; + +namespace Timeline.Services.Exceptions +{ + [Serializable] + public class ImageException : Exception + { + public enum ErrorReason + { + /// + /// Decoding image failed. + /// + CantDecode, + /// + /// Decoding succeeded but the real type is not the specified type. + /// + UnmatchedFormat, + /// + /// Image is not of required size. + /// + NotSquare, + /// + /// Other unknown errer. + /// + Unknown + } + + public ImageException() : this(null) { } + public ImageException(string? message) : this(message, null) { } + public ImageException(string? message, Exception? inner) : this(ErrorReason.Unknown, null, null, null, message, inner) { } + + public ImageException(ErrorReason error, byte[]? data, string? requestType = null, string? realType = null, string? message = null, Exception? inner = null) : base(MakeMessage(error).AppendAdditionalMessage(message), inner) { Error = error; ImageData = data; RequestType = requestType; RealType = realType; } + + protected ImageException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + private static string MakeMessage(ErrorReason? reason) => + string.Format(CultureInfo.InvariantCulture, Resources.Services.Exceptions.ImageException, reason switch + { + ErrorReason.CantDecode => Resources.Services.Exceptions.ImageExceptionCantDecode, + ErrorReason.UnmatchedFormat => Resources.Services.Exceptions.ImageExceptionUnmatchedFormat, + ErrorReason.NotSquare => Resources.Services.Exceptions.ImageExceptionBadSize, + _ => Resources.Services.Exceptions.ImageExceptionUnknownError + }); + + public ErrorReason Error { get; } +#pragma warning disable CA1819 // Properties should not return arrays + public byte[]? ImageData { get; } +#pragma warning restore CA1819 // Properties should not return arrays + public string? RequestType { get; } + + // This field will be null if decoding failed. + public string? RealType { get; } + } +} diff --git a/BackEnd/Timeline/Services/Exceptions/TimelineNotExistException.cs b/BackEnd/Timeline/Services/Exceptions/TimelineNotExistException.cs new file mode 100644 index 00000000..70970b24 --- /dev/null +++ b/BackEnd/Timeline/Services/Exceptions/TimelineNotExistException.cs @@ -0,0 +1,21 @@ +using System; +using System.Globalization; + +namespace Timeline.Services.Exceptions +{ + [Serializable] + public class TimelineNotExistException : EntityNotExistException + { + public TimelineNotExistException() : this(null, null) { } + public TimelineNotExistException(string? timelineName) : this(timelineName, null) { } + public TimelineNotExistException(string? timelineName, Exception? inner) : this(timelineName, null, inner) { } + public TimelineNotExistException(string? timelineName, string? message, Exception? inner = null) + : base(EntityNames.Timeline, null, string.Format(CultureInfo.InvariantCulture, Resources.Services.Exceptions.TimelineNotExistException, timelineName ?? "").AppendAdditionalMessage(message), inner) { TimelineName = timelineName; } + + protected TimelineNotExistException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + public string? TimelineName { get; set; } + } +} diff --git a/BackEnd/Timeline/Services/Exceptions/TimelinePostNoDataException.cs b/BackEnd/Timeline/Services/Exceptions/TimelinePostNoDataException.cs new file mode 100644 index 00000000..c4b6bf62 --- /dev/null +++ b/BackEnd/Timeline/Services/Exceptions/TimelinePostNoDataException.cs @@ -0,0 +1,15 @@ +using System; + +namespace Timeline.Services.Exceptions +{ + [Serializable] + public class TimelinePostNoDataException : Exception + { + public TimelinePostNoDataException() : this(null, null) { } + public TimelinePostNoDataException(string? message) : this(message, null) { } + public TimelinePostNoDataException(string? message, Exception? inner) : base(Resources.Services.Exceptions.TimelineNoDataException.AppendAdditionalMessage(message), inner) { } + protected TimelinePostNoDataException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + } +} diff --git a/BackEnd/Timeline/Services/Exceptions/TimelinePostNotExistException.cs b/BackEnd/Timeline/Services/Exceptions/TimelinePostNotExistException.cs new file mode 100644 index 00000000..f95dd410 --- /dev/null +++ b/BackEnd/Timeline/Services/Exceptions/TimelinePostNotExistException.cs @@ -0,0 +1,33 @@ +using System; +using System.Globalization; + +namespace Timeline.Services.Exceptions +{ + [Serializable] + public class TimelinePostNotExistException : EntityNotExistException + { + public TimelinePostNotExistException() : this(null, null, false, null, null) { } + [Obsolete("This has no meaning.")] + public TimelinePostNotExistException(string? message) : this(message, null) { } + [Obsolete("This has no meaning.")] + public TimelinePostNotExistException(string? message, Exception? inner) : this(null, null, false, message, inner) { } + protected TimelinePostNotExistException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + public TimelinePostNotExistException(string? timelineName, long? id, bool isDelete, string? message = null, Exception? inner = null) : base(EntityNames.TimelinePost, null, MakeMessage(timelineName, id, isDelete).AppendAdditionalMessage(message), inner) { TimelineName = timelineName; Id = id; IsDelete = isDelete; } + + private static string MakeMessage(string? timelineName, long? id, bool isDelete) + { + return string.Format(CultureInfo.InvariantCulture, isDelete ? Resources.Services.Exceptions.TimelinePostNotExistExceptionDeleted : Resources.Services.Exceptions.TimelinePostNotExistException, timelineName ?? "", id); + } + + public string? TimelineName { get; set; } + public long? Id { get; set; } + + /// + /// True if the post is deleted. False if the post does not exist at all. + /// + public bool IsDelete { get; set; } + } +} diff --git a/BackEnd/Timeline/Services/Exceptions/UserNotExistException.cs b/BackEnd/Timeline/Services/Exceptions/UserNotExistException.cs new file mode 100644 index 00000000..7ef714df --- /dev/null +++ b/BackEnd/Timeline/Services/Exceptions/UserNotExistException.cs @@ -0,0 +1,40 @@ +using System; +using System.Globalization; + +namespace Timeline.Services.Exceptions +{ + /// + /// The user requested does not exist. + /// + [Serializable] + public class UserNotExistException : EntityNotExistException + { + public UserNotExistException() : this(null, null, null, null) { } + public UserNotExistException(string? username, Exception? inner) : this(username, null, null, inner) { } + + public UserNotExistException(string? username) : this(username, null, null, null) { } + + public UserNotExistException(long id) : this(null, id, null, null) { } + + public UserNotExistException(string? username, long? id, string? message, Exception? inner) : base(EntityNames.User, null, + string.Format(CultureInfo.InvariantCulture, Resources.Services.Exceptions.UserNotExistException, username ?? "", id).AppendAdditionalMessage(message), inner) + { + Username = username; + Id = id; + } + + protected UserNotExistException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + /// + /// The username of the user that does not exist. + /// + public string? Username { get; set; } + + /// + /// The id of the user that does not exist. + /// + public long? Id { get; set; } + } +} diff --git a/BackEnd/Timeline/Services/ImageValidator.cs b/BackEnd/Timeline/Services/ImageValidator.cs new file mode 100644 index 00000000..59424a7c --- /dev/null +++ b/BackEnd/Timeline/Services/ImageValidator.cs @@ -0,0 +1,54 @@ +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats; +using System; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Services.Exceptions; + +namespace Timeline.Services +{ + public interface IImageValidator + { + /// + /// Validate a image data. + /// + /// The data of the image. Can't be null. + /// If not null, the real image format will be check against the requested format and throw if not match. If null, then do not check. + /// If true, image must be square. + /// The format. + /// Thrown when is null. + /// Thrown when image data can't be decoded or real type does not match request type or image is not square when required. + Task Validate(byte[] data, string? requestType = null, bool square = false); + } + + public class ImageValidator : IImageValidator + { + public ImageValidator() + { + } + + public async Task Validate(byte[] data, string? requestType = null, bool square = false) + { + if (data == null) + throw new ArgumentNullException(nameof(data)); + + var format = await Task.Run(() => + { + try + { + using var image = Image.Load(data, out IImageFormat format); + if (requestType != null && !format.MimeTypes.Contains(requestType)) + throw new ImageException(ImageException.ErrorReason.UnmatchedFormat, data, requestType, format.DefaultMimeType); + if (square && image.Width != image.Height) + throw new ImageException(ImageException.ErrorReason.NotSquare, data, requestType, format.DefaultMimeType); + return format; + } + catch (UnknownImageFormatException e) + { + throw new ImageException(ImageException.ErrorReason.CantDecode, data, requestType, null, null, e); + } + }); + return format; + } + } +} diff --git a/BackEnd/Timeline/Services/JwtUserTokenBadFormatException.cs b/BackEnd/Timeline/Services/JwtUserTokenBadFormatException.cs new file mode 100644 index 00000000..c528c3e3 --- /dev/null +++ b/BackEnd/Timeline/Services/JwtUserTokenBadFormatException.cs @@ -0,0 +1,48 @@ +using System; +using System.Globalization; +using static Timeline.Resources.Services.Exception; + +namespace Timeline.Services +{ + [Serializable] + public class JwtUserTokenBadFormatException : UserTokenBadFormatException + { + public enum ErrorKind + { + NoIdClaim, + IdClaimBadFormat, + NoVersionClaim, + VersionClaimBadFormat, + Other + } + + public JwtUserTokenBadFormatException() : this("", ErrorKind.Other) { } + public JwtUserTokenBadFormatException(string message) : base(message) { } + public JwtUserTokenBadFormatException(string message, Exception inner) : base(message, inner) { } + + public JwtUserTokenBadFormatException(string token, ErrorKind type) : base(token, GetErrorMessage(type)) { ErrorType = type; } + public JwtUserTokenBadFormatException(string token, ErrorKind type, Exception inner) : base(token, GetErrorMessage(type), inner) { ErrorType = type; } + public JwtUserTokenBadFormatException(string token, ErrorKind type, string message, Exception inner) : base(token, message, inner) { ErrorType = type; } + protected JwtUserTokenBadFormatException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + public ErrorKind ErrorType { get; set; } + + private static string GetErrorMessage(ErrorKind type) + { + var reason = type switch + { + ErrorKind.NoIdClaim => JwtUserTokenBadFormatExceptionIdMissing, + ErrorKind.IdClaimBadFormat => JwtUserTokenBadFormatExceptionIdBadFormat, + ErrorKind.NoVersionClaim => JwtUserTokenBadFormatExceptionVersionMissing, + ErrorKind.VersionClaimBadFormat => JwtUserTokenBadFormatExceptionVersionBadFormat, + ErrorKind.Other => JwtUserTokenBadFormatExceptionOthers, + _ => JwtUserTokenBadFormatExceptionUnknown + }; + + return string.Format(CultureInfo.CurrentCulture, + Resources.Services.Exception.JwtUserTokenBadFormatException, reason); + } + } +} diff --git a/BackEnd/Timeline/Services/PasswordBadFormatException.cs b/BackEnd/Timeline/Services/PasswordBadFormatException.cs new file mode 100644 index 00000000..2029ebb4 --- /dev/null +++ b/BackEnd/Timeline/Services/PasswordBadFormatException.cs @@ -0,0 +1,27 @@ +using System; + +namespace Timeline.Services +{ + + [Serializable] + public class PasswordBadFormatException : Exception + { + public PasswordBadFormatException() : base(Resources.Services.Exception.PasswordBadFormatException) { } + public PasswordBadFormatException(string message) : base(message) { } + public PasswordBadFormatException(string message, Exception inner) : base(message, inner) { } + + public PasswordBadFormatException(string password, string validationMessage) : this() + { + Password = password; + ValidationMessage = validationMessage; + } + + protected PasswordBadFormatException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + public string Password { get; set; } = ""; + + public string ValidationMessage { get; set; } = ""; + } +} diff --git a/BackEnd/Timeline/Services/PasswordService.cs b/BackEnd/Timeline/Services/PasswordService.cs new file mode 100644 index 00000000..8114a520 --- /dev/null +++ b/BackEnd/Timeline/Services/PasswordService.cs @@ -0,0 +1,224 @@ +using Microsoft.AspNetCore.Cryptography.KeyDerivation; +using System; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; + +namespace Timeline.Services +{ + /// + /// Hashed password is of bad format. + /// + /// + [Serializable] + public class HashedPasswordBadFromatException : Exception + { + private static string MakeMessage(string reason) + { + return Resources.Services.Exception.HashedPasswordBadFromatException + " Reason: " + reason; + } + + public HashedPasswordBadFromatException() : base(Resources.Services.Exception.HashedPasswordBadFromatException) { } + + public HashedPasswordBadFromatException(string message) : base(message) { } + public HashedPasswordBadFromatException(string message, Exception inner) : base(message, inner) { } + + public HashedPasswordBadFromatException(string hashedPassword, string reason) : base(MakeMessage(reason)) { HashedPassword = hashedPassword; } + public HashedPasswordBadFromatException(string hashedPassword, string reason, Exception inner) : base(MakeMessage(reason), inner) { HashedPassword = hashedPassword; } + protected HashedPasswordBadFromatException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + public string? HashedPassword { get; set; } + } + + public interface IPasswordService + { + /// + /// Hash a password. + /// + /// The password to hash. + /// A hashed representation of the supplied . + /// Thrown when is null. + string HashPassword(string password); + + /// + /// Verify whether the password fits into the hashed one. + /// + /// Usually you only need to check the returned bool value. + /// Catching usually is not necessary. + /// Because if your program logic is right and always call + /// and in pair, this exception will never be thrown. + /// A thrown one usually means the data you saved is corupted, which is a critical problem. + /// + /// The hashed password. + /// The password supplied for comparison. + /// True indicating password is right. Otherwise false. + /// Thrown when or is null. + /// Thrown when the hashed password is of bad format. + bool VerifyPassword(string hashedPassword, string providedPassword); + } + + /// + /// Copied from https://github.com/aspnet/AspNetCore/blob/master/src/Identity/Extensions.Core/src/PasswordHasher.cs + /// Remove V2 format and unnecessary format version check. + /// Remove configuration options. + /// Remove user related parts. + /// Change the exceptions. + /// + public class PasswordService : IPasswordService + { + /* ======================= + * HASHED PASSWORD FORMATS + * ======================= + * + * Version 3: + * PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations. + * Format: { 0x01, prf (UInt32), iter count (UInt32), salt length (UInt32), salt, subkey } + * (All UInt32s are stored big-endian.) + */ + + private readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create(); + + public PasswordService() + { + } + + // Compares two byte arrays for equality. The method is specifically written so that the loop is not optimized. + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + private static bool ByteArraysEqual(byte[] a, byte[] b) + { + if (a == null && b == null) + { + return true; + } + if (a == null || b == null || a.Length != b.Length) + { + return false; + } + var areSame = true; + for (var i = 0; i < a.Length; i++) + { + areSame &= (a[i] == b[i]); + } + return areSame; + } + + public string HashPassword(string password) + { + if (password == null) + throw new ArgumentNullException(nameof(password)); + return Convert.ToBase64String(HashPasswordV3(password, _rng)); + } + + private static byte[] HashPasswordV3(string password, RandomNumberGenerator rng) + { + return HashPasswordV3(password, rng, + prf: KeyDerivationPrf.HMACSHA256, + iterCount: 10000, + saltSize: 128 / 8, + numBytesRequested: 256 / 8); + } + + private static byte[] HashPasswordV3(string password, RandomNumberGenerator rng, KeyDerivationPrf prf, int iterCount, int saltSize, int numBytesRequested) + { + // Produce a version 3 (see comment above) text hash. + byte[] salt = new byte[saltSize]; + rng.GetBytes(salt); + byte[] subkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, numBytesRequested); + + var outputBytes = new byte[13 + salt.Length + subkey.Length]; + outputBytes[0] = 0x01; // format marker + WriteNetworkByteOrder(outputBytes, 1, (uint)prf); + WriteNetworkByteOrder(outputBytes, 5, (uint)iterCount); + WriteNetworkByteOrder(outputBytes, 9, (uint)saltSize); + Buffer.BlockCopy(salt, 0, outputBytes, 13, salt.Length); + Buffer.BlockCopy(subkey, 0, outputBytes, 13 + saltSize, subkey.Length); + return outputBytes; + } + + public bool VerifyPassword(string hashedPassword, string providedPassword) + { + if (hashedPassword == null) + throw new ArgumentNullException(nameof(hashedPassword)); + if (providedPassword == null) + throw new ArgumentNullException(nameof(providedPassword)); + + byte[] decodedHashedPassword; + try + { + decodedHashedPassword = Convert.FromBase64String(hashedPassword); + } + catch (FormatException e) + { + throw new HashedPasswordBadFromatException(hashedPassword, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotBase64, e); + } + + // read the format marker from the hashed password + if (decodedHashedPassword.Length == 0) + { + throw new HashedPasswordBadFromatException(hashedPassword, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotLength0); + } + + return (decodedHashedPassword[0]) switch + { + 0x01 => VerifyHashedPasswordV3(decodedHashedPassword, providedPassword, hashedPassword), + _ => throw new HashedPasswordBadFromatException(hashedPassword, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotUnknownMarker), + }; + } + + private static bool VerifyHashedPasswordV3(byte[] hashedPassword, string password, string hashedPasswordString) + { + try + { + // Read header information + KeyDerivationPrf prf = (KeyDerivationPrf)ReadNetworkByteOrder(hashedPassword, 1); + int iterCount = (int)ReadNetworkByteOrder(hashedPassword, 5); + int saltLength = (int)ReadNetworkByteOrder(hashedPassword, 9); + + // Read the salt: must be >= 128 bits + if (saltLength < 128 / 8) + { + throw new HashedPasswordBadFromatException(hashedPasswordString, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotSaltTooShort); + } + byte[] salt = new byte[saltLength]; + Buffer.BlockCopy(hashedPassword, 13, salt, 0, salt.Length); + + // Read the subkey (the rest of the payload): must be >= 128 bits + int subkeyLength = hashedPassword.Length - 13 - salt.Length; + if (subkeyLength < 128 / 8) + { + throw new HashedPasswordBadFromatException(hashedPasswordString, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotSubkeyTooShort); + } + byte[] expectedSubkey = new byte[subkeyLength]; + Buffer.BlockCopy(hashedPassword, 13 + salt.Length, expectedSubkey, 0, expectedSubkey.Length); + + // Hash the incoming password and verify it + byte[] actualSubkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, subkeyLength); + return ByteArraysEqual(actualSubkey, expectedSubkey); + } + catch (Exception e) + { + // This should never occur except in the case of a malformed payload, where + // we might go off the end of the array. Regardless, a malformed payload + // implies verification failed. + throw new HashedPasswordBadFromatException(hashedPasswordString, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotOthers, e); + } + } + + private static uint ReadNetworkByteOrder(byte[] buffer, int offset) + { + return ((uint)(buffer[offset + 0]) << 24) + | ((uint)(buffer[offset + 1]) << 16) + | ((uint)(buffer[offset + 2]) << 8) + | ((uint)(buffer[offset + 3])); + } + + private static void WriteNetworkByteOrder(byte[] buffer, int offset, uint value) + { + buffer[offset + 0] = (byte)(value >> 24); + buffer[offset + 1] = (byte)(value >> 16); + buffer[offset + 2] = (byte)(value >> 8); + buffer[offset + 3] = (byte)(value >> 0); + } + } +} diff --git a/BackEnd/Timeline/Services/PathProvider.cs b/BackEnd/Timeline/Services/PathProvider.cs new file mode 100644 index 00000000..1baba5c0 --- /dev/null +++ b/BackEnd/Timeline/Services/PathProvider.cs @@ -0,0 +1,42 @@ +using Microsoft.Extensions.Configuration; +using System.IO; +using Timeline.Configs; + +namespace Timeline.Services +{ + public interface IPathProvider + { + public string GetWorkingDirectory(); + public string GetDatabaseFilePath(); + public string GetDatabaseBackupDirectory(); + } + + public class PathProvider : IPathProvider + { + private readonly IConfiguration _configuration; + + private readonly string _workingDirectory; + + + public PathProvider(IConfiguration configuration) + { + _configuration = configuration; + _workingDirectory = configuration.GetValue(ApplicationConfiguration.WorkDirKey) ?? ApplicationConfiguration.DefaultWorkDir; + } + + public string GetWorkingDirectory() + { + return _workingDirectory; + } + + public string GetDatabaseFilePath() + { + return Path.Combine(_workingDirectory, ApplicationConfiguration.DatabaseFileName); + } + + public string GetDatabaseBackupDirectory() + { + return Path.Combine(_workingDirectory, ApplicationConfiguration.DatabaseBackupDirectoryName); + } + } +} diff --git a/BackEnd/Timeline/Services/TimelineService.cs b/BackEnd/Timeline/Services/TimelineService.cs new file mode 100644 index 00000000..4bcae596 --- /dev/null +++ b/BackEnd/Timeline/Services/TimelineService.cs @@ -0,0 +1,1166 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using SixLabors.ImageSharp; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Timeline.Entities; +using Timeline.Helpers; +using Timeline.Models; +using Timeline.Models.Validation; +using Timeline.Services.Exceptions; +using static Timeline.Resources.Services.TimelineService; + +namespace Timeline.Services +{ + public static class TimelineHelper + { + public static string ExtractTimelineName(string name, out bool isPersonal) + { + if (name.StartsWith("@", StringComparison.OrdinalIgnoreCase)) + { + isPersonal = true; + return name.Substring(1); + } + else + { + isPersonal = false; + return name; + } + } + } + + public enum TimelineUserRelationshipType + { + Own = 0b1, + Join = 0b10, + Default = Own | Join + } + + public class TimelineUserRelationship + { + public TimelineUserRelationship(TimelineUserRelationshipType type, long userId) + { + Type = type; + UserId = userId; + } + + public TimelineUserRelationshipType Type { get; set; } + public long UserId { get; set; } + } + + public class PostData : ICacheableData + { +#pragma warning disable CA1819 // Properties should not return arrays + public byte[] Data { get; set; } = default!; +#pragma warning restore CA1819 // Properties should not return arrays + public string Type { get; set; } = default!; + public string ETag { get; set; } = default!; + public DateTime? LastModified { get; set; } // TODO: Why nullable? + } + + /// + /// This define the interface of both personal timeline and ordinary timeline. + /// + public interface ITimelineService + { + /// + /// Get the timeline last modified time (not include name change). + /// + /// The name of the timeline. + /// The timeline info. + /// Thrown when is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + Task GetTimelineLastModifiedTime(string timelineName); + + /// + /// Get the timeline unique id. + /// + /// The name of the timeline. + /// The timeline info. + /// Thrown when is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + Task GetTimelineUniqueId(string timelineName); + + /// + /// Get the timeline info. + /// + /// The name of the timeline. + /// The timeline info. + /// Thrown when is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + Task GetTimeline(string timelineName); + + /// + /// Set the properties of a timeline. + /// + /// The name of the timeline. + /// The new properties. Null member means not to change. + /// Thrown when or is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + Task ChangeProperty(string timelineName, TimelineChangePropertyRequest newProperties); + + /// + /// Get all the posts in the timeline. + /// + /// The name of the timeline. + /// The time that posts have been modified since. + /// Whether include deleted posts. + /// A list of all posts. + /// Thrown when is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + Task> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false); + + /// + /// Get the etag of data of a post. + /// + /// The name of the timeline of the post. + /// The id of the post. + /// The etag of the data. + /// Thrown when is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + /// Thrown when post of does not exist or has been deleted. + /// Thrown when post has no data. + /// + Task GetPostDataETag(string timelineName, long postId); + + /// + /// Get the data of a post. + /// + /// The name of the timeline of the post. + /// The id of the post. + /// The etag of the data. + /// Thrown when is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + /// Thrown when post of does not exist or has been deleted. + /// Thrown when post has no data. + /// + Task GetPostData(string timelineName, long postId); + + /// + /// Create a new text post in timeline. + /// + /// The name of the timeline to create post against. + /// The author's user id. + /// The content text. + /// The time of the post. If null, then current time is used. + /// The info of the created post. + /// Thrown when or is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + /// Thrown if user of does not exist. + Task CreateTextPost(string timelineName, long authorId, string text, DateTime? time); + + /// + /// Create a new image post in timeline. + /// + /// The name of the timeline to create post against. + /// The author's user id. + /// The image data. + /// The time of the post. If null, then use current time. + /// The info of the created post. + /// Thrown when or is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + /// Thrown if user of does not exist. + /// Thrown if data is not a image. Validated by . + Task CreateImagePost(string timelineName, long authorId, byte[] imageData, DateTime? time); + + /// + /// Delete a post. + /// + /// The name of the timeline to delete post against. + /// The id of the post to delete. + /// Thrown when is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + /// Thrown when the post with given id does not exist or is deleted already. + /// + /// First use to check the permission. + /// + Task DeletePost(string timelineName, long postId); + + /// + /// Delete all posts of the given user. Used when delete a user. + /// + /// The id of the user. + Task DeleteAllPostsOfUser(long userId); + + /// + /// Change member of timeline. + /// + /// The name of the timeline. + /// A list of usernames of members to add. May be null. + /// A list of usernames of members to remove. May be null. + /// Thrown when is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + /// Thrown when names in or is not a valid username. + /// Thrown when one of the user to change does not exist. + /// + /// Operating on a username that is of bad format or does not exist always throws. + /// Add a user that already is a member has no effects. + /// Remove a user that is not a member also has not effects. + /// Add and remove an identical user results in no effects. + /// More than one same usernames are regarded as one. + /// + Task ChangeMember(string timelineName, IList? membersToAdd, IList? membersToRemove); + + /// + /// Check whether a user can manage(change timeline info, member, ...) a timeline. + /// + /// The name of the timeline. + /// The id of the user to check on. + /// True if the user can manage the timeline, otherwise false. + /// Thrown when is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + /// + /// This method does not check whether visitor is administrator. + /// Return false if user with user id does not exist. + /// + Task HasManagePermission(string timelineName, long userId); + + /// + /// Verify whether a visitor has the permission to read a timeline. + /// + /// The name of the timeline. + /// The id of the user to check on. Null means visitor without account. + /// True if can read, false if can't read. + /// Thrown when is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + /// + /// This method does not check whether visitor is administrator. + /// Return false if user with visitor id does not exist. + /// + Task HasReadPermission(string timelineName, long? visitorId); + + /// + /// Verify whether a user has the permission to modify a post. + /// + /// The name of the timeline. + /// The id of the post. + /// The id of the user to check on. + /// True if you want it to throw . Default false. + /// True if can modify, false if can't modify. + /// Thrown when is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + /// Thrown when the post with given id does not exist or is deleted already and is true. + /// + /// Unless is true, this method should return true if the post does not exist. + /// If the post is deleted, its author info still exists, so it is checked as the post is not deleted unless is true. + /// This method does not check whether the user is administrator. + /// It only checks whether he is the author of the post or the owner of the timeline. + /// Return false when user with modifier id does not exist. + /// + Task HasPostModifyPermission(string timelineName, long postId, long modifierId, bool throwOnPostNotExist = false); + + /// + /// Verify whether a user is member of a timeline. + /// + /// The name of the timeline. + /// The id of user to check on. + /// True if it is a member, false if not. + /// Thrown when is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + /// + /// Timeline owner is also considered as a member. + /// Return false when user with user id does not exist. + /// + Task IsMemberOf(string timelineName, long userId); + + /// + /// Get all timelines including personal and ordinary timelines. + /// + /// Filter timelines related (own or is a member) to specific user. + /// Filter timelines with given visibility. If null or empty, all visibilities are returned. Duplicate value are ignored. + /// The list of timelines. + /// + /// If user with related user id does not exist, empty list will be returned. + /// + Task> GetTimelines(TimelineUserRelationship? relate = null, List? visibility = null); + + /// + /// Create a timeline. + /// + /// The name of the timeline. + /// The id of owner of the timeline. + /// The info of the new timeline. + /// Thrown when is null. + /// Thrown when timeline name is invalid. + /// Thrown when the timeline already exists. + /// Thrown when the owner user does not exist. + Task CreateTimeline(string timelineName, long ownerId); + + /// + /// Delete a timeline. + /// + /// The name of the timeline to delete. + /// Thrown when is null. + /// Thrown when timeline name is invalid. + /// Thrown when the timeline does not exist. + Task DeleteTimeline(string timelineName); + + /// + /// Change name of a timeline. + /// + /// The old timeline name. + /// The new timeline name. + /// The new timeline info. + /// Thrown when or is null. + /// Thrown when or is of invalid format. + /// Thrown when timeline does not exist. + /// Thrown when a timeline with new name already exists. + /// + /// You can only change name of general timeline. + /// + Task ChangeTimelineName(string oldTimelineName, string newTimelineName); + } + + public class TimelineService : ITimelineService + { + public TimelineService(ILogger logger, DatabaseContext database, IDataManager dataManager, IUserService userService, IImageValidator imageValidator, IClock clock) + { + _logger = logger; + _database = database; + _dataManager = dataManager; + _userService = userService; + _imageValidator = imageValidator; + _clock = clock; + } + + private readonly ILogger _logger; + + private readonly DatabaseContext _database; + + private readonly IDataManager _dataManager; + + private readonly IUserService _userService; + + private readonly IImageValidator _imageValidator; + + private readonly IClock _clock; + + private readonly UsernameValidator _usernameValidator = new UsernameValidator(); + + private readonly TimelineNameValidator _timelineNameValidator = new TimelineNameValidator(); + + private void ValidateTimelineName(string name, string paramName) + { + if (!_timelineNameValidator.Validate(name, out var message)) + { + throw new ArgumentException(ExceptionTimelineNameBadFormat.AppendAdditionalMessage(message), paramName); + } + } + + /// Remember to include Members when query. + private async Task MapTimelineFromEntity(TimelineEntity entity) + { + var owner = await _userService.GetUserById(entity.OwnerId); + + var members = new List(); + foreach (var memberEntity in entity.Members) + { + members.Add(await _userService.GetUserById(memberEntity.UserId)); + } + + var name = entity.Name ?? ("@" + owner.Username); + + return new Models.Timeline + { + UniqueID = entity.UniqueId, + Name = name, + NameLastModified = entity.NameLastModified, + Title = string.IsNullOrEmpty(entity.Title) ? name : entity.Title, + Description = entity.Description ?? "", + Owner = owner, + Visibility = entity.Visibility, + Members = members, + CreateTime = entity.CreateTime, + LastModified = entity.LastModified + }; + } + + private async Task MapTimelinePostFromEntity(TimelinePostEntity entity, string timelineName) + { + User? author = entity.AuthorId.HasValue ? await _userService.GetUserById(entity.AuthorId.Value) : null; + + ITimelinePostContent? content = null; + + if (entity.Content != null) + { + var type = entity.ContentType; + + content = type switch + { + TimelinePostContentTypes.Text => new TextTimelinePostContent(entity.Content), + TimelinePostContentTypes.Image => new ImageTimelinePostContent(entity.Content), + _ => throw new DatabaseCorruptedException(string.Format(CultureInfo.InvariantCulture, ExceptionDatabaseUnknownContentType, type)) + }; + } + + return new TimelinePost( + id: entity.LocalId, + author: author, + content: content, + time: entity.Time, + lastUpdated: entity.LastUpdated, + timelineName: timelineName + ); + } + + private TimelineEntity CreateNewTimelineEntity(string? name, long ownerId) + { + var currentTime = _clock.GetCurrentTime(); + + return new TimelineEntity + { + Name = name, + NameLastModified = currentTime, + OwnerId = ownerId, + Visibility = TimelineVisibility.Register, + CreateTime = currentTime, + LastModified = currentTime, + CurrentPostLocalId = 0, + Members = new List() + }; + } + + + + // Get timeline id by name. If it is a personal timeline and it does not exist, it will be created. + // + // This method will check the name format and if it is invalid, ArgumentException is thrown. + // + // For personal timeline, if the user does not exist, TimelineNotExistException will be thrown with UserNotExistException as inner exception. + // For ordinary timeline, if the timeline does not exist, TimelineNotExistException will be thrown. + // + // It follows all timeline-related function common interface contracts. + private async Task FindTimelineId(string timelineName) + { + timelineName = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); + + if (isPersonal) + { + long userId; + try + { + userId = await _userService.GetUserIdByUsername(timelineName); + } + catch (ArgumentException e) + { + throw new ArgumentException(ExceptionFindTimelineUsernameBadFormat, nameof(timelineName), e); + } + catch (UserNotExistException e) + { + throw new TimelineNotExistException(timelineName, e); + } + + var timelineEntity = await _database.Timelines.Where(t => t.OwnerId == userId && t.Name == null).Select(t => new { t.Id }).SingleOrDefaultAsync(); + + if (timelineEntity != null) + { + return timelineEntity.Id; + } + else + { + var newTimelineEntity = CreateNewTimelineEntity(null, userId); + _database.Timelines.Add(newTimelineEntity); + await _database.SaveChangesAsync(); + + return newTimelineEntity.Id; + } + } + else + { + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + ValidateTimelineName(timelineName, nameof(timelineName)); + + var timelineEntity = await _database.Timelines.Where(t => t.Name == timelineName).Select(t => new { t.Id }).SingleOrDefaultAsync(); + + if (timelineEntity == null) + { + throw new TimelineNotExistException(timelineName); + } + else + { + return timelineEntity.Id; + } + } + } + + public async Task GetTimelineLastModifiedTime(string timelineName) + { + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + var timelineId = await FindTimelineId(timelineName); + + var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.LastModified }).SingleAsync(); + + return timelineEntity.LastModified; + } + + public async Task GetTimelineUniqueId(string timelineName) + { + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + var timelineId = await FindTimelineId(timelineName); + + var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.UniqueId }).SingleAsync(); + + return timelineEntity.UniqueId; + } + + public async Task GetTimeline(string timelineName) + { + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + var timelineId = await FindTimelineId(timelineName); + + var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Include(t => t.Members).SingleAsync(); + + return await MapTimelineFromEntity(timelineEntity); + } + + public async Task> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false) + { + modifiedSince = modifiedSince?.MyToUtc(); + + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + var timelineId = await FindTimelineId(timelineName); + IQueryable query = _database.TimelinePosts.Where(p => p.TimelineId == timelineId); + + if (!includeDeleted) + { + query = query.Where(p => p.Content != null); + } + + if (modifiedSince.HasValue) + { + query = query.Include(p => p.Author).Where(p => p.LastUpdated >= modifiedSince || (p.Author != null && p.Author.UsernameChangeTime >= modifiedSince)); + } + + query = query.OrderBy(p => p.Time); + + var postEntities = await query.ToListAsync(); + + var posts = new List(); + foreach (var entity in postEntities) + { + posts.Add(await MapTimelinePostFromEntity(entity, timelineName)); + } + return posts; + } + + public async Task GetPostDataETag(string timelineName, long postId) + { + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + var timelineId = await FindTimelineId(timelineName); + + var postEntity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync(); + + if (postEntity == null) + throw new TimelinePostNotExistException(timelineName, postId, false); + + if (postEntity.Content == null) + throw new TimelinePostNotExistException(timelineName, postId, true); + + if (postEntity.ContentType != TimelinePostContentTypes.Image) + throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost); + + var tag = postEntity.Content; + + return tag; + } + + public async Task GetPostData(string timelineName, long postId) + { + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + var timelineId = await FindTimelineId(timelineName); + var postEntity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync(); + + if (postEntity == null) + throw new TimelinePostNotExistException(timelineName, postId, false); + + if (postEntity.Content == null) + throw new TimelinePostNotExistException(timelineName, postId, true); + + if (postEntity.ContentType != TimelinePostContentTypes.Image) + throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost); + + var tag = postEntity.Content; + + byte[] data; + + try + { + data = await _dataManager.GetEntry(tag); + } + catch (InvalidOperationException e) + { + throw new DatabaseCorruptedException(ExceptionGetDataDataEntryNotExist, e); + } + + if (postEntity.ExtraContent == null) + { + _logger.LogWarning(LogGetDataNoFormat); + var format = Image.DetectFormat(data); + postEntity.ExtraContent = format.DefaultMimeType; + await _database.SaveChangesAsync(); + } + + return new PostData + { + Data = data, + Type = postEntity.ExtraContent, + ETag = tag, + LastModified = postEntity.LastUpdated + }; + } + + public async Task CreateTextPost(string timelineName, long authorId, string text, DateTime? time) + { + time = time?.MyToUtc(); + + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + if (text == null) + throw new ArgumentNullException(nameof(text)); + + var timelineId = await FindTimelineId(timelineName); + var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); + + var author = await _userService.GetUserById(authorId); + + var currentTime = _clock.GetCurrentTime(); + var finalTime = time ?? currentTime; + + timelineEntity.CurrentPostLocalId += 1; + + var postEntity = new TimelinePostEntity + { + LocalId = timelineEntity.CurrentPostLocalId, + ContentType = TimelinePostContentTypes.Text, + Content = text, + AuthorId = authorId, + TimelineId = timelineId, + Time = finalTime, + LastUpdated = currentTime + }; + _database.TimelinePosts.Add(postEntity); + await _database.SaveChangesAsync(); + + + return new TimelinePost( + id: postEntity.LocalId, + content: new TextTimelinePostContent(text), + time: finalTime, + author: author, + lastUpdated: currentTime, + timelineName: timelineName + ); + } + + public async Task CreateImagePost(string timelineName, long authorId, byte[] data, DateTime? time) + { + time = time?.MyToUtc(); + + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + if (data == null) + throw new ArgumentNullException(nameof(data)); + + var timelineId = await FindTimelineId(timelineName); + var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); + + var author = await _userService.GetUserById(authorId); + + var imageFormat = await _imageValidator.Validate(data); + + var imageFormatText = imageFormat.DefaultMimeType; + + var tag = await _dataManager.RetainEntry(data); + + var currentTime = _clock.GetCurrentTime(); + var finalTime = time ?? currentTime; + + timelineEntity.CurrentPostLocalId += 1; + + var postEntity = new TimelinePostEntity + { + LocalId = timelineEntity.CurrentPostLocalId, + ContentType = TimelinePostContentTypes.Image, + Content = tag, + ExtraContent = imageFormatText, + AuthorId = authorId, + TimelineId = timelineId, + Time = finalTime, + LastUpdated = currentTime + }; + _database.TimelinePosts.Add(postEntity); + await _database.SaveChangesAsync(); + + return new TimelinePost( + id: postEntity.LocalId, + content: new ImageTimelinePostContent(tag), + time: finalTime, + author: author, + lastUpdated: currentTime, + timelineName: timelineName + ); + } + + public async Task DeletePost(string timelineName, long id) + { + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + var timelineId = await FindTimelineId(timelineName); + + var post = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == id).SingleOrDefaultAsync(); + + if (post == null) + throw new TimelinePostNotExistException(timelineName, id, false); + + if (post.Content == null) + throw new TimelinePostNotExistException(timelineName, id, true); + + string? dataTag = null; + + if (post.ContentType == TimelinePostContentTypes.Image) + { + dataTag = post.Content; + } + + post.Content = null; + post.LastUpdated = _clock.GetCurrentTime(); + + await _database.SaveChangesAsync(); + + if (dataTag != null) + { + await _dataManager.FreeEntry(dataTag); + } + } + + public async Task DeleteAllPostsOfUser(long userId) + { + var posts = await _database.TimelinePosts.Where(p => p.AuthorId == userId).ToListAsync(); + + var now = _clock.GetCurrentTime(); + + var dataTags = new List(); + + foreach (var post in posts) + { + if (post.Content != null) + { + if (post.ContentType == TimelinePostContentTypes.Image) + { + dataTags.Add(post.Content); + } + post.Content = null; + } + post.LastUpdated = now; + } + + await _database.SaveChangesAsync(); + + foreach (var dataTag in dataTags) + { + await _dataManager.FreeEntry(dataTag); + } + } + + public async Task ChangeProperty(string timelineName, TimelineChangePropertyRequest newProperties) + { + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + if (newProperties == null) + throw new ArgumentNullException(nameof(newProperties)); + + var timelineId = await FindTimelineId(timelineName); + + var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); + + var changed = false; + + if (newProperties.Title != null) + { + changed = true; + timelineEntity.Title = newProperties.Title; + } + + if (newProperties.Description != null) + { + changed = true; + timelineEntity.Description = newProperties.Description; + } + + if (newProperties.Visibility.HasValue) + { + changed = true; + timelineEntity.Visibility = newProperties.Visibility.Value; + } + + if (changed) + { + var currentTime = _clock.GetCurrentTime(); + timelineEntity.LastModified = currentTime; + } + + await _database.SaveChangesAsync(); + } + + public async Task ChangeMember(string timelineName, IList? add, IList? remove) + { + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + List? RemoveDuplicateAndCheckFormat(IList? list, string paramName) + { + if (list != null) + { + List result = new List(); + var count = list.Count; + for (var index = 0; index < count; index++) + { + var username = list[index]; + if (result.Contains(username)) + { + continue; + } + var (validationResult, message) = _usernameValidator.Validate(username); + if (!validationResult) + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionChangeMemberUsernameBadFormat, index), nameof(paramName)); + result.Add(username); + } + return result; + } + else + { + return null; + } + } + var simplifiedAdd = RemoveDuplicateAndCheckFormat(add, nameof(add)); + var simplifiedRemove = RemoveDuplicateAndCheckFormat(remove, nameof(remove)); + + // remove those both in add and remove + if (simplifiedAdd != null && simplifiedRemove != null) + { + var usersToClean = simplifiedRemove.Where(u => simplifiedAdd.Contains(u)).ToList(); + foreach (var u in usersToClean) + { + simplifiedAdd.Remove(u); + simplifiedRemove.Remove(u); + } + + if (simplifiedAdd.Count == 0) + simplifiedAdd = null; + + if (simplifiedRemove.Count == 0) + simplifiedRemove = null; + } + + if (simplifiedAdd == null && simplifiedRemove == null) + return; + + var timelineId = await FindTimelineId(timelineName); + + async Task?> CheckExistenceAndGetId(List? list) + { + if (list == null) + return null; + + List result = new List(); + foreach (var username in list) + { + result.Add(await _userService.GetUserIdByUsername(username)); + } + return result; + } + var userIdsAdd = await CheckExistenceAndGetId(simplifiedAdd); + var userIdsRemove = await CheckExistenceAndGetId(simplifiedRemove); + + if (userIdsAdd != null) + { + var membersToAdd = userIdsAdd.Select(id => new TimelineMemberEntity { UserId = id, TimelineId = timelineId }).ToList(); + _database.TimelineMembers.AddRange(membersToAdd); + } + + if (userIdsRemove != null) + { + var membersToRemove = await _database.TimelineMembers.Where(m => m.TimelineId == timelineId && userIdsRemove.Contains(m.UserId)).ToListAsync(); + _database.TimelineMembers.RemoveRange(membersToRemove); + } + + var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); + timelineEntity.LastModified = _clock.GetCurrentTime(); + + await _database.SaveChangesAsync(); + } + + public async Task HasManagePermission(string timelineName, long userId) + { + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + var timelineId = await FindTimelineId(timelineName); + var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync(); + + return userId == timelineEntity.OwnerId; + } + + public async Task HasReadPermission(string timelineName, long? visitorId) + { + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + var timelineId = await FindTimelineId(timelineName); + var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.Visibility }).SingleAsync(); + + if (timelineEntity.Visibility == TimelineVisibility.Public) + return true; + + if (timelineEntity.Visibility == TimelineVisibility.Register && visitorId != null) + return true; + + if (visitorId == null) + { + return false; + } + else + { + var memberEntity = await _database.TimelineMembers.Where(m => m.UserId == visitorId && m.TimelineId == timelineId).SingleOrDefaultAsync(); + return memberEntity != null; + } + } + + public async Task HasPostModifyPermission(string timelineName, long postId, long modifierId, bool throwOnPostNotExist = false) + { + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + var timelineId = await FindTimelineId(timelineName); + + var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync(); + + var postEntity = await _database.TimelinePosts.Where(p => p.Id == postId).Select(p => new { p.Content, p.AuthorId }).SingleOrDefaultAsync(); + + if (postEntity == null) + { + if (throwOnPostNotExist) + throw new TimelinePostNotExistException(timelineName, postId, false); + else + return true; + } + + if (postEntity.Content == null && throwOnPostNotExist) + { + throw new TimelinePostNotExistException(timelineName, postId, true); + } + + return timelineEntity.OwnerId == modifierId || postEntity.AuthorId == modifierId; + } + + public async Task IsMemberOf(string timelineName, long userId) + { + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + var timelineId = await FindTimelineId(timelineName); + + var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync(); + + if (userId == timelineEntity.OwnerId) + return true; + + return await _database.TimelineMembers.AnyAsync(m => m.TimelineId == timelineId && m.UserId == userId); + } + + public async Task> GetTimelines(TimelineUserRelationship? relate = null, List? visibility = null) + { + List entities; + + IQueryable ApplyTimelineVisibilityFilter(IQueryable query) + { + if (visibility != null && visibility.Count != 0) + { + return query.Where(t => visibility.Contains(t.Visibility)); + } + return query; + } + + bool allVisibilities = visibility == null || visibility.Count == 0; + + if (relate == null) + { + entities = await ApplyTimelineVisibilityFilter(_database.Timelines).Include(t => t.Members).ToListAsync(); + } + else + { + entities = new List(); + + if ((relate.Type & TimelineUserRelationshipType.Own) != 0) + { + entities.AddRange(await ApplyTimelineVisibilityFilter(_database.Timelines.Where(t => t.OwnerId == relate.UserId)).Include(t => t.Members).ToListAsync()); + } + + if ((relate.Type & TimelineUserRelationshipType.Join) != 0) + { + entities.AddRange(await ApplyTimelineVisibilityFilter(_database.TimelineMembers.Where(m => m.UserId == relate.UserId).Include(m => m.Timeline).ThenInclude(t => t.Members).Select(m => m.Timeline)).ToListAsync()); + } + } + + var result = new List(); + + foreach (var entity in entities) + { + result.Add(await MapTimelineFromEntity(entity)); + } + + return result; + } + + public async Task CreateTimeline(string name, long owner) + { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + ValidateTimelineName(name, nameof(name)); + + var user = await _userService.GetUserById(owner); + + var conflict = await _database.Timelines.AnyAsync(t => t.Name == name); + + if (conflict) + throw new EntityAlreadyExistException(EntityNames.Timeline, null, ExceptionTimelineNameConflict); + + var newEntity = CreateNewTimelineEntity(name, user.Id!.Value); + + _database.Timelines.Add(newEntity); + await _database.SaveChangesAsync(); + + return await MapTimelineFromEntity(newEntity); + } + + public async Task DeleteTimeline(string name) + { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + ValidateTimelineName(name, nameof(name)); + + var entity = await _database.Timelines.Where(t => t.Name == name).SingleOrDefaultAsync(); + + if (entity == null) + throw new TimelineNotExistException(name); + + _database.Timelines.Remove(entity); + await _database.SaveChangesAsync(); + } + + public async Task ChangeTimelineName(string oldTimelineName, string newTimelineName) + { + if (oldTimelineName == null) + throw new ArgumentNullException(nameof(oldTimelineName)); + if (newTimelineName == null) + throw new ArgumentNullException(nameof(newTimelineName)); + + ValidateTimelineName(oldTimelineName, nameof(oldTimelineName)); + ValidateTimelineName(newTimelineName, nameof(newTimelineName)); + + var entity = await _database.Timelines.Include(t => t.Members).Where(t => t.Name == oldTimelineName).SingleOrDefaultAsync(); + + if (entity == null) + throw new TimelineNotExistException(oldTimelineName); + + if (oldTimelineName == newTimelineName) + return await MapTimelineFromEntity(entity); + + var conflict = await _database.Timelines.AnyAsync(t => t.Name == newTimelineName); + + if (conflict) + throw new EntityAlreadyExistException(EntityNames.Timeline, null, ExceptionTimelineNameConflict); + + var now = _clock.GetCurrentTime(); + + entity.Name = newTimelineName; + entity.NameLastModified = now; + entity.LastModified = now; + + await _database.SaveChangesAsync(); + + return await MapTimelineFromEntity(entity); + } + } +} diff --git a/BackEnd/Timeline/Services/UserAvatarService.cs b/BackEnd/Timeline/Services/UserAvatarService.cs new file mode 100644 index 00000000..b41c45fd --- /dev/null +++ b/BackEnd/Timeline/Services/UserAvatarService.cs @@ -0,0 +1,265 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Entities; +using Timeline.Helpers; +using Timeline.Services.Exceptions; + +namespace Timeline.Services +{ + public class Avatar + { + public string Type { get; set; } = default!; + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "DTO Object")] + public byte[] Data { get; set; } = default!; + } + + public class AvatarInfo + { + public Avatar Avatar { get; set; } = default!; + public DateTime LastModified { get; set; } + + public CacheableData ToCacheableData() + { + return new CacheableData(Avatar.Type, Avatar.Data, LastModified); + } + } + + /// + /// Provider for default user avatar. + /// + /// + /// Mainly for unit tests. + /// + public interface IDefaultUserAvatarProvider + { + /// + /// Get the etag of default avatar. + /// + /// + Task GetDefaultAvatarETag(); + + /// + /// Get the default avatar. + /// + Task GetDefaultAvatar(); + } + + public interface IUserAvatarService + { + /// + /// Get the etag of a user's avatar. Warning: This method does not check the user existence. + /// + /// The id of the user to get avatar etag of. + /// The etag. + Task GetAvatarETag(long id); + + /// + /// Get avatar of a user. If the user has no avatar set, a default one is returned. Warning: This method does not check the user existence. + /// + /// The id of the user to get avatar of. + /// The avatar info. + Task GetAvatar(long id); + + /// + /// Set avatar for a user. Warning: This method does not check the user existence. + /// + /// The id of the user to set avatar for. + /// The avatar. Can be null to delete the saved avatar. + /// The etag of the avatar. + /// Thrown if any field in is null when is not null. + /// Thrown if avatar is of bad format. + Task SetAvatar(long id, Avatar? avatar); + } + + // TODO! : Make this configurable. + public class DefaultUserAvatarProvider : IDefaultUserAvatarProvider + { + private readonly IETagGenerator _eTagGenerator; + + private readonly string _avatarPath; + + private byte[] _cacheData = default!; + private DateTime _cacheLastModified; + private string _cacheETag = default!; + + public DefaultUserAvatarProvider(IWebHostEnvironment environment, IETagGenerator eTagGenerator) + { + _avatarPath = Path.Combine(environment.ContentRootPath, "default-avatar.png"); + _eTagGenerator = eTagGenerator; + } + + private async Task CheckAndInit() + { + var path = _avatarPath; + if (_cacheData == null || File.GetLastWriteTime(path) > _cacheLastModified) + { + _cacheData = await File.ReadAllBytesAsync(path); + _cacheLastModified = File.GetLastWriteTime(path); + _cacheETag = await _eTagGenerator.Generate(_cacheData); + } + } + + public async Task GetDefaultAvatarETag() + { + await CheckAndInit(); + return _cacheETag; + } + + public async Task GetDefaultAvatar() + { + await CheckAndInit(); + return new AvatarInfo + { + Avatar = new Avatar + { + Type = "image/png", + Data = _cacheData + }, + LastModified = _cacheLastModified + }; + } + } + + public class UserAvatarService : IUserAvatarService + { + + private readonly ILogger _logger; + + private readonly DatabaseContext _database; + + private readonly IDefaultUserAvatarProvider _defaultUserAvatarProvider; + + private readonly IImageValidator _imageValidator; + + private readonly IDataManager _dataManager; + + private readonly IClock _clock; + + public UserAvatarService( + ILogger logger, + DatabaseContext database, + IDefaultUserAvatarProvider defaultUserAvatarProvider, + IImageValidator imageValidator, + IDataManager dataManager, + IClock clock) + { + _logger = logger; + _database = database; + _defaultUserAvatarProvider = defaultUserAvatarProvider; + _imageValidator = imageValidator; + _dataManager = dataManager; + _clock = clock; + } + + public async Task GetAvatarETag(long id) + { + var eTag = (await _database.UserAvatars.Where(a => a.UserId == id).Select(a => new { a.DataTag }).SingleOrDefaultAsync())?.DataTag; + if (eTag == null) + return await _defaultUserAvatarProvider.GetDefaultAvatarETag(); + else + return eTag; + } + + public async Task GetAvatar(long id) + { + var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == id).Select(a => new { a.Type, a.DataTag, a.LastModified }).SingleOrDefaultAsync(); + + if (avatarEntity != null) + { + if (!LanguageHelper.AreSame(avatarEntity.DataTag == null, avatarEntity.Type == null)) + { + var message = Resources.Services.UserAvatarService.ExceptionDatabaseCorruptedDataAndTypeNotSame; + _logger.LogCritical(message); + throw new DatabaseCorruptedException(message); + } + + + if (avatarEntity.DataTag != null) + { + var data = await _dataManager.GetEntry(avatarEntity.DataTag); + return new AvatarInfo + { + Avatar = new Avatar + { + Type = avatarEntity.Type!, + Data = data + }, + LastModified = avatarEntity.LastModified + }; + } + } + var defaultAvatar = await _defaultUserAvatarProvider.GetDefaultAvatar(); + if (avatarEntity != null) + defaultAvatar.LastModified = defaultAvatar.LastModified > avatarEntity.LastModified ? defaultAvatar.LastModified : avatarEntity.LastModified; + return defaultAvatar; + } + + public async Task SetAvatar(long id, Avatar? avatar) + { + if (avatar != null) + { + if (avatar.Data == null) + throw new ArgumentException(Resources.Services.UserAvatarService.ExceptionAvatarDataNull, nameof(avatar)); + if (string.IsNullOrEmpty(avatar.Type)) + throw new ArgumentException(Resources.Services.UserAvatarService.ExceptionAvatarTypeNullOrEmpty, nameof(avatar)); + } + + var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == id).SingleOrDefaultAsync(); + + if (avatar == null) + { + if (avatarEntity != null && avatarEntity.DataTag != null) + { + await _dataManager.FreeEntry(avatarEntity.DataTag); + avatarEntity.DataTag = null; + avatarEntity.Type = null; + avatarEntity.LastModified = _clock.GetCurrentTime(); + await _database.SaveChangesAsync(); + _logger.LogInformation(Resources.Services.UserAvatarService.LogUpdateEntity); + } + return await _defaultUserAvatarProvider.GetDefaultAvatarETag(); + } + else + { + await _imageValidator.Validate(avatar.Data, avatar.Type, true); + var tag = await _dataManager.RetainEntry(avatar.Data); + var oldTag = avatarEntity?.DataTag; + var create = avatarEntity == null; + if (avatarEntity == null) + { + avatarEntity = new UserAvatarEntity(); + _database.UserAvatars.Add(avatarEntity); + } + avatarEntity.DataTag = tag; + avatarEntity.Type = avatar.Type; + avatarEntity.LastModified = _clock.GetCurrentTime(); + avatarEntity.UserId = id; + await _database.SaveChangesAsync(); + _logger.LogInformation(create ? + Resources.Services.UserAvatarService.LogCreateEntity + : Resources.Services.UserAvatarService.LogUpdateEntity); + if (oldTag != null) + { + await _dataManager.FreeEntry(oldTag); + } + + return avatarEntity.DataTag; + } + } + } + + public static class UserAvatarServiceCollectionExtensions + { + public static void AddUserAvatarService(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + } + } +} diff --git a/BackEnd/Timeline/Services/UserDeleteService.cs b/BackEnd/Timeline/Services/UserDeleteService.cs new file mode 100644 index 00000000..845de573 --- /dev/null +++ b/BackEnd/Timeline/Services/UserDeleteService.cs @@ -0,0 +1,69 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Entities; +using Timeline.Helpers; +using Timeline.Models.Validation; +using static Timeline.Resources.Services.UserService; + +namespace Timeline.Services +{ + public interface IUserDeleteService + { + /// + /// Delete a user of given username. + /// + /// Username of the user to delete. Can't be null. + /// True if user is deleted, false if user not exist. + /// Thrown if is null. + /// Thrown when is of bad format. + Task DeleteUser(string username); + } + + public class UserDeleteService : IUserDeleteService + { + private readonly ILogger _logger; + + private readonly DatabaseContext _databaseContext; + + private readonly ITimelineService _timelineService; + + private readonly UsernameValidator _usernameValidator = new UsernameValidator(); + + public UserDeleteService(ILogger logger, DatabaseContext databaseContext, ITimelineService timelineService) + { + _logger = logger; + _databaseContext = databaseContext; + _timelineService = timelineService; + } + + public async Task DeleteUser(string username) + { + if (username == null) + throw new ArgumentNullException(nameof(username)); + + if (!_usernameValidator.Validate(username, out var message)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionUsernameBadFormat, message), nameof(username)); + } + + var user = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync(); + if (user == null) + return false; + + await _timelineService.DeleteAllPostsOfUser(user.Id); + + _databaseContext.Users.Remove(user); + + await _databaseContext.SaveChangesAsync(); + _logger.LogInformation(Log.Format(LogDatabaseRemove, ("Id", user.Id), ("Username", user.Username))); + + return true; + } + + } +} diff --git a/BackEnd/Timeline/Services/UserRoleConvert.cs b/BackEnd/Timeline/Services/UserRoleConvert.cs new file mode 100644 index 00000000..f27ee1bb --- /dev/null +++ b/BackEnd/Timeline/Services/UserRoleConvert.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Timeline.Entities; + +namespace Timeline.Services +{ + public static class UserRoleConvert + { + public const string UserRole = UserRoles.User; + public const string AdminRole = UserRoles.Admin; + + public static string[] ToArray(bool administrator) + { + return administrator ? new string[] { UserRole, AdminRole } : new string[] { UserRole }; + } + + public static string[] ToArray(string s) + { + return s.Split(',').ToArray(); + } + + public static bool ToBool(IReadOnlyCollection roles) + { + return roles.Contains(AdminRole); + } + + public static string ToString(IReadOnlyCollection roles) + { + return string.Join(',', roles); + } + + public static string ToString(bool administrator) + { + return administrator ? UserRole + "," + AdminRole : UserRole; + } + + public static bool ToBool(string s) + { + return s.Contains("admin", StringComparison.InvariantCulture); + } + } +} diff --git a/BackEnd/Timeline/Services/UserService.cs b/BackEnd/Timeline/Services/UserService.cs new file mode 100644 index 00000000..821bc33d --- /dev/null +++ b/BackEnd/Timeline/Services/UserService.cs @@ -0,0 +1,437 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using System; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Entities; +using Timeline.Helpers; +using Timeline.Models; +using Timeline.Models.Validation; +using Timeline.Services.Exceptions; +using static Timeline.Resources.Services.UserService; + +namespace Timeline.Services +{ + public interface IUserService + { + /// + /// Try to verify the given username and password. + /// + /// The username of the user to verify. + /// The password of the user to verify. + /// The user info and auth info. + /// Thrown when or is null. + /// Thrown when is of bad format or is empty. + /// Thrown when the user with given username does not exist. + /// Thrown when password is wrong. + Task VerifyCredential(string username, string password); + + /// + /// Try to get a user by id. + /// + /// The id of the user. + /// The user info. + /// Thrown when the user with given id does not exist. + Task GetUserById(long id); + + /// + /// Get the user info of given username. + /// + /// Username of the user. + /// The info of the user. + /// Thrown when is null. + /// Thrown when is of bad format. + /// Thrown when the user with given username does not exist. + Task GetUserByUsername(string username); + + /// + /// Get the user id of given username. + /// + /// Username of the user. + /// The id of the user. + /// Thrown when is null. + /// Thrown when is of bad format. + /// Thrown when the user with given username does not exist. + Task GetUserIdByUsername(string username); + + /// + /// List all users. + /// + /// The user info of users. + Task GetUsers(); + + /// + /// Create a user with given info. + /// + /// The info of new user. + /// The the new user. + /// Thrown when is null. + /// Thrown when some fields in is bad. + /// Thrown when a user with given username already exists. + /// + /// must not be null and must be a valid username. + /// must not be null or empty. + /// is false by default (null). + /// must be a valid nickname if set. It is empty by default. + /// Other fields are ignored. + /// + Task CreateUser(User info); + + /// + /// Modify a user's info. + /// + /// The id of the user. + /// The new info. May be null. + /// The new user info. + /// Thrown when some fields in is bad. + /// Thrown when user with given id does not exist. + /// + /// Only , , and will be used. + /// If null, then not change. + /// Other fields are ignored. + /// Version will increase if password is changed. + /// + /// must be a valid username if set. + /// can't be empty if set. + /// must be a valid nickname if set. + /// + /// + /// + Task ModifyUser(long id, User? info); + + /// + /// Modify a user's info. + /// + /// The username of the user. + /// The new info. May be null. + /// The new user info. + /// Thrown when is null. + /// Thrown when is of bad format or some fields in is bad. + /// Thrown when user with given id does not exist. + /// Thrown when user with the newusername already exist. + /// + /// Only , and will be used. + /// If null, then not change. + /// Other fields are ignored. + /// After modified, even if nothing is changed, version will increase. + /// + /// must be a valid username if set. + /// can't be empty if set. + /// must be a valid nickname if set. + /// + /// Note: Whether is set or not, version will increase and not set to the specified value if there is one. + /// + /// + Task ModifyUser(string username, User? info); + + /// + /// Try to change a user's password with old password. + /// + /// The id of user to change password of. + /// Old password. + /// New password. + /// Thrown if or is null. + /// Thrown if or is empty. + /// Thrown if the user with given username does not exist. + /// Thrown if the old password is wrong. + Task ChangePassword(long id, string oldPassword, string newPassword); + } + + public class UserService : IUserService + { + private readonly ILogger _logger; + private readonly IClock _clock; + + private readonly DatabaseContext _databaseContext; + + private readonly IPasswordService _passwordService; + + private readonly UsernameValidator _usernameValidator = new UsernameValidator(); + private readonly NicknameValidator _nicknameValidator = new NicknameValidator(); + public UserService(ILogger logger, DatabaseContext databaseContext, IPasswordService passwordService, IClock clock) + { + _logger = logger; + _clock = clock; + _databaseContext = databaseContext; + _passwordService = passwordService; + } + + private void CheckUsernameFormat(string username, string? paramName) + { + if (!_usernameValidator.Validate(username, out var message)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionUsernameBadFormat, message), paramName); + } + } + + private static void CheckPasswordFormat(string password, string? paramName) + { + if (password.Length == 0) + { + throw new ArgumentException(ExceptionPasswordEmpty, paramName); + } + } + + private void CheckNicknameFormat(string nickname, string? paramName) + { + if (!_nicknameValidator.Validate(nickname, out var message)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionNicknameBadFormat, message), paramName); + } + } + + private static void ThrowUsernameConflict() + { + throw new EntityAlreadyExistException(EntityNames.User, ExceptionUsernameConflict); + } + + private static User CreateUserFromEntity(UserEntity entity) + { + return new User + { + UniqueId = entity.UniqueId, + Username = entity.Username, + Administrator = UserRoleConvert.ToBool(entity.Roles), + Nickname = string.IsNullOrEmpty(entity.Nickname) ? entity.Username : entity.Nickname, + Id = entity.Id, + Version = entity.Version, + CreateTime = entity.CreateTime, + UsernameChangeTime = entity.UsernameChangeTime, + LastModified = entity.LastModified + }; + } + + public async Task VerifyCredential(string username, string password) + { + if (username == null) + throw new ArgumentNullException(nameof(username)); + if (password == null) + throw new ArgumentNullException(nameof(password)); + + CheckUsernameFormat(username, nameof(username)); + CheckPasswordFormat(password, nameof(password)); + + var entity = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync(); + + if (entity == null) + throw new UserNotExistException(username); + + if (!_passwordService.VerifyPassword(entity.Password, password)) + throw new BadPasswordException(password); + + return CreateUserFromEntity(entity); + } + + public async Task GetUserById(long id) + { + var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); + + if (user == null) + throw new UserNotExistException(id); + + return CreateUserFromEntity(user); + } + + public async Task GetUserByUsername(string username) + { + if (username == null) + throw new ArgumentNullException(nameof(username)); + + CheckUsernameFormat(username, nameof(username)); + + var entity = await _databaseContext.Users.Where(user => user.Username == username).SingleOrDefaultAsync(); + + if (entity == null) + throw new UserNotExistException(username); + + return CreateUserFromEntity(entity); + } + + public async Task GetUserIdByUsername(string username) + { + if (username == null) + throw new ArgumentNullException(nameof(username)); + + CheckUsernameFormat(username, nameof(username)); + + var entity = await _databaseContext.Users.Where(user => user.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); + + if (entity == null) + throw new UserNotExistException(username); + + return entity.Id; + } + + public async Task GetUsers() + { + var entities = await _databaseContext.Users.ToArrayAsync(); + return entities.Select(user => CreateUserFromEntity(user)).ToArray(); + } + + public async Task CreateUser(User info) + { + if (info == null) + throw new ArgumentNullException(nameof(info)); + + if (info.Username == null) + throw new ArgumentException(ExceptionUsernameNull, nameof(info)); + CheckUsernameFormat(info.Username, nameof(info)); + + if (info.Password == null) + throw new ArgumentException(ExceptionPasswordNull, nameof(info)); + CheckPasswordFormat(info.Password, nameof(info)); + + if (info.Nickname != null) + CheckNicknameFormat(info.Nickname, nameof(info)); + + var username = info.Username; + + var conflict = await _databaseContext.Users.AnyAsync(u => u.Username == username); + if (conflict) + ThrowUsernameConflict(); + + var administrator = info.Administrator ?? false; + var password = info.Password; + var nickname = info.Nickname; + + var newEntity = new UserEntity + { + Username = username, + Password = _passwordService.HashPassword(password), + Roles = UserRoleConvert.ToString(administrator), + Nickname = nickname, + Version = 1 + }; + _databaseContext.Users.Add(newEntity); + await _databaseContext.SaveChangesAsync(); + + _logger.LogInformation(Log.Format(LogDatabaseCreate, + ("Id", newEntity.Id), ("Username", username), ("Administrator", administrator))); + + return CreateUserFromEntity(newEntity); + } + + private void ValidateModifyUserInfo(User? info) + { + if (info != null) + { + if (info.Username != null) + CheckUsernameFormat(info.Username, nameof(info)); + + if (info.Password != null) + CheckPasswordFormat(info.Password, nameof(info)); + + if (info.Nickname != null) + CheckNicknameFormat(info.Nickname, nameof(info)); + } + } + + private async Task UpdateUserEntity(UserEntity entity, User? info) + { + if (info != null) + { + var now = _clock.GetCurrentTime(); + bool updateLastModified = false; + + var username = info.Username; + if (username != null && username != entity.Username) + { + var conflict = await _databaseContext.Users.AnyAsync(u => u.Username == username); + if (conflict) + ThrowUsernameConflict(); + + entity.Username = username; + entity.UsernameChangeTime = now; + updateLastModified = true; + } + + var password = info.Password; + if (password != null) + { + entity.Password = _passwordService.HashPassword(password); + entity.Version += 1; + } + + var administrator = info.Administrator; + if (administrator.HasValue && UserRoleConvert.ToBool(entity.Roles) != administrator) + { + entity.Roles = UserRoleConvert.ToString(administrator.Value); + updateLastModified = true; + } + + var nickname = info.Nickname; + if (nickname != null && nickname != entity.Nickname) + { + entity.Nickname = nickname; + updateLastModified = true; + } + + if (updateLastModified) + { + entity.LastModified = now; + } + } + } + + + public async Task ModifyUser(long id, User? info) + { + ValidateModifyUserInfo(info); + + var entity = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); + if (entity == null) + throw new UserNotExistException(id); + + await UpdateUserEntity(entity, info); + + await _databaseContext.SaveChangesAsync(); + _logger.LogInformation(LogDatabaseUpdate, ("Id", id)); + + return CreateUserFromEntity(entity); + } + + public async Task ModifyUser(string username, User? info) + { + if (username == null) + throw new ArgumentNullException(nameof(username)); + CheckUsernameFormat(username, nameof(username)); + + ValidateModifyUserInfo(info); + + var entity = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync(); + if (entity == null) + throw new UserNotExistException(username); + + await UpdateUserEntity(entity, info); + + await _databaseContext.SaveChangesAsync(); + _logger.LogInformation(LogDatabaseUpdate, ("Username", username)); + + return CreateUserFromEntity(entity); + } + + public async Task ChangePassword(long id, string oldPassword, string newPassword) + { + if (oldPassword == null) + throw new ArgumentNullException(nameof(oldPassword)); + if (newPassword == null) + throw new ArgumentNullException(nameof(newPassword)); + CheckPasswordFormat(oldPassword, nameof(oldPassword)); + CheckPasswordFormat(newPassword, nameof(newPassword)); + + var entity = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); + + if (entity == null) + throw new UserNotExistException(id); + + if (!_passwordService.VerifyPassword(entity.Password, oldPassword)) + throw new BadPasswordException(oldPassword); + + entity.Password = _passwordService.HashPassword(newPassword); + entity.Version += 1; + await _databaseContext.SaveChangesAsync(); + _logger.LogInformation(Log.Format(LogDatabaseUpdate, ("Id", id), ("Operation", "Change password"))); + } + } +} diff --git a/BackEnd/Timeline/Services/UserTokenException.cs b/BackEnd/Timeline/Services/UserTokenException.cs new file mode 100644 index 00000000..d25fabb3 --- /dev/null +++ b/BackEnd/Timeline/Services/UserTokenException.cs @@ -0,0 +1,68 @@ +using System; + +namespace Timeline.Services +{ + + [Serializable] + public class UserTokenException : Exception + { + public UserTokenException() { } + public UserTokenException(string message) : base(message) { } + public UserTokenException(string message, Exception inner) : base(message, inner) { } + public UserTokenException(string token, string message) : base(message) { Token = token; } + public UserTokenException(string token, string message, Exception inner) : base(message, inner) { Token = token; } + protected UserTokenException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + public string Token { get; private set; } = ""; + } + + + [Serializable] + public class UserTokenTimeExpireException : UserTokenException + { + public UserTokenTimeExpireException() : base(Resources.Services.Exception.UserTokenTimeExpireException) { } + public UserTokenTimeExpireException(string message) : base(message) { } + public UserTokenTimeExpireException(string message, Exception inner) : base(message, inner) { } + public UserTokenTimeExpireException(string token, DateTime expireTime, DateTime verifyTime) : base(token, Resources.Services.Exception.UserTokenTimeExpireException) { ExpireTime = expireTime; VerifyTime = verifyTime; } + public UserTokenTimeExpireException(string token, DateTime expireTime, DateTime verifyTime, Exception inner) : base(token, Resources.Services.Exception.UserTokenTimeExpireException, inner) { ExpireTime = expireTime; VerifyTime = verifyTime; } + protected UserTokenTimeExpireException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + public DateTime ExpireTime { get; private set; } + + public DateTime VerifyTime { get; private set; } + } + + [Serializable] + public class UserTokenBadVersionException : UserTokenException + { + public UserTokenBadVersionException() : base(Resources.Services.Exception.UserTokenBadVersionException) { } + public UserTokenBadVersionException(string message) : base(message) { } + public UserTokenBadVersionException(string message, Exception inner) : base(message, inner) { } + public UserTokenBadVersionException(string token, long tokenVersion, long requiredVersion) : base(token, Resources.Services.Exception.UserTokenBadVersionException) { TokenVersion = tokenVersion; RequiredVersion = requiredVersion; } + public UserTokenBadVersionException(string token, long tokenVersion, long requiredVersion, Exception inner) : base(token, Resources.Services.Exception.UserTokenBadVersionException, inner) { TokenVersion = tokenVersion; RequiredVersion = requiredVersion; } + protected UserTokenBadVersionException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + public long TokenVersion { get; set; } + + public long RequiredVersion { get; set; } + } + + [Serializable] + public class UserTokenBadFormatException : UserTokenException + { + public UserTokenBadFormatException() : base(Resources.Services.Exception.UserTokenBadFormatException) { } + public UserTokenBadFormatException(string token) : base(token, Resources.Services.Exception.UserTokenBadFormatException) { } + public UserTokenBadFormatException(string token, string message) : base(token, message) { } + public UserTokenBadFormatException(string token, Exception inner) : base(token, Resources.Services.Exception.UserTokenBadFormatException, inner) { } + public UserTokenBadFormatException(string token, string message, Exception inner) : base(token, message, inner) { } + protected UserTokenBadFormatException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + } +} diff --git a/BackEnd/Timeline/Services/UserTokenManager.cs b/BackEnd/Timeline/Services/UserTokenManager.cs new file mode 100644 index 00000000..813dae67 --- /dev/null +++ b/BackEnd/Timeline/Services/UserTokenManager.cs @@ -0,0 +1,97 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Threading.Tasks; +using Timeline.Helpers; +using Timeline.Models; +using Timeline.Services.Exceptions; + +namespace Timeline.Services +{ + public class UserTokenCreateResult + { + public string Token { get; set; } = default!; + public User User { get; set; } = default!; + } + + public interface IUserTokenManager + { + /// + /// Try to create a token for given username and password. + /// + /// The username. + /// The password. + /// The expire time of the token. + /// The created token and the user info. + /// Thrown when or is null. + /// Thrown when is of bad format. + /// Thrown when the user with does not exist. + /// Thrown when is wrong. + public Task CreateToken(string username, string password, DateTime? expireAt = null); + + /// + /// Verify a token and get the saved user info. This also check the database for existence of the user. + /// + /// The token. + /// The user stored in token. + /// Thrown when is null. + /// Thrown when the token is expired. + /// Thrown when the token is of bad version. + /// Thrown when the token is of bad format. + /// Thrown when the user specified by the token does not exist. Usually the user had been deleted after the token was issued. + public Task VerifyToken(string token); + } + + public class UserTokenManager : IUserTokenManager + { + private readonly ILogger _logger; + private readonly IUserService _userService; + private readonly IUserTokenService _userTokenService; + private readonly IClock _clock; + + public UserTokenManager(ILogger logger, IUserService userService, IUserTokenService userTokenService, IClock clock) + { + _logger = logger; + _userService = userService; + _userTokenService = userTokenService; + _clock = clock; + } + + public async Task CreateToken(string username, string password, DateTime? expireAt = null) + { + expireAt = expireAt?.MyToUtc(); + + if (username == null) + throw new ArgumentNullException(nameof(username)); + if (password == null) + throw new ArgumentNullException(nameof(password)); + + var user = await _userService.VerifyCredential(username, password); + var token = _userTokenService.GenerateToken(new UserTokenInfo { Id = user.Id!.Value, Version = user.Version!.Value, ExpireAt = expireAt }); + + return new UserTokenCreateResult { Token = token, User = user }; + } + + + public async Task VerifyToken(string token) + { + if (token == null) + throw new ArgumentNullException(nameof(token)); + + var tokenInfo = _userTokenService.VerifyToken(token); + + if (tokenInfo.ExpireAt.HasValue) + { + var currentTime = _clock.GetCurrentTime(); + if (tokenInfo.ExpireAt < currentTime) + throw new UserTokenTimeExpireException(token, tokenInfo.ExpireAt.Value, currentTime); + } + + var user = await _userService.GetUserById(tokenInfo.Id); + + if (tokenInfo.Version < user.Version) + throw new UserTokenBadVersionException(token, tokenInfo.Version, user.Version.Value); + + return user; + } + } +} diff --git a/BackEnd/Timeline/Services/UserTokenService.cs b/BackEnd/Timeline/Services/UserTokenService.cs new file mode 100644 index 00000000..86f3a0f7 --- /dev/null +++ b/BackEnd/Timeline/Services/UserTokenService.cs @@ -0,0 +1,149 @@ +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using System; +using System.Globalization; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using Timeline.Configs; +using Timeline.Entities; + +namespace Timeline.Services +{ + public class UserTokenInfo + { + public long Id { get; set; } + public long Version { get; set; } + public DateTime? ExpireAt { get; set; } + } + + public interface IUserTokenService + { + /// + /// Create a token for a given token info. + /// + /// The info to generate token. + /// Return the generated token. + /// Thrown when is null. + string GenerateToken(UserTokenInfo tokenInfo); + + /// + /// Verify a token and get the saved info. + /// + /// The token to verify. + /// The saved info in token. + /// Thrown when is null. + /// Thrown when the token is of bad format. + /// + /// If this method throw , it usually means the token is not created by this service. + /// + UserTokenInfo VerifyToken(string token); + } + + public class JwtUserTokenService : IUserTokenService + { + private const string VersionClaimType = "timeline_version"; + + private readonly IOptionsMonitor _jwtConfig; + private readonly IClock _clock; + + private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler(); + private SymmetricSecurityKey _tokenSecurityKey; + + public JwtUserTokenService(IOptionsMonitor jwtConfig, IClock clock, DatabaseContext database) + { + _jwtConfig = jwtConfig; + _clock = clock; + + var key = database.JwtToken.Select(t => t.Key).SingleOrDefault(); + + if (key == null) + { + throw new InvalidOperationException(Resources.Services.UserTokenService.JwtKeyNotExist); + } + + _tokenSecurityKey = new SymmetricSecurityKey(key); + } + + public string GenerateToken(UserTokenInfo tokenInfo) + { + if (tokenInfo == null) + throw new ArgumentNullException(nameof(tokenInfo)); + + var config = _jwtConfig.CurrentValue; + + var identity = new ClaimsIdentity(); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, tokenInfo.Id.ToString(CultureInfo.InvariantCulture.NumberFormat), ClaimValueTypes.Integer64)); + identity.AddClaim(new Claim(VersionClaimType, tokenInfo.Version.ToString(CultureInfo.InvariantCulture.NumberFormat), ClaimValueTypes.Integer64)); + + var tokenDescriptor = new SecurityTokenDescriptor() + { + Subject = identity, + Issuer = config.Issuer, + Audience = config.Audience, + SigningCredentials = new SigningCredentials(_tokenSecurityKey, SecurityAlgorithms.HmacSha384), + IssuedAt = _clock.GetCurrentTime(), + Expires = tokenInfo.ExpireAt.GetValueOrDefault(_clock.GetCurrentTime().AddSeconds(config.DefaultExpireOffset)), + NotBefore = _clock.GetCurrentTime() // I must explicitly set this or it will use the current time by default and mock is not work in which case test will not pass. + }; + + var token = _tokenHandler.CreateToken(tokenDescriptor); + var tokenString = _tokenHandler.WriteToken(token); + + return tokenString; + } + + + public UserTokenInfo VerifyToken(string token) + { + if (token == null) + throw new ArgumentNullException(nameof(token)); + + var config = _jwtConfig.CurrentValue; + try + { + var principal = _tokenHandler.ValidateToken(token, new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateIssuerSigningKey = true, + ValidateLifetime = false, + ValidIssuer = config.Issuer, + ValidAudience = config.Audience, + IssuerSigningKey = _tokenSecurityKey + }, out var t); + + var idClaim = principal.FindFirstValue(ClaimTypes.NameIdentifier); + if (idClaim == null) + throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.NoIdClaim); + if (!long.TryParse(idClaim, out var id)) + throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.IdClaimBadFormat); + + var versionClaim = principal.FindFirstValue(VersionClaimType); + if (versionClaim == null) + throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.NoVersionClaim); + if (!long.TryParse(versionClaim, out var version)) + throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.VersionClaimBadFormat); + + var decodedToken = (JwtSecurityToken)t; + var exp = decodedToken.Payload.Exp; + DateTime? expireAt = null; + if (exp.HasValue) + { + expireAt = EpochTime.DateTime(exp.Value); + } + + return new UserTokenInfo + { + Id = id, + Version = version, + ExpireAt = expireAt + }; + } + catch (Exception e) when (e is SecurityTokenException || e is ArgumentException) + { + throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.Other, e); + } + } + } +} diff --git a/BackEnd/Timeline/Startup.cs b/BackEnd/Timeline/Startup.cs new file mode 100644 index 00000000..576836eb --- /dev/null +++ b/BackEnd/Timeline/Startup.cs @@ -0,0 +1,185 @@ +using AutoMapper; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using NSwag; +using NSwag.Generation.Processors.Security; +using System; +using System.ComponentModel; +using System.Net.Mime; +using System.Text.Json.Serialization; +using Timeline.Auth; +using Timeline.Configs; +using Timeline.Entities; +using Timeline.Formatters; +using Timeline.Helpers; +using Timeline.Models.Converters; +using Timeline.Routes; +using Timeline.Services; +using Timeline.Swagger; + +namespace Timeline +{ + public class Startup + { + private readonly bool disableFrontEnd; + private readonly bool useMockFrontEnd; + + public Startup(IConfiguration configuration, IWebHostEnvironment environment) + { + Environment = environment; + Configuration = configuration; + + disableFrontEnd = Configuration.GetValue(ApplicationConfiguration.DisableFrontEndKey) ?? false; + useMockFrontEnd = Configuration.GetValue(ApplicationConfiguration.UseMockFrontEndKey) ?? false; + } + + public IWebHostEnvironment Environment { get; } + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + TypeDescriptor.AddAttributes(typeof(DateTime), new TypeConverterAttribute(typeof(MyDateTimeConverter))); + + services.AddControllers(setup => + { + setup.InputFormatters.Add(new StringInputFormatter()); + setup.InputFormatters.Add(new BytesInputFormatter()); + setup.Filters.Add(new ConsumesAttribute(MediaTypeNames.Application.Json, "text/json")); + setup.Filters.Add(new ProducesAttribute(MediaTypeNames.Application.Json, "text/json")); + setup.UseApiRoutePrefix("api"); + }) + .AddJsonOptions(options => + { + options.JsonSerializerOptions.IgnoreNullValues = true; + options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + options.JsonSerializerOptions.Converters.Add(new JsonDateTimeConverter()); + }) + .ConfigureApiBehaviorOptions(options => + { + options.InvalidModelStateResponseFactory = InvalidModelResponseFactory.Factory; + }); + + services.Configure(Configuration.GetSection("Jwt")); + services.AddAuthentication(AuthenticationConstants.Scheme) + .AddScheme(AuthenticationConstants.Scheme, AuthenticationConstants.DisplayName, o => { }); + services.AddAuthorization(); + + services.AddSingleton(); + + services.AddSingleton(); + + services.AddAutoMapper(GetType().Assembly); + + services.AddTransient(); + + services.AddTransient(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.AddScoped(); + services.AddScoped(); + + services.AddScoped(); + + services.AddUserAvatarService(); + + services.AddScoped(); + + services.TryAddSingleton(); + + services.AddDbContext((services, options) => + { + var pathProvider = services.GetRequiredService(); + options.UseSqlite($"Data Source={pathProvider.GetDatabaseFilePath()}"); + }); + + services.AddSwaggerDocument(document => + { + document.DocumentName = "Timeline"; + document.Title = "Timeline REST API Reference"; + document.Version = typeof(Startup).Assembly.GetName().Version?.ToString() ?? "unknown version"; + document.DocumentProcessors.Add(new DocumentDescriptionDocumentProcessor()); + document.DocumentProcessors.Add( + new SecurityDefinitionAppender("JWT", + new OpenApiSecurityScheme + { + Type = OpenApiSecuritySchemeType.ApiKey, + Name = "Authorization", + In = OpenApiSecurityApiKeyLocation.Header, + Description = "Create token via `/api/token/create` ." + })); + document.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor("JWT")); + document.OperationProcessors.Add(new DefaultDescriptionOperationProcessor()); + document.OperationProcessors.Add(new ByteDataRequestOperationProcessor()); + }); + + if (!disableFrontEnd) + { + if (useMockFrontEnd) + { + services.AddSpaStaticFiles(config => + { + config.RootPath = "MockClientApp"; + }); + + } + else if (!Environment.IsDevelopment()) // In development, we don't want to serve dist. Or it will take precedence than front end dev server. + { + services.AddSpaStaticFiles(config => + { + config.RootPath = "ClientApp"; + }); + } + } + } + + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app) + { + app.UseRouting(); + + if (!disableFrontEnd && (useMockFrontEnd || !Environment.IsDevelopment())) + { + app.UseSpaStaticFiles(new StaticFileOptions + { + ServeUnknownFileTypes = true + }); + } + + app.UseOpenApi(); + app.UseReDoc(); + + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + + UnknownEndpointMiddleware.Attach(app); + + if (!disableFrontEnd) + { + app.UseSpa(spa => + { + if (!useMockFrontEnd && (Configuration.GetValue(ApplicationConfiguration.UseProxyFrontEndKey) ?? false)) + { + spa.UseProxyToSpaDevelopmentServer(new UriBuilder("http", "localhost", 3000).Uri); + } + }); + } + } + } +} diff --git a/BackEnd/Timeline/Swagger/ApiConvention.cs b/BackEnd/Timeline/Swagger/ApiConvention.cs new file mode 100644 index 00000000..dbf0b2fe --- /dev/null +++ b/BackEnd/Timeline/Swagger/ApiConvention.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Mvc; + +[assembly: ApiConventionType(typeof(Timeline.Controllers.ApiConvention))] + +namespace Timeline.Controllers +{ + // There is some bug if nullable is enable. So disable it. +#nullable disable + /// + /// My api convention. + /// + public static class ApiConvention + { + } +} diff --git a/BackEnd/Timeline/Swagger/ByteDataRequestOperationProcessor.cs b/BackEnd/Timeline/Swagger/ByteDataRequestOperationProcessor.cs new file mode 100644 index 00000000..887831ac --- /dev/null +++ b/BackEnd/Timeline/Swagger/ByteDataRequestOperationProcessor.cs @@ -0,0 +1,27 @@ +using NJsonSchema; +using NSwag; +using NSwag.Generation.Processors; +using NSwag.Generation.Processors.Contexts; +using System.Linq; +using Timeline.Models; + +namespace Timeline.Swagger +{ + /// + /// Coerce ByteData body type into the right one. + /// + public class ByteDataRequestOperationProcessor : IOperationProcessor + { + /// + public bool Process(OperationProcessorContext context) + { + var hasByteDataBody = context.MethodInfo.GetParameters().Where(p => p.ParameterType == typeof(ByteData)).Any(); + if (hasByteDataBody) + { + var bodyParameter = context.OperationDescription.Operation.Parameters.Where(p => p.Kind == OpenApiParameterKind.Body).Single(); + bodyParameter.Schema = JsonSchema.FromType(); + } + return true; + } + } +} diff --git a/BackEnd/Timeline/Swagger/DefaultDescriptionOperationProcessor.cs b/BackEnd/Timeline/Swagger/DefaultDescriptionOperationProcessor.cs new file mode 100644 index 00000000..4967cc6a --- /dev/null +++ b/BackEnd/Timeline/Swagger/DefaultDescriptionOperationProcessor.cs @@ -0,0 +1,39 @@ +using NSwag.Generation.Processors; +using NSwag.Generation.Processors.Contexts; +using System.Collections.Generic; + +namespace Timeline.Swagger +{ + /// + /// Swagger operation processor that adds default description to response. + /// + public class DefaultDescriptionOperationProcessor : IOperationProcessor + { + private readonly Dictionary defaultDescriptionMap = new Dictionary + { + ["200"] = "Succeeded to perform the operation.", + ["304"] = "Item does not change.", + ["400"] = "See code and message for error info.", + ["401"] = "You need to log in to perform this operation.", + ["403"] = "You have no permission to perform the operation.", + ["404"] = "Item does not exist. See code and message for error info." + }; + + /// + public bool Process(OperationProcessorContext context) + { + var responses = context.OperationDescription.Operation.Responses; + + foreach (var (httpStatusCode, res) in responses) + { + if (!string.IsNullOrEmpty(res.Description)) continue; + if (defaultDescriptionMap.ContainsKey(httpStatusCode)) + { + res.Description = defaultDescriptionMap[httpStatusCode]; + } + } + + return true; + } + } +} diff --git a/BackEnd/Timeline/Swagger/DocumentDescriptionDocumentProcessor.cs b/BackEnd/Timeline/Swagger/DocumentDescriptionDocumentProcessor.cs new file mode 100644 index 00000000..dc5ddd96 --- /dev/null +++ b/BackEnd/Timeline/Swagger/DocumentDescriptionDocumentProcessor.cs @@ -0,0 +1,55 @@ +using NSwag.Generation.Processors; +using NSwag.Generation.Processors.Contexts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Timeline.Models.Http; + +namespace Timeline.Swagger +{ + public class DocumentDescriptionDocumentProcessor : IDocumentProcessor + { + private static Dictionary GetAllErrorCodes() + { + var errorCodes = new Dictionary(); + + void RecursiveCheckErrorCode(Type type) + { + foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy) + .Where(fi => fi.IsLiteral && !fi.IsInitOnly && fi.FieldType == typeof(int))) + { + var name = (type.FullName + "." + field.Name).Remove(0, typeof(ErrorCodes).FullName!.Length + 1).Replace("+", ".", StringComparison.OrdinalIgnoreCase); + int value = (int)field.GetRawConstantValue()!; + errorCodes.Add(name, value); + } + + foreach (var nestedType in type.GetNestedTypes()) + { + RecursiveCheckErrorCode(nestedType); + } + } + + RecursiveCheckErrorCode(typeof(ErrorCodes)); + + return errorCodes; + } + + public void Process(DocumentProcessorContext context) + { + StringBuilder description = new StringBuilder(); + description.AppendLine("# Error Codes"); + description.AppendLine("name | value"); + description.AppendLine("---- | -----"); + foreach (var (name, value) in GetAllErrorCodes()) + { + description.AppendLine($"`{name}` | `{value}`"); + } + + context.Document.Info.Description = description.ToString(); + } + } +} diff --git a/BackEnd/Timeline/Timeline.csproj b/BackEnd/Timeline/Timeline.csproj new file mode 100644 index 00000000..5131cdb0 --- /dev/null +++ b/BackEnd/Timeline/Timeline.csproj @@ -0,0 +1,263 @@ + + + netcoreapp3.1 + 1f6fb74d-4277-4bc0-aeea-b1fc5ffb0b43 + crupest + + false + + 8.0 + enable + + true + Latest + ClientApp\ + + 0.3.0 + true + true + + 1701;1702;1591 + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + True + True + AuthHandler.resx + + + True + True + ControllerAuthExtensions.resx + + + True + True + TimelineController.resx + + + True + True + TokenController.resx + + + True + True + UserAvatarController.resx + + + True + True + UserController.resx + + + True + True + Entities.resx + + + True + True + Filters.resx + + + True + True + DataCacheHelper.resx + + + True + True + Messages.resx + + + True + True + Common.resx + + + True + True + Exception.resx + + + True + True + NicknameValidator.resx + + + True + True + NameValidator.resx + + + True + True + Validator.resx + + + True + True + DataManager.resx + + + True + True + Exception.resx + + + True + True + Exceptions.resx + + + True + True + TimelineService.resx + + + True + True + UserAvatarService.resx + + + True + True + UserService.resx + + + True + True + UserTokenService.resx + + + + + + ResXFileCodeGenerator + AuthHandler.Designer.cs + + + ResXFileCodeGenerator + ControllerAuthExtensions.Designer.cs + + + ResXFileCodeGenerator + TimelineController.Designer.cs + + + Designer + ResXFileCodeGenerator + TokenController.Designer.cs + + + ResXFileCodeGenerator + UserAvatarController.Designer.cs + + + ResXFileCodeGenerator + UserController.Designer.cs + + + ResXFileCodeGenerator + Entities.Designer.cs + + + ResXFileCodeGenerator + Filters.Designer.cs + + + ResXFileCodeGenerator + DataCacheHelper.Designer.cs + + + ResXFileCodeGenerator + Messages.Designer.cs + + + ResXFileCodeGenerator + Common.Designer.cs + + + ResXFileCodeGenerator + Exception.Designer.cs + + + ResXFileCodeGenerator + NicknameValidator.Designer.cs + + + ResXFileCodeGenerator + NameValidator.Designer.cs + + + ResXFileCodeGenerator + Validator.Designer.cs + + + ResXFileCodeGenerator + DataManager.Designer.cs + + + ResXFileCodeGenerator + Exception.Designer.cs + + + ResXFileCodeGenerator + Exceptions.Designer.cs + + + ResXFileCodeGenerator + TimelineService.Designer.cs + + + ResXFileCodeGenerator + UserAvatarService.Designer.cs + + + ResXFileCodeGenerator + UserService.Designer.cs + + + ResXFileCodeGenerator + UserTokenService.Designer.cs + + + \ No newline at end of file diff --git a/BackEnd/Timeline/appsettings.Development.json b/BackEnd/Timeline/appsettings.Development.json new file mode 100644 index 00000000..a2880cbf --- /dev/null +++ b/BackEnd/Timeline/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/BackEnd/Timeline/appsettings.json b/BackEnd/Timeline/appsettings.json new file mode 100644 index 00000000..804ca43a --- /dev/null +++ b/BackEnd/Timeline/appsettings.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning" + } + }, + "Jwt": { + "Issuer": "api.crupest.xyz", + "Audience": "api.crupest.xyz" + } +} diff --git a/BackEnd/Timeline/default-avatar.png b/BackEnd/Timeline/default-avatar.png new file mode 100644 index 00000000..4086e1d2 Binary files /dev/null and b/BackEnd/Timeline/default-avatar.png differ diff --git a/BackEnd/Timeline/packages.lock.json b/BackEnd/Timeline/packages.lock.json new file mode 100644 index 00000000..ed92c672 --- /dev/null +++ b/BackEnd/Timeline/packages.lock.json @@ -0,0 +1,1563 @@ +{ + "version": 1, + "dependencies": { + ".NETCoreApp,Version=v3.1": { + "AutoMapper": { + "type": "Direct", + "requested": "[10.1.1, )", + "resolved": "10.1.1", + "contentHash": "uMgbqOdu9ZG5cIOty0C85hzzayBH2i9BthnS5FlMqKtMSHDv4ts81a2jS1VFaDBVhlBeIqJ/kQKjQY95BZde9w==", + "dependencies": { + "Microsoft.CSharp": "4.7.0", + "System.Reflection.Emit": "4.7.0" + } + }, + "AutoMapper.Extensions.Microsoft.DependencyInjection": { + "type": "Direct", + "requested": "[8.1.0, )", + "resolved": "8.1.0", + "contentHash": "dQyGCAYcHbGuimVvCMu4Ea2S1oYOlgO9XfVdClmY5wgygJMZoS57emPzH0qNfknmtzMm4QbDO9i237W5IDjU1A==", + "dependencies": { + "AutoMapper": "[10.1.0, 11.0.0)", + "Microsoft.Extensions.DependencyInjection.Abstractions": "3.0.0", + "Microsoft.Extensions.Options": "3.0.0" + } + }, + "Microsoft.AspNetCore.SpaServices.Extensions": { + "type": "Direct", + "requested": "[3.1.9, )", + "resolved": "3.1.9", + "contentHash": "ciy2GCvRnh9C22laArLsaItS+72U6Hqf4nDYShdvFgcen2ZV+NNSitb/B3vsmFfIPM8m4mf2x4T+vZ6OlI5XaA==", + "dependencies": { + "Microsoft.AspNetCore.SpaServices": "3.1.9", + "Microsoft.Extensions.FileProviders.Physical": "3.1.9" + } + }, + "Microsoft.CodeAnalysis.FxCopAnalyzers": { + "type": "Direct", + "requested": "[3.3.0, )", + "resolved": "3.3.0", + "contentHash": "k3Icqx8kc+NrHImuiB8Jc/wd32Xeyd2B/7HOR5Qu9pyKzXQ4ikPeBAwzG2FSTuYhyIuNWvwL5k9yYBbbVz6w9w==", + "dependencies": { + "Microsoft.CodeAnalysis.VersionCheckAnalyzer": "[3.3.0]", + "Microsoft.CodeQuality.Analyzers": "[3.3.0]", + "Microsoft.NetCore.Analyzers": "[3.3.0]", + "Microsoft.NetFramework.Analyzers": "[3.3.0]" + } + }, + "Microsoft.EntityFrameworkCore": { + "type": "Direct", + "requested": "[3.1.9, )", + "resolved": "3.1.9", + "contentHash": "u3A2W0BvAuAF2jgW+WX+C+Sh8sMGX5Kl1hdA0gu6A/XSrZQoW/BUP4a/q2n3iitDGndaorqjAKx+Spb9gBto+w==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "1.1.1", + "Microsoft.Bcl.HashCode": "1.1.0", + "Microsoft.EntityFrameworkCore.Abstractions": "3.1.9", + "Microsoft.EntityFrameworkCore.Analyzers": "3.1.9", + "Microsoft.Extensions.Caching.Memory": "3.1.9", + "Microsoft.Extensions.DependencyInjection": "3.1.9", + "Microsoft.Extensions.Logging": "3.1.9", + "System.Collections.Immutable": "1.7.1", + "System.ComponentModel.Annotations": "4.7.0", + "System.Diagnostics.DiagnosticSource": "4.7.1" + } + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Direct", + "requested": "[3.1.9, )", + "resolved": "3.1.9", + "contentHash": "eXGyx/Lb1fiiKtnIStdxGrfBSSQg8oZytE10f1T/2xAx12W9dKB9U9fg05cwNCDC0S2CXILsmZHYaGqCSXVAqQ==" + }, + "Microsoft.EntityFrameworkCore.Sqlite": { + "type": "Direct", + "requested": "[3.1.9, )", + "resolved": "3.1.9", + "contentHash": "sMFCWv/1UcsFQZeGQcbfPbEZKZ1oKZqWZXTbc7PEZVMIXu82nbavstdNQ84x5IBXJkxl8iW3zjChb/FRBr5uLQ==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Sqlite.Core": "3.1.9", + "SQLitePCLRaw.bundle_e_sqlite3": "2.0.2" + } + }, + "Microsoft.EntityFrameworkCore.Tools": { + "type": "Direct", + "requested": "[3.1.9, )", + "resolved": "3.1.9", + "contentHash": "mSgwjp0h5iqW5V49SVijR5O+kNpI1nitcbN12n9FYx/Ga6oCEFwXR/llBDesD6ASHw3Mx16vodJYJ7CEBx5rig==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Design": "3.1.9" + } + }, + "NSwag.AspNetCore": { + "type": "Direct", + "requested": "[13.8.2, )", + "resolved": "13.8.2", + "contentHash": "SNGlVSZoMyywBWueZBxl3B/nfaIM0fAcuNhTD/cfMKUn3Cn/Oi8d45HZY5vAPqczvppTbk4cZXyVwWDOfgiPbA==", + "dependencies": { + "Microsoft.AspNetCore.Mvc.Core": "1.0.4", + "Microsoft.AspNetCore.Mvc.Formatters.Json": "1.0.4", + "Microsoft.AspNetCore.StaticFiles": "1.0.4", + "Microsoft.Extensions.ApiDescription.Server": "3.0.0", + "Microsoft.Extensions.FileProviders.Embedded": "1.0.1", + "NSwag.Annotations": "13.8.2", + "NSwag.Core": "13.8.2", + "NSwag.Generation": "13.8.2", + "NSwag.Generation.AspNetCore": "13.8.2", + "System.IO.FileSystem": "4.3.0", + "System.Xml.XPath.XDocument": "4.0.1" + } + }, + "SixLabors.ImageSharp": { + "type": "Direct", + "requested": "[1.0.1, )", + "resolved": "1.0.1", + "contentHash": "DjLoFNdUfsDP7RhPpr5hcUhl1XiejqBML9uDWuOUwCkc0Y+sG9IJLLbqSOi9XeoWqPviwdcDm1F8nKdF0qTYIQ==" + }, + "System.IdentityModel.Tokens.Jwt": { + "type": "Direct", + "requested": "[6.8.0, )", + "resolved": "6.8.0", + "contentHash": "5tBCjAub2Bhd5qmcd0WhR5s354e4oLYa//kOWrkX+6/7ZbDDJjMTfwLSOiZ/MMpWdE4DWPLOfTLOq/juj9CKzA==", + "dependencies": { + "Microsoft.IdentityModel.JsonWebTokens": "6.8.0", + "Microsoft.IdentityModel.Tokens": "6.8.0" + } + }, + "Microsoft.AspNetCore.Authorization": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "cN2KJkfHcKwh82c9WGx4Tqfd2h5HflU/Mu5vYLMHON8WahHU9hE32ciIXcEIoKLNpu+zs1u1cN/qxcKTdqu89w==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "1.0.2", + "Microsoft.Extensions.Options": "1.0.2", + "System.Security.Claims": "4.0.1" + } + }, + "Microsoft.AspNetCore.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "1.0.4", + "contentHash": "ybY8FOkdNfBPB5PLv1JO+It/94ftBzGUI1WqU4XySbIWyhw2TPmmKAUuO9uvJoR0qpsFup8FJz6trsBcBITg9w==", + "dependencies": { + "Microsoft.AspNetCore.Hosting.Server.Abstractions": "1.0.4", + "Microsoft.AspNetCore.Http.Abstractions": "1.0.3", + "Microsoft.Extensions.Configuration.Abstractions": "1.0.2", + "Microsoft.Extensions.DependencyInjection.Abstractions": "1.0.2", + "Microsoft.Extensions.FileProviders.Abstractions": "1.0.1", + "Microsoft.Extensions.Logging.Abstractions": "1.0.2" + } + }, + "Microsoft.AspNetCore.Hosting.Server.Abstractions": { + "type": "Transitive", + "resolved": "1.0.4", + "contentHash": "XUiQPe/CflK1i0Voo9S6/G1iQh00gQ6sMqi3LRtKeceBbO6AOostaAUdhjyME92MapI4VFNl+Z+/KXUlMAExJQ==", + "dependencies": { + "Microsoft.AspNetCore.Http.Features": "1.0.3", + "Microsoft.Extensions.Configuration.Abstractions": "1.0.2" + } + }, + "Microsoft.AspNetCore.Http": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "kfNOIGGgVtMzsSWZzXBqz5zsdo8ssBa90YHzZt95N8ARGXoolSaBHy6yBoMm/XcpbXM+m/x1fixTTMIWMgzJdQ==", + "dependencies": { + "Microsoft.AspNetCore.Http.Abstractions": "1.0.3", + "Microsoft.AspNetCore.WebUtilities": "1.0.3", + "Microsoft.Extensions.ObjectPool": "1.0.1", + "Microsoft.Extensions.Options": "1.0.2", + "Microsoft.Net.Http.Headers": "1.0.3", + "System.Buffers": "4.0.0", + "System.Threading": "4.0.11" + } + }, + "Microsoft.AspNetCore.Http.Abstractions": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "nnjvAf7ag6P0DyD/0nhRGjLpv+3DkPU0juF8aQh46X8uF4kzjJdrh65oL+4PVOu3K6BgSg6OVUs0QC0SE0FRtg==", + "dependencies": { + "Microsoft.AspNetCore.Http.Features": "1.0.3", + "System.Globalization.Extensions": "4.0.1", + "System.Linq.Expressions": "4.1.1", + "System.Reflection.TypeExtensions": "4.1.0", + "System.Runtime.InteropServices": "4.1.0", + "System.Text.Encodings.Web": "4.0.1" + } + }, + "Microsoft.AspNetCore.Http.Extensions": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "+7Sd+14nexIJqcB4S1Eur9kzeMZ5CBtrxkei+PNbD78fg8vO3+TcCgrl5SBNTsUB/VJAfD/s0fgs5t+hHRj2Pg==", + "dependencies": { + "Microsoft.AspNetCore.Http.Abstractions": "1.0.3", + "Microsoft.Extensions.FileProviders.Abstractions": "1.0.1", + "Microsoft.Net.Http.Headers": "1.0.3", + "System.Buffers": "4.0.0", + "System.IO.FileSystem": "4.0.1" + } + }, + "Microsoft.AspNetCore.Http.Features": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "Ihq57tseNyPbJTmFXY4jQ4JkxLP0lh45VRwocQci/sFx+qcJGvWB+sJJ2/YPLy/qTWFAEfNAcswuY3OsNH9Gwg==", + "dependencies": { + "Microsoft.Extensions.Primitives": "1.0.1", + "System.Collections": "4.0.11", + "System.ComponentModel": "4.0.1", + "System.Linq": "4.1.0", + "System.Net.Primitives": "4.0.11", + "System.Net.WebSockets": "4.0.0", + "System.Runtime.Extensions": "4.1.0", + "System.Security.Claims": "4.0.1", + "System.Security.Cryptography.X509Certificates": "4.1.0", + "System.Security.Principal": "4.0.1" + } + }, + "Microsoft.AspNetCore.JsonPatch": { + "type": "Transitive", + "resolved": "1.0.0", + "contentHash": "WVaSVS+dDlWCR/qerHnBxU9tIeJ9GMA3M5tg4cxH7/cJYZZLnr2zvaFHGB+cRRNCKKTJ0pFRxT7ES8knhgAAaA==", + "dependencies": { + "Microsoft.CSharp": "4.0.1", + "Newtonsoft.Json": "9.0.1", + "System.Collections.Concurrent": "4.0.12", + "System.ComponentModel.TypeConverter": "4.1.0", + "System.Diagnostics.Debug": "4.0.11", + "System.Globalization": "4.0.11", + "System.Linq": "4.1.0", + "System.Reflection.Extensions": "4.0.1", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime.Extensions": "4.1.0", + "System.Runtime.Serialization.Primitives": "4.1.1", + "System.Text.Encoding.Extensions": "4.0.11" + } + }, + "Microsoft.AspNetCore.Mvc.Abstractions": { + "type": "Transitive", + "resolved": "1.0.4", + "contentHash": "Isqgif1nuB+um86cEkpL8KnoxFCUCXBsbs9PuiuzElvlSiv4Ek3LvtrSUcbivekDDfys8CDbJhxwEI7WKJieAQ==", + "dependencies": { + "Microsoft.AspNetCore.Routing.Abstractions": "1.0.4", + "Microsoft.CSharp": "4.0.1", + "Microsoft.Net.Http.Headers": "1.0.3", + "System.ComponentModel.TypeConverter": "4.1.0", + "System.Reflection.Extensions": "4.0.1", + "System.Text.Encoding.Extensions": "4.0.11" + } + }, + "Microsoft.AspNetCore.Mvc.ApiExplorer": { + "type": "Transitive", + "resolved": "1.0.4", + "contentHash": "ujCFTM42U2WKUBhdaoLoiI+wVHgYhrmDrkl5+hWJ7EJW4fhp42w4cRZ97tjuveWr+M6JZjpS0q+7PVofQzFUiw==", + "dependencies": { + "Microsoft.AspNetCore.Mvc.Core": "1.0.4" + } + }, + "Microsoft.AspNetCore.Mvc.Core": { + "type": "Transitive", + "resolved": "1.0.4", + "contentHash": "1ukcttN1+T82hWXE8WS5kawkruolKI6LPVqVI4rTzN16kFszS/UqTrcwSUEnmTRpmWgFo665V3c2GpdQ9B6znw==", + "dependencies": { + "Microsoft.AspNetCore.Authorization": "1.0.3", + "Microsoft.AspNetCore.Hosting.Abstractions": "1.0.3", + "Microsoft.AspNetCore.Http": "1.0.3", + "Microsoft.AspNetCore.Mvc.Abstractions": "1.0.4", + "Microsoft.AspNetCore.Routing": "1.0.4", + "Microsoft.Extensions.DependencyModel": "1.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "1.0.1", + "Microsoft.Extensions.Logging.Abstractions": "1.0.2", + "Microsoft.Extensions.PlatformAbstractions": "1.0.0", + "System.Buffers": "4.0.0", + "System.Diagnostics.DiagnosticSource": "4.0.0", + "System.Text.Encoding": "4.0.11" + } + }, + "Microsoft.AspNetCore.Mvc.Formatters.Json": { + "type": "Transitive", + "resolved": "1.0.4", + "contentHash": "i8WWK2GwlBHfOL+d+kknJWPks6DS9tbN6nfJZU4yb+/wfUAYd311B2CIHzdat3IewubnK1TYONwrhQcs2FbLeA==", + "dependencies": { + "Microsoft.AspNetCore.JsonPatch": "1.0.0", + "Microsoft.AspNetCore.Mvc.Core": "1.0.4" + } + }, + "Microsoft.AspNetCore.NodeServices": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "bbd3FlSPWiRQrIcBLa5TaOvo4gjmmiNMkxA8VmZ6u0eIpS0Yj35/eTopaGdtzqwlqj5jXbdRoib1MruXuPaW8A==", + "dependencies": { + "Microsoft.Extensions.Logging.Console": "3.1.9", + "Newtonsoft.Json": "12.0.2" + } + }, + "Microsoft.AspNetCore.Routing": { + "type": "Transitive", + "resolved": "1.0.4", + "contentHash": "mdIF3ckRothHWuCSFkk6YXACj5zxi5qM+cEAHjcpP04/wCHUoV0gGVnW+HI+LyFXE6JUwu2zXn5tfsCpW0U+SA==", + "dependencies": { + "Microsoft.AspNetCore.Http.Extensions": "1.0.3", + "Microsoft.AspNetCore.Routing.Abstractions": "1.0.4", + "Microsoft.Extensions.Logging.Abstractions": "1.0.2", + "Microsoft.Extensions.ObjectPool": "1.0.1", + "Microsoft.Extensions.Options": "1.0.2", + "System.Collections": "4.0.11", + "System.Text.RegularExpressions": "4.1.0" + } + }, + "Microsoft.AspNetCore.Routing.Abstractions": { + "type": "Transitive", + "resolved": "1.0.4", + "contentHash": "GHxVt6LlXHFsCUd2Un+/vY1tBTXxnogfbDO0b8G5EGmkapSK+dOGOLJviscxQkp338Uabs081JEIdkRymI5GXA==", + "dependencies": { + "Microsoft.AspNetCore.Http.Abstractions": "1.0.3", + "System.Collections.Concurrent": "4.0.12", + "System.Reflection.Extensions": "4.0.1", + "System.Threading.Tasks": "4.0.11" + } + }, + "Microsoft.AspNetCore.SpaServices": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "Fb+N2ZyF1wNrGeWggT+Ovv6W8AAVxfi4V/SnuEsBOR+nmkFhty9zyh6IDRRS98GJK6OE3adqqPbWMtJqbxYnNA==", + "dependencies": { + "Microsoft.AspNetCore.NodeServices": "3.1.9" + } + }, + "Microsoft.AspNetCore.StaticFiles": { + "type": "Transitive", + "resolved": "1.0.4", + "contentHash": "2pNvwewAazhaaCdw2CGUvIcDrNQMlqP57JgBDf3v+pRj1rZ29HVnpvkX6a+TrmRYlJNmmxHOKEt468uE/gDcFw==", + "dependencies": { + "Microsoft.AspNetCore.Hosting.Abstractions": "1.0.4", + "Microsoft.AspNetCore.Http.Extensions": "1.0.3", + "Microsoft.Extensions.FileProviders.Abstractions": "1.0.1", + "Microsoft.Extensions.Logging.Abstractions": "1.0.2", + "Microsoft.Extensions.WebEncoders": "1.0.3" + } + }, + "Microsoft.AspNetCore.WebUtilities": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "snSGNs5EEisqivDjDiskFkFyu+DV2Ib9sMPOBQKtoFwI5H1W5YNB/rIVqDZQL16zj/uzdwwxrdE/5xhkVyf6gQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "1.0.1", + "System.Buffers": "4.0.0", + "System.Collections": "4.0.11", + "System.IO": "4.1.0", + "System.IO.FileSystem": "4.0.1", + "System.Text.Encodings.Web": "4.0.1" + } + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "1.1.1", + "contentHash": "yuvf07qFWFqtK3P/MRkEKLhn5r2UbSpVueRziSqj0yJQIKFwG1pq9mOayK3zE5qZCTs0CbrwL9M6R8VwqyGy2w==" + }, + "Microsoft.Bcl.HashCode": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "J2G1k+u5unBV+aYcwxo94ip16Rkp65pgWFb0R6zwJipzWNMgvqlWeuI7/+R+e8bob66LnSG+llLJ+z8wI94cHg==" + }, + "Microsoft.CodeAnalysis.VersionCheckAnalyzer": { + "type": "Transitive", + "resolved": "3.3.0", + "contentHash": "xjLM3DRFZMan3nQyBQEM1mBw6VqQybi4iMJhMFW6Ic1E1GCvqJR3ABOwEL7WtQjDUzxyrGld9bASnAos7G/Xyg==" + }, + "Microsoft.CodeQuality.Analyzers": { + "type": "Transitive", + "resolved": "3.3.0", + "contentHash": "zZ3miq6u22UFQKhfJyLnVEJ+DgeOopLh3eKJnKAcOetPP2hiv3wa7kHZlBDeTvtqJQiAQhAVbttket8XxjN1zw==" + }, + "Microsoft.CSharp": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA==" + }, + "Microsoft.Data.Sqlite.Core": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "+u4PeT1npi2EzhxGc5r1Z2z73zuXw+TlKVZm44WQhNCUw4LtUVDaxGSpUhrjW+X4snBCBfr4kT/uJyKnL4R4og==", + "dependencies": { + "SQLitePCLRaw.core": "2.0.2" + } + }, + "Microsoft.DotNet.PlatformAbstractions": { + "type": "Transitive", + "resolved": "3.1.6", + "contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg==" + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "IR6Y4RJVlw0QXdWXjF3Kx9s1QLiicJus+BFBKr43lBtriV20j3yrWMoaZ9W1AUUgnicZXpXVcNfklqtmwb9Sxw==" + }, + "Microsoft.EntityFrameworkCore.Design": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "2zgP7BWcw5nqGQiT4bEtiI6ras+4pvKg5D+tA3AYvjEifzzaWvmRTb3B9nRHpIYJAhPtmWNBVnVXLbu3fS1OYA==", + "dependencies": { + "Microsoft.CSharp": "4.7.0", + "Microsoft.EntityFrameworkCore.Relational": "3.1.9" + } + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "7fhWuSfrCYlv/hvOX5OhbFJF/G9f8sifqTrJiYnAYLDOvNizwv7t9tFPD8JwaF3zM2S54O5/Vni2NxvwzSaW2w==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "3.1.9" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite.Core": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "Da6h8LdpJwKc1az9DMWt2Mt6gHXPRZqwiumV1Zx0AuM3EThyokVDzBGy2sti0AcBhcQMLJHPEr5R9xuiWvaYYQ==", + "dependencies": { + "Microsoft.Data.Sqlite.Core": "3.1.9", + "Microsoft.DotNet.PlatformAbstractions": "3.1.6", + "Microsoft.EntityFrameworkCore.Relational": "3.1.9", + "Microsoft.Extensions.DependencyModel": "3.1.6" + } + }, + "Microsoft.Extensions.ApiDescription.Server": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "LH4OE/76F6sOCslif7+Xh3fS/wUUrE5ryeXAMcoCnuwOQGT5Smw0p57IgDh/pHgHaGz/e+AmEQb7pRgb++wt0w==" + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "/2QsPAsUZD4qvftZkUKHRRRryPDXWh606/iNXPLrulwHLMr9JNsKBJWVqylT3qU92nJok5VoqSblkY9mSyxFyg==", + "dependencies": { + "Microsoft.Extensions.Primitives": "3.1.9" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "/JrVMVetX/kpJQUIlJ6NLQ3zbF0yyryXpo4+uFCqYIUZzgmWk8DS/zSKcyj1tQ3410+vhDEAPngxC+hg0IlJeg==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "3.1.9", + "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.9", + "Microsoft.Extensions.Logging.Abstractions": "3.1.9", + "Microsoft.Extensions.Options": "3.1.9" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "lqdkOGNeTMKG981Q7yWGlRiFbIlsRwTlMMiybT+WOzUCFBS/wc25tZgh7Wm/uRoBbWefgvokzmnea7ZjmFedmA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "3.1.9" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "vOJxPKczaHpXeZFrxARxYwsEulhEouXc5aZGgMdkhV/iEXX9/pfjqKk76rTG+4CsJjHV+G/4eMhvOIaQMHENNA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "3.1.9" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "BG6HcT7tARYakftqfQu+cLksgIWG1NdxMY+igI12hdZrUK+WjS973NiRyuao/U9yyTeM9NPwRnC61hCmG3G3jg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "3.1.9" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "ORqfrAACcvTInie1oGola5uky344/PiNfgayTPuZWV4WnSfIQZJQm/ZLpGshJE3h7TqwYaYElGazK/yaM2bFLA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.9" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "8PkcaPwiTPOhqshoY4+rQUbz86X6YpLDLUqXOezh7L2A3pgpBmeBBByYIffofBlvQxDdQ0zB2DkWjbZWyCxRWg==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "3.1.6", + "contentHash": "/UlDKULIVkLQYn1BaHcy/rc91ApDxJb7T75HcCbGdqwvxhnRQRKM2di1E70iCPMF9zsr6f4EgQTotBGxFIfXmw==", + "dependencies": { + "System.Text.Json": "4.7.2" + } + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "Q4SGwEFZKiZbpzPgdGbQUULxtcH1zXMOwCPKSm6QwVcOCGshf3QLfBh+O/GyFH4B0RfZ16nKyeW1mMONlRyjUw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "3.1.9" + } + }, + "Microsoft.Extensions.FileProviders.Embedded": { + "type": "Transitive", + "resolved": "1.0.1", + "contentHash": "nSEa8bH3fVdTYGqK4twOKLxxgKIW3cz9g9mrzhPh/CmdvGJWKRTIlBIZi7lz+lqNQpxean5vbAo84R/mU+JpGA==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "1.0.1", + "System.Runtime.Extensions": "4.1.0" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "HWDSsblTCQp7EEJJmnLzttIhFGzDu+DGqBbOvGCdFT0+pkCuBkn3EiWpEEcm5WMTO5njmsbLSK9ZuUUf2zPsFg==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "3.1.9", + "Microsoft.Extensions.FileSystemGlobbing": "3.1.9" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "5bnewG1aBiSESPNwcXGIxDDRN95uqdy+fqZZ8Z63Et5rRNlAwAfXHOrg+FTht7UjHobjvtjzquMCbAWhWEPHIw==" + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "+V3i0jCQCO6IIOf6e+fL0SqrZd2x/Krug9EEL1JHa9R03RsbEpltCtjVY5hxedyuyuQKwvLoR12sCfu/9XEUAw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Binder": "3.1.9", + "Microsoft.Extensions.DependencyInjection": "3.1.9", + "Microsoft.Extensions.Logging.Abstractions": "3.1.9", + "Microsoft.Extensions.Options": "3.1.9" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "W5fbF8qVR9SMVVJqDQLIR7meWbev6Pu/lbrm7LDNr4Sp7HOotr4k2UULTdFSXOi5aoDdkQZpWnq0ZSpjrR3tjg==" + }, + "Microsoft.Extensions.Logging.Configuration": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "hv6XsGgikrbkolBJdF1usl9R/nrliC5mifMqHMEY9zWcCLwNkXMJiS8p0lbosrnpVAMi4PbNx39DB51Dqscd0w==", + "dependencies": { + "Microsoft.Extensions.Logging": "3.1.9", + "Microsoft.Extensions.Options.ConfigurationExtensions": "3.1.9" + } + }, + "Microsoft.Extensions.Logging.Console": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "8Dusl1rkDivmvLrwj6QAo917xMHPiDBzG3IG3agiyDdtsC/fRp+1VN5iIN+O09PtEaMged2OLA6wCDwfSTSTZw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "3.1.9", + "Microsoft.Extensions.Logging": "3.1.9", + "Microsoft.Extensions.Logging.Configuration": "3.1.9" + } + }, + "Microsoft.Extensions.ObjectPool": { + "type": "Transitive", + "resolved": "1.0.1", + "contentHash": "pJMOnxuqmG37OjccfvtqVoo3bQGoN+0EJUzzp7+2uxSdioER82caAk6Yi/z5aysapn5XENNIIa7SaYnYKSS69A==", + "dependencies": { + "System.Diagnostics.Debug": "4.0.11", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime.Extensions": "4.1.0", + "System.Threading": "4.0.11" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "EIb3G1DL+Rl9MvJR7LjI1wCy2nfTN4y8MflbOftn1HLYQBj/Rwl8kUbGTrSFE01c99Wm4ETjWVsjqKcpFvhPng==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.9", + "Microsoft.Extensions.Primitives": "3.1.9" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "u5jh7RW+Ev81YqK1ZoBG0lftp2MA9xqXiTiRL46XzaPj2ScNUyiVbzcVY0fPbE27UOpT2hj+yPzRSOMIIo55UA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "3.1.9", + "Microsoft.Extensions.Configuration.Binder": "3.1.9", + "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.9", + "Microsoft.Extensions.Options": "3.1.9" + } + }, + "Microsoft.Extensions.PlatformAbstractions": { + "type": "Transitive", + "resolved": "1.0.0", + "contentHash": "zyjUzrOmuevOAJpIo3Mt5GmpALVYCVdLZ99keMbmCxxgQH7oxzU58kGHzE6hAgYEiWsdfMJLjVR7r+vSmaJmtg==", + "dependencies": { + "System.AppContext": "4.1.0", + "System.Reflection": "4.1.0", + "System.Reflection.Extensions": "4.0.1", + "System.Reflection.TypeExtensions": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime.Extensions": "4.1.0" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "IrHecH0eGG7/XoeEtv++oLg/sJHRNyeCqlA9RhAo6ig4GpOTjtDr32sBMYuuLtUq8ALahneWkrOzoBAwJ4L4iA==" + }, + "Microsoft.Extensions.WebEncoders": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "TClNvczWRxF6bVPhn5EK3Y3QNi5jTP68Qur+5Fk+MQLPeBI18WN7X145DDJ6bFeNOwgdCHl73lHs5uZp9ish1A==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "1.0.2", + "Microsoft.Extensions.Options": "1.0.2", + "System.Text.Encodings.Web": "4.0.1" + } + }, + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "Transitive", + "resolved": "6.8.0", + "contentHash": "+7JIww64PkMt7NWFxoe4Y/joeF7TAtA/fQ0b2GFGcagzB59sKkTt/sMZWR6aSZht5YC7SdHi3W6yM1yylRGJCQ==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "6.8.0" + } + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "6.8.0", + "contentHash": "Rfh/p4MaN4gkmhPxwbu8IjrmoDncGfHHPh1sTnc0AcM/Oc39/fzC9doKNWvUAjzFb8LqA6lgZyblTrIsX/wDXg==" + }, + "Microsoft.IdentityModel.Tokens": { + "type": "Transitive", + "resolved": "6.8.0", + "contentHash": "gTqzsGcmD13HgtNePPcuVHZ/NXWmyV+InJgalW/FhWpII1D7V1k0obIseGlWMeA4G+tZfeGMfXr0klnWbMR/mQ==", + "dependencies": { + "Microsoft.CSharp": "4.5.0", + "Microsoft.IdentityModel.Logging": "6.8.0", + "System.Security.Cryptography.Cng": "4.5.0" + } + }, + "Microsoft.Net.Http.Headers": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "2F8USh4hR5xppvaxtw2EStX74Ih+HhRj7aQD1uaB9JmTGy478F7t4VU+IdZXauEDrvS7LYAyyhmOExsUFK3PAw==", + "dependencies": { + "System.Buffers": "4.0.0", + "System.Collections": "4.0.11", + "System.Diagnostics.Contracts": "4.0.1", + "System.Globalization": "4.0.11", + "System.Linq": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime.Extensions": "4.1.0", + "System.Text.Encoding": "4.0.11" + } + }, + "Microsoft.NetCore.Analyzers": { + "type": "Transitive", + "resolved": "3.3.0", + "contentHash": "6qptTHUu1Wfszuf83NhU0IoAb4j7YWOpJs6oc6S4G/nI6aGGWKH/Xi5Vs9L/8lrI74ijEEzPcIwafSQW5ASHtA==" + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" + }, + "Microsoft.NETCore.Targets": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==" + }, + "Microsoft.NetFramework.Analyzers": { + "type": "Transitive", + "resolved": "3.3.0", + "contentHash": "JTfMic5fEFWICePbr7GXOGPranqS9Qxu2U/BZEcnnGbK1SFW8TxRyGp6O1L52xsbfOdqmzjc0t5ubhDrjj+Xpg==" + }, + "Microsoft.Win32.Primitives": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "fQnBHO9DgcmkC9dYSJoBqo6sH1VJwJprUHh8F3hbcRlxiQiBUuTntdk8tUwV490OqC2kQUrinGwZyQHTieuXRA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0" + } + }, + "Namotion.Reflection": { + "type": "Transitive", + "resolved": "1.0.14", + "contentHash": "wuJGiFvGfehH2w7jAhMbCJt0/rvUuHyqSZn0sMhNTviDfBZRyX8LFlR/ndQcofkGWulPDfH5nKYTeGXE8xBHPA==", + "dependencies": { + "Microsoft.CSharp": "4.3.0" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "12.0.2", + "contentHash": "rTK0s2EKlfHsQsH6Yx2smvcTCeyoDNgCW7FEYyV01drPlh2T243PR2DiDXqtC5N4GDm4Ma/lkxfW5a/4793vbA==" + }, + "NJsonSchema": { + "type": "Transitive", + "resolved": "10.2.1", + "contentHash": "/BtWbYTusyoSgQkCB4eYijMfZotB/rfASDsl1k9evlkm5vlOP4s4Y09TOzBChU77d/qUABVYL1Xf+TB8E0Wfpw==", + "dependencies": { + "Namotion.Reflection": "1.0.14", + "Newtonsoft.Json": "9.0.1" + } + }, + "NSwag.Annotations": { + "type": "Transitive", + "resolved": "13.8.2", + "contentHash": "/GO+35CjPYQTPS5/Q8udM5JAMEWVo8JsrkV2Uw3OW4/AJU9iOS7t6WJid6ZlkpLMjnW7oex9mvJ2EZNE4eOG/Q==" + }, + "NSwag.Core": { + "type": "Transitive", + "resolved": "13.8.2", + "contentHash": "Hm6pU9qFJuXLo3b27+JTXztfeuI/15Ob1sDsfUu4rchN0+bMogtn8Lia8KVbcalw/M+hXc0rWTFp5ueP23e+iA==", + "dependencies": { + "NJsonSchema": "10.2.1", + "Newtonsoft.Json": "9.0.1" + } + }, + "NSwag.Generation": { + "type": "Transitive", + "resolved": "13.8.2", + "contentHash": "LBIrpHFRZeMMbqL1hdyGb7r8v+T52aOCARxwfAmzE+MlOHVpjsIxyNSXht9EzBFMbSH0tj7CK2Ugo7bm+zUssg==", + "dependencies": { + "NJsonSchema": "10.2.1", + "NSwag.Core": "13.8.2", + "Newtonsoft.Json": "9.0.1" + } + }, + "NSwag.Generation.AspNetCore": { + "type": "Transitive", + "resolved": "13.8.2", + "contentHash": "0ydVv6OidspZ/MS6qmU8hswGtXwq5YZPg+2a2PHGD6jNp2Fef4j1wC3xa3hplDAq7cK+BgpyDKtvj9+X01+P5g==", + "dependencies": { + "Microsoft.AspNetCore.Mvc.ApiExplorer": "1.0.4", + "Microsoft.AspNetCore.Mvc.Core": "1.0.4", + "Microsoft.AspNetCore.Mvc.Formatters.Json": "1.0.4", + "NJsonSchema": "10.2.1", + "NSwag.Generation": "13.8.2" + } + }, + "runtime.native.System": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "QfS/nQI7k/BLgmLrw7qm7YBoULEvgWnPI+cYsbfCVFTW8Aj+i8JhccxcFMu1RWms0YZzF+UHguNBK4Qn89e2Sg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1" + } + }, + "runtime.native.System.Net.Http": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "Nh0UPZx2Vifh8r+J+H2jxifZUD3sBrmolgiFWJd2yiNrxO0xTa6bAw3YwRn1VOiSen/tUXMS31ttNItCZ6lKuA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1" + } + }, + "runtime.native.System.Security.Cryptography": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "2CQK0jmO6Eu7ZeMgD+LOFbNJSXHFVQbCJJkEyEwowh1SCgYnrn9W9RykMfpeeVGw7h4IBvYikzpGUlmZTUafJw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1" + } + }, + "SQLitePCLRaw.bundle_e_sqlite3": { + "type": "Transitive", + "resolved": "2.0.2", + "contentHash": "OVPI/nh5AqfLCIKhAYqjCa6AHhc7oKApGcGM3UhMRSerFiBx58nSpGwxVFdMgjOCWZR+fA49nzsnKlWp5hFo8w==", + "dependencies": { + "SQLitePCLRaw.core": "2.0.2", + "SQLitePCLRaw.lib.e_sqlite3": "2.0.2", + "SQLitePCLRaw.provider.dynamic_cdecl": "2.0.2" + } + }, + "SQLitePCLRaw.core": { + "type": "Transitive", + "resolved": "2.0.2", + "contentHash": "TFSBX426OelS1tkaVC254NVVlrJIe9YLhWPkEvuqJj2104QpmDmEYOhfdfDJD1E/2SmqDhoRw1ek5cQHj8olcQ==", + "dependencies": { + "System.Memory": "4.5.3" + } + }, + "SQLitePCLRaw.lib.e_sqlite3": { + "type": "Transitive", + "resolved": "2.0.2", + "contentHash": "S+Tsqe/M7wsc+9HeediI6UHtBKf2X586aRwhi1aBVLGe0WxkAo52O9ZxwEy/v8XMLefcrEMupd2e9CDlIT6QCw==" + }, + "SQLitePCLRaw.provider.dynamic_cdecl": { + "type": "Transitive", + "resolved": "2.0.2", + "contentHash": "ZSwacbKJUsxJEZxwT23uZVrGbaIvXcADZDz5Sr66fikO5eehdcceDncjzwzTzWfW13di8gpTpstx3WJSt/Ci5Q==", + "dependencies": { + "SQLitePCLRaw.core": "2.0.2" + } + }, + "System.AppContext": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "3QjO4jNV7PdKkmQAVp9atA+usVnKRwI3Kx1nMwJ93T0LcQfx7pKAYk0nKz5wn1oP5iqlhZuy6RXOFdhr7rDwow==", + "dependencies": { + "System.Runtime": "4.1.0" + } + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "msXumHfjjURSkvxUjYuq4N2ghHoRi2VpXcKMA7gK6ujQfU3vGpl+B6ld0ATRg+FZFpRyA6PgEPA+VlIkTeNf2w==", + "dependencies": { + "System.Diagnostics.Debug": "4.0.11", + "System.Diagnostics.Tracing": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Threading": "4.0.11" + } + }, + "System.Collections": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "YUJGz6eFKqS0V//mLt25vFGrrCvOnsXjlvFQs+KimpwNxug9x0Pzy4PlFMU3Q2IzqAa9G2L4LsK3+9vCBK7oTg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.Collections.Concurrent": { + "type": "Transitive", + "resolved": "4.0.12", + "contentHash": "2gBcbb3drMLgxlI0fBfxMA31ec6AEyYCHygGse4vxceJan8mRIWeKJ24BFzN7+bi/NFTgdIgufzb94LWO5EERQ==", + "dependencies": { + "System.Collections": "4.0.11", + "System.Diagnostics.Debug": "4.0.11", + "System.Diagnostics.Tracing": "4.1.0", + "System.Globalization": "4.0.11", + "System.Reflection": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Threading": "4.0.11", + "System.Threading.Tasks": "4.0.11" + } + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "1.7.1", + "contentHash": "B43Zsz5EfMwyEbnObwRxW5u85fzJma3lrDeGcSAV1qkhSRTNY5uXAByTn9h9ddNdhM+4/YoLc/CI43umjwIl9Q==" + }, + "System.Collections.NonGeneric": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "hMxFT2RhhlffyCdKLDXjx8WEC5JfCvNozAZxCablAuFRH74SCV4AgzE8yJCh/73bFnEoZgJ9MJmkjQ0dJmnKqA==", + "dependencies": { + "System.Diagnostics.Debug": "4.0.11", + "System.Globalization": "4.0.11", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Threading": "4.0.11" + } + }, + "System.Collections.Specialized": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "/HKQyVP0yH1I0YtK7KJL/28snxHNH/bi+0lgk/+MbURF6ULhAE31MDI+NZDerNWu264YbxklXCCygISgm+HMug==", + "dependencies": { + "System.Collections.NonGeneric": "4.0.1", + "System.Globalization": "4.0.11", + "System.Globalization.Extensions": "4.0.1", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Threading": "4.0.11" + } + }, + "System.ComponentModel": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "oBZFnm7seFiVfugsIyOvQCWobNZs7FzqDV/B7tx20Ep/l3UUFCPDkdTnCNaJZTU27zjeODmy2C/cP60u3D4c9w==", + "dependencies": { + "System.Runtime": "4.1.0" + } + }, + "System.ComponentModel.Annotations": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "0YFqjhp/mYkDGpU0Ye1GjE53HMp9UVfGN7seGpAMttAC0C40v5gw598jCgpbBLMmCo0E5YRLBv5Z2doypO49ZQ==" + }, + "System.ComponentModel.Primitives": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "sc/7eVCdxPrp3ljpgTKVaQGUXiW05phNWvtv/m2kocXqrUQvTVWKou1Edas2aDjTThLPZOxPYIGNb/HN0QjURg==", + "dependencies": { + "System.ComponentModel": "4.0.1", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.ComponentModel.TypeConverter": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "MnDAlaeJZy9pdB5ZdOlwdxfpI+LJQ6e0hmH7d2+y2LkiD8DRJynyDYl4Xxf3fWFm7SbEwBZh4elcfzONQLOoQw==", + "dependencies": { + "System.Collections": "4.0.11", + "System.Collections.NonGeneric": "4.0.1", + "System.Collections.Specialized": "4.0.1", + "System.ComponentModel": "4.0.1", + "System.ComponentModel.Primitives": "4.1.0", + "System.Globalization": "4.0.11", + "System.Linq": "4.1.0", + "System.Reflection": "4.1.0", + "System.Reflection.Extensions": "4.0.1", + "System.Reflection.Primitives": "4.0.1", + "System.Reflection.TypeExtensions": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Threading": "4.0.11" + } + }, + "System.Diagnostics.Contracts": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "HvQQjy712vnlpPxaloZYkuE78Gn353L0SJLJVeLcNASeg9c4qla2a1Xq8I7B3jZoDzKPtHTkyVO7AZ5tpeQGuA==", + "dependencies": { + "System.Runtime": "4.1.0" + } + }, + "System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "w5U95fVKHY4G8ASs/K5iK3J5LY+/dLFd4vKejsnI/ZhBsWS9hQakfx3Zr7lRWKg4tAw9r4iktyvsTagWkqYCiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "4.7.1", + "contentHash": "j81Lovt90PDAq8kLpaJfJKV/rWdWuEk6jfV+MBkee33vzYLEUsy4gXK8laa9V2nZlLM9VM9yA/OOQxxPEJKAMw==" + }, + "System.Diagnostics.Tools": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "xBfJ8pnd4C17dWaC9FM6aShzbJcRNMChUMD42I6772KGGrqaFdumwhn9OdM68erj1ueNo3xdQ1EwiFjK5k8p0g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "vDN1PoMZCkkdNjvZLql592oYJZgS7URcJzJ7bxeBgGtx5UtR5leNm49VmfHGqIffX4FKacHbI3H6UyNSHQknBg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.Globalization": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "B95h0YLEL2oSnwF/XjqSWKnwKOy/01VWkNlsCeMTFJLLabflpGV26nK164eRs5GiaRSBGpOxQ3pKoSnnyZN5pg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.Globalization.Calendars": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "L1c6IqeQ88vuzC1P81JeHmHA8mxq8a18NUBNXnIY/BVb+TCyAaGIFbhpZt60h9FJNmisymoQkHEFSE9Vslja1Q==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Globalization": "4.0.11", + "System.Runtime": "4.1.0" + } + }, + "System.Globalization.Extensions": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "KKo23iKeOaIg61SSXwjANN7QYDr/3op3OWGGzDzz7mypx0Za0fZSeG0l6cco8Ntp8YMYkIQcAqlk8yhm5/Uhcg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "System.Globalization": "4.0.11", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Runtime.InteropServices": "4.1.0" + } + }, + "System.IO": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.FileSystem": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3wEMARTnuio+ulnvi+hkRNROYwa1kylvYahhcLk4HSoVdl+xxTFVeVlYOfLwrDPImGls0mDqbMhrza8qnWPTdA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.FileSystem.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "6QOb2XFLch7bEc4lIcJH49nJN2HV+OC3fHDgsLVsBVBk3Y4hFAnOBGzJ2lUu7CyDDFo9IBWkSsnbkT6IBwwiMw==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Linq": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "bQ0iYFOQI0nuTnt+NQADns6ucV4DUvMdwN6CbkB1yj8i7arTGiTN5eok1kQwdnnNWSDZfIUySQY+J3d5KjWn0g==", + "dependencies": { + "System.Collections": "4.0.11", + "System.Diagnostics.Debug": "4.0.11", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0" + } + }, + "System.Linq.Expressions": { + "type": "Transitive", + "resolved": "4.1.1", + "contentHash": "bXwi8FrK/XIGPvtk1ZnawffhqLPyacj7dZnbFaV52YGaQigNqGEzNAByAIvL9FlEe3TCzoInorHF91IK//Q3Xg==", + "dependencies": { + "System.Collections": "4.0.11", + "System.Diagnostics.Debug": "4.0.11", + "System.Globalization": "4.0.11", + "System.IO": "4.1.0", + "System.Linq": "4.1.0", + "System.ObjectModel": "4.0.12", + "System.Reflection": "4.1.0", + "System.Reflection.Emit": "4.0.1", + "System.Reflection.Emit.ILGeneration": "4.0.1", + "System.Reflection.Emit.Lightweight": "4.0.1", + "System.Reflection.Extensions": "4.0.1", + "System.Reflection.Primitives": "4.0.1", + "System.Reflection.TypeExtensions": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Threading": "4.0.11" + } + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.3", + "contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA==" + }, + "System.Net.Primitives": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "hVvfl4405DRjA2408luZekbPhplJK03j2Y2lSfMlny7GHXlkByw1iLnc9mgKW0GdQn73vvMcWrWewAhylXA4Nw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Handles": "4.0.1" + } + }, + "System.Net.WebSockets": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "2KJo8hir6Edi9jnMDAMhiJoI691xRBmKcbNpwjrvpIMOCTYOtBpSsSEGBxBDV7PKbasJNaFp1+PZz1D7xS41Hg==", + "dependencies": { + "Microsoft.Win32.Primitives": "4.0.1", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Threading.Tasks": "4.0.11" + } + }, + "System.ObjectModel": { + "type": "Transitive", + "resolved": "4.0.12", + "contentHash": "tAgJM1xt3ytyMoW4qn4wIqgJYm7L7TShRZG4+Q4Qsi2PCcj96pXN7nRywS9KkB3p/xDUjc2HSwP9SROyPYDYKQ==", + "dependencies": { + "System.Collections": "4.0.11", + "System.Diagnostics.Debug": "4.0.11", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Threading": "4.0.11" + } + }, + "System.Reflection": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "JCKANJ0TI7kzoQzuwB/OoJANy1Lg338B6+JVacPl4TpUwi3cReg3nMLplMq2uqYfHFQpKIlHAUVAJlImZz/4ng==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.IO": "4.1.0", + "System.Reflection.Primitives": "4.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.Reflection.Emit": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "VR4kk8XLKebQ4MZuKuIni/7oh+QGFmZW3qORd1GvBq/8026OpW501SzT/oypwiQl4TvT8ErnReh/NzY9u+C6wQ==" + }, + "System.Reflection.Emit.ILGeneration": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "Ov6dU8Bu15Bc7zuqttgHF12J5lwSWyTf1S+FJouUXVMSqImLZzYaQ+vRr1rQ0OZ0HqsrwWl4dsKHELckQkVpgA==", + "dependencies": { + "System.Reflection": "4.1.0", + "System.Reflection.Primitives": "4.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.Reflection.Emit.Lightweight": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "sSzHHXueZ5Uh0OLpUQprhr+ZYJrLPA2Cmr4gn0wj9+FftNKXx8RIMKvO9qnjk2ebPYUjZ+F2ulGdPOsvj+MEjA==", + "dependencies": { + "System.Reflection": "4.1.0", + "System.Reflection.Emit.ILGeneration": "4.0.1", + "System.Reflection.Primitives": "4.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.Reflection.Extensions": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "GYrtRsZcMuHF3sbmRHfMYpvxZoIN2bQGrYGerUiWLEkqdEUQZhH3TRSaC/oI4wO0II1RKBPlpIa1TOMxIcOOzQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Reflection": "4.1.0", + "System.Runtime": "4.1.0" + } + }, + "System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "4inTox4wTBaDhB7V3mPvp9XlCbeGYWVEM9/fXALd52vNEAVisc1BoVWQPuUuD0Ga//dNbA/WeMy9u9mzLxGTHQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.Reflection.TypeExtensions": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "tsQ/ptQ3H5FYfON8lL4MxRk/8kFyE0A+tGPXmVP967cT/gzLHYxIejIYSxp4JmIeFHVP78g/F2FE1mUUTbDtrg==", + "dependencies": { + "System.Reflection": "4.1.0", + "System.Runtime": "4.1.0" + } + }, + "System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "TxwVeUNoTgUOdQ09gfTjvW411MF+w9MBYL7AtNVc+HtBCFlutPLhUCdZjNkjbhj3bNQWMdHboF0KIWEOjJssbA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Globalization": "4.0.11", + "System.Reflection": "4.1.0", + "System.Runtime": "4.1.0" + } + }, + "System.Runtime": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "CUOHjTT/vgP0qGW22U4/hDlOqXmcPq5YicBaXdUR2UiUoLwBT+olO6we4DVbq57jeX5uXH2uerVZhf0qGj+sVQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.Runtime.Handles": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "16eu3kjHS633yYdkjwShDHZLRNMKVi/s0bY8ODiqJ2RfMhDMAwxZaUaWVnZ2P71kr/or+X9o/xFWtNqz8ivieQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Reflection": "4.1.0", + "System.Reflection.Primitives": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Handles": "4.0.1" + } + }, + "System.Runtime.Numerics": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "+XbKFuzdmLP3d1o9pdHu2nxjNr2OEPqGzKeegPLCUMM71a0t50A/rOcIRmGs9wR7a8KuHX6hYs/7/TymIGLNqg==", + "dependencies": { + "System.Globalization": "4.0.11", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0" + } + }, + "System.Runtime.Serialization.Primitives": { + "type": "Transitive", + "resolved": "4.1.1", + "contentHash": "HZ6Du5QrTG8MNJbf4e4qMO3JRAkIboGT5Fk804uZtg3Gq516S7hAqTm2UZKUHa7/6HUGdVy3AqMQKbns06G/cg==", + "dependencies": { + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.Security.Claims": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "4Jlp0OgJLS/Voj1kyFP6MJlIYp3crgfH8kNQk2p7+4JYfc1aAmh9PZyAMMbDhuoolGNtux9HqSOazsioRiDvCw==", + "dependencies": { + "System.Collections": "4.0.11", + "System.Globalization": "4.0.11", + "System.IO": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Security.Principal": "4.0.1" + } + }, + "System.Security.Cryptography.Algorithms": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "8JQFxbLVdrtIOKMDN38Fn0GWnqYZw/oMlwOUG/qz1jqChvyZlnUmu+0s7wLx7JYua/nAXoESpHA3iw11QFWhXg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "System.Collections": "4.0.11", + "System.IO": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Runtime.Handles": "4.0.1", + "System.Runtime.InteropServices": "4.1.0", + "System.Runtime.Numerics": "4.0.1", + "System.Security.Cryptography.Encoding": "4.0.0", + "System.Security.Cryptography.Primitives": "4.0.0", + "System.Text.Encoding": "4.0.11", + "runtime.native.System.Security.Cryptography": "4.0.0" + } + }, + "System.Security.Cryptography.Cng": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "WG3r7EyjUe9CMPFSs6bty5doUqT+q9pbI80hlNzo2SkPkZ4VTuZkGWjpp77JB8+uaL4DFPRdBsAY+DX3dBK92A==" + }, + "System.Security.Cryptography.Csp": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "/i1Usuo4PgAqgbPNC0NjbO3jPW//BoBlTpcWFD1EHVbidH21y4c1ap5bbEMSGAXjAShhMH4abi/K8fILrnu4BQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "System.IO": "4.1.0", + "System.Reflection": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Runtime.Handles": "4.0.1", + "System.Runtime.InteropServices": "4.1.0", + "System.Security.Cryptography.Algorithms": "4.2.0", + "System.Security.Cryptography.Encoding": "4.0.0", + "System.Security.Cryptography.Primitives": "4.0.0", + "System.Text.Encoding": "4.0.11", + "System.Threading": "4.0.11" + } + }, + "System.Security.Cryptography.Encoding": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "FbKgE5MbxSQMPcSVRgwM6bXN3GtyAh04NkV8E5zKCBE26X0vYW0UtTa2FIgkH33WVqBVxRgxljlVYumWtU+HcQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "System.Collections": "4.0.11", + "System.Collections.Concurrent": "4.0.12", + "System.Linq": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Runtime.Handles": "4.0.1", + "System.Runtime.InteropServices": "4.1.0", + "System.Security.Cryptography.Primitives": "4.0.0", + "System.Text.Encoding": "4.0.11", + "runtime.native.System.Security.Cryptography": "4.0.0" + } + }, + "System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "HUG/zNUJwEiLkoURDixzkzZdB5yGA5pQhDP93ArOpDPQMteURIGERRNzzoJlmTreLBWr5lkFSjjMSk8ySEpQMw==", + "dependencies": { + "System.Collections": "4.0.11", + "System.IO": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Runtime.Handles": "4.0.1", + "System.Runtime.InteropServices": "4.1.0", + "System.Runtime.Numerics": "4.0.1", + "System.Security.Cryptography.Algorithms": "4.2.0", + "System.Security.Cryptography.Encoding": "4.0.0", + "System.Security.Cryptography.Primitives": "4.0.0", + "System.Text.Encoding": "4.0.11", + "runtime.native.System.Security.Cryptography": "4.0.0" + } + }, + "System.Security.Cryptography.Primitives": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "Wkd7QryWYjkQclX0bngpntW5HSlMzeJU24UaLJQ7YTfI8ydAVAaU2J+HXLLABOVJlKTVvAeL0Aj39VeTe7L+oA==", + "dependencies": { + "System.Diagnostics.Debug": "4.0.11", + "System.Globalization": "4.0.11", + "System.IO": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Threading": "4.0.11", + "System.Threading.Tasks": "4.0.11" + } + }, + "System.Security.Cryptography.X509Certificates": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "4HEfsQIKAhA1+ApNn729Gi09zh+lYWwyIuViihoMDWp1vQnEkL2ct7mAbhBlLYm+x/L4Rr/pyGge1lIY635e0w==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "System.Collections": "4.0.11", + "System.Diagnostics.Debug": "4.0.11", + "System.Globalization": "4.0.11", + "System.Globalization.Calendars": "4.0.1", + "System.IO": "4.1.0", + "System.IO.FileSystem": "4.0.1", + "System.IO.FileSystem.Primitives": "4.0.1", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Runtime.Handles": "4.0.1", + "System.Runtime.InteropServices": "4.1.0", + "System.Runtime.Numerics": "4.0.1", + "System.Security.Cryptography.Algorithms": "4.2.0", + "System.Security.Cryptography.Cng": "4.2.0", + "System.Security.Cryptography.Csp": "4.0.0", + "System.Security.Cryptography.Encoding": "4.0.0", + "System.Security.Cryptography.OpenSsl": "4.0.0", + "System.Security.Cryptography.Primitives": "4.0.0", + "System.Text.Encoding": "4.0.11", + "System.Threading": "4.0.11", + "runtime.native.System": "4.0.0", + "runtime.native.System.Net.Http": "4.0.1", + "runtime.native.System.Security.Cryptography": "4.0.0" + } + }, + "System.Security.Principal": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "On+SKhXY5rzxh/S8wlH1Rm0ogBlu7zyHNxeNBiXauNrhHRXAe9EuX8Yl5IOzLPGU5Z4kLWHMvORDOCG8iu9hww==", + "dependencies": { + "System.Runtime": "4.1.0" + } + }, + "System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Text.Encoding.Extensions": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "jtbiTDtvfLYgXn8PTfWI+SiBs51rrmO4AAckx4KR6vFK9Wzf6tI8kcRdsYQNwriUeQ1+CtQbM1W4cMbLXnj/OQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "System.Text.Encoding": "4.0.11" + } + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "GgJDO6/1bW6kkttxIiPK2jsqllQ3ifaeeBAJJrcoJq0lAclIZsAZZdEqi6JHq+QLZXL2UsjyWb8K8EOH7nOSPw==", + "dependencies": { + "System.Diagnostics.Debug": "4.0.11", + "System.IO": "4.1.0", + "System.Reflection": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Threading": "4.0.11" + } + }, + "System.Text.Json": { + "type": "Transitive", + "resolved": "4.7.2", + "contentHash": "TcMd95wcrubm9nHvJEQs70rC0H/8omiSGGpU4FQ/ZA1URIqD4pjmFJh2Mfv1yH1eHgJDWTi2hMDXwTET+zOOyg==" + }, + "System.Text.RegularExpressions": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "i88YCXpRTjCnoSQZtdlHkAOx4KNNik4hMy83n0+Ftlb7jvV6ZiZWMpnEZHhjBp6hQVh8gWd/iKNPzlPF7iyA2g==", + "dependencies": { + "System.Collections": "4.0.11", + "System.Globalization": "4.0.11", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Threading": "4.0.11" + } + }, + "System.Threading": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "N+3xqIcg3VDKyjwwCGaZ9HawG9aC6cSDI+s7ROma310GQo8vilFZa86hqKppwTHleR/G0sfOzhvgnUxWCR/DrQ==", + "dependencies": { + "System.Runtime": "4.1.0", + "System.Threading.Tasks": "4.0.11" + } + }, + "System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "pH4FZDsZQ/WmgJtN4LWYmRdJAEeVkyriSwrv2Teoe5FOU0Yxlb6II6GL8dBPOfRmutHGATduj3ooMt7dJ2+i+w==", + "dependencies": { + "System.Collections": "4.0.11", + "System.Runtime": "4.1.0", + "System.Threading.Tasks": "4.0.11" + } + }, + "System.Xml.ReaderWriter": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "ZIiLPsf67YZ9zgr31vzrFaYQqxRPX9cVHjtPSnmx4eN6lbS/yEyYNr2vs1doGDEscF0tjCZFsk9yUg1sC9e8tg==", + "dependencies": { + "System.Collections": "4.0.11", + "System.Diagnostics.Debug": "4.0.11", + "System.Globalization": "4.0.11", + "System.IO": "4.1.0", + "System.IO.FileSystem": "4.0.1", + "System.IO.FileSystem.Primitives": "4.0.1", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Runtime.InteropServices": "4.1.0", + "System.Text.Encoding": "4.0.11", + "System.Text.Encoding.Extensions": "4.0.11", + "System.Text.RegularExpressions": "4.1.0", + "System.Threading.Tasks": "4.0.11", + "System.Threading.Tasks.Extensions": "4.0.0" + } + }, + "System.Xml.XDocument": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "Mk2mKmPi0nWaoiYeotq1dgeNK1fqWh61+EK+w4Wu8SWuTYLzpUnschb59bJtGywaPq7SmTuPf44wrXRwbIrukg==", + "dependencies": { + "System.Collections": "4.0.11", + "System.Diagnostics.Debug": "4.0.11", + "System.Diagnostics.Tools": "4.0.1", + "System.Globalization": "4.0.11", + "System.IO": "4.1.0", + "System.Reflection": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Text.Encoding": "4.0.11", + "System.Threading": "4.0.11", + "System.Xml.ReaderWriter": "4.0.11" + } + }, + "System.Xml.XPath": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "UWd1H+1IJ9Wlq5nognZ/XJdyj8qPE4XufBUkAW59ijsCPjZkZe0MUzKKJFBr+ZWBe5Wq1u1d5f2CYgE93uH7DA==", + "dependencies": { + "System.Collections": "4.0.11", + "System.Diagnostics.Debug": "4.0.11", + "System.Globalization": "4.0.11", + "System.IO": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Threading": "4.0.11", + "System.Xml.ReaderWriter": "4.0.11" + } + }, + "System.Xml.XPath.XDocument": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "FLhdYJx4331oGovQypQ8JIw2kEmNzCsjVOVYY/16kZTUoquZG85oVn7yUhBE2OZt1yGPSXAL0HTEfzjlbNpM7Q==", + "dependencies": { + "System.Diagnostics.Debug": "4.0.11", + "System.Linq": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Threading": "4.0.11", + "System.Xml.ReaderWriter": "4.0.11", + "System.Xml.XDocument": "4.0.11", + "System.Xml.XPath": "4.0.1" + } + }, + "timeline.errorcodes": { + "type": "Project" + } + } + } +} \ No newline at end of file diff --git a/BackEnd/tools/convert-eol.py b/BackEnd/tools/convert-eol.py new file mode 100644 index 00000000..3ea8ed7c --- /dev/null +++ b/BackEnd/tools/convert-eol.py @@ -0,0 +1,35 @@ +# This is a python script that converts all text source codes into +# CRLF (Windows line ending) eol format and UTF-8 with NO BOM encoding. + +import glob +import os.path + +project_root = os.path.relpath(os.path.join(os.path.dirname(__file__), '..')) + + +def convert(file_path): + with open(file_path, 'r', encoding='utf-8') as open_file: + content = open_file.read() + + #if there is BOM, remove BOM + if content[0] == '\ufeff': + content = content[1:] + + with open(file_path, 'w', encoding='utf-8', newline='\r\n') as open_file: + open_file.write(content) + + +glob_list = [ + './nuget.config', + '**/*.sln', + '**/*.cs', + '**/*.csproj', + '**/appsettings*.json' +] + +for glob_pattern in glob_list: + for f in glob.glob(glob_pattern, recursive=True): + print('Converting {}'.format(f)) + convert(f) + +print('Done!!!') diff --git a/Directory.Build.props b/Directory.Build.props deleted file mode 100644 index 5030ba17..00000000 --- a/Directory.Build.props +++ /dev/null @@ -1,6 +0,0 @@ - - - true - true - - diff --git a/Dockerfile b/Dockerfile index 5be1d333..b9b958b0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,16 @@ -FROM crupest/timeline-build-env:latest AS build -WORKDIR /timeline-app -COPY . . -RUN dotnet publish Timeline/Timeline.csproj --configuration Release --output ./Timeline/publish/ -r linux-x64 --self-contained false - -FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 -WORKDIR /app -ENV ASPNETCORE_FORWARDEDHEADERS_ENABLED true -COPY --from=build /timeline-app/Timeline/publish . -ENTRYPOINT ["dotnet", "Timeline.dll"] +FROM node:latest AS front-build +WORKDIR /timeline-app +COPY FrontEnd . +RUN yarn build + +FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS back-build +WORKDIR /timeline-app +COPY BackEnd . +COPY --from=front-build /timeline-app/dist /timeline-app/Timeline/ClientApp +RUN dotnet publish Timeline/Timeline.csproj --configuration Release --output ./Timeline/publish/ -r linux-x64 --self-contained false + +FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 +WORKDIR /app +ENV ASPNETCORE_FORWARDEDHEADERS_ENABLED true +COPY --from=back-build /timeline-app/Timeline/publish . +ENTRYPOINT ["dotnet", "Timeline.dll"] diff --git a/FrontEnd/.babelrc b/FrontEnd/.babelrc new file mode 100644 index 00000000..092f2f73 --- /dev/null +++ b/FrontEnd/.babelrc @@ -0,0 +1,27 @@ +{ + "presets": [ + "@babel/env", + "@babel/preset-react" + ], + "plugins": [ + "@babel/plugin-syntax-dynamic-import", + "@babel/plugin-proposal-class-properties", + "@babel/plugin-proposal-optional-chaining", + "@babel/plugin-proposal-nullish-coalescing-operator", + [ + "@babel/plugin-proposal-decorators", + { + "decoratorsBeforeExport": true + } + ], + [ + "babel-plugin-transform-builtin-extend", + { + "globals": [ + "Error", + "Array" + ] + } + ] + ] +} \ No newline at end of file diff --git a/FrontEnd/.editorconfig b/FrontEnd/.editorconfig new file mode 100644 index 00000000..779719e0 --- /dev/null +++ b/FrontEnd/.editorconfig @@ -0,0 +1,14 @@ +root = true +end_of_line = lf + +[*.ts] +tab_width = 2 + +[*.tsx] +tab_width = 2 + +[*.css] +tab_width = 2 + +[*.sass] +tab_width = 2 diff --git a/FrontEnd/.eslintignore b/FrontEnd/.eslintignore new file mode 100644 index 00000000..f29f7466 --- /dev/null +++ b/FrontEnd/.eslintignore @@ -0,0 +1,6 @@ +.yarn +node_modules +dist +webpack.*.js +.eslintrc.js +postcss.config.js diff --git a/FrontEnd/.eslintrc.js b/FrontEnd/.eslintrc.js new file mode 100644 index 00000000..900489ed --- /dev/null +++ b/FrontEnd/.eslintrc.js @@ -0,0 +1,47 @@ +module.exports = { + env: { + browser: true, + es2020: true, + }, + extends: [ + "eslint:recommended", + "plugin:react/recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking", + "plugin:prettier/recommended", + "prettier/react", + "prettier/@typescript-eslint", + "plugin:react-hooks/recommended", + ], + globals: { + Atomics: "readonly", + SharedArrayBuffer: "readonly", + }, + parser: "@typescript-eslint/parser", + parserOptions: { + project: ["./src/app/tsconfig.json", "./src/sw/tsconfig.json"], + ecmaFeatures: { + jsx: true, + }, + sourceType: "module", + }, + plugins: ["react", "@typescript-eslint", "react-hooks"], + settings: { + react: { + version: "detect", + }, + }, + rules: { + "react/prop-types": "off", + "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], + "@typescript-eslint/explicit-function-return-type": [ + "warn", + { + allowExpressions: true, + allowTypedFunctionExpressions: true, + allowHigherOrderFunctions: true, + }, + ], + }, +}; diff --git a/FrontEnd/.gitattributes b/FrontEnd/.gitattributes new file mode 100644 index 00000000..c1aa21ac --- /dev/null +++ b/FrontEnd/.gitattributes @@ -0,0 +1 @@ +.yarn/** linguist-vendored \ No newline at end of file diff --git a/FrontEnd/.gitignore b/FrontEnd/.gitignore new file mode 100644 index 00000000..1de0b58f --- /dev/null +++ b/FrontEnd/.gitignore @@ -0,0 +1,32 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules + +.yarn/* +!.yarn/cache +!.yarn/releases +!.yarn/plugins +!.yarn/sdks +!.yarn/versions + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +.vscode/launch.json + +/dist \ No newline at end of file diff --git a/FrontEnd/.vscode/extensions.json b/FrontEnd/.vscode/extensions.json new file mode 100644 index 00000000..be640996 --- /dev/null +++ b/FrontEnd/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + "recommendations": [ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "arcanis.vscode-zipfs", + "syler.sass-indented", + "editorconfig.editorconfig" + ] +} diff --git a/FrontEnd/.vscode/preview.yml b/FrontEnd/.vscode/preview.yml new file mode 100644 index 00000000..b2cf70af --- /dev/null +++ b/FrontEnd/.vscode/preview.yml @@ -0,0 +1,10 @@ + +# .vscode/preview.yml +autoOpen: false # 打开工作空间时是否自动开启所有应用的预览 +apps: + - port: 3000 # 应用的端口 + run: yarn start:mock # 应用的启动命令 + root: . # 应用的启动目录 + name: timeline # 应用名称 + description: Timeline App # 应用描述 + autoOpen: false # 打开工作空间时是否自动开启预览(优先级高于根级 autoOpen) diff --git a/FrontEnd/.vscode/settings.json b/FrontEnd/.vscode/settings.json new file mode 100644 index 00000000..3db658ba --- /dev/null +++ b/FrontEnd/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "eslint.nodePath": ".yarn/sdks", + "prettier.prettierPath": ".yarn/sdks/prettier/index.js", + "typescript.tsdk": ".yarn/sdks/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true +} diff --git a/FrontEnd/.yarnrc.yml b/FrontEnd/.yarnrc.yml new file mode 100644 index 00000000..2e4e43a7 --- /dev/null +++ b/FrontEnd/.yarnrc.yml @@ -0,0 +1,5 @@ +plugins: + - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs + spec: "@yarnpkg/plugin-interactive-tools" + +yarnPath: .yarn/releases/yarn-2.1.1.cjs diff --git a/FrontEnd/LICENSE b/FrontEnd/LICENSE new file mode 100644 index 00000000..238cd2d9 --- /dev/null +++ b/FrontEnd/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 杨宇千 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/FrontEnd/package.json b/FrontEnd/package.json new file mode 100644 index 00000000..65c5cbe2 --- /dev/null +++ b/FrontEnd/package.json @@ -0,0 +1,111 @@ +{ + "name": "timeline", + "version": "0.1.0", + "private": true, + "homepage": "https://crupest.xyz", + "keywords": [], + "description": "Timeline app.", + "dependencies": { + "axios": "^0.21.0", + "bootstrap": "^4.5.3", + "bootstrap-icons": "^1.0.0", + "classnames": "^2.2.6", + "clsx": "^1.1.1", + "core-js": "^3.6.5", + "i18next": "^19.8.3", + "i18next-browser-languagedetector": "^6.0.1", + "localforage": "^1.9.0", + "lodash": "^4.17.20", + "pepjs": "^0.5.2", + "react": "^17.0.1", + "react-bootstrap": "^1.4.0", + "react-dom": "^17.0.1", + "react-hot-loader": "^4.13.0", + "react-i18next": "^11.7.3", + "react-inlinesvg": "^2.1.1", + "react-responsive": "^8.1.0", + "react-router": "^5.2.0", + "react-router-bootstrap": "^0.25.0", + "react-router-dom": "^5.2.0", + "regenerator-runtime": "^0.13.7", + "rxjs": "^6.6.3", + "workbox-precaching": "^5.1.4", + "workbox-routing": "^5.1.4", + "workbox-strategies": "^5.1.4", + "workbox-window": "^5.1.4", + "xregexp": "^4.3.0" + }, + "scripts": { + "start": "webpack-dev-server --config ./webpack.config.dev.js", + "build": "webpack --config ./webpack.config.prod.js", + "lint": "eslint src/ --ext .js --ext .jsx --ext .ts --ext .tsx" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@babel/core": "^7.12.3", + "@babel/plugin-proposal-class-properties": "^7.12.1", + "@babel/plugin-proposal-decorators": "^7.12.1", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1", + "@babel/plugin-proposal-optional-chaining": "^7.12.1", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/preset-env": "^7.12.1", + "@babel/preset-react": "^7.12.1", + "@babel/preset-typescript": "^7.12.1", + "@hot-loader/react-dom": "^17.0.0", + "@types/classnames": "^2.2.10", + "@types/lodash": "^4.14.162", + "@types/node": "^14.14.5", + "@types/react": "^16.9.53", + "@types/react-dom": "^16.9.8", + "@types/react-responsive": "^8.0.2", + "@types/react-router": "^5.1.8", + "@types/react-router-bootstrap": "^0.24.5", + "@types/react-router-dom": "^5.1.6", + "@types/webpack-env": "^1.15.3", + "@types/xregexp": "^4.3.0", + "@typescript-eslint/eslint-plugin": "^4.6.0", + "@typescript-eslint/parser": "^4.6.0", + "@yarnpkg/pnpify": "^2.3.3", + "babel-loader": "^8.1.0", + "babel-plugin-transform-builtin-extend": "^1.1.2", + "clean-webpack-plugin": "^3.0.0", + "copy-webpack-plugin": "^6.2.1", + "css-loader": "^5.0.0", + "eslint": "^7.12.1", + "eslint-config-prettier": "^6.14.0", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-prettier": "^3.1.4", + "eslint-plugin-react": "^7.21.5", + "eslint-plugin-react-hooks": "^4.2.0", + "file-loader": "^6.1.1", + "html-webpack-plugin": "^4.5.0", + "http-server": "^0.12.3", + "mini-css-extract-plugin": "^1.2.0", + "postcss": "^8.1.4", + "postcss-loader": "^4.0.4", + "postcss-preset-env": "^6.7.0", + "prettier": "^2.1.2", + "sass": "^1.27.0", + "sass-loader": "^10.0.4", + "style-loader": "^2.0.0", + "ts-loader": "^8.0.7", + "typescript": "^4.0.5", + "url-loader": "^4.1.1", + "webpack": "^5.2.0", + "webpack-chain": "^6.5.1", + "webpack-cli": "^4.1.0", + "webpack-dev-server": "^3.11.0", + "workbox-webpack-plugin": "^5.1.4" + } +} diff --git a/FrontEnd/postcss.config.js b/FrontEnd/postcss.config.js new file mode 100644 index 00000000..74ee8155 --- /dev/null +++ b/FrontEnd/postcss.config.js @@ -0,0 +1,10 @@ +module.exports = { + plugins: [ + [ + "postcss-preset-env", + { + // Options + }, + ], + ], +}; diff --git a/FrontEnd/public/android-chrome-192x192.png b/FrontEnd/public/android-chrome-192x192.png new file mode 100644 index 00000000..da9b6b81 Binary files /dev/null and b/FrontEnd/public/android-chrome-192x192.png differ diff --git a/FrontEnd/public/android-chrome-512x512.png b/FrontEnd/public/android-chrome-512x512.png new file mode 100644 index 00000000..fa84e055 Binary files /dev/null and b/FrontEnd/public/android-chrome-512x512.png differ diff --git a/FrontEnd/public/apple-touch-icon.png b/FrontEnd/public/apple-touch-icon.png new file mode 100644 index 00000000..d5a3fb45 Binary files /dev/null and b/FrontEnd/public/apple-touch-icon.png differ diff --git a/FrontEnd/public/browserconfig.xml b/FrontEnd/public/browserconfig.xml new file mode 100644 index 00000000..f2c89409 --- /dev/null +++ b/FrontEnd/public/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #2d89ef + + + diff --git a/FrontEnd/public/favicon-16x16.png b/FrontEnd/public/favicon-16x16.png new file mode 100644 index 00000000..6c978995 Binary files /dev/null and b/FrontEnd/public/favicon-16x16.png differ diff --git a/FrontEnd/public/favicon-32x32.png b/FrontEnd/public/favicon-32x32.png new file mode 100644 index 00000000..bbde902f Binary files /dev/null and b/FrontEnd/public/favicon-32x32.png differ diff --git a/FrontEnd/public/favicon.ico b/FrontEnd/public/favicon.ico new file mode 100644 index 00000000..d4cd3db6 Binary files /dev/null and b/FrontEnd/public/favicon.ico differ diff --git a/FrontEnd/public/mstile-144x144.png b/FrontEnd/public/mstile-144x144.png new file mode 100644 index 00000000..61eaaf43 Binary files /dev/null and b/FrontEnd/public/mstile-144x144.png differ diff --git a/FrontEnd/public/mstile-150x150.png b/FrontEnd/public/mstile-150x150.png new file mode 100644 index 00000000..85fa83ee Binary files /dev/null and b/FrontEnd/public/mstile-150x150.png differ diff --git a/FrontEnd/public/mstile-310x150.png b/FrontEnd/public/mstile-310x150.png new file mode 100644 index 00000000..41889953 Binary files /dev/null and b/FrontEnd/public/mstile-310x150.png differ diff --git a/FrontEnd/public/mstile-310x310.png b/FrontEnd/public/mstile-310x310.png new file mode 100644 index 00000000..cddce02e Binary files /dev/null and b/FrontEnd/public/mstile-310x310.png differ diff --git a/FrontEnd/public/mstile-70x70.png b/FrontEnd/public/mstile-70x70.png new file mode 100644 index 00000000..52f59d43 Binary files /dev/null and b/FrontEnd/public/mstile-70x70.png differ diff --git a/FrontEnd/public/safari-pinned-tab.svg b/FrontEnd/public/safari-pinned-tab.svg new file mode 100644 index 00000000..e91f046a --- /dev/null +++ b/FrontEnd/public/safari-pinned-tab.svg @@ -0,0 +1,25 @@ + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + diff --git a/FrontEnd/public/site.webmanifest b/FrontEnd/public/site.webmanifest new file mode 100644 index 00000000..74f0901a --- /dev/null +++ b/FrontEnd/public/site.webmanifest @@ -0,0 +1,22 @@ +{ + "$schema": "http://json.schemastore.org/web-manifest", + + "name": "Timeline", + "short_name": "Timeline", + "description": "Record your life in Timeline! Created by crupest.", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/FrontEnd/sandbox.config.json b/FrontEnd/sandbox.config.json new file mode 100644 index 00000000..90f99b70 --- /dev/null +++ b/FrontEnd/sandbox.config.json @@ -0,0 +1,11 @@ +{ + "infiniteLoopProtection": true, + "hardReloadOnChange": false, + "view": "browser", + "container": { + "port": 3000, + "startScript": "start:mock" + }, + "port": 3000, + "startScript": "start:mock" +} diff --git a/FrontEnd/src/app/App.tsx b/FrontEnd/src/app/App.tsx new file mode 100644 index 00000000..b68eddb6 --- /dev/null +++ b/FrontEnd/src/app/App.tsx @@ -0,0 +1,84 @@ +import React from "react"; +import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; +import { hot } from "react-hot-loader/root"; + +import AppBar from "./views/common/AppBar"; +import LoadingPage from "./views/common/LoadingPage"; +import Home from "./views/home"; +import Login from "./views/login"; +import Settings from "./views/settings"; +import About from "./views/about"; +import User from "./views/user"; +import TimelinePage from "./views/timeline"; +import AlertHost from "./views/common/alert/AlertHost"; + +import { dataStorage } from "./services/common"; +import { userService, useRawUser } from "./services/user"; + +const NoMatch: React.FC = () => { + return ( + <> + +
+
Ah-oh, 404!
+ + ); +}; + +const LazyAdmin = React.lazy( + () => import(/* webpackChunkName: "admin" */ "./views/admin/Admin") +); + +const App: React.FC = () => { + const [loading, setLoading] = React.useState(true); + + const user = useRawUser(); + + React.useEffect(() => { + void userService.checkLoginState(); + void dataStorage.ready().then(() => setLoading(false)); + }, []); + + if (user === undefined || loading) { + return ; + } else { + return ( + }> + + + + + + + + + + + + + + + + + + + + + + {user && user.administrator && ( + + + + )} + + + + + + + + ); + } +}; + +export default hot(App); diff --git a/FrontEnd/src/app/common.ts b/FrontEnd/src/app/common.ts new file mode 100644 index 00000000..0a2d345f --- /dev/null +++ b/FrontEnd/src/app/common.ts @@ -0,0 +1,44 @@ +import React from "react"; +import { Observable, Subject } from "rxjs"; + +// This error is thrown when ui goes wrong with bad logic. +// Such as a variable should not be null, but it does. +// This error should never occur. If it does, it indicates there is some logic bug in codes. +export class UiLogicError extends Error {} + +export function useEventEmiiter(): [() => Observable, () => void] { + const ref = React.useRef | null>(null); + + return React.useMemo(() => { + const getter = (): Subject => { + if (ref.current == null) { + ref.current = new Subject(); + } + return ref.current; + }; + const trigger = (): void => { + getter().next(null); + }; + return [getter, trigger]; + }, []); +} + +export function useValueEventEmiiter(): [ + () => Observable, + (value: T) => void +] { + const ref = React.useRef | null>(null); + + return React.useMemo(() => { + const getter = (): Subject => { + if (ref.current == null) { + ref.current = new Subject(); + } + return ref.current; + }; + const trigger = (value: T): void => { + getter().next(value); + }; + return [getter, trigger]; + }, []); +} diff --git a/FrontEnd/src/app/http/common.ts b/FrontEnd/src/app/http/common.ts new file mode 100644 index 00000000..54203d1a --- /dev/null +++ b/FrontEnd/src/app/http/common.ts @@ -0,0 +1,161 @@ +import { AxiosError, AxiosResponse } from "axios"; + +export const apiBaseUrl = "/api"; + +export function base64(blob: Blob): Promise { + 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/FrontEnd/src/app/http/timeline.ts b/FrontEnd/src/app/http/timeline.ts new file mode 100644 index 00000000..eb7d5065 --- /dev/null +++ b/FrontEnd/src/app/http/timeline.ts @@ -0,0 +1,544 @@ +import axios, { AxiosError } from "axios"; + +import { updateQueryString, applyQueryParameters } from "../utilities/url"; + +import { + apiBaseUrl, + extractResponseData, + convertToNetworkError, + base64, + convertToIfStatusCodeIs, + convertToIfErrorCodeIs, + BlobWithEtag, + NotModified, + convertToNotModified, + convertToForbiddenError, + convertToBlobWithEtag, +} from "./common"; +import { HttpUser } from "./user"; + +export const kTimelineVisibilities = ["Public", "Register", "Private"] as const; + +export type TimelineVisibility = typeof kTimelineVisibilities[number]; + +export interface HttpTimelineInfo { + uniqueId: string; + name: string; + description: string; + owner: HttpUser; + visibility: TimelineVisibility; + lastModified: Date; + members: HttpUser[]; +} + +export interface HttpTimelineListQuery { + visibility?: TimelineVisibility; + relate?: string; + relateType?: "own" | "join"; +} + +export interface HttpTimelinePostRequest { + name: string; +} + +export interface HttpTimelinePostTextContent { + type: "text"; + text: string; +} + +export interface HttpTimelinePostImageContent { + type: "image"; +} + +export type HttpTimelinePostContent = + | HttpTimelinePostTextContent + | HttpTimelinePostImageContent; + +export interface HttpTimelinePostInfo { + id: number; + content: HttpTimelinePostContent; + time: Date; + lastUpdated: Date; + author: HttpUser; + deleted: false; +} + +export interface HttpTimelineDeletedPostInfo { + id: number; + time: Date; + lastUpdated: Date; + author?: HttpUser; + deleted: true; +} + +export type HttpTimelineGenericPostInfo = + | HttpTimelinePostInfo + | HttpTimelineDeletedPostInfo; + +export interface HttpTimelinePostPostRequestTextContent { + type: "text"; + text: string; +} + +export interface HttpTimelinePostPostRequestImageContent { + type: "image"; + data: Blob; +} + +export type HttpTimelinePostPostRequestContent = + | HttpTimelinePostPostRequestTextContent + | HttpTimelinePostPostRequestImageContent; + +export interface HttpTimelinePostPostRequest { + content: HttpTimelinePostPostRequestContent; + time?: Date; +} + +export interface HttpTimelinePatchRequest { + visibility?: TimelineVisibility; + description?: string; +} + +export class HttpTimelineNotExistError extends Error { + constructor(public innerError?: AxiosError) { + super(); + } +} + +export class HttpTimelinePostNotExistError extends Error { + constructor(public innerError?: AxiosError) { + super(); + } +} + +export class HttpTimelineNameConflictError extends Error { + constructor(public innerError?: AxiosError) { + super(); + } +} + +//-------------------- begin: internal model -------------------- + +interface RawTimelineInfo { + uniqueId: string; + name: string; + description: string; + owner: HttpUser; + visibility: TimelineVisibility; + lastModified: string; + members: HttpUser[]; +} + +interface RawTimelinePostTextContent { + type: "text"; + text: string; +} + +interface RawTimelinePostImageContent { + type: "image"; + url: string; +} + +type RawTimelinePostContent = + | RawTimelinePostTextContent + | RawTimelinePostImageContent; + +interface RawTimelinePostInfo { + id: number; + content: RawTimelinePostContent; + time: string; + lastUpdated: string; + author: HttpUser; + deleted: false; +} + +interface RawTimelineDeletedPostInfo { + id: number; + time: string; + lastUpdated: string; + author: HttpUser; + deleted: true; +} + +type RawTimelineGenericPostInfo = + | RawTimelinePostInfo + | RawTimelineDeletedPostInfo; + +interface RawTimelinePostPostRequestTextContent { + type: "text"; + text: string; +} + +interface RawTimelinePostPostRequestImageContent { + type: "image"; + data: string; +} + +type RawTimelinePostPostRequestContent = + | RawTimelinePostPostRequestTextContent + | RawTimelinePostPostRequestImageContent; + +interface RawTimelinePostPostRequest { + content: RawTimelinePostPostRequestContent; + time?: string; +} + +//-------------------- end: internal model -------------------- + +function processRawTimelineInfo(raw: RawTimelineInfo): HttpTimelineInfo { + return { + ...raw, + lastModified: new Date(raw.lastModified), + }; +} + +function processRawTimelinePostInfo( + raw: RawTimelinePostInfo +): HttpTimelinePostInfo; +function processRawTimelinePostInfo( + raw: RawTimelineGenericPostInfo +): HttpTimelineGenericPostInfo; +function processRawTimelinePostInfo( + raw: RawTimelineGenericPostInfo +): HttpTimelineGenericPostInfo { + return { + ...raw, + time: new Date(raw.time), + lastUpdated: new Date(raw.lastUpdated), + }; +} + +export interface IHttpTimelineClient { + listTimeline(query: HttpTimelineListQuery): Promise; + 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/FrontEnd/src/app/http/token.ts b/FrontEnd/src/app/http/token.ts new file mode 100644 index 00000000..ae0cf3f6 --- /dev/null +++ b/FrontEnd/src/app/http/token.ts @@ -0,0 +1,72 @@ +import axios, { AxiosError } from "axios"; + +import { + apiBaseUrl, + convertToNetworkError, + convertToIfErrorCodeIs, + extractResponseData, +} from "./common"; +import { HttpUser } from "./user"; + +export interface HttpCreateTokenRequest { + username: string; + password: string; + expire: number; +} + +export interface HttpCreateTokenResponse { + token: string; + user: HttpUser; +} + +export interface HttpVerifyTokenRequest { + token: string; +} + +export interface HttpVerifyTokenResponse { + user: HttpUser; +} + +export class HttpCreateTokenBadCredentialError extends Error { + constructor(public innerError?: AxiosError) { + super(); + } +} + +export interface IHttpTokenClient { + create(req: HttpCreateTokenRequest): Promise; + 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/FrontEnd/src/app/http/user.ts b/FrontEnd/src/app/http/user.ts new file mode 100644 index 00000000..a0a02cce --- /dev/null +++ b/FrontEnd/src/app/http/user.ts @@ -0,0 +1,134 @@ +import axios, { AxiosError } from "axios"; + +import { + apiBaseUrl, + convertToNetworkError, + extractResponseData, + convertToIfStatusCodeIs, + convertToIfErrorCodeIs, + NotModified, + BlobWithEtag, + convertToBlobWithEtag, + convertToNotModified, +} from "./common"; + +export interface HttpUser { + uniqueId: string; + username: string; + administrator: boolean; + nickname: string; +} + +export interface HttpUserPatchRequest { + nickname?: string; +} + +export interface HttpChangePasswordRequest { + oldPassword: string; + newPassword: string; +} + +export class HttpUserNotExistError extends Error { + constructor(public innerError?: AxiosError) { + super(); + } +} + +export class HttpChangePasswordBadCredentialError extends Error { + constructor(public innerError?: AxiosError) { + super(); + } +} + +export interface IHttpUserClient { + get(username: string): Promise; + 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; +} diff --git a/FrontEnd/src/app/i18n.ts b/FrontEnd/src/app/i18n.ts new file mode 100644 index 00000000..cdced7bf --- /dev/null +++ b/FrontEnd/src/app/i18n.ts @@ -0,0 +1,79 @@ +import i18n, { BackendModule, ResourceKey } from "i18next"; +import LanguageDetector from "i18next-browser-languagedetector"; +import { initReactI18next } from "react-i18next"; + +const backend: BackendModule = { + type: "backend", + async read(language, namespace, callback) { + function error(message: string): void { + callback(new Error(message), false); + } + + function success(result: ResourceKey): void { + callback(null, result); + } + + if (namespace !== "translation") { + error("Namespace must be 'translation'."); + } + + if (language === "en") { + const res = ( + await import( + /* webpackChunkName: "locales-en" */ "./locales/en/translation" + ) + ).default; + success(res); + } else if (language === "zh-cn" || language === "zh") { + const res = ( + await import( + /* webpackChunkName: "locales-zh" */ "./locales/zh/translation" + ) + ).default; + success(res); + } else { + error(`Language ${language} is not supported.`); + } + }, + init() {}, // eslint-disable-line @typescript-eslint/no-empty-function + create() {}, // eslint-disable-line @typescript-eslint/no-empty-function +}; + +export const i18nPromise = i18n + .use(LanguageDetector) + .use(backend) + .use(initReactI18next) // bind react-i18next to the instance + .init({ + fallbackLng: false, + lowerCaseLng: true, + + debug: process.env.NODE_ENV === "development", + + interpolation: { + escapeValue: false, // not needed for react!! + }, + + // react i18next special options (optional) + // override if needed - omit if ok with defaults + /* + react: { + bindI18n: 'languageChanged', + bindI18nStore: '', + transEmptyNodeValue: '', + transSupportBasicHtmlNodes: true, + transKeepBasicHtmlNodesFor: ['br', 'strong', 'i'], + useSuspense: true, + } + */ + }); + +if (module.hot) { + module.hot.accept( + ["./locales/en/translation", "./locales/zh/translation"], + () => { + void i18n.reloadResources(); + } + ); +} + +export default i18n; diff --git a/FrontEnd/src/app/index.ejs b/FrontEnd/src/app/index.ejs new file mode 100644 index 00000000..c2ff4182 --- /dev/null +++ b/FrontEnd/src/app/index.ejs @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + <%= htmlWebpackPlugin.options.title %> + + + +
+ + + diff --git a/FrontEnd/src/app/index.sass b/FrontEnd/src/app/index.sass new file mode 100644 index 00000000..8e87e4ac --- /dev/null +++ b/FrontEnd/src/app/index.sass @@ -0,0 +1,66 @@ +@import '~bootstrap/scss/bootstrap' + +@import './views/common/common' +@import './views/common/alert/alert' +@import './views/home/home' +@import './views/about/about' +@import './views/login/login' +@import './views/timeline-common/timeline-common' +@import './views/timeline/timeline' +@import './views/user/user' + +body + margin: 0 + +small + line-height: 1.2 + +.flex-fix-length + flex-grow: 0 + flex-shrink: 0 + +.position-lt + left: 0 + top: 0 + +.avatar + width: 60px + &.large + width: 100px + &.small + width: 40px + +.mt-appbar + margin-top: 56px + +.icon-button + font-size: 1.4em + &.large + font-size: 1.6em + +.cursor-pointer + cursor: pointer + +textarea + resize: none + +.white-space-no-wrap + white-space: nowrap + +.cru-card + @extend .shadow + @extend .border + @extend .border-primary + @extend .rounded + @extend .bg-light + +.full-viewport-center-child + position: fixed + width: 100vw + height: 100vh + display: flex + justify-content: center + align-items: center + +.text-orange + color: $orange diff --git a/FrontEnd/src/app/index.tsx b/FrontEnd/src/app/index.tsx new file mode 100644 index 00000000..00a75a4a --- /dev/null +++ b/FrontEnd/src/app/index.tsx @@ -0,0 +1,15 @@ +import "regenerator-runtime"; +import "core-js/modules/es.promise"; +import "core-js/modules/es.array.iterator"; +import "pepjs"; + +import React from "react"; +import ReactDOM from "react-dom"; + +import "./index.sass"; + +import "./i18n"; + +import App from "./App"; + +ReactDOM.render(, document.getElementById("app")); diff --git a/FrontEnd/src/app/locales/en/translation.ts b/FrontEnd/src/app/locales/en/translation.ts new file mode 100644 index 00000000..c7f33d1e --- /dev/null +++ b/FrontEnd/src/app/locales/en/translation.ts @@ -0,0 +1,202 @@ +import TranslationResource from "../scheme"; + +const translation: TranslationResource = { + welcome: "Welcome!", + search: "Search", + loadFailReload: "Load failed, click <1>here to reload.", + serviceWorker: { + availableOffline: + "Timeline is now cached in your computer and you can use it offline. 🎉🎉🎉", + upgradePrompt: "App is getting a new version!", + upgradeNow: "Update Now", + upgradeSuccess: + "Congratulations! App update succeeded! Still you can use it offline. 🎉🎉🎉", + externalActivatedPrompt: + "A new version of app is activated. Please refresh the page. Or it may be broken.", + reloadNow: "Refresh Now", + }, + nav: { + settings: "Settings", + login: "Login", + about: "About", + }, + chooseImage: "Choose a image", + loadImageError: "Failed to load image.", + home: { + go: "Go!", + allTimeline: "All Timelines", + joinTimeline: "Joined Timelines", + ownTimeline: "Owned Timelines", + offlinePrompt: + "Oh oh, it seems you are offline. Here list some timelines cached locally. You can view them or click <1>here to refresh.", + createButton: "Create Timeline", + createDialog: { + title: "Create Timeline!", + name: "Name", + nameFormat: + "Name must consist of only letter including non-English letter, digit, hyphen(-) and underline(_) and be no longer than 26.", + badFormat: "Bad format.", + noEmpty: "Empty is not allowed.", + tooLong: "Too long.", + }, + }, + operationDialog: { + retry: "Retry", + nextStep: "Next", + previousStep: "Previous", + confirm: "Confirm", + cancel: "Cancel", + ok: "OK!", + processing: "Processing...", + success: "Success!", + error: "An error occured.", + }, + timeline: { + messageCantSee: "Sorry, you are not allowed to see this timeline.😅", + userNotExist: "The user does not exist!", + timelineNotExist: "The timeline does not exist!", + manage: "Manage", + memberButton: "Member", + send: "Send", + deletePostFailed: "Failed to delete post.", + sendPostFailed: "Failed to send post.", + visibility: { + public: "public to everyone", + register: "only registed people can see", + private: "only members can see", + }, + visibilityTooltip: { + public: + "Everyone including those without accounts can see content of the timeline.", + register: + "Only those who have an account and logined can see content of the timeline.", + private: "Only members of this timeline can see content of the timeline.", + }, + dialogChangeProperty: { + title: "Change Timeline Properties", + visibility: "Visibility", + description: "Description", + }, + member: { + alreadyMember: "The user is already a member.", + add: "Add", + remove: "Remove", + }, + manageItem: { + nickname: "Nickname", + avatar: "Avatar", + property: "Timeline Property", + member: "Timeline Member", + delete: "Delete Timeline", + }, + deleteDialog: { + title: "Delete Timeline", + inputPrompt: + "This is a dangerous action. If you are sure to delete timeline<1>{{name}}, please input its name below and click confirm button.", + notMatch: "Name does not match.", + }, + postSyncState: { + syncing: "Syncing", + synced: "Synced", + offline: "Offline", + }, + post: { + deleteDialog: { + title: "Confirm Delete", + prompt: + "Are you sure to delete the post? This operation is not recoverable.", + }, + }, + }, + user: { + username: "username", + password: "password", + login: "login", + rememberMe: "Remember Me", + welcomeBack: "Welcome back!", + verifyTokenFailed: "User login info is expired. Please login again!", + verifyTokenFailedNetwork: + "Verifying user login info failed. Please check your network and refresh page!", + }, + login: { + emptyUsername: "Username can't be empty.", + emptyPassword: "Password can't be empty.", + badCredential: "Username or password is invalid.", + alreadyLogin: "Already login! Redirect to home page in 3s!", + }, + userPage: { + dialogChangeNickname: { + title: "Change Nickname", + inputLabel: "New nickname", + }, + dialogChangeAvatar: { + title: "Change Avatar", + previewImgAlt: "preview", + prompt: { + select: "Please select a picture.", + crop: "Please crop the picture.", + processingCrop: "Cropping picture...", + uploading: "Uploading...", + preview: "Please preview avatar", + }, + upload: "upload", + }, + }, + settings: { + subheaders: { + account: "Account", + customization: "Customization", + }, + languagePrimary: "Choose display language.", + languageSecondary: + "You language preference will be saved locally. Next time you visit this page, last language option will be used.", + changePassword: "Change account's password.", + logout: "Log out this account.", + gotoSelf: + "Click here to go to timeline of myself to change nickname and avatar.", + dialogChangePassword: { + title: "Change Password", + prompt: + "You are changing your password. You need to input the correct old password. After change, you need to login again and all old login will be invalid.", + inputOldPassword: "Old password", + inputNewPassword: "New password", + inputRetypeNewPassword: "Retype new password", + errorEmptyOldPassword: "Old password can't be empty.", + errorEmptyNewPassword: "New password can't be empty.", + errorRetypeNotMatch: "Password retyped does not match.", + }, + dialogConfirmLogout: { + title: "Confirm Logout", + prompt: + "Are you sure to log out? All cached data in the browser will be deleted.", + }, + }, + about: { + author: { + title: "Site Developer", + fullname: "Fullname: ", + nickname: "Nickname: ", + introduction: "Introduction: ", + introductionContent: "A programmer coding based on coincidence", + links: "Links: ", + }, + site: { + title: "Site Information", + content: + "The name of this site is <1>Timeline, which is a Web App with <3>timeline as its core concept. Its frontend and backend are both developed by <5>me, and open source on GitHub. It is relatively easy to deploy it on your own server, which is also one of my goals. Welcome to comment anything in GitHub repository.", + repo: "GitHub Repo", + }, + credits: { + title: "Credits", + content: + "Timeline is works standing on shoulders of gaints. Special appreciation for many open source projects listed below or not. Related licenses could be found in GitHub repository.", + frontend: "Frontend: ", + backend: "Backend: ", + }, + }, + admin: { + title: "admin", + }, +}; + +export default translation; diff --git a/FrontEnd/src/app/locales/scheme.ts b/FrontEnd/src/app/locales/scheme.ts new file mode 100644 index 00000000..9e3534ac --- /dev/null +++ b/FrontEnd/src/app/locales/scheme.ts @@ -0,0 +1,182 @@ +export default interface TranslationResource { + welcome: string; + search: string; + chooseImage: string; + loadImageError: string; + loadFailReload: string; + serviceWorker: { + availableOffline: string; + upgradePrompt: string; + upgradeNow: string; + upgradeSuccess: string; + externalActivatedPrompt: string; + reloadNow: string; + }; + nav: { + settings: string; + login: string; + about: string; + }; + home: { + go: string; + allTimeline: string; + joinTimeline: string; + ownTimeline: string; + offlinePrompt: string; + createButton: string; + createDialog: { + title: string; + name: string; + nameFormat: string; + badFormat: string; + noEmpty: string; + tooLong: string; + }; + }; + operationDialog: { + retry: string; + nextStep: string; + previousStep: string; + confirm: string; + cancel: string; + ok: string; + processing: string; + success: string; + error: string; + }; + timeline: { + messageCantSee: string; + userNotExist: string; + timelineNotExist: string; + manage: string; + memberButton: string; + send: string; + deletePostFailed: string; + sendPostFailed: string; + visibility: { + public: string; + register: string; + private: string; + }; + visibilityTooltip: { + public: string; + register: string; + private: string; + }; + dialogChangeProperty: { + title: string; + visibility: string; + description: string; + }; + member: { + alreadyMember: string; + add: string; + remove: string; + }; + manageItem: { + nickname: string; + avatar: string; + property: string; + member: string; + delete: string; + }; + deleteDialog: { + title: string; + inputPrompt: string; + notMatch: string; + }; + postSyncState: { + syncing: string; + synced: string; + offline: string; + }; + post: { + deleteDialog: { + title: string; + prompt: string; + }; + }; + }; + user: { + username: string; + password: string; + login: string; + rememberMe: string; + welcomeBack: string; + verifyTokenFailed: string; + verifyTokenFailedNetwork: string; + }; + login: { + emptyUsername: string; + emptyPassword: string; + badCredential: string; + alreadyLogin: string; + }; + userPage: { + dialogChangeNickname: { + title: string; + inputLabel: string; + }; + dialogChangeAvatar: { + title: string; + previewImgAlt: string; + prompt: { + select: string; + crop: string; + processingCrop: string; + preview: string; + uploading: string; + }; + upload: string; + }; + }; + settings: { + subheaders: { + account: string; + customization: string; + }; + languagePrimary: string; + languageSecondary: string; + changePassword: string; + logout: string; + gotoSelf: string; + dialogChangePassword: { + title: string; + prompt: string; + inputOldPassword: string; + inputNewPassword: string; + inputRetypeNewPassword: string; + errorEmptyOldPassword: string; + errorEmptyNewPassword: string; + errorRetypeNotMatch: string; + }; + dialogConfirmLogout: { + title: string; + prompt: string; + }; + }; + about: { + author: { + title: string; + fullname: string; + nickname: string; + introduction: string; + introductionContent: string; + links: string; + }; + site: { + title: string; + content: string; + repo: string; + }; + credits: { + title: string; + content: string; + frontend: string; + backend: string; + }; + }; + admin: { + title: string; + }; +} diff --git a/FrontEnd/src/app/locales/zh/translation.ts b/FrontEnd/src/app/locales/zh/translation.ts new file mode 100644 index 00000000..df316366 --- /dev/null +++ b/FrontEnd/src/app/locales/zh/translation.ts @@ -0,0 +1,195 @@ +import TranslationResource from "../scheme"; + +const translation: TranslationResource = { + welcome: "欢迎!", + search: "搜索", + loadFailReload: "加载失败,<1>点击重试。", + serviceWorker: { + availableOffline: "Timeline 已经缓存在本地,你可以离线使用它。🎉🎉🎉", + upgradePrompt: "App 有新版本!", + upgradeNow: "现在升级", + upgradeSuccess: "App 升级成功,当然,你仍可以离线使用它。 🎉🎉🎉", + externalActivatedPrompt: + "一个新的 App 版本已经激活,请刷新页面使用,否则页面可能会出现故障。", + reloadNow: "立刻刷新", + }, + nav: { + settings: "设置", + login: "登陆", + about: "关于", + }, + chooseImage: "选择一个图片", + loadImageError: "加载图片失败", + home: { + go: "冲!", + allTimeline: "所有的时间线", + joinTimeline: "加入的时间线", + ownTimeline: "拥有的时间线", + offlinePrompt: + "你好像处于离线状态。以下是一些缓存在本地的时间线。你可以查看它们或者<1>点击重新获取在线信息。", + createButton: "创建时间线", + createDialog: { + title: "创建时间线!", + name: "名字", + nameFormat: + "名字只能由字母、汉字、数字、下划线(_)和连字符(-)构成,且长度不能超过26.", + badFormat: "格式错误", + noEmpty: "不能为空", + tooLong: "太长了", + }, + }, + operationDialog: { + retry: "重试", + nextStep: "下一步", + previousStep: "上一步", + confirm: "确定", + cancel: "取消", + ok: "好的!", + processing: "处理中...", + success: "成功!", + error: "出错啦!", + }, + timeline: { + messageCantSee: "不好意思,你没有权限查看这个时间线。😅", + userNotExist: "该用户不存在!", + timelineNotExist: "该时间线不存在!", + manage: "管理", + memberButton: "成员", + send: "发送", + deletePostFailed: "删除消息失败。", + sendPostFailed: "发送消息失败。", + visibility: { + public: "对所有人公开", + register: "仅注册可见", + private: "仅成员可见", + }, + visibilityTooltip: { + public: "所有人都可以看到这个时间线的内容,包括没有注册的人。", + register: "只有拥有本网站的账号且登陆了的人才能看到这个时间线的内容。", + private: "只有这个时间线的成员可以看到这个时间线的内容。", + }, + dialogChangeProperty: { + title: "修改时间线属性", + visibility: "可见性", + description: "描述", + }, + member: { + alreadyMember: "该用户已经是一个成员。", + add: "添加", + remove: "移除", + }, + manageItem: { + nickname: "昵称", + avatar: "头像", + property: "时间线属性", + member: "时间线成员", + delete: "删除时间线", + }, + deleteDialog: { + title: "删除时间线", + inputPrompt: + "这是一个危险的操作。如果您确认要删除时间线<1>{{name}},请在下面输入它的名字并点击确认。", + notMatch: "名字不匹配", + }, + postSyncState: { + syncing: "同步中", + synced: "同步成功", + offline: "离线", + }, + post: { + deleteDialog: { + title: "确认删除", + prompt: "确定删除这个消息?这个操作不可撤销。", + }, + }, + }, + user: { + username: "用户名", + password: "密码", + login: "登录", + rememberMe: "记住我", + welcomeBack: "欢迎回来!", + verifyTokenFailed: "用户登录信息已过期,请重新登陆!", + verifyTokenFailedNetwork: + "验证用户登录信息失败,请检查网络连接并刷新页面!", + }, + login: { + emptyUsername: "用户名不能为空。", + emptyPassword: "密码不能为空。", + badCredential: "用户名或密码错误。", + alreadyLogin: "已经登陆,三秒后导航到首页!", + }, + userPage: { + dialogChangeNickname: { + title: "更改昵称", + inputLabel: "新昵称", + }, + dialogChangeAvatar: { + title: "修改头像", + previewImgAlt: "预览", + prompt: { + select: "请选择一个图片", + crop: "请裁剪图片", + processingCrop: "正在裁剪图片", + uploading: "正在上传", + preview: "请预览图片", + }, + upload: "上传", + }, + }, + settings: { + subheaders: { + account: "账户", + customization: "个性化", + }, + languagePrimary: "选择显示的语言。", + languageSecondary: + "您的语言偏好将会存储在本地,下次浏览时将自动使用上次保存的语言选项。", + changePassword: "更改账号的密码。", + logout: "注销此账号。", + gotoSelf: "点击前往个人时间线修改昵称和头像!", + dialogChangePassword: { + title: "修改密码", + prompt: + "您正在修改密码,您需要输入正确的旧密码。成功修改后您需要重新登陆,而且以前所有的登录都会失效。", + inputOldPassword: "旧密码", + inputNewPassword: "新密码", + inputRetypeNewPassword: "再次输入新密码", + errorEmptyOldPassword: "旧密码不能为空。", + errorEmptyNewPassword: "新密码不能为空", + errorRetypeNotMatch: "两次输入的密码不一致", + }, + dialogConfirmLogout: { + title: "确定注销", + prompt: "您确定注销此账号?这将删除所有已经缓存在浏览器的数据。", + }, + }, + about: { + author: { + title: "网站作者", + fullname: "姓名:", + nickname: "昵称:", + introduction: "简介:", + introductionContent: "一个基于巧合编程的代码爱好者。", + links: "链接:", + }, + site: { + title: "网站信息", + content: + "这个网站的名字叫 <1>Timeline,是一个以<3>时间线为核心概念的 Web App . 它的前端和后端都是由<5>我开发,并且在 GitHub 上开源。大家可以相对轻松的把它们部署在自己的服务器上,这也是我的目标之一。欢迎大家前往 GitHub 仓库提出任何意见。", + repo: "GitHub 仓库", + }, + credits: { + title: "鸣谢", + content: + "Timeline 是站在巨人肩膀上的作品,感谢以下列出的和其他未列出的许多开源项目,相关 License 请在 GitHub 仓库中查看。", + frontend: "前端:", + backend: "后端:", + }, + }, + admin: { + title: "管理", + }, +}; + +export default translation; diff --git a/FrontEnd/src/app/service-worker.tsx b/FrontEnd/src/app/service-worker.tsx new file mode 100644 index 00000000..3be54bc1 --- /dev/null +++ b/FrontEnd/src/app/service-worker.tsx @@ -0,0 +1,113 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { Button } from "react-bootstrap"; + +import { pushAlert } from "./services/alert"; + +if ("serviceWorker" in navigator) { + let isThisTriggerUpgrade = false; + + const upgradeSuccessLocalStorageKey = "TIMELINE_UPGRADE_SUCCESS"; + + if (window.localStorage.getItem(upgradeSuccessLocalStorageKey)) { + pushAlert({ + message: { + type: "i18n", + key: "serviceWorker.upgradeSuccess", + }, + type: "success", + }); + window.localStorage.removeItem(upgradeSuccessLocalStorageKey); + } + + void import("workbox-window").then(({ Workbox, messageSW }) => { + const wb = new Workbox("/sw.js"); + let registration: ServiceWorkerRegistration | undefined; + + // externalactivated is not usable but I still use its name. + wb.addEventListener("controlling", () => { + const upgradeReload = (): void => { + window.localStorage.setItem(upgradeSuccessLocalStorageKey, "true"); + window.location.reload(); + }; + + if (isThisTriggerUpgrade) { + upgradeReload(); + } else { + const Message: React.FC = () => { + const { t } = useTranslation(); + return ( + <> + {t("serviceWorker.externalActivatedPrompt")} + + + ); + }; + + pushAlert({ + message: Message, + dismissTime: "never", + type: "warning", + }); + } + }); + + wb.addEventListener("activated", (event) => { + if (!event.isUpdate) { + pushAlert({ + message: { + type: "i18n", + key: "serviceWorker.availableOffline", + }, + type: "success", + }); + } + }); + + const showSkipWaitingPrompt = (): void => { + const upgrade = (): void => { + isThisTriggerUpgrade = true; + if (registration && registration.waiting) { + // Send a message to the waiting service worker, + // instructing it to activate. + // Note: for this to work, you have to add a message + // listener in your service worker. See below. + void messageSW(registration.waiting, { type: "SKIP_WAITING" }); + } + }; + + const UpgradeMessage: React.FC = () => { + const { t } = useTranslation(); + return ( + <> + {t("serviceWorker.upgradePrompt")} + + + ); + }; + + pushAlert({ + message: UpgradeMessage, + dismissTime: "never", + type: "success", + }); + }; + + // Add an event listener to detect when the registered + // service worker has installed but is waiting to activate. + wb.addEventListener("waiting", showSkipWaitingPrompt); + wb.addEventListener("externalwaiting", showSkipWaitingPrompt); + + void wb.register().then((reg) => { + registration = reg; + }); + }); +} diff --git a/FrontEnd/src/app/services/DataHub.ts b/FrontEnd/src/app/services/DataHub.ts new file mode 100644 index 00000000..93a9b41f --- /dev/null +++ b/FrontEnd/src/app/services/DataHub.ts @@ -0,0 +1,225 @@ +import { pull } from "lodash"; +import { Observable, BehaviorSubject, combineLatest } from "rxjs"; +import { map } from "rxjs/operators"; + +export type Subscriber = (data: TData) => void; + +export type WithSyncStatus = T & { syncing: boolean }; + +export class DataLine { + private _current: TData | undefined = undefined; + + private _syncPromise: Promise | null = null; + private _syncingSubject = new BehaviorSubject(false); + + private _observers: Subscriber[] = []; + + constructor( + private config: { + sync: () => Promise; + destroyable?: (value: TData | undefined) => boolean; + disableInitSync?: boolean; + } + ) { + if (config.disableInitSync !== true) { + setImmediate(() => void this.sync()); + } + } + + private subscribe(subscriber: Subscriber): void { + this._observers.push(subscriber); + if (this._current !== undefined) { + subscriber(this._current); + } + } + + private unsubscribe(subscriber: Subscriber): void { + if (!this._observers.includes(subscriber)) return; + pull(this._observers, subscriber); + } + + getObservable(): Observable { + return new Observable((observer) => { + const f = (data: TData): void => { + observer.next(data); + }; + this.subscribe(f); + + return () => { + this.unsubscribe(f); + }; + }); + } + + getSyncStatusObservable(): Observable { + return this._syncingSubject.asObservable(); + } + + getDataWithSyncStatusObservable(): Observable> { + return combineLatest([ + this.getObservable(), + this.getSyncStatusObservable(), + ]).pipe( + map(([data, syncing]) => ({ + ...data, + syncing, + })) + ); + } + + get value(): TData | undefined { + return this._current; + } + + next(value: TData): void { + this._current = value; + this._observers.forEach((observer) => observer(value)); + } + + get isSyncing(): boolean { + return this._syncPromise != null; + } + + sync(): Promise { + if (this._syncPromise == null) { + this._syncingSubject.next(true); + this._syncPromise = this.config.sync().then(() => { + this._syncingSubject.next(false); + this._syncPromise = null; + }); + } + + return this._syncPromise; + } + + syncWithAction( + syncAction: (line: DataLine) => Promise + ): Promise { + if (this._syncPromise == null) { + this._syncingSubject.next(true); + this._syncPromise = syncAction(this).then(() => { + this._syncingSubject.next(false); + this._syncPromise = null; + }); + } + + return this._syncPromise; + } + + get destroyable(): boolean { + const customDestroyable = this.config?.destroyable; + + return ( + this._observers.length === 0 && + !this.isSyncing && + (customDestroyable != null ? customDestroyable(this._current) : true) + ); + } +} + +export class DataHub { + private sync: (key: TKey, line: DataLine) => Promise; + private keyToString: (key: TKey) => string; + private destroyable?: (key: TKey, value: TData | undefined) => boolean; + + private readonly subscriptionLineMap = new Map>(); + + private cleanTimerId = 0; + + // setup is called after creating line and if it returns a function as destroyer, then when the line is destroyed the destroyer will be called. + constructor(config: { + sync: (key: TKey, line: DataLine) => Promise; + keyToString?: (key: TKey) => string; + destroyable?: (key: TKey, value: TData | undefined) => boolean; + }) { + this.sync = config.sync; + this.keyToString = + config.keyToString ?? + ((value): string => { + if (typeof value === "string") return value; + else + throw new Error( + "Default keyToString function only pass string value." + ); + }); + + this.destroyable = config.destroyable; + } + + private cleanLines(): void { + const toDelete: string[] = []; + for (const [key, line] of this.subscriptionLineMap.entries()) { + if (line.destroyable) { + toDelete.push(key); + } + } + + if (toDelete.length === 0) return; + + for (const key of toDelete) { + this.subscriptionLineMap.delete(key); + } + + if (this.subscriptionLineMap.size === 0) { + window.clearInterval(this.cleanTimerId); + this.cleanTimerId = 0; + } + } + + private createLine(key: TKey, disableInitSync = false): DataLine { + const keyString = this.keyToString(key); + const { destroyable } = this; + const newLine: DataLine = new DataLine({ + sync: () => this.sync(key, newLine), + destroyable: + destroyable != null ? (value) => destroyable(key, value) : undefined, + disableInitSync: disableInitSync, + }); + this.subscriptionLineMap.set(keyString, newLine); + if (this.subscriptionLineMap.size === 1) { + this.cleanTimerId = window.setInterval(this.cleanLines.bind(this), 20000); + } + return newLine; + } + + getObservable(key: TKey): Observable { + return this.getLineOrCreate(key).getObservable(); + } + + getSyncStatusObservable(key: TKey): Observable { + return this.getLineOrCreate(key).getSyncStatusObservable(); + } + + getDataWithSyncStatusObservable( + key: TKey + ): Observable> { + return this.getLineOrCreate(key).getDataWithSyncStatusObservable(); + } + + getLine(key: TKey): DataLine | null { + const keyString = this.keyToString(key); + return this.subscriptionLineMap.get(keyString) ?? null; + } + + getLineOrCreate(key: TKey): DataLine { + const keyString = this.keyToString(key); + return this.subscriptionLineMap.get(keyString) ?? this.createLine(key); + } + + getLineOrCreateWithoutInitSync(key: TKey): DataLine { + const keyString = this.keyToString(key); + return ( + this.subscriptionLineMap.get(keyString) ?? this.createLine(key, true) + ); + } + + optionalInitLineWithSyncAction( + key: TKey, + syncAction: (line: DataLine) => Promise + ): Promise { + const optionalLine = this.getLine(key); + if (optionalLine != null) return Promise.resolve(); + const line = this.createLine(key, true); + return line.syncWithAction(syncAction); + } +} diff --git a/FrontEnd/src/app/services/alert.ts b/FrontEnd/src/app/services/alert.ts new file mode 100644 index 00000000..e4c0e653 --- /dev/null +++ b/FrontEnd/src/app/services/alert.ts @@ -0,0 +1,61 @@ +import React from "react"; +import pull from "lodash/pull"; + +export interface AlertInfo { + type?: "primary" | "secondary" | "success" | "danger" | "warning" | "info"; + message: string | React.FC | { type: "i18n"; key: string }; + dismissTime?: number | "never"; +} + +export interface AlertInfoEx extends AlertInfo { + id: number; +} + +export type AlertConsumer = (alerts: AlertInfoEx) => void; + +export class AlertService { + private consumers: AlertConsumer[] = []; + private savedAlerts: AlertInfoEx[] = []; + private currentId = 1; + + private produce(alert: AlertInfoEx): void { + for (const consumer of this.consumers) { + consumer(alert); + } + } + + registerConsumer(consumer: AlertConsumer): void { + this.consumers.push(consumer); + if (this.savedAlerts.length !== 0) { + for (const alert of this.savedAlerts) { + this.produce(alert); + } + this.savedAlerts = []; + } + } + + unregisterConsumer(consumer: AlertConsumer): void { + pull(this.consumers, consumer); + } + + push(alert: AlertInfo): void { + const newAlert: AlertInfoEx = { ...alert, id: this.currentId++ }; + if (this.consumers.length === 0) { + this.savedAlerts.push(newAlert); + } else { + this.produce(newAlert); + } + } +} + +export const alertService = new AlertService(); + +export function pushAlert(alert: AlertInfo): void { + alertService.push(alert); +} + +export const kAlertHostId = "alert-host"; + +export function getAlertHost(): HTMLElement | null { + return document.getElementById(kAlertHostId); +} diff --git a/FrontEnd/src/app/services/common.ts b/FrontEnd/src/app/services/common.ts new file mode 100644 index 00000000..3bb6b9d7 --- /dev/null +++ b/FrontEnd/src/app/services/common.ts @@ -0,0 +1,23 @@ +import localforage from "localforage"; + +import { HttpNetworkError } from "@/http/common"; + +export const dataStorage = localforage.createInstance({ + name: "data", + description: "Database for offline data.", + driver: localforage.INDEXEDDB, +}); + +export class ForbiddenError extends Error { + constructor(message?: string) { + super(message); + } +} + +export function throwIfNotNetworkError(e: unknown): void { + if (!(e instanceof HttpNetworkError)) { + throw e; + } +} + +export type BlobOrStatus = Blob | "loading" | "error"; diff --git a/FrontEnd/src/app/services/timeline.ts b/FrontEnd/src/app/services/timeline.ts new file mode 100644 index 00000000..9db76281 --- /dev/null +++ b/FrontEnd/src/app/services/timeline.ts @@ -0,0 +1,702 @@ +import React from "react"; +import XRegExp from "xregexp"; +import { Observable, from, combineLatest, of } from "rxjs"; +import { map, switchMap, startWith } from "rxjs/operators"; +import { uniqBy } from "lodash"; + +import { convertError } from "@/utilities/rxjs"; +import { + TimelineVisibility, + HttpTimelineInfo, + HttpTimelinePatchRequest, + HttpTimelinePostPostRequest, + HttpTimelinePostPostRequestContent, + HttpTimelinePostPostRequestTextContent, + HttpTimelinePostPostRequestImageContent, + HttpTimelinePostInfo, + HttpTimelinePostTextContent, + getHttpTimelineClient, + HttpTimelineNotExistError, + HttpTimelineNameConflictError, +} from "@/http/timeline"; +import { BlobWithEtag, NotModified, HttpForbiddenError } from "@/http/common"; +import { HttpUser } from "@/http/user"; + +export { kTimelineVisibilities } from "@/http/timeline"; + +export type { TimelineVisibility } from "@/http/timeline"; + +import { dataStorage, throwIfNotNetworkError, BlobOrStatus } from "./common"; +import { DataHub, WithSyncStatus } from "./DataHub"; +import { UserAuthInfo, checkLogin, userService, userInfoService } from "./user"; + +export type TimelineInfo = HttpTimelineInfo; +export type TimelineChangePropertyRequest = HttpTimelinePatchRequest; +export type TimelineCreatePostRequest = HttpTimelinePostPostRequest; +export type TimelineCreatePostContent = HttpTimelinePostPostRequestContent; +export type TimelineCreatePostTextContent = HttpTimelinePostPostRequestTextContent; +export type TimelineCreatePostImageContent = HttpTimelinePostPostRequestImageContent; + +export type TimelinePostTextContent = HttpTimelinePostTextContent; + +export interface TimelinePostImageContent { + type: "image"; + data: BlobOrStatus; +} + +export type TimelinePostContent = + | TimelinePostTextContent + | TimelinePostImageContent; + +export interface TimelinePostInfo { + id: number; + content: TimelinePostContent; + time: Date; + lastUpdated: Date; + author: HttpUser; +} + +export const timelineVisibilityTooltipTranslationMap: Record< + TimelineVisibility, + string +> = { + Public: "timeline.visibilityTooltip.public", + Register: "timeline.visibilityTooltip.register", + Private: "timeline.visibilityTooltip.private", +}; + +export class TimelineNotExistError extends Error {} +export class TimelineNameConflictError extends Error {} + +export type TimelineWithSyncStatus = WithSyncStatus< + | { + type: "cache"; + timeline: TimelineInfo; + } + | { + type: "offline" | "synced"; + timeline: TimelineInfo | null; + } +>; + +export type TimelinePostsWithSyncState = WithSyncStatus<{ + type: + | "cache" + | "offline" // Sync failed and use cache. + | "synced" // Sync succeeded. + | "forbid" // The list is forbidden to see. + | "notexist"; // The timeline does not exist. + posts: TimelinePostInfo[]; +}>; + +type TimelineData = Omit & { + owner: string; + members: string[]; +}; + +type TimelinePostData = Omit & { + author: string; +}; + +export class TimelineService { + private getCachedTimeline( + timelineName: string + ): Promise { + return dataStorage.getItem(`timeline.${timelineName}`); + } + + private saveTimeline( + timelineName: string, + data: TimelineData + ): Promise { + return dataStorage + .setItem(`timeline.${timelineName}`, data) + .then(); + } + + private async clearTimelineData(timelineName: string): Promise { + const keys = (await dataStorage.keys()).filter((k) => + k.startsWith(`timeline.${timelineName}`) + ); + await Promise.all(keys.map((k) => dataStorage.removeItem(k))); + } + + private convertHttpTimelineToData(timeline: HttpTimelineInfo): TimelineData { + return { + ...timeline, + owner: timeline.owner.username, + members: timeline.members.map((m) => m.username), + }; + } + + private _timelineHub = new DataHub< + string, + | { + type: "cache"; + timeline: TimelineData; + } + | { + type: "offline" | "synced"; + timeline: TimelineData | null; + } + >({ + sync: async (key, line) => { + const cache = await this.getCachedTimeline(key); + + if (line.value == undefined) { + if (cache != null) { + line.next({ type: "cache", timeline: cache }); + } + } + + try { + const httpTimeline = await getHttpTimelineClient().getTimeline(key); + + userInfoService.saveUsers([ + httpTimeline.owner, + ...httpTimeline.members, + ]); + + const timeline = this.convertHttpTimelineToData(httpTimeline); + + if (cache != null && timeline.uniqueId !== cache.uniqueId) { + console.log( + `Timeline with name ${key} has changed to a new one. Clear old data.` + ); + await this.clearTimelineData(key); // If timeline has changed, clear all old data. + } + + await this.saveTimeline(key, timeline); + + line.next({ type: "synced", timeline }); + } catch (e) { + if (e instanceof HttpTimelineNotExistError) { + line.next({ type: "synced", timeline: null }); + } else { + if (cache == null) { + line.next({ type: "offline", timeline: null }); + } else { + line.next({ type: "offline", timeline: cache }); + } + throwIfNotNetworkError(e); + } + } + }, + }); + + syncTimeline(timelineName: string): Promise { + return this._timelineHub.getLineOrCreate(timelineName).sync(); + } + + getTimeline$(timelineName: string): Observable { + return this._timelineHub.getDataWithSyncStatusObservable(timelineName).pipe( + switchMap((state) => { + const { timeline } = state; + if (timeline != null) { + return combineLatest( + [timeline.owner, ...timeline.members].map((u) => + userInfoService.getUser$(u) + ) + ).pipe( + map((users) => { + return { + ...state, + timeline: { + ...timeline, + owner: users[0], + members: users.slice(1), + }, + }; + }) + ); + } else { + return of(state as TimelineWithSyncStatus); + } + }) + ); + } + + createTimeline(timelineName: string): Observable { + const user = checkLogin(); + return from( + getHttpTimelineClient().postTimeline( + { + name: timelineName, + }, + user.token + ) + ).pipe( + convertError(HttpTimelineNameConflictError, TimelineNameConflictError) + ); + } + + changeTimelineProperty( + timelineName: string, + req: TimelineChangePropertyRequest + ): Observable { + const user = checkLogin(); + return from( + getHttpTimelineClient() + .patchTimeline(timelineName, req, user.token) + .then((timeline) => { + void this.syncTimeline(timelineName); + return timeline; + }) + ); + } + + deleteTimeline(timelineName: string): Observable { + const user = checkLogin(); + return from( + getHttpTimelineClient().deleteTimeline(timelineName, user.token) + ); + } + + addMember(timelineName: string, username: string): Observable { + const user = checkLogin(); + return from( + getHttpTimelineClient() + .memberPut(timelineName, username, user.token) + .then(() => { + void this.syncTimeline(timelineName); + }) + ); + } + + removeMember(timelineName: string, username: string): Observable { + const user = checkLogin(); + return from( + getHttpTimelineClient() + .memberDelete(timelineName, username, user.token) + .then(() => { + void this.syncTimeline(timelineName); + }) + ); + } + + private convertHttpPostToData(post: HttpTimelinePostInfo): TimelinePostData { + return { + ...post, + author: post.author.username, + }; + } + + private convertHttpPostToDataList( + posts: HttpTimelinePostInfo[] + ): TimelinePostData[] { + return posts.map((post) => this.convertHttpPostToData(post)); + } + + private getCachedPosts( + timelineName: string + ): Promise { + return dataStorage.getItem( + `timeline.${timelineName}.posts` + ); + } + + private savePosts( + timelineName: string, + data: TimelinePostData[] + ): Promise { + return dataStorage + .setItem(`timeline.${timelineName}.posts`, data) + .then(); + } + + private syncPosts(timelineName: string): Promise { + return this._postsHub.getLineOrCreate(timelineName).sync(); + } + + private _postsHub = new DataHub< + string, + { + type: "cache" | "offline" | "synced" | "forbid" | "notexist"; + posts: TimelinePostData[]; + } + >({ + sync: async (key, line) => { + // Wait for timeline synced. In case the timeline has changed to another and old data has been cleaned. + await this.syncTimeline(key); + + if (line.value == null) { + const cache = await this.getCachedPosts(key); + if (cache != null) { + line.next({ type: "cache", posts: cache }); + } + } + + const now = new Date(); + + const lastUpdatedTime = await dataStorage.getItem( + `timeline.${key}.lastUpdated` + ); + + try { + if (lastUpdatedTime == null) { + const httpPosts = await getHttpTimelineClient().listPost( + key, + userService.currentUser?.token + ); + + userInfoService.saveUsers( + uniqBy( + httpPosts.map((post) => post.author), + "username" + ) + ); + + const posts = this.convertHttpPostToDataList(httpPosts); + await this.savePosts(key, posts); + await dataStorage.setItem(`timeline.${key}.lastUpdated`, now); + + line.next({ type: "synced", posts }); + } else { + const httpPosts = await getHttpTimelineClient().listPost( + key, + userService.currentUser?.token, + { + modifiedSince: lastUpdatedTime, + includeDeleted: true, + } + ); + + const deletedIds = httpPosts + .filter((p) => p.deleted) + .map((p) => p.id); + const changed = httpPosts.filter( + (p): p is HttpTimelinePostInfo => !p.deleted + ); + + userInfoService.saveUsers( + uniqBy( + httpPosts + .map((post) => post.author) + .filter((u): u is HttpUser => u != null), + "username" + ) + ); + + const cache = (await this.getCachedPosts(key)) ?? []; + + const posts = cache.filter((p) => !deletedIds.includes(p.id)); + + for (const changedPost of changed) { + const savedChangedPostIndex = posts.findIndex( + (p) => p.id === changedPost.id + ); + if (savedChangedPostIndex === -1) { + posts.push(this.convertHttpPostToData(changedPost)); + } else { + posts[savedChangedPostIndex] = this.convertHttpPostToData( + changedPost + ); + } + } + + await this.savePosts(key, posts); + await dataStorage.setItem(`timeline.${key}.lastUpdated`, now); + line.next({ type: "synced", posts }); + } + } catch (e) { + if (e instanceof HttpTimelineNotExistError) { + line.next({ type: "notexist", posts: [] }); + } else if (e instanceof HttpForbiddenError) { + line.next({ type: "forbid", posts: [] }); + } else { + const cache = await this.getCachedPosts(key); + if (cache == null) { + line.next({ type: "offline", posts: [] }); + } else { + line.next({ type: "offline", posts: cache }); + } + throwIfNotNetworkError(e); + } + } + }, + }); + + getPosts$(timelineName: string): Observable { + return this._postsHub.getDataWithSyncStatusObservable(timelineName).pipe( + switchMap((state) => { + if (state.posts.length === 0) { + return of({ + ...state, + posts: [], + }); + } + + return combineLatest([ + combineLatest( + state.posts.map((post) => userInfoService.getUser$(post.author)) + ), + combineLatest( + state.posts.map((post) => { + if (post.content.type === "image") { + return this.getPostData$(timelineName, post.id); + } else { + return of(null); + } + }) + ), + ]).pipe( + map(([authors, datas]) => { + return { + ...state, + posts: state.posts.map((post, i) => { + const { content } = post; + + return { + ...post, + author: authors[i], + content: (() => { + if (content.type === "text") return content; + else + return { + type: "image", + data: datas[i], + } as TimelinePostImageContent; + })(), + }; + }), + }; + }) + ); + }) + ); + } + + private getCachedPostData(key: { + timelineName: string; + postId: number; + }): Promise { + return dataStorage.getItem( + `timeline.${key.timelineName}.post.${key.postId}.data` + ); + } + + private savePostData( + key: { + timelineName: string; + postId: number; + }, + data: BlobWithEtag + ): Promise { + return dataStorage + .setItem( + `timeline.${key.timelineName}.post.${key.postId}.data`, + data + ) + .then(); + } + + private syncPostData(key: { + timelineName: string; + postId: number; + }): Promise { + return this._postDataHub.getLineOrCreate(key).sync(); + } + + private _postDataHub = new DataHub< + { timelineName: string; postId: number }, + | { data: Blob; type: "cache" | "synced" | "offline" } + | { data?: undefined; type: "notexist" | "offline" } + >({ + keyToString: (key) => `${key.timelineName}.${key.postId}`, + sync: async (key, line) => { + const cache = await this.getCachedPostData(key); + if (line.value == null) { + if (cache != null) { + line.next({ type: "cache", data: cache.data }); + } + } + + if (cache == null) { + try { + const res = await getHttpTimelineClient().getPostData( + key.timelineName, + key.postId + ); + await this.savePostData(key, res); + line.next({ data: res.data, type: "synced" }); + } catch (e) { + line.next({ type: "offline" }); + throwIfNotNetworkError(e); + } + } else { + try { + const res = await getHttpTimelineClient().getPostData( + key.timelineName, + key.postId, + cache.etag + ); + if (res instanceof NotModified) { + line.next({ data: cache.data, type: "synced" }); + } else { + await this.savePostData(key, res); + line.next({ data: res.data, type: "synced" }); + } + } catch (e) { + line.next({ data: cache.data, type: "offline" }); + throwIfNotNetworkError(e); + } + } + }, + }); + + getPostData$(timelineName: string, postId: number): Observable { + return this._postDataHub.getObservable({ timelineName, postId }).pipe( + map((state): BlobOrStatus => state.data ?? "error"), + startWith("loading") + ); + } + + createPost( + timelineName: string, + request: TimelineCreatePostRequest + ): Observable { + const user = checkLogin(); + return from( + getHttpTimelineClient() + .postPost(timelineName, request, user.token) + .then(() => { + void this.syncPosts(timelineName); + }) + ); + } + + deletePost(timelineName: string, postId: number): Observable { + const user = checkLogin(); + return from( + getHttpTimelineClient() + .deletePost(timelineName, postId, user.token) + .then(() => { + void this.syncPosts(timelineName); + }) + ); + } + + isMemberOf(username: string, timeline: TimelineInfo): boolean { + return timeline.members.findIndex((m) => m.username == username) >= 0; + } + + hasReadPermission( + user: UserAuthInfo | null | undefined, + timeline: TimelineInfo + ): boolean { + if (user != null && user.administrator) return true; + + const { visibility } = timeline; + if (visibility === "Public") { + return true; + } else if (visibility === "Register") { + if (user != null) return true; + } else if (visibility === "Private") { + if ( + user != null && + (user.username === timeline.owner.username || + this.isMemberOf(user.username, timeline)) + ) { + return true; + } + } + return false; + } + + hasPostPermission( + user: UserAuthInfo | null | undefined, + timeline: TimelineInfo + ): boolean { + if (user != null && user.administrator) return true; + + return ( + user != null && + (timeline.owner.username === user.username || + this.isMemberOf(user.username, timeline)) + ); + } + + hasManagePermission( + user: UserAuthInfo | null | undefined, + timeline: TimelineInfo + ): boolean { + if (user != null && user.administrator) return true; + + return user != null && user.username == timeline.owner.username; + } + + hasModifyPostPermission( + user: UserAuthInfo | null | undefined, + timeline: TimelineInfo, + post: TimelinePostInfo + ): boolean { + if (user != null && user.administrator) return true; + + return ( + user != null && + (user.username === timeline.owner.username || + user.username === post.author.username) + ); + } +} + +export const timelineService = new TimelineService(); + +const timelineNameReg = XRegExp("^[-_\\p{L}]*$", "u"); + +export function validateTimelineName(name: string): boolean { + return timelineNameReg.test(name); +} + +export function useTimelineInfo( + timelineName: string +): TimelineWithSyncStatus | undefined { + const [state, setState] = React.useState( + undefined + ); + React.useEffect(() => { + const subscription = timelineService + .getTimeline$(timelineName) + .subscribe((data) => { + setState(data); + }); + return () => { + subscription.unsubscribe(); + }; + }, [timelineName]); + return state; +} + +export function usePostList( + timelineName: string | null | undefined +): TimelinePostsWithSyncState | undefined { + const [state, setState] = React.useState< + TimelinePostsWithSyncState | undefined + >(undefined); + React.useEffect(() => { + if (timelineName == null) { + setState(undefined); + return; + } + + const subscription = timelineService + .getPosts$(timelineName) + .subscribe((data) => { + setState(data); + }); + return () => { + subscription.unsubscribe(); + }; + }, [timelineName]); + return state; +} + +export async function getAllCachedTimelineNames(): Promise { + const keys = await dataStorage.keys(); + return keys + .filter( + (key) => + key.startsWith("timeline.") && (key.match(/\./g) ?? []).length === 1 + ) + .map((key) => key.substr("timeline.".length)); +} diff --git a/FrontEnd/src/app/services/user.ts b/FrontEnd/src/app/services/user.ts new file mode 100644 index 00000000..f253fc19 --- /dev/null +++ b/FrontEnd/src/app/services/user.ts @@ -0,0 +1,393 @@ +import React, { useState, useEffect } from "react"; +import { BehaviorSubject, Observable, from } from "rxjs"; +import { map, filter } from "rxjs/operators"; + +import { UiLogicError } from "@/common"; +import { convertError } from "@/utilities/rxjs"; + +import { HttpNetworkError, BlobWithEtag, NotModified } from "@/http/common"; +import { + getHttpTokenClient, + HttpCreateTokenBadCredentialError, +} from "@/http/token"; +import { + getHttpUserClient, + HttpUserNotExistError, + HttpUser, +} from "@/http/user"; + +import { dataStorage, throwIfNotNetworkError } from "./common"; +import { DataHub } from "./DataHub"; +import { pushAlert } from "./alert"; + +export type User = HttpUser; + +export interface UserAuthInfo { + username: string; + administrator: boolean; +} + +export interface UserWithToken extends User { + token: string; +} + +export interface LoginCredentials { + username: string; + password: string; +} + +export class BadCredentialError { + message = "login.badCredential"; +} + +const USER_STORAGE_KEY = "currentuser"; + +export class UserService { + private userSubject = new BehaviorSubject( + undefined + ); + + get user$(): Observable { + return this.userSubject; + } + + get currentUser(): UserWithToken | null | undefined { + return this.userSubject.value; + } + + async checkLoginState(): Promise { + if (this.currentUser !== undefined) { + console.warn("Already checked user. Can't check twice."); + } + + const savedUser = await dataStorage.getItem( + USER_STORAGE_KEY + ); + + if (savedUser == null) { + this.userSubject.next(null); + return null; + } + + this.userSubject.next(savedUser); + + const savedToken = savedUser.token; + try { + const res = await getHttpTokenClient().verify({ token: savedToken }); + const user: UserWithToken = { ...res.user, token: savedToken }; + await dataStorage.setItem(USER_STORAGE_KEY, user); + this.userSubject.next(user); + pushAlert({ + type: "success", + message: { + type: "i18n", + key: "user.welcomeBack", + }, + }); + return user; + } catch (error) { + if (error instanceof HttpNetworkError) { + pushAlert({ + type: "danger", + message: { type: "i18n", key: "user.verifyTokenFailedNetwork" }, + }); + return savedUser; + } else { + await dataStorage.removeItem(USER_STORAGE_KEY); + this.userSubject.next(null); + pushAlert({ + type: "danger", + message: { type: "i18n", key: "user.verifyTokenFailed" }, + }); + return null; + } + } + } + + async login( + credentials: LoginCredentials, + rememberMe: boolean + ): Promise { + if (this.currentUser) { + throw new UiLogicError("Already login."); + } + try { + const res = await getHttpTokenClient().create({ + ...credentials, + expire: 30, + }); + const user: UserWithToken = { + ...res.user, + token: res.token, + }; + if (rememberMe) { + await dataStorage.setItem(USER_STORAGE_KEY, user); + } + this.userSubject.next(user); + } catch (e) { + if (e instanceof HttpCreateTokenBadCredentialError) { + throw new BadCredentialError(); + } else { + throw e; + } + } + } + + async logout(): Promise { + if (this.currentUser === undefined) { + throw new UiLogicError("Please check user first."); + } + if (this.currentUser === null) { + throw new UiLogicError("No login."); + } + await dataStorage.removeItem(USER_STORAGE_KEY); + this.userSubject.next(null); + } + + changePassword( + oldPassword: string, + newPassword: string + ): Observable { + if (this.currentUser == undefined) { + throw new UiLogicError("Not login or checked now, can't log out."); + } + const $ = from( + getHttpUserClient().changePassword( + { + oldPassword, + newPassword, + }, + this.currentUser.token + ) + ); + $.subscribe(() => { + void this.logout(); + }); + return $; + } +} + +export const userService = new UserService(); + +export function useRawUser(): UserWithToken | null | undefined { + const [user, setUser] = useState( + userService.currentUser + ); + useEffect(() => { + const subscription = userService.user$.subscribe((u) => setUser(u)); + return () => { + subscription.unsubscribe(); + }; + }); + return user; +} + +export function useUser(): UserWithToken | null { + const [user, setUser] = useState(() => { + const initUser = userService.currentUser; + if (initUser === undefined) { + throw new UiLogicError( + "This is a logic error in user module. Current user can't be undefined in useUser." + ); + } + return initUser; + }); + useEffect(() => { + const sub = userService.user$.subscribe((u) => { + if (u === undefined) { + throw new UiLogicError( + "This is a logic error in user module. User emitted can't be undefined later." + ); + } + setUser(u); + }); + return () => { + sub.unsubscribe(); + }; + }); + return user; +} + +export function useUserLoggedIn(): UserWithToken { + const user = useUser(); + if (user == null) { + throw new UiLogicError("You assert user has logged in but actually not."); + } + return user; +} + +export function checkLogin(): UserWithToken { + const user = userService.currentUser; + if (user == null) { + throw new UiLogicError("You must login to perform the operation."); + } + return user; +} + +export class UserNotExistError extends Error {} + +export class UserInfoService { + saveUser(user: HttpUser): void { + const key = user.username; + void this._userHub.optionalInitLineWithSyncAction(key, async (line) => { + await this.doSaveUser(user); + line.next({ user, type: "synced" }); + }); + } + + saveUsers(users: HttpUser[]): void { + return users.forEach((user) => this.saveUser(user)); + } + + private getCachedUser(username: string): Promise { + return dataStorage.getItem(`user.${username}`); + } + + private doSaveUser(user: HttpUser): Promise { + return dataStorage.setItem(`user.${user.username}`, user).then(); + } + + syncUser(username: string): Promise { + return this._userHub.getLineOrCreate(username).sync(); + } + + private _userHub = new DataHub< + string, + | { user: User; type: "cache" | "synced" | "offline" } + | { user?: undefined; type: "notexist" | "offline" } + >({ + sync: async (key, line) => { + if (line.value == undefined) { + const cache = await this.getCachedUser(key); + if (cache != null) { + line.next({ user: cache, type: "cache" }); + } + } + + try { + const res = await getHttpUserClient().get(key); + await this.doSaveUser(res); + line.next({ user: res, type: "synced" }); + } catch (e) { + if (e instanceof HttpUserNotExistError) { + line.next({ type: "notexist" }); + } else { + const cache = await this.getCachedUser(key); + line.next({ user: cache ?? undefined, type: "offline" }); + throwIfNotNetworkError(e); + } + } + }, + }); + + getUser$(username: string): Observable { + return this._userHub.getObservable(username).pipe( + map((state) => state?.user), + filter((user): user is User => user != null) + ); + } + + private getCachedAvatar(username: string): Promise { + return dataStorage.getItem(`user.${username}.avatar`); + } + + private saveAvatar(username: string, data: BlobWithEtag): Promise { + return dataStorage + .setItem(`user.${username}.avatar`, data) + .then(); + } + + syncAvatar(username: string): Promise { + return this._avatarHub.getLineOrCreate(username).sync(); + } + + private _avatarHub = new DataHub< + string, + | { data: Blob; type: "cache" | "synced" | "offline" } + | { data?: undefined; type: "notexist" | "offline" } + >({ + sync: async (key, line) => { + const cache = await this.getCachedAvatar(key); + if (line.value == null) { + if (cache != null) { + line.next({ data: cache.data, type: "cache" }); + } + } + + if (cache == null) { + try { + const avatar = await getHttpUserClient().getAvatar(key); + await this.saveAvatar(key, avatar); + line.next({ data: avatar.data, type: "synced" }); + } catch (e) { + line.next({ type: "offline" }); + throwIfNotNetworkError(e); + } + } else { + try { + const res = await getHttpUserClient().getAvatar(key, cache.etag); + if (res instanceof NotModified) { + line.next({ data: cache.data, type: "synced" }); + } else { + const avatar = res; + await this.saveAvatar(key, avatar); + line.next({ data: avatar.data, type: "synced" }); + } + } catch (e) { + line.next({ data: cache.data, type: "offline" }); + throwIfNotNetworkError(e); + } + } + }, + }); + + getAvatar$(username: string): Observable { + return this._avatarHub.getObservable(username).pipe( + map((state) => state.data), + filter((blob): blob is Blob => blob != null) + ); + } + + getUserInfo(username: string): Observable { + return from(getHttpUserClient().get(username)).pipe( + convertError(HttpUserNotExistError, UserNotExistError) + ); + } + + async setAvatar(username: string, blob: Blob): Promise { + const user = checkLogin(); + await getHttpUserClient().putAvatar(username, blob, user.token); + this._avatarHub.getLine(username)?.next({ data: blob, type: "synced" }); + } + + async setNickname(username: string, nickname: string): Promise { + const user = checkLogin(); + return getHttpUserClient() + .patch(username, { nickname }, user.token) + .then((user) => { + this.saveUser(user); + }); + } +} + +export const userInfoService = new UserInfoService(); + +export function useAvatar(username?: string): Blob | undefined { + const [state, setState] = React.useState(undefined); + React.useEffect(() => { + if (username == null) { + setState(undefined); + return; + } + + const subscription = userInfoService + .getAvatar$(username) + .subscribe((blob) => { + setState(blob); + }); + return () => { + subscription.unsubscribe(); + }; + }, [username]); + return state; +} diff --git a/FrontEnd/src/app/tsconfig.json b/FrontEnd/src/app/tsconfig.json new file mode 100644 index 00000000..17ee69cb --- /dev/null +++ b/FrontEnd/src/app/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "lib": [ + "dom", + "dom.iterable", + "esnext" + ] + }, + "include": [ + "." + ] +} diff --git a/FrontEnd/src/app/typings.d.ts b/FrontEnd/src/app/typings.d.ts new file mode 100644 index 00000000..34381682 --- /dev/null +++ b/FrontEnd/src/app/typings.d.ts @@ -0,0 +1,24 @@ +declare module "*.png" { + const content: string; + export default content; +} + +declare module "*.jpeg" { + const content: string; + export default content; +} + +declare module "*.jpg" { + const content: string; + export default content; +} + +declare module "*.gif" { + const content: string; + export default content; +} + +declare module "*.svg" { + const content: string; + export default content; +} diff --git a/FrontEnd/src/app/utilities/rxjs.ts b/FrontEnd/src/app/utilities/rxjs.ts new file mode 100644 index 00000000..0730b899 --- /dev/null +++ b/FrontEnd/src/app/utilities/rxjs.ts @@ -0,0 +1,14 @@ +import { OperatorFunction } from "rxjs"; +import { catchError } from "rxjs/operators"; + +export function convertError( + oldErrorType: { new (...args: never[]): unknown }, + newErrorType: { new (): NewError } +): OperatorFunction { + return catchError((error) => { + if (error instanceof oldErrorType) { + throw new newErrorType(); + } + throw error; + }); +} diff --git a/FrontEnd/src/app/utilities/url.ts b/FrontEnd/src/app/utilities/url.ts new file mode 100644 index 00000000..17ead5b2 --- /dev/null +++ b/FrontEnd/src/app/utilities/url.ts @@ -0,0 +1,52 @@ +//copied from https://stackoverflow.com/questions/5999118/how-can-i-add-or-update-a-query-string-parameter +export function updateQueryString( + key: string, + value: undefined | string | null, + url: string +): string { + const re = new RegExp("([?&])" + key + "=.*?(&|#|$)(.*)", "gi"); + let hash; + + if (re.test(url)) { + if (typeof value !== "undefined" && value !== null) { + return url.replace(re, "$1" + key + "=" + value + "$2$3"); + } else { + hash = url.split("#"); + url = hash[0].replace(re, "$1$3").replace(/(&|\?)$/, ""); + if (typeof hash[1] !== "undefined" && hash[1] !== null) { + url += "#" + hash[1]; + } + return url; + } + } else { + if (typeof value !== "undefined" && value !== null) { + const separator = url.includes("?") ? "&" : "?"; + hash = url.split("#"); + url = hash[0] + separator + key + "=" + value; + if (typeof hash[1] !== "undefined" && hash[1] !== null) { + url += "#" + hash[1]; + } + return url; + } else { + return url; + } + } +} + +export function applyQueryParameters(url: string, query: T): string { + if (query == null) return url; + + for (const [key, value] of Object.entries(query)) { + if (typeof value === "string") url = updateQueryString(key, value, url); + else if (typeof value === "number") + url = updateQueryString(key, String(value), url); + else if (typeof value === "boolean") + url = updateQueryString(key, value ? "true" : "false", url); + else if (value instanceof Date) + url = updateQueryString(key, value.toISOString(), url); + else { + console.error("Unknown query parameter type. Param: ", value); + } + } + return url; +} diff --git a/FrontEnd/src/app/views/about/about.sass b/FrontEnd/src/app/views/about/about.sass new file mode 100644 index 00000000..f4d00cae --- /dev/null +++ b/FrontEnd/src/app/views/about/about.sass @@ -0,0 +1,4 @@ +.about-link-icon + @extend .mx-2 + width: 1.2em + height: 1.2em diff --git a/FrontEnd/src/app/views/about/author-avatar.png b/FrontEnd/src/app/views/about/author-avatar.png new file mode 100644 index 00000000..d890d8d0 Binary files /dev/null and b/FrontEnd/src/app/views/about/author-avatar.png differ diff --git a/FrontEnd/src/app/views/about/github.png b/FrontEnd/src/app/views/about/github.png new file mode 100644 index 00000000..ea6ff545 Binary files /dev/null and b/FrontEnd/src/app/views/about/github.png differ diff --git a/FrontEnd/src/app/views/about/index.tsx b/FrontEnd/src/app/views/about/index.tsx new file mode 100644 index 00000000..e7771cec --- /dev/null +++ b/FrontEnd/src/app/views/about/index.tsx @@ -0,0 +1,164 @@ +import React from "react"; +import { useTranslation, Trans } from "react-i18next"; + +import authorAvatarUrl from "./author-avatar.png"; +import githubLogoUrl from "./github.png"; + +const frontendCredits: { + name: string; + url: string; +}[] = [ + { + name: "reactjs", + url: "https://reactjs.org", + }, + { + name: "typescript", + url: "https://www.typescriptlang.org", + }, + { + name: "bootstrap", + url: "https://getbootstrap.com", + }, + { + name: "react-bootstrap", + url: "https://react-bootstrap.github.io", + }, + { + name: "babeljs", + url: "https://babeljs.io", + }, + { + name: "webpack", + url: "https://webpack.js.org", + }, + { + name: "sass", + url: "https://sass-lang.com", + }, + { + name: "eslint", + url: "https://eslint.org", + }, + { + name: "prettier", + url: "https://prettier.io", + }, + { + name: "pepjs", + url: "https://github.com/jquery/PEP", + }, + { + name: "react-inlinesvg", + url: "https://github.com/gilbarbara/react-inlinesvg", + }, +]; + +const backendCredits: { + name: string; + url: string; +}[] = [ + { + name: "ASP.NET Core", + url: "https://dotnet.microsoft.com/learn/aspnet/what-is-aspnet-core", + }, + { name: "sqlite", url: "https://sqlite.org" }, + { + name: "ImageSharp", + url: "https://github.com/SixLabors/ImageSharp", + }, +]; + +const AboutPage: React.FC = () => { + const { t } = useTranslation(); + + return ( +
+
+

{t("about.author.title")}

+
+
+ +
+

+ {t("about.author.fullname")} + 杨宇千 +

+

+ {t("about.author.nickname")} + crupest +

+

+ {t("about.author.introduction")} + {t("about.author.introductionContent")} +

+
+
+

+ {t("about.author.links")} + + + +

+
+
+
+

{t("about.site.title")}

+

+ + 01234 + 56 + +

+

+ + {t("about.site.repo")} + +

+
+
+

{t("about.credits.title")}

+

{t("about.credits.content")}

+

{t("about.credits.frontend")}

+
    + {frontendCredits.map((item, index) => { + return ( +
  • + + {item.name} + +
  • + ); + })} +
  • ...
  • +
+

{t("about.credits.backend")}

+
    + {backendCredits.map((item, index) => { + return ( +
  • + + {item.name} + +
  • + ); + })} +
  • ...
  • +
+
+
+ ); +}; + +export default AboutPage; diff --git a/FrontEnd/src/app/views/admin/Admin.tsx b/FrontEnd/src/app/views/admin/Admin.tsx new file mode 100644 index 00000000..9c0250e7 --- /dev/null +++ b/FrontEnd/src/app/views/admin/Admin.tsx @@ -0,0 +1,75 @@ +import React, { Fragment } from "react"; +import { + Redirect, + Route, + Switch, + useRouteMatch, + useHistory, +} from "react-router"; +import { Nav } from "react-bootstrap"; + +import { UserWithToken } from "@/services/user"; + +import UserAdmin from "./UserAdmin"; + +interface AdminProps { + user: UserWithToken; +} + +const Admin: React.FC = (props) => { + const match = useRouteMatch(); + const history = useHistory(); + type TabNames = "users" | "more"; + + const tabName = history.location.pathname.replace(match.path + "/", ""); + + function toggle(newTab: TabNames): void { + history.push(`${match.url}/${newTab}`); + } + + const createRoute = ( + name: string, + body: React.ReactNode + ): React.ReactNode => { + return ( + +
+ + {body} + + ); + }; + + return ( + + + + {createRoute("users", )} + {createRoute("more",
More Page Works
)} +
+
+ ); +}; + +export default Admin; diff --git a/FrontEnd/src/app/views/admin/UserAdmin.tsx b/FrontEnd/src/app/views/admin/UserAdmin.tsx new file mode 100644 index 00000000..18b77ca8 --- /dev/null +++ b/FrontEnd/src/app/views/admin/UserAdmin.tsx @@ -0,0 +1,460 @@ +import React, { useState, useEffect } from "react"; +import axios from "axios"; +import { + ListGroup, + Row, + Col, + Dropdown, + Spinner, + Button, +} from "react-bootstrap"; + +import OperationDialog from "../common/OperationDialog"; +import { User, UserWithToken } from "@/services/user"; + +const apiBaseUrl = "/api"; + +async function fetchUserList(_token: string): Promise { + const res = await axios.get(`${apiBaseUrl}/users`); + return res.data; +} + +interface CreateUserInfo { + username: string; + password: string; + administrator: boolean; +} + +async function createUser(user: CreateUserInfo, token: string): Promise { + const res = await axios.post( + `${apiBaseUrl}/userop/createuser?token=${token}`, + user + ); + return res.data; +} + +function deleteUser(username: string, token: string): Promise { + return axios.delete(`${apiBaseUrl}/users/${username}?token=${token}`); +} + +function changeUsername( + oldUsername: string, + newUsername: string, + token: string +): Promise { + return axios.patch(`${apiBaseUrl}/users/${oldUsername}?token=${token}`, { + username: newUsername, + }); +} + +function changePassword( + username: string, + newPassword: string, + token: string +): Promise { + return axios.patch(`${apiBaseUrl}/users/${username}?token=${token}`, { + password: newPassword, + }); +} + +function changePermission( + username: string, + newPermission: boolean, + token: string +): Promise { + return axios.patch(`${apiBaseUrl}/users/${username}?token=${token}`, { + administrator: newPermission, + }); +} + +const kChangeUsername = "changeusername"; +const kChangePassword = "changepassword"; +const kChangePermission = "changepermission"; +const kDelete = "delete"; + +type TChangeUsername = typeof kChangeUsername; +type TChangePassword = typeof kChangePassword; +type TChangePermission = typeof kChangePermission; +type TDelete = typeof kDelete; + +type ContextMenuItem = + | TChangeUsername + | TChangePassword + | TChangePermission + | TDelete; + +interface UserCardProps { + onContextMenu: (item: ContextMenuItem) => void; + user: User; +} + +const UserItem: React.FC = (props) => { + const user = props.user; + + const createClickCallback = (item: ContextMenuItem): (() => void) => { + return () => { + props.onContextMenu(item); + }; + }; + + return ( + + + +

{user.username}

+ + {user.administrator ? "administrator" : "user"} + + + + + + Manage + + + + Change Username + + + Change Password + + + Change Permission + + + Delete + + + + +
+
+ ); +}; + +interface DialogProps { + open: boolean; + close: () => void; +} + +interface CreateUserDialogProps extends DialogProps { + process: (user: CreateUserInfo) => Promise; +} + +const CreateUserDialog: React.FC = (props) => { + return ( + + props.process({ + username: username as string, + password: password as string, + administrator: administrator as boolean, + }) + } + close={props.close} + open={props.open} + /> + ); +}; + +const UsernameLabel: React.FC = (props) => { + return {props.children}; +}; + +interface UserDeleteDialogProps extends DialogProps { + username: string; + process: () => Promise; +} + +const UserDeleteDialog: React.FC = (props) => { + return ( + ( + <> + {"You are deleting user "} + {props.username} + {" !"} + + )} + onProcess={props.process} + /> + ); +}; + +interface UserModifyDialogProps extends DialogProps { + username: string; + process: (value: T) => Promise; +} + +const UserChangeUsernameDialog: React.FC> = ( + props +) => { + return ( + ( + <> + {"You are change the username of user "} + {props.username} + {" !"} + + )} + inputScheme={[{ type: "text", label: "New Username" }]} + onProcess={([newUsername]) => { + return props.process(newUsername as string); + }} + /> + ); +}; + +const UserChangePasswordDialog: React.FC> = ( + props +) => { + return ( + ( + <> + {"You are change the password of user "} + {props.username} + {" !"} + + )} + inputScheme={[{ type: "text", label: "New Password" }]} + onProcess={([newPassword]) => { + return props.process(newPassword as string); + }} + /> + ); +}; + +interface UserChangePermissionDialogProps extends DialogProps { + username: string; + newPermission: boolean; + process: () => Promise; +} + +const UserChangePermissionDialog: React.FC = ( + props +) => { + return ( + ( + <> + {"You are change user "} + {props.username} + {" to "} + + {props.newPermission ? "administrator" : "normal user"} + + {" !"} + + )} + onProcess={props.process} + /> + ); +}; + +interface UserAdminProps { + user: UserWithToken; +} + +const UserAdmin: React.FC = (props) => { + type DialogInfo = + | null + | { + type: "create"; + } + | { type: TDelete; username: string } + | { + type: TChangeUsername; + username: string; + } + | { + type: TChangePassword; + username: string; + } + | { + type: TChangePermission; + username: string; + newPermission: boolean; + }; + + const [users, setUsers] = useState(null); + const [dialog, setDialog] = useState(null); + + const token = props.user.token; + + useEffect(() => { + let subscribe = true; + void fetchUserList(props.user.token).then((us) => { + if (subscribe) { + setUsers(us); + } + }); + return () => { + subscribe = false; + }; + }, [props.user]); + + let dialogNode: React.ReactNode; + if (dialog) + switch (dialog.type) { + case "create": + dialogNode = ( + setDialog(null)} + process={async (user) => { + const u = await createUser(user, token); + setUsers((oldUsers) => [...(oldUsers ?? []), u]); + }} + /> + ); + break; + case "delete": + dialogNode = ( + setDialog(null)} + username={dialog.username} + process={async () => { + await deleteUser(dialog.username, token); + setUsers((oldUsers) => + (oldUsers ?? []).filter((u) => u.username !== dialog.username) + ); + }} + /> + ); + break; + case kChangeUsername: + dialogNode = ( + setDialog(null)} + username={dialog.username} + process={async (newUsername) => { + await changeUsername(dialog.username, newUsername, token); + setUsers((oldUsers) => { + const users = (oldUsers ?? []).slice(); + const findedUser = users.find( + (u) => u.username === dialog.username + ); + if (findedUser) findedUser.username = newUsername; + return users; + }); + }} + /> + ); + break; + case kChangePassword: + dialogNode = ( + setDialog(null)} + username={dialog.username} + process={async (newPassword) => { + await changePassword(dialog.username, newPassword, token); + }} + /> + ); + break; + case kChangePermission: { + const newPermission = dialog.newPermission; + dialogNode = ( + setDialog(null)} + username={dialog.username} + newPermission={newPermission} + process={async () => { + await changePermission(dialog.username, newPermission, token); + setUsers((oldUsers) => { + const users = (oldUsers ?? []).slice(); + const findedUser = users.find( + (u) => u.username === dialog.username + ); + if (findedUser) findedUser.administrator = newPermission; + return users; + }); + }} + /> + ); + break; + } + } + + if (users) { + const userComponents = users.map((user) => { + return ( + { + setDialog( + item === kChangePermission + ? { + type: kChangePermission, + username: user.username, + newPermission: !user.administrator, + } + : { + type: item, + username: user.username, + } + ); + }} + /> + ); + }); + + return ( + <> + + {userComponents} + {dialogNode} + + ); + } else { + return ; + } +}; + +export default UserAdmin; diff --git a/FrontEnd/src/app/views/common/AppBar.tsx b/FrontEnd/src/app/views/common/AppBar.tsx new file mode 100644 index 00000000..ee4ead8f --- /dev/null +++ b/FrontEnd/src/app/views/common/AppBar.tsx @@ -0,0 +1,64 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { LinkContainer } from "react-router-bootstrap"; +import { Navbar, Nav } from "react-bootstrap"; + +import { useUser, useAvatar } from "@/services/user"; + +import TimelineLogo from "./TimelineLogo"; +import BlobImage from "./BlobImage"; + +const AppBar: React.FC = (_) => { + const user = useUser(); + const avatar = useAvatar(user?.username); + + const { t } = useTranslation(); + + const isAdministrator = user && user.administrator; + + return ( + + + + + Timeline + + + + + + + + + + ); +}; + +export default AppBar; diff --git a/FrontEnd/src/app/views/common/BlobImage.tsx b/FrontEnd/src/app/views/common/BlobImage.tsx new file mode 100644 index 00000000..0dd25c52 --- /dev/null +++ b/FrontEnd/src/app/views/common/BlobImage.tsx @@ -0,0 +1,27 @@ +import React from "react"; + +const BlobImage: React.FC< + Omit, "src"> & { + blob?: Blob | unknown; + } +> = (props) => { + const { blob, ...otherProps } = props; + + const [url, setUrl] = React.useState(undefined); + + React.useEffect(() => { + if (blob instanceof Blob) { + const url = URL.createObjectURL(blob); + setUrl(url); + return () => { + URL.revokeObjectURL(url); + }; + } else { + setUrl(undefined); + } + }, [blob]); + + return ; +}; + +export default BlobImage; diff --git a/FrontEnd/src/app/views/common/ImageCropper.tsx b/FrontEnd/src/app/views/common/ImageCropper.tsx new file mode 100644 index 00000000..b9db8b99 --- /dev/null +++ b/FrontEnd/src/app/views/common/ImageCropper.tsx @@ -0,0 +1,306 @@ +import React from "react"; +import clsx from "clsx"; + +import { UiLogicError } from "@/common"; + +export interface Clip { + left: number; + top: number; + width: number; +} + +interface NormailizedClip extends Clip { + height: number; +} + +interface ImageInfo { + width: number; + height: number; + landscape: boolean; + ratio: number; + maxClipWidth: number; + maxClipHeight: number; +} + +interface ImageCropperSavedState { + clip: NormailizedClip; + x: number; + y: number; + pointerId: number; +} + +export interface ImageCropperProps { + clip: Clip | null; + imageUrl: string; + onChange: (clip: Clip) => void; + imageElementCallback?: (element: HTMLImageElement | null) => void; + className?: string; +} + +const ImageCropper = (props: ImageCropperProps): React.ReactElement => { + const { clip, imageUrl, onChange, imageElementCallback, className } = props; + + const [oldState, setOldState] = React.useState( + null + ); + const [imageInfo, setImageInfo] = React.useState(null); + + const normalizeClip = (c: Clip | null | undefined): NormailizedClip => { + if (c == null) { + return { left: 0, top: 0, width: 0, height: 0 }; + } + + return { + left: c.left || 0, + top: c.top || 0, + width: c.width || 0, + height: imageInfo != null ? (c.width || 0) / imageInfo.ratio : 0, + }; + }; + + const c = normalizeClip(clip); + + const imgElementRef = React.useRef(null); + + const onImageRef = React.useCallback( + (e: HTMLImageElement | null) => { + imgElementRef.current = e; + if (imageElementCallback != null && e == null) { + imageElementCallback(null); + } + }, + [imageElementCallback] + ); + + const onImageLoad = React.useCallback( + (e: React.SyntheticEvent) => { + const img = e.currentTarget; + const landscape = img.naturalWidth >= img.naturalHeight; + + const info = { + width: img.naturalWidth, + height: img.naturalHeight, + landscape, + ratio: img.naturalHeight / img.naturalWidth, + maxClipWidth: landscape ? img.naturalHeight / img.naturalWidth : 1, + maxClipHeight: landscape ? 1 : img.naturalWidth / img.naturalHeight, + }; + setImageInfo(info); + onChange({ left: 0, top: 0, width: info.maxClipWidth }); + if (imageElementCallback != null) { + imageElementCallback(img); + } + }, + [onChange, imageElementCallback] + ); + + const onPointerDown = React.useCallback( + (e: React.PointerEvent) => { + if (oldState != null) return; + e.currentTarget.setPointerCapture(e.pointerId); + setOldState({ + x: e.clientX, + y: e.clientY, + clip: c, + pointerId: e.pointerId, + }); + }, + [oldState, c] + ); + + const onPointerUp = React.useCallback( + (e: React.PointerEvent) => { + if (oldState == null || oldState.pointerId !== e.pointerId) return; + e.currentTarget.releasePointerCapture(e.pointerId); + setOldState(null); + }, + [oldState] + ); + + const onPointerMove = React.useCallback( + (e: React.PointerEvent) => { + if (oldState == null) return; + + const oldClip = oldState.clip; + + const movement = { x: e.clientX - oldState.x, y: e.clientY - oldState.y }; + + const { current: imgElement } = imgElementRef; + + if (imgElement == null) throw new UiLogicError("Image element is null."); + + const moveRatio = { + x: movement.x / imgElement.width, + y: movement.y / imgElement.height, + }; + + const newRatio = { + x: oldClip.left + moveRatio.x, + y: oldClip.top + moveRatio.y, + }; + if (newRatio.x < 0) { + newRatio.x = 0; + } else if (newRatio.x > 1 - oldClip.width) { + newRatio.x = 1 - oldClip.width; + } + if (newRatio.y < 0) { + newRatio.y = 0; + } else if (newRatio.y > 1 - oldClip.height) { + newRatio.y = 1 - oldClip.height; + } + + onChange({ left: newRatio.x, top: newRatio.y, width: oldClip.width }); + }, + [oldState, onChange] + ); + + const onHandlerPointerMove = React.useCallback( + (e: React.PointerEvent) => { + if (oldState == null) return; + + const oldClip = oldState.clip; + + const movement = { x: e.clientX - oldState.x, y: e.clientY - oldState.y }; + + const ratio = imageInfo == null ? 1 : imageInfo.ratio; + + const { current: imgElement } = imgElementRef; + + if (imgElement == null) throw new UiLogicError("Image element is null."); + + const moveRatio = { + x: movement.x / imgElement.width, + y: movement.x / imgElement.width / ratio, + }; + + const newRatio = { + x: oldClip.width + moveRatio.x, + y: oldClip.height + moveRatio.y, + }; + + const maxRatio = { + x: Math.min(1 - oldClip.left, newRatio.x), + y: Math.min(1 - oldClip.top, newRatio.y), + }; + + const maxWidthRatio = Math.min(maxRatio.x, maxRatio.y * ratio); + + let newWidth; + if (newRatio.x < 0) { + newWidth = 0; + } else if (newRatio.x > maxWidthRatio) { + newWidth = maxWidthRatio; + } else { + newWidth = newRatio.x; + } + + onChange({ left: oldClip.left, top: oldClip.top, width: newWidth }); + }, + [imageInfo, oldState, onChange] + ); + + const toPercentage = (n: number): string => `${n}%`; + + // fuck!!! I just can't find a better way to implement this in pure css + const containerStyle: React.CSSProperties = (() => { + if (imageInfo == null) { + return { width: "100%", paddingTop: "100%", height: 0 }; + } else { + if (imageInfo.ratio > 1) { + return { + width: toPercentage(100 / imageInfo.ratio), + paddingTop: "100%", + height: 0, + }; + } else { + return { + width: "100%", + paddingTop: toPercentage(100 * imageInfo.ratio), + height: 0, + }; + } + } + })(); + + return ( +
+ to crop +
+
+
+
+
+ ); +}; + +export default ImageCropper; + +export function applyClipToImage( + image: HTMLImageElement, + clip: Clip, + mimeType: string +): Promise { + return new Promise((resolve, reject) => { + const naturalSize = { + width: image.naturalWidth, + height: image.naturalHeight, + }; + const clipArea = { + x: naturalSize.width * clip.left, + y: naturalSize.height * clip.top, + length: naturalSize.width * clip.width, + }; + + const canvas = document.createElement("canvas"); + canvas.width = clipArea.length; + canvas.height = clipArea.length; + const context = canvas.getContext("2d"); + + if (context == null) throw new Error("Failed to create context."); + + context.drawImage( + image, + clipArea.x, + clipArea.y, + clipArea.length, + clipArea.length, + 0, + 0, + clipArea.length, + clipArea.length + ); + + canvas.toBlob((blob) => { + if (blob == null) { + reject(new Error("canvas.toBlob returns null")); + } else { + resolve(blob); + } + }, mimeType); + }); +} diff --git a/FrontEnd/src/app/views/common/LoadingButton.tsx b/FrontEnd/src/app/views/common/LoadingButton.tsx new file mode 100644 index 00000000..154334a7 --- /dev/null +++ b/FrontEnd/src/app/views/common/LoadingButton.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { Button, ButtonProps, Spinner } from "react-bootstrap"; + +const LoadingButton: React.FC<{ loading?: boolean } & ButtonProps> = ({ + loading, + variant, + disabled, + ...otherProps +}) => { + return ( + + ); +}; + +export default LoadingButton; diff --git a/FrontEnd/src/app/views/common/LoadingPage.tsx b/FrontEnd/src/app/views/common/LoadingPage.tsx new file mode 100644 index 00000000..590fafa0 --- /dev/null +++ b/FrontEnd/src/app/views/common/LoadingPage.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import { Spinner } from "react-bootstrap"; + +const LoadingPage: React.FC = () => { + return ( +
+ +
+ ); +}; + +export default LoadingPage; diff --git a/FrontEnd/src/app/views/common/OperationDialog.tsx b/FrontEnd/src/app/views/common/OperationDialog.tsx new file mode 100644 index 00000000..841392a6 --- /dev/null +++ b/FrontEnd/src/app/views/common/OperationDialog.tsx @@ -0,0 +1,364 @@ +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Form, Button, Modal } from "react-bootstrap"; + +import { UiLogicError } from "@/common"; + +import LoadingButton from "./LoadingButton"; + +interface DefaultErrorPromptProps { + error?: string; +} + +const DefaultErrorPrompt: React.FC = (props) => { + const { t } = useTranslation(); + + let result =

{t("operationDialog.error")}

; + + if (props.error != null) { + result = ( + <> + {result} +

{props.error}

+ + ); + } + + return result; +}; + +export type OperationInputOptionalError = undefined | null | string; + +export interface OperationInputErrorInfo { + [index: number]: OperationInputOptionalError; +} + +export type OperationInputValidator = ( + value: TValue, + values: (string | boolean)[] +) => OperationInputOptionalError | OperationInputErrorInfo; + +export interface OperationTextInputInfo { + type: "text"; + password?: boolean; + label?: string; + initValue?: string; + textFieldProps?: Omit< + React.InputHTMLAttributes, + "type" | "value" | "onChange" | "aria-relevant" + >; + helperText?: string; + validator?: OperationInputValidator; +} + +export interface OperationBoolInputInfo { + type: "bool"; + label: string; + initValue?: boolean; +} + +export interface OperationSelectInputInfoOption { + value: string; + label: string; + icon?: React.ReactElement; +} + +export interface OperationSelectInputInfo { + type: "select"; + label: string; + options: OperationSelectInputInfoOption[]; + initValue?: string; +} + +export type OperationInputInfo = + | OperationTextInputInfo + | OperationBoolInputInfo + | OperationSelectInputInfo; + +interface OperationResult { + type: "success" | "failure"; + data: unknown; +} + +interface OperationDialogProps { + open: boolean; + close: () => void; + title: React.ReactNode; + titleColor?: "default" | "dangerous" | "create" | string; + onProcess: (inputs: (string | boolean)[]) => Promise; + inputScheme?: OperationInputInfo[]; + inputPrompt?: string | (() => React.ReactNode); + processPrompt?: () => React.ReactNode; + successPrompt?: (data: unknown) => React.ReactNode; + failurePrompt?: (error: unknown) => React.ReactNode; + onSuccessAndClose?: () => void; +} + +const OperationDialog: React.FC = (props) => { + const inputScheme = props.inputScheme ?? []; + + const { t } = useTranslation(); + + type Step = "input" | "process" | OperationResult; + const [step, setStep] = useState("input"); + const [values, setValues] = useState<(boolean | string)[]>( + inputScheme.map((i) => { + if (i.type === "bool") { + return i.initValue ?? false; + } else if (i.type === "text" || i.type === "select") { + return i.initValue ?? ""; + } else { + throw new UiLogicError("Unknown input scheme."); + } + }) + ); + const [inputError, setInputError] = useState({}); + + const close = (): void => { + if (step !== "process") { + props.close(); + if ( + typeof step === "object" && + step.type === "success" && + props.onSuccessAndClose + ) { + props.onSuccessAndClose(); + } + } else { + console.log("Attempt to close modal when processing."); + } + }; + + const onConfirm = (): void => { + setStep("process"); + props.onProcess(values).then( + (d: unknown) => { + setStep({ + type: "success", + data: d, + }); + }, + (e: unknown) => { + setStep({ + type: "failure", + data: e, + }); + } + ); + }; + + let body: React.ReactNode; + if (step === "input" || step === "process") { + const process = step === "process"; + + let inputPrompt = + typeof props.inputPrompt === "function" + ? props.inputPrompt() + : props.inputPrompt; + inputPrompt =
{inputPrompt}
; + + const updateValue = ( + index: number, + newValue: string | boolean + ): (string | boolean)[] => { + const oldValues = values; + const newValues = oldValues.slice(); + newValues[index] = newValue; + setValues(newValues); + return newValues; + }; + + const testErrorInfo = (errorInfo: OperationInputErrorInfo): boolean => { + for (let i = 0; i < inputScheme.length; i++) { + if (inputScheme[i].type === "text" && errorInfo[i] != null) { + return true; + } + } + return false; + }; + + const calculateError = ( + oldError: OperationInputErrorInfo, + index: number, + newError: OperationInputOptionalError | OperationInputErrorInfo + ): OperationInputErrorInfo => { + if (newError === undefined) { + return oldError; + } else if (newError === null || typeof newError === "string") { + return { ...oldError, [index]: newError }; + } else { + const newInputError: OperationInputErrorInfo = { ...oldError }; + for (const [index, error] of Object.entries(newError)) { + if (error !== undefined) { + newInputError[+index] = error as OperationInputOptionalError; + } + } + return newInputError; + } + }; + + const validateAll = (): boolean => { + let newInputError = inputError; + for (let i = 0; i < inputScheme.length; i++) { + const item = inputScheme[i]; + if (item.type === "text") { + newInputError = calculateError( + newInputError, + i, + item.validator?.(values[i] as string, values) + ); + } + } + const result = !testErrorInfo(newInputError); + setInputError(newInputError); + return result; + }; + + body = ( + <> + + {inputPrompt} + {inputScheme.map((item, index) => { + const value = values[index]; + const error: string | undefined = ((e) => + typeof e === "string" ? t(e) : undefined)(inputError?.[index]); + + if (item.type === "text") { + return ( + + {item.label && {t(item.label)}} + { + const v = e.target.value; + const newValues = updateValue(index, v); + setInputError( + calculateError( + inputError, + index, + item.validator?.(v, newValues) + ) + ); + }} + isInvalid={error != null} + disabled={process} + /> + {error != null && ( + + {error} + + )} + {item.helperText && ( + {t(item.helperText)} + )} + + ); + } else if (item.type === "bool") { + return ( + + + type="checkbox" + checked={value as boolean} + onChange={(event) => { + updateValue(index, event.currentTarget.checked); + }} + label={t(item.label)} + disabled={process} + /> + + ); + } else if (item.type === "select") { + return ( + + {t(item.label)} + { + updateValue(index, event.target.value); + }} + disabled={process} + > + {item.options.map((option, i) => { + return ( + + ); + })} + + + ); + } + })} + + + + { + if (validateAll()) { + onConfirm(); + } + }} + > + {t("operationDialog.confirm")} + + + + ); + } else { + let content: React.ReactNode; + const result = step; + if (result.type === "success") { + content = + props.successPrompt?.(result.data) ?? t("operationDialog.success"); + if (typeof content === "string") + content =

{content}

; + } else { + content = props.failurePrompt?.(result.data) ?? ; + if (typeof content === "string") + content = ; + } + body = ( + <> + {content} + + + + + ); + } + + const title = typeof props.title === "string" ? t(props.title) : props.title; + + return ( + + + {title} + + {body} + + ); +}; + +export default OperationDialog; diff --git a/FrontEnd/src/app/views/common/SearchInput.tsx b/FrontEnd/src/app/views/common/SearchInput.tsx new file mode 100644 index 00000000..9833d515 --- /dev/null +++ b/FrontEnd/src/app/views/common/SearchInput.tsx @@ -0,0 +1,63 @@ +import React, { useCallback } from "react"; +import clsx from "clsx"; +import { useTranslation } from "react-i18next"; +import { Spinner, Form, Button } from "react-bootstrap"; + +export interface SearchInputProps { + value: string; + onChange: (value: string) => void; + onButtonClick: () => void; + className?: string; + loading?: boolean; + buttonText?: string; + placeholder?: string; + additionalButton?: React.ReactNode; +} + +const SearchInput: React.FC = (props) => { + const { onChange, onButtonClick } = props; + + const { t } = useTranslation(); + + const onInputChange = useCallback( + (event: React.ChangeEvent): void => { + onChange(event.currentTarget.value); + }, + [onChange] + ); + + const onInputKeyPress = useCallback( + (event: React.KeyboardEvent): void => { + if (event.key === "Enter") { + onButtonClick(); + } + }, + [onButtonClick] + ); + + return ( +
+ +
+ {props.additionalButton} +
+
+ {props.loading ? ( + + ) : ( + + )} +
+ + ); +}; + +export default SearchInput; diff --git a/FrontEnd/src/app/views/common/TimelineLogo.tsx b/FrontEnd/src/app/views/common/TimelineLogo.tsx new file mode 100644 index 00000000..27d188fc --- /dev/null +++ b/FrontEnd/src/app/views/common/TimelineLogo.tsx @@ -0,0 +1,26 @@ +import React, { SVGAttributes } from "react"; + +export interface TimelineLogoProps extends SVGAttributes { + color?: string; +} + +const TimelineLogo: React.FC = (props) => { + const { color, ...forwardProps } = props; + const coercedColor = color ?? "currentcolor"; + return ( + + + + + + ); +}; + +export default TimelineLogo; diff --git a/FrontEnd/src/app/views/common/UserTimelineLogo.tsx b/FrontEnd/src/app/views/common/UserTimelineLogo.tsx new file mode 100644 index 00000000..29f6a69f --- /dev/null +++ b/FrontEnd/src/app/views/common/UserTimelineLogo.tsx @@ -0,0 +1,26 @@ +import React, { SVGAttributes } from "react"; + +export interface UserTimelineLogoProps extends SVGAttributes { + color?: string; +} + +const UserTimelineLogo: React.FC = (props) => { + const { color, ...forwardProps } = props; + const coercedColor = color ?? "currentcolor"; + + return ( + + + + + + + + + + + + ); +}; + +export default UserTimelineLogo; diff --git a/FrontEnd/src/app/views/common/alert/AlertHost.tsx b/FrontEnd/src/app/views/common/alert/AlertHost.tsx new file mode 100644 index 00000000..c74f18e2 --- /dev/null +++ b/FrontEnd/src/app/views/common/alert/AlertHost.tsx @@ -0,0 +1,101 @@ +import React, { useCallback } from "react"; +import without from "lodash/without"; +import concat from "lodash/concat"; +import { useTranslation } from "react-i18next"; +import { Alert } from "react-bootstrap"; + +import { + alertService, + AlertInfoEx, + kAlertHostId, + AlertInfo, +} from "@/services/alert"; + +interface AutoCloseAlertProps { + alert: AlertInfo; + close: () => void; +} + +export const AutoCloseAlert: React.FC = (props) => { + const { alert } = props; + const { dismissTime } = alert; + + const { t } = useTranslation(); + + React.useEffect(() => { + const tag = + dismissTime === "never" + ? null + : typeof dismissTime === "number" + ? window.setTimeout(props.close, dismissTime) + : window.setTimeout(props.close, 5000); + return () => { + if (tag != null) { + window.clearTimeout(tag); + } + }; + }, [dismissTime, props.close]); + + return ( + + {(() => { + const { message } = alert; + if (typeof message === "function") { + const Message = message; + return ; + } else if (typeof message === "object" && message.type === "i18n") { + return t(message.key); + } else return alert.message; + })()} + + ); +}; + +// oh what a bad name! +interface AlertInfoExEx extends AlertInfoEx { + close: () => void; +} + +const AlertHost: React.FC = () => { + const [alerts, setAlerts] = React.useState([]); + + // react guarantee that state setters are stable, so we don't need to add it to dependency list + + const consume = useCallback((alert: AlertInfoEx): void => { + const alertEx: AlertInfoExEx = { + ...alert, + close: () => { + setAlerts((oldAlerts) => { + return without(oldAlerts, alertEx); + }); + }, + }; + setAlerts((oldAlerts) => { + return concat(oldAlerts, alertEx); + }); + }, []); + + React.useEffect(() => { + alertService.registerConsumer(consume); + return () => { + alertService.unregisterConsumer(consume); + }; + }, [consume]); + + return ( +
+ {alerts.map((alert) => { + return ( + + ); + })} +
+ ); +}; + +export default AlertHost; diff --git a/FrontEnd/src/app/views/common/alert/alert.sass b/FrontEnd/src/app/views/common/alert/alert.sass new file mode 100644 index 00000000..c3560b87 --- /dev/null +++ b/FrontEnd/src/app/views/common/alert/alert.sass @@ -0,0 +1,15 @@ +.alert-container + position: fixed + z-index: $zindex-popover + +@include media-breakpoint-up(sm) + .alert-container + bottom: 0 + right: 0 + +@include media-breakpoint-down(sm) + .alert-container + bottom: 0 + right: 0 + left: 0 + text-align: center diff --git a/FrontEnd/src/app/views/common/common.sass b/FrontEnd/src/app/views/common/common.sass new file mode 100644 index 00000000..78e6fd14 --- /dev/null +++ b/FrontEnd/src/app/views/common/common.sass @@ -0,0 +1,33 @@ +.image-cropper-container + position: relative + box-sizing: border-box + user-select: none + +.image-cropper-container img + position: absolute + left: 0 + top: 0 + width: 100% + height: 100% + +.image-cropper-mask-container + position: absolute + left: 0 + top: 0 + right: 0 + bottom: 0 + overflow: hidden + +.image-cropper-mask + position: absolute + box-shadow: 0 0 0 10000px rgba(255, 255, 255, 80%) + touch-action: none + +.image-cropper-handler + position: absolute + width: 26px + height: 26px + border: black solid 2px + border-radius: 50% + background: white + touch-action: none diff --git a/FrontEnd/src/app/views/home/BoardWithUser.tsx b/FrontEnd/src/app/views/home/BoardWithUser.tsx new file mode 100644 index 00000000..dcd39cbe --- /dev/null +++ b/FrontEnd/src/app/views/home/BoardWithUser.tsx @@ -0,0 +1,101 @@ +import React from "react"; +import { Row, Col } from "react-bootstrap"; +import { useTranslation } from "react-i18next"; + +import { UserWithToken } from "@/services/user"; +import { TimelineInfo } from "@/services/timeline"; +import { getHttpTimelineClient } from "@/http/timeline"; + +import TimelineBoard from "./TimelineBoard"; +import OfflineBoard from "./OfflineBoard"; + +const BoardWithUser: React.FC<{ user: UserWithToken }> = ({ user }) => { + const { t } = useTranslation(); + + const [ownTimelines, setOwnTimelines] = React.useState< + TimelineInfo[] | "offline" | "loading" + >("loading"); + const [joinTimelines, setJoinTimelines] = React.useState< + TimelineInfo[] | "offline" | "loading" + >("loading"); + + React.useEffect(() => { + let subscribe = true; + if (ownTimelines === "loading") { + void getHttpTimelineClient() + .listTimeline({ relate: user.username, relateType: "own" }) + .then( + (timelines) => { + if (subscribe) { + setOwnTimelines(timelines); + } + }, + () => { + setOwnTimelines("offline"); + } + ); + } + return () => { + subscribe = false; + }; + }, [user, ownTimelines]); + + React.useEffect(() => { + let subscribe = true; + if (joinTimelines === "loading") { + void getHttpTimelineClient() + .listTimeline({ relate: user.username, relateType: "join" }) + .then( + (timelines) => { + if (subscribe) { + setJoinTimelines(timelines); + } + }, + () => { + setJoinTimelines("offline"); + } + ); + } + return () => { + subscribe = false; + }; + }, [user, joinTimelines]); + + return ( + + {ownTimelines === "offline" && joinTimelines === "offline" ? ( + + { + setOwnTimelines("loading"); + setJoinTimelines("loading"); + }} + /> + + ) : ( + <> + + { + setOwnTimelines("loading"); + }} + /> + + + { + setJoinTimelines("loading"); + }} + /> + + + )} + + ); +}; + +export default BoardWithUser; diff --git a/FrontEnd/src/app/views/home/BoardWithoutUser.tsx b/FrontEnd/src/app/views/home/BoardWithoutUser.tsx new file mode 100644 index 00000000..ebfddb50 --- /dev/null +++ b/FrontEnd/src/app/views/home/BoardWithoutUser.tsx @@ -0,0 +1,60 @@ +import React from "react"; +import { Row, Col } from "react-bootstrap"; + +import { TimelineInfo } from "@/services/timeline"; +import { getHttpTimelineClient } from "@/http/timeline"; + +import TimelineBoard from "./TimelineBoard"; +import OfflineBoard from "./OfflineBoard"; + +const BoardWithoutUser: React.FC = () => { + const [publicTimelines, setPublicTimelines] = React.useState< + TimelineInfo[] | "offline" | "loading" + >("loading"); + + React.useEffect(() => { + let subscribe = true; + if (publicTimelines === "loading") { + void getHttpTimelineClient() + .listTimeline({ visibility: "Public" }) + .then( + (timelines) => { + if (subscribe) { + setPublicTimelines(timelines); + } + }, + () => { + setPublicTimelines("offline"); + } + ); + } + return () => { + subscribe = false; + }; + }, [publicTimelines]); + + return ( + + {publicTimelines === "offline" ? ( + + { + setPublicTimelines("loading"); + }} + /> + + ) : ( + + { + setPublicTimelines("loading"); + }} + /> + + )} + + ); +}; + +export default BoardWithoutUser; diff --git a/FrontEnd/src/app/views/home/OfflineBoard.tsx b/FrontEnd/src/app/views/home/OfflineBoard.tsx new file mode 100644 index 00000000..fc05bd74 --- /dev/null +++ b/FrontEnd/src/app/views/home/OfflineBoard.tsx @@ -0,0 +1,61 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import { Trans } from "react-i18next"; + +import { getAllCachedTimelineNames } from "@/services/timeline"; +import UserTimelineLogo from "../common/UserTimelineLogo"; +import TimelineLogo from "../common/TimelineLogo"; + +export interface OfflineBoardProps { + onReload: () => void; +} + +const OfflineBoard: React.FC = ({ onReload }) => { + const [timelines, setTimelines] = React.useState([]); + + React.useEffect(() => { + let subscribe = true; + void getAllCachedTimelineNames().then((t) => { + if (subscribe) setTimelines(t); + }); + return () => { + subscribe = false; + }; + }); + + return ( + <> + + 0 + { + onReload(); + e.preventDefault(); + }} + > + 1 + + 2 + + {timelines.map((timeline) => { + const isPersonal = timeline.startsWith("@"); + const url = isPersonal + ? `/users/${timeline.slice(1)}` + : `/timelines/${timeline}`; + return ( +
+ {isPersonal ? ( + + ) : ( + + )} + {timeline} +
+ ); + })} + + ); +}; + +export default OfflineBoard; diff --git a/FrontEnd/src/app/views/home/TimelineBoard.tsx b/FrontEnd/src/app/views/home/TimelineBoard.tsx new file mode 100644 index 00000000..a3d176e1 --- /dev/null +++ b/FrontEnd/src/app/views/home/TimelineBoard.tsx @@ -0,0 +1,73 @@ +import React from "react"; +import clsx from "clsx"; +import { Link } from "react-router-dom"; +import { Trans } from "react-i18next"; +import { Spinner } from "react-bootstrap"; + +import { TimelineInfo } from "@/services/timeline"; +import TimelineLogo from "../common/TimelineLogo"; +import UserTimelineLogo from "../common/UserTimelineLogo"; + +export interface TimelineBoardProps { + title?: string; + timelines: TimelineInfo[] | "offline" | "loading"; + onReload: () => void; + className?: string; +} + +const TimelineBoard: React.FC = (props) => { + const { title, timelines, className } = props; + + return ( +
+ {title != null &&

{title}

} + {(() => { + if (timelines === "loading") { + return ( +
+ +
+ ); + } else if (timelines === "offline") { + return ( + + ); + } else { + return timelines.map((timeline) => { + const { name } = timeline; + const isPersonal = name.startsWith("@"); + const url = isPersonal + ? `/users/${timeline.owner.username}` + : `/timelines/${name}`; + return ( +
+ {isPersonal ? ( + + ) : ( + + )} + {name} +
+ ); + }); + } + })()} +
+ ); +}; + +export default TimelineBoard; diff --git a/FrontEnd/src/app/views/home/TimelineCreateDialog.tsx b/FrontEnd/src/app/views/home/TimelineCreateDialog.tsx new file mode 100644 index 00000000..d9467719 --- /dev/null +++ b/FrontEnd/src/app/views/home/TimelineCreateDialog.tsx @@ -0,0 +1,53 @@ +import React from "react"; +import { useHistory } from "react-router"; + +import { validateTimelineName, timelineService } from "@/services/timeline"; +import OperationDialog from "../common/OperationDialog"; + +interface TimelineCreateDialogProps { + open: boolean; + close: () => void; +} + +const TimelineCreateDialog: React.FC = (props) => { + const history = useHistory(); + + let nameSaved: string; + + return ( + { + if (name.length === 0) { + return "home.createDialog.noEmpty"; + } else if (name.length > 26) { + return "home.createDialog.tooLong"; + } else if (!validateTimelineName(name)) { + return "home.createDialog.badFormat"; + } else { + return null; + } + }, + }, + ]} + onProcess={([name]) => { + nameSaved = name as string; + return timelineService.createTimeline(nameSaved).toPromise(); + }} + onSuccessAndClose={() => { + history.push(`timelines/${nameSaved}`); + }} + failurePrompt={(e) => `${e as string}`} + /> + ); +}; + +export default TimelineCreateDialog; diff --git a/FrontEnd/src/app/views/home/home.sass b/FrontEnd/src/app/views/home/home.sass new file mode 100644 index 00000000..28a2e5f3 --- /dev/null +++ b/FrontEnd/src/app/views/home/home.sass @@ -0,0 +1,13 @@ +.timeline-board-item + font-size: 1.1em + @extend .my-2 + .icon + height: 1.3em + @extend .mr-2 + +.timeline-board + @extend .cru-card + @extend .d-flex + @extend .flex-column + @extend .p-3 + min-height: 200px diff --git a/FrontEnd/src/app/views/home/index.tsx b/FrontEnd/src/app/views/home/index.tsx new file mode 100644 index 00000000..760adcea --- /dev/null +++ b/FrontEnd/src/app/views/home/index.tsx @@ -0,0 +1,99 @@ +import React from "react"; +import { useHistory } from "react-router"; +import { useTranslation } from "react-i18next"; +import { Row, Container, Button, Col } from "react-bootstrap"; + +import { useUser } from "@/services/user"; +import SearchInput from "../common/SearchInput"; + +import BoardWithoutUser from "./BoardWithoutUser"; +import BoardWithUser from "./BoardWithUser"; +import TimelineCreateDialog from "./TimelineCreateDialog"; + +const HomePage: React.FC = () => { + const history = useHistory(); + + const { t } = useTranslation(); + + const user = useUser(); + + const [navText, setNavText] = React.useState(""); + + const [dialog, setDialog] = React.useState<"create" | null>(null); + + const goto = React.useCallback((): void => { + if (navText === "") { + history.push("users/crupest"); + } else if (navText.startsWith("@")) { + history.push(`users/${navText.slice(1)}`); + } else { + history.push(`timelines/${navText}`); + } + }, [navText, history]); + + return ( + <> + + + + { + setDialog("create"); + }} + > + {t("home.createButton")} + + ) + } + /> + + + {(() => { + if (user == null) { + return ; + } else { + return ; + } + })()} + + + {dialog === "create" && ( + { + setDialog(null); + }} + /> + )} + + ); +}; + +export default HomePage; diff --git a/FrontEnd/src/app/views/login/index.tsx b/FrontEnd/src/app/views/login/index.tsx new file mode 100644 index 00000000..61b9a525 --- /dev/null +++ b/FrontEnd/src/app/views/login/index.tsx @@ -0,0 +1,151 @@ +import React from "react"; +import { useHistory } from "react-router"; +import { useTranslation } from "react-i18next"; +import { Container, Form } from "react-bootstrap"; + +import { useUser, userService } from "@/services/user"; + +import AppBar from "../common/AppBar"; +import LoadingButton from "../common/LoadingButton"; + +const LoginPage: React.FC = (_) => { + const { t } = useTranslation(); + const history = useHistory(); + const [username, setUsername] = React.useState(""); + const [usernameDirty, setUsernameDirty] = React.useState(false); + const [password, setPassword] = React.useState(""); + const [passwordDirty, setPasswordDirty] = React.useState(false); + const [rememberMe, setRememberMe] = React.useState(true); + const [process, setProcess] = React.useState(false); + const [error, setError] = React.useState(null); + + const user = useUser(); + + React.useEffect(() => { + if (user != null) { + const id = setTimeout(() => history.push("/"), 3000); + return () => { + clearTimeout(id); + }; + } + }, [history, user]); + + if (user != null) { + return ( + <> + +

{t("login.alreadyLogin")}

+ + ); + } + + const submit = (): void => { + if (username === "" || password === "") { + setUsernameDirty(true); + setPasswordDirty(true); + return; + } + + setProcess(true); + userService + .login( + { + username: username, + password: password, + }, + rememberMe + ) + .then( + () => { + if (history.length === 0) { + history.push("/"); + } else { + history.goBack(); + } + }, + (e: Error) => { + setProcess(false); + setError(e.message); + } + ); + }; + + const onEnterPressInPassword: React.KeyboardEventHandler = (e) => { + if (e.key === "Enter") { + submit(); + } + }; + + return ( + +

{t("welcome")}

+
+ + {t("user.username")} + { + setUsername(e.target.value); + setUsernameDirty(true); + }} + value={username} + isInvalid={usernameDirty && username === ""} + /> + {usernameDirty && username === "" && ( + + {t("login.emptyUsername")} + + )} + + + {t("user.password")} + { + setPassword(e.target.value); + setPasswordDirty(true); + }} + value={password} + onKeyDown={onEnterPressInPassword} + isInvalid={passwordDirty && password === ""} + /> + {passwordDirty && password === "" && ( + + {t("login.emptyPassword")} + + )} + + + + id="remember-me" + type="checkbox" + checked={rememberMe} + onChange={(e) => { + setRememberMe(e.currentTarget.checked); + }} + label={t("user.rememberMe")} + /> + + {error ?

{t(error)}

: null} +
+ { + submit(); + e.preventDefault(); + }} + disabled={username === "" || password === "" ? true : undefined} + > + {t("user.login")} + +
+
+
+ ); +}; + +export default LoginPage; diff --git a/FrontEnd/src/app/views/login/login.sass b/FrontEnd/src/app/views/login/login.sass new file mode 100644 index 00000000..0bf385f5 --- /dev/null +++ b/FrontEnd/src/app/views/login/login.sass @@ -0,0 +1,2 @@ +.login-container + max-width: 600px diff --git a/FrontEnd/src/app/views/settings/index.tsx b/FrontEnd/src/app/views/settings/index.tsx new file mode 100644 index 00000000..964e7442 --- /dev/null +++ b/FrontEnd/src/app/views/settings/index.tsx @@ -0,0 +1,209 @@ +import React, { useState } from "react"; +import { useHistory } from "react-router"; +import { useTranslation } from "react-i18next"; +import { Form, Container, Row, Col, Button, Modal } from "react-bootstrap"; + +import { useUser, userService } from "@/services/user"; +import OperationDialog, { + OperationInputErrorInfo, +} from "../common/OperationDialog"; + +interface ChangePasswordDialogProps { + open: boolean; + close: () => void; +} + +const ChangePasswordDialog: React.FC = (props) => { + const history = useHistory(); + const { t } = useTranslation(); + + const [redirect, setRedirect] = useState(false); + + return ( + + v === "" + ? "settings.dialogChangePassword.errorEmptyOldPassword" + : null, + }, + { + type: "text", + label: t("settings.dialogChangePassword.inputNewPassword"), + password: true, + validator: (v, values) => { + const error: OperationInputErrorInfo = {}; + error[1] = + v === "" + ? "settings.dialogChangePassword.errorEmptyNewPassword" + : null; + if (v === values[2]) { + error[2] = null; + } else { + if (values[2] !== "") { + error[2] = "settings.dialogChangePassword.errorRetypeNotMatch"; + } + } + return error; + }, + }, + { + type: "text", + label: t("settings.dialogChangePassword.inputRetypeNewPassword"), + password: true, + validator: (v, values) => + v !== values[1] + ? "settings.dialogChangePassword.errorRetypeNotMatch" + : null, + }, + ]} + onProcess={async ([oldPassword, newPassword]) => { + await userService + .changePassword(oldPassword as string, newPassword as string) + .toPromise(); + await userService.logout(); + setRedirect(true); + }} + close={() => { + props.close(); + if (redirect) { + history.push("/login"); + } + }} + /> + ); +}; + +const ConfirmLogoutDialog: React.FC<{ + toggle: () => void; + onConfirm: () => void; +}> = ({ toggle, onConfirm }) => { + const { t } = useTranslation(); + + return ( + + + + {t("settings.dialogConfirmLogout.title")} + + + {t("settings.dialogConfirmLogout.prompt")} + + + + + + ); +}; + +const SettingsPage: React.FC = (_) => { + const { i18n, t } = useTranslation(); + const user = useUser(); + const history = useHistory(); + + const [dialog, setDialog] = useState( + null + ); + + const language = i18n.language.slice(0, 2); + + return ( + + {user ? ( + <> + + +
{ + history.push(`/users/${user.username}`); + }} + > + {t("settings.gotoSelf")} +
+ +
+ + +
setDialog("changepassword")} + > + {t("settings.changePassword")} +
+ +
+ + +
{ + setDialog("logout"); + }} + > + {t("settings.logout")} +
+ +
+ + ) : null} + + +
{t("settings.languagePrimary")}
+

{t("settings.languageSecondary")}

+ + + { + void i18n.changeLanguage(e.target.value); + }} + > + + + + +
+ {(() => { + switch (dialog) { + case "changepassword": + return ( + { + setDialog(null); + }} + /> + ); + case "logout": + return ( + setDialog(null)} + onConfirm={() => { + void userService.logout().then(() => { + history.push("/"); + }); + }} + /> + ); + default: + return null; + } + })()} +
+ ); +}; + +export default SettingsPage; diff --git a/FrontEnd/src/app/views/timeline-common/CollapseButton.tsx b/FrontEnd/src/app/views/timeline-common/CollapseButton.tsx new file mode 100644 index 00000000..3c52150f --- /dev/null +++ b/FrontEnd/src/app/views/timeline-common/CollapseButton.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import clsx from "clsx"; +import Svg from "react-inlinesvg"; +import arrowsAngleContractIcon from "bootstrap-icons/icons/arrows-angle-contract.svg"; +import arrowsAngleExpandIcon from "bootstrap-icons/icons/arrows-angle-expand.svg"; + +const CollapseButton: React.FC<{ + collapse: boolean; + onClick: () => void; + className?: string; + style?: React.CSSProperties; +}> = ({ collapse, onClick, className, style }) => { + return ( + + ); +}; + +export default CollapseButton; diff --git a/FrontEnd/src/app/views/timeline-common/InfoCardTemplate.tsx b/FrontEnd/src/app/views/timeline-common/InfoCardTemplate.tsx new file mode 100644 index 00000000..a8de20aa --- /dev/null +++ b/FrontEnd/src/app/views/timeline-common/InfoCardTemplate.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import clsx from "clsx"; + +import { TimelineCardComponentProps } from "../timeline-common/TimelinePageTemplateUI"; +import SyncStatusBadge from "../timeline-common/SyncStatusBadge"; +import CollapseButton from "../timeline-common/CollapseButton"; + +const InfoCardTemplate: React.FC< + Pick< + TimelineCardComponentProps<"">, + "collapse" | "toggleCollapse" | "syncStatus" | "className" + > & { children: React.ReactElement[] } +> = ({ collapse, toggleCollapse, syncStatus, className, children }) => { + return ( +
+
+ + +
+ +
{children}
+
+ ); +}; + +export default InfoCardTemplate; diff --git a/FrontEnd/src/app/views/timeline-common/SyncStatusBadge.tsx b/FrontEnd/src/app/views/timeline-common/SyncStatusBadge.tsx new file mode 100644 index 00000000..e67cfb43 --- /dev/null +++ b/FrontEnd/src/app/views/timeline-common/SyncStatusBadge.tsx @@ -0,0 +1,58 @@ +import React from "react"; +import clsx from "clsx"; +import { useTranslation } from "react-i18next"; + +import { UiLogicError } from "@/common"; + +export type TimelineSyncStatus = "syncing" | "synced" | "offline"; + +const SyncStatusBadge: React.FC<{ + status: TimelineSyncStatus; + style?: React.CSSProperties; + className?: string; +}> = ({ status, style, className }) => { + const { t } = useTranslation(); + + return ( +
+ {(() => { + switch (status) { + case "syncing": { + return ( + <> + + + {t("timeline.postSyncState.syncing")} + + + ); + } + case "synced": { + return ( + <> + + + {t("timeline.postSyncState.synced")} + + + ); + } + case "offline": { + return ( + <> + + + {t("timeline.postSyncState.offline")} + + + ); + } + default: + throw new UiLogicError("Unknown sync state."); + } + })()} +
+ ); +}; + +export default SyncStatusBadge; diff --git a/FrontEnd/src/app/views/timeline-common/Timeline.tsx b/FrontEnd/src/app/views/timeline-common/Timeline.tsx new file mode 100644 index 00000000..fd051d45 --- /dev/null +++ b/FrontEnd/src/app/views/timeline-common/Timeline.tsx @@ -0,0 +1,84 @@ +import React from "react"; +import clsx from "clsx"; + +import { TimelinePostInfo } from "@/services/timeline"; + +import TimelineItem from "./TimelineItem"; + +export interface TimelinePostInfoEx extends TimelinePostInfo { + deletable: boolean; +} + +export type TimelineDeleteCallback = (index: number, id: number) => void; + +export interface TimelineProps { + className?: string; + posts: TimelinePostInfoEx[]; + onDelete: TimelineDeleteCallback; + onResize?: () => void; + containerRef?: React.Ref; +} + +const Timeline: React.FC = (props) => { + const { posts, onDelete, onResize } = props; + + const [indexShowDeleteButton, setIndexShowDeleteButton] = React.useState< + number + >(-1); + + const onItemClick = React.useCallback(() => { + setIndexShowDeleteButton(-1); + }, []); + + const onToggleDelete = React.useMemo(() => { + return posts.map((post, i) => { + return post.deletable + ? () => { + setIndexShowDeleteButton((oldIndexShowDeleteButton) => { + return oldIndexShowDeleteButton !== i ? i : -1; + }); + } + : undefined; + }); + }, [posts]); + + const onItemDelete = React.useMemo(() => { + return posts.map((post, i) => { + return () => { + onDelete(i, post.id); + }; + }); + }, [posts, onDelete]); + + return ( +
+ {(() => { + const length = posts.length; + return posts.map((post, i) => { + const toggleMore = onToggleDelete[i]; + + return ( + + ); + }); + })()} +
+ ); +}; + +export default Timeline; diff --git a/FrontEnd/src/app/views/timeline-common/TimelineItem.tsx b/FrontEnd/src/app/views/timeline-common/TimelineItem.tsx new file mode 100644 index 00000000..4db23371 --- /dev/null +++ b/FrontEnd/src/app/views/timeline-common/TimelineItem.tsx @@ -0,0 +1,172 @@ +import React from "react"; +import clsx from "clsx"; +import { Link } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import Svg from "react-inlinesvg"; +import chevronDownIcon from "bootstrap-icons/icons/chevron-down.svg"; +import trashIcon from "bootstrap-icons/icons/trash.svg"; +import { Modal, Button } from "react-bootstrap"; + +import { useAvatar } from "@/services/user"; +import { TimelinePostInfo } from "@/services/timeline"; + +import BlobImage from "../common/BlobImage"; + +const TimelinePostDeleteConfirmDialog: React.FC<{ + toggle: () => void; + onConfirm: () => void; +}> = ({ toggle, onConfirm }) => { + const { t } = useTranslation(); + + return ( + + + + {t("timeline.post.deleteDialog.title")} + + + {t("timeline.post.deleteDialog.prompt")} + + + + + + ); +}; + +export interface TimelineItemProps { + post: TimelinePostInfo; + current?: boolean; + more?: { + isOpen: boolean; + toggle: () => void; + onDelete: () => void; + }; + onClick?: () => void; + onResize?: () => void; + className?: string; + style?: React.CSSProperties; +} + +const TimelineItem: React.FC = (props) => { + const { i18n } = useTranslation(); + + const current = props.current === true; + + const { more, onResize } = props; + + const avatar = useAvatar(props.post.author.username); + + const [deleteDialog, setDeleteDialog] = React.useState(false); + const toggleDeleteDialog = React.useCallback( + () => setDeleteDialog((old) => !old), + [] + ); + + return ( +
+
+
+
+
+
+
+
+ {current &&
} +
+
+
+
+ + + {props.post.time.toLocaleString(i18n.languages)} + + {props.post.author.nickname} + + {more != null ? ( + { + more.toggle(); + e.stopPropagation(); + }} + /> + ) : null} +
+
+ + + + {(() => { + const { content } = props.post; + if (content.type === "text") { + return content.text; + } else { + return ( + + ); + } + })()} +
+
+ {more != null && more.isOpen ? ( + <> +
+ { + toggleDeleteDialog(); + e.stopPropagation(); + }} + /> +
+ {deleteDialog ? ( + { + toggleDeleteDialog(); + more.toggle(); + }} + onConfirm={more.onDelete} + /> + ) : null} + + ) : null} +
+ ); +}; + +export default TimelineItem; diff --git a/FrontEnd/src/app/views/timeline-common/TimelineMember.tsx b/FrontEnd/src/app/views/timeline-common/TimelineMember.tsx new file mode 100644 index 00000000..67a8543a --- /dev/null +++ b/FrontEnd/src/app/views/timeline-common/TimelineMember.tsx @@ -0,0 +1,211 @@ +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Container, ListGroup, Modal, Row, Col, Button } from "react-bootstrap"; + +import { User, useAvatar } from "@/services/user"; + +import SearchInput from "../common/SearchInput"; +import BlobImage from "../common/BlobImage"; + +const TimelineMemberItem: React.FC<{ + user: User; + owner: boolean; + onRemove?: (username: string) => void; +}> = ({ user, owner, onRemove }) => { + const { t } = useTranslation(); + + const avatar = useAvatar(user.username); + + return ( + + + + + + + {user.nickname} + + {"@" + user.username} + + + {(() => { + if (owner) { + return null; + } + if (onRemove == null) { + return null; + } + return ( + + ); + })()} + + + ); +}; + +export interface TimelineMemberCallbacks { + onCheckUser: (username: string) => Promise; + onAddUser: (user: User) => Promise; + onRemoveUser: (username: string) => void; +} + +export interface TimelineMemberProps { + members: User[]; + edit: TimelineMemberCallbacks | null | undefined; +} + +const TimelineMember: React.FC = (props) => { + const { t } = useTranslation(); + + const [userSearchText, setUserSearchText] = useState(""); + const [userSearchState, setUserSearchState] = useState< + | { + type: "user"; + data: User; + } + | { type: "error"; data: string } + | { type: "loading" } + | { type: "init" } + >({ type: "init" }); + + const userSearchAvatar = useAvatar( + userSearchState.type === "user" ? userSearchState.data.username : undefined + ); + + const members = props.members; + + return ( + + + {members.map((member, index) => ( + + ))} + + {(() => { + const edit = props.edit; + if (edit != null) { + return ( + <> + { + setUserSearchText(v); + }} + loading={userSearchState.type === "loading"} + onButtonClick={() => { + if (userSearchText === "") { + setUserSearchState({ + type: "error", + data: "login.emptyUsername", + }); + return; + } + + setUserSearchState({ type: "loading" }); + edit.onCheckUser(userSearchText).then( + (u) => { + if (u == null) { + setUserSearchState({ + type: "error", + data: "timeline.userNotExist", + }); + } else { + setUserSearchState({ type: "user", data: u }); + } + }, + (e) => { + setUserSearchState({ + type: "error", + data: `${e as string}`, + }); + } + ); + }} + /> + {(() => { + if (userSearchState.type === "user") { + const u = userSearchState.data; + const addable = + members.findIndex((m) => m.username === u.username) === -1; + return ( + <> + {!addable ? ( +

{t("timeline.member.alreadyMember")}

+ ) : null} + + + + + + + {u.nickname} + + {"@" + u.username} + + + + + + + ); + } else if (userSearchState.type === "error") { + return ( +

{t(userSearchState.data)}

+ ); + } + })()} + + ); + } else { + return null; + } + })()} +
+ ); +}; + +export default TimelineMember; + +export interface TimelineMemberDialogProps extends TimelineMemberProps { + open: boolean; + onClose: () => void; +} + +export const TimelineMemberDialog: React.FC = ( + props +) => { + return ( + + + + ); +}; diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx new file mode 100644 index 00000000..d5c91622 --- /dev/null +++ b/FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx @@ -0,0 +1,185 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { of } from "rxjs"; +import { catchError } from "rxjs/operators"; + +import { UiLogicError } from "@/common"; +import { pushAlert } from "@/services/alert"; +import { useUser, userInfoService, UserNotExistError } from "@/services/user"; +import { + timelineService, + usePostList, + useTimelineInfo, +} from "@/services/timeline"; + +import { TimelineDeleteCallback } from "./Timeline"; +import { TimelineMemberDialog } from "./TimelineMember"; +import TimelinePropertyChangeDialog from "./TimelinePropertyChangeDialog"; +import { TimelinePageTemplateUIProps } from "./TimelinePageTemplateUI"; +import { TimelinePostSendCallback } from "./TimelinePostEdit"; + +export interface TimelinePageTemplateProps { + name: string; + onManage: (item: TManageItem) => void; + UiComponent: React.ComponentType< + Omit, "CardComponent"> + >; + notFoundI18nKey: string; +} + +export default function TimelinePageTemplate( + props: TimelinePageTemplateProps +): React.ReactElement | null { + const { t } = useTranslation(); + + const { name } = props; + + const service = timelineService; + + const user = useUser(); + + const [dialog, setDialog] = React.useState( + null + ); + + const timelineState = useTimelineInfo(name); + + const timeline = timelineState?.timeline; + + const postListState = usePostList(name); + + const error: string | undefined = (() => { + if (timelineState != null) { + const { type, timeline } = timelineState; + if (type === "offline" && timeline == null) return "Network Error"; + if (type === "synced" && timeline == null) + return t(props.notFoundI18nKey); + } + return undefined; + })(); + + const closeDialog = React.useCallback((): void => { + setDialog(null); + }, []); + + let dialogElement: React.ReactElement | undefined; + + if (dialog === "property") { + if (timeline == null) { + throw new UiLogicError( + "Timeline is null but attempt to open change property dialog." + ); + } + + dialogElement = ( + { + return service.changeTimelineProperty(name, req).toPromise().then(); + }} + /> + ); + } else if (dialog === "member") { + if (timeline == null) { + throw new UiLogicError( + "Timeline is null but attempt to open change property dialog." + ); + } + + dialogElement = ( + { + return userInfoService + .getUserInfo(u) + .pipe( + catchError((e) => { + if (e instanceof UserNotExistError) { + return of(null); + } else { + throw e; + } + }) + ) + .toPromise(); + }, + onAddUser: (u) => { + return service.addMember(name, u.username).toPromise().then(); + }, + onRemoveUser: (u) => { + service.removeMember(name, u); + }, + } + : null + } + /> + ); + } + + const { UiComponent } = props; + + const onDelete: TimelineDeleteCallback = React.useCallback( + (index, id) => { + service.deletePost(name, id).subscribe(null, () => { + pushAlert({ + type: "danger", + message: t("timeline.deletePostFailed"), + }); + }); + }, + [service, name, t] + ); + + const onPost: TimelinePostSendCallback = React.useCallback( + (req) => { + return service.createPost(name, req).toPromise().then(); + }, + [service, name] + ); + + const onManageProp = props.onManage; + + const onManage = React.useCallback( + (item: "property" | TManageItem) => { + if (item === "property") { + setDialog(item); + } else { + onManageProp(item); + } + }, + [onManageProp] + ); + + return ( + <> + setDialog("member")} + /> + {dialogElement} + + ); +} diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePageTemplateUI.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePageTemplateUI.tsx new file mode 100644 index 00000000..6c2c43c1 --- /dev/null +++ b/FrontEnd/src/app/views/timeline-common/TimelinePageTemplateUI.tsx @@ -0,0 +1,243 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { fromEvent } from "rxjs"; +import { Spinner } from "react-bootstrap"; + +import { getAlertHost } from "@/services/alert"; +import { useEventEmiiter, UiLogicError } from "@/common"; +import { + TimelineInfo, + TimelinePostsWithSyncState, + timelineService, +} from "@/services/timeline"; +import { userService } from "@/services/user"; + +import Timeline, { + TimelinePostInfoEx, + TimelineDeleteCallback, +} from "./Timeline"; +import TimelineTop from "./TimelineTop"; +import TimelinePostEdit, { TimelinePostSendCallback } from "./TimelinePostEdit"; +import { TimelineSyncStatus } from "./SyncStatusBadge"; + +export interface TimelineCardComponentProps { + timeline: TimelineInfo; + onManage?: (item: TManageItems | "property") => void; + onMember: () => void; + className?: string; + collapse: boolean; + syncStatus: TimelineSyncStatus; + toggleCollapse: () => void; +} + +export interface TimelinePageTemplateUIProps { + timeline?: TimelineInfo; + postListState?: TimelinePostsWithSyncState; + CardComponent: React.ComponentType>; + onMember: () => void; + onManage?: (item: TManageItems | "property") => void; + onPost?: TimelinePostSendCallback; + onDelete: TimelineDeleteCallback; + error?: string; +} + +export default function TimelinePageTemplateUI( + props: TimelinePageTemplateUIProps +): React.ReactElement | null { + const { timeline, postListState } = props; + + const { t } = useTranslation(); + + const bottomSpaceRef = React.useRef(null); + + const onPostEditHeightChange = React.useCallback((height: number): void => { + const { current: bottomSpaceDiv } = bottomSpaceRef; + if (bottomSpaceDiv != null) { + bottomSpaceDiv.style.height = `${height}px`; + } + if (height === 0) { + const alertHost = getAlertHost(); + if (alertHost != null) { + alertHost.style.removeProperty("margin-bottom"); + } + } else { + const alertHost = getAlertHost(); + if (alertHost != null) { + alertHost.style.marginBottom = `${height}px`; + } + } + }, []); + + const timelineRef = React.useRef(null); + + const [getResizeEvent, triggerResizeEvent] = useEventEmiiter(); + + React.useEffect(() => { + const { current: timelineElement } = timelineRef; + if (timelineElement != null) { + let loadingScrollToBottom = true; + let pinBottom = false; + + const isAtBottom = (): boolean => + window.innerHeight + window.scrollY + 10 >= document.body.scrollHeight; + + const disableLoadingScrollToBottom = (): void => { + loadingScrollToBottom = false; + if (isAtBottom()) pinBottom = true; + }; + + const checkAndScrollToBottom = (): void => { + if (loadingScrollToBottom || pinBottom) { + window.scrollTo(0, document.body.scrollHeight); + } + }; + + const subscriptions = [ + fromEvent(timelineElement, "wheel").subscribe( + disableLoadingScrollToBottom + ), + fromEvent(timelineElement, "pointerdown").subscribe( + disableLoadingScrollToBottom + ), + fromEvent(timelineElement, "keydown").subscribe( + disableLoadingScrollToBottom + ), + fromEvent(window, "scroll").subscribe(() => { + if (loadingScrollToBottom) return; + + if (isAtBottom()) { + pinBottom = true; + } else { + pinBottom = false; + } + }), + fromEvent(window, "resize").subscribe(checkAndScrollToBottom), + getResizeEvent().subscribe(checkAndScrollToBottom), + ]; + + return () => { + subscriptions.forEach((s) => s.unsubscribe()); + }; + } + }, [getResizeEvent, triggerResizeEvent, timeline, postListState]); + + const genCardCollapseLocalStorageKey = (uniqueId: string): string => + `timeline.${uniqueId}.cardCollapse`; + + const cardCollapseLocalStorageKey = + timeline != null ? genCardCollapseLocalStorageKey(timeline.uniqueId) : null; + + const [cardCollapse, setCardCollapse] = React.useState(true); + React.useEffect(() => { + if (cardCollapseLocalStorageKey != null) { + const savedCollapse = + window.localStorage.getItem(cardCollapseLocalStorageKey) === "true"; + setCardCollapse(savedCollapse); + } + }, [cardCollapseLocalStorageKey]); + + const toggleCardCollapse = (): void => { + const newState = !cardCollapse; + setCardCollapse(newState); + if (timeline != null) { + window.localStorage.setItem( + genCardCollapseLocalStorageKey(timeline.uniqueId), + newState.toString() + ); + } + }; + + let body: React.ReactElement; + + if (props.error != null) { + body =

{t(props.error)}

; + } else { + if (timeline != null) { + let timelineBody: React.ReactElement; + if (postListState != null) { + if (postListState.type === "notexist") { + throw new UiLogicError( + "Timeline is not null but post list state is notexist." + ); + } + if (postListState.type === "forbid") { + timelineBody = ( +

{t("timeline.messageCantSee")}

+ ); + } else { + const posts: TimelinePostInfoEx[] = postListState.posts.map( + (post) => ({ + ...post, + deletable: timelineService.hasModifyPostPermission( + userService.currentUser, + timeline, + post + ), + }) + ); + + timelineBody = ( + + ); + if (props.onPost != null) { + timelineBody = ( + <> + {timelineBody} +
+ + + ); + } + } + } else { + timelineBody = ( +
+ +
+ ); + } + + const { CardComponent } = props; + const syncStatus: TimelineSyncStatus = + postListState == null || postListState.syncing + ? "syncing" + : postListState.type === "synced" + ? "synced" + : "offline"; + + body = ( + <> + + + {timelineBody} + + ); + } else { + body = ( +
+ +
+ ); + } + } + + return body; +} diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx new file mode 100644 index 00000000..dfa2f879 --- /dev/null +++ b/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx @@ -0,0 +1,241 @@ +import React from "react"; +import clsx from "clsx"; +import { useTranslation } from "react-i18next"; +import Svg from "react-inlinesvg"; +import { Button, Spinner, Row, Col, Form } from "react-bootstrap"; +import textIcon from "bootstrap-icons/icons/card-text.svg"; +import imageIcon from "bootstrap-icons/icons/image.svg"; + +import { UiLogicError } from "@/common"; + +import { pushAlert } from "@/services/alert"; +import { TimelineCreatePostRequest } from "@/services/timeline"; + +interface TimelinePostEditImageProps { + onSelect: (blob: Blob | null) => void; +} + +const TimelinePostEditImage: React.FC = (props) => { + const { onSelect } = props; + const { t } = useTranslation(); + + const [file, setFile] = React.useState(null); + const [fileUrl, setFileUrl] = React.useState(null); + const [error, setError] = React.useState(null); + + React.useEffect(() => { + if (file != null) { + const url = URL.createObjectURL(file); + setFileUrl(url); + return () => { + URL.revokeObjectURL(url); + }; + } + }, [file]); + + const onInputChange: React.ChangeEventHandler = React.useCallback( + (e) => { + const files = e.target.files; + if (files == null || files.length === 0) { + setFile(null); + setFileUrl(null); + } else { + setFile(files[0]); + } + onSelect(null); + setError(null); + }, + [onSelect] + ); + + const onImgLoad = React.useCallback(() => { + onSelect(file); + }, [onSelect, file]); + + const onImgError = React.useCallback(() => { + setError("loadImageError"); + }, []); + + return ( + <> + + {fileUrl && error == null && ( + + )} + {error != null &&
{t(error)}
} + + ); +}; + +export type TimelinePostSendCallback = ( + content: TimelineCreatePostRequest +) => Promise; + +export interface TimelinePostEditProps { + className?: string; + onPost: TimelinePostSendCallback; + onHeightChange?: (height: number) => void; + timelineUniqueId: string; +} + +const TimelinePostEdit: React.FC = (props) => { + const { onPost } = props; + + const { t } = useTranslation(); + + const [state, setState] = React.useState<"input" | "process">("input"); + const [kind, setKind] = React.useState<"text" | "image">("text"); + const [text, setText] = React.useState(""); + const [imageBlob, setImageBlob] = React.useState(null); + + const draftLocalStorageKey = `timeline.${props.timelineUniqueId}.postDraft`; + + React.useEffect(() => { + setText(window.localStorage.getItem(draftLocalStorageKey) ?? ""); + }, [draftLocalStorageKey]); + + const canSend = kind === "text" || (kind === "image" && imageBlob != null); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const containerRef = React.useRef(null!); + + const notifyHeightChange = (): void => { + if (props.onHeightChange) { + props.onHeightChange(containerRef.current.clientHeight); + } + }; + + React.useEffect(() => { + if (props.onHeightChange) { + props.onHeightChange(containerRef.current.clientHeight); + } + return () => { + if (props.onHeightChange) { + props.onHeightChange(0); + } + }; + }); + + const toggleKind = React.useCallback(() => { + setKind((oldKind) => (oldKind === "text" ? "image" : "text")); + setImageBlob(null); + }, []); + + const onSend = React.useCallback(() => { + setState("process"); + + const req: TimelineCreatePostRequest = (() => { + switch (kind) { + case "text": + return { + content: { + type: "text", + text: text, + }, + } as TimelineCreatePostRequest; + case "image": + if (imageBlob == null) { + throw new UiLogicError( + "Content type is image but image blob is null." + ); + } + return { + content: { + type: "image", + data: imageBlob, + }, + } as TimelineCreatePostRequest; + default: + throw new UiLogicError("Unknown content type."); + } + })(); + + onPost(req).then( + (_) => { + if (kind === "text") { + setText(""); + window.localStorage.removeItem(draftLocalStorageKey); + } + setState("input"); + setKind("text"); + }, + (_) => { + pushAlert({ + type: "danger", + message: t("timeline.sendPostFailed"), + }); + setState("input"); + } + ); + }, [onPost, kind, text, imageBlob, t, draftLocalStorageKey]); + + const onImageSelect = React.useCallback((blob: Blob | null) => { + setImageBlob(blob); + }, []); + + return ( +
+ + + {kind === "text" ? ( + ) => { + const value = event.currentTarget.value; + setText(value); + window.localStorage.setItem(draftLocalStorageKey, value); + }} + /> + ) : ( + + )} + + + {(() => { + if (state === "input") { + return ( + <> +
+ +
+ + + ); + } else { + return ; + } + })()} + +
+
+ ); +}; + +export default TimelinePostEdit; diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePropertyChangeDialog.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePropertyChangeDialog.tsx new file mode 100644 index 00000000..87638f31 --- /dev/null +++ b/FrontEnd/src/app/views/timeline-common/TimelinePropertyChangeDialog.tsx @@ -0,0 +1,72 @@ +import React from "react"; + +import { + TimelineVisibility, + kTimelineVisibilities, + TimelineChangePropertyRequest, +} from "@/services/timeline"; + +import OperationDialog, { + OperationSelectInputInfoOption, +} from "../common/OperationDialog"; + +export interface TimelinePropertyInfo { + visibility: TimelineVisibility; + description: string; +} + +export interface TimelinePropertyChangeDialogProps { + open: boolean; + close: () => void; + oldInfo: TimelinePropertyInfo; + onProcess: (request: TimelineChangePropertyRequest) => Promise; +} + +const labelMap: { [key in TimelineVisibility]: string } = { + Private: "timeline.visibility.private", + Public: "timeline.visibility.public", + Register: "timeline.visibility.register", +}; + +const TimelinePropertyChangeDialog: React.FC = ( + props +) => { + return ( + ( + (v) => ({ + label: labelMap[v], + value: v, + }) + ), + initValue: props.oldInfo.visibility, + }, + { + type: "text", + label: "timeline.dialogChangeProperty.description", + initValue: props.oldInfo.description, + }, + ]} + open={props.open} + close={props.close} + onProcess={([newVisibility, newDescription]) => { + const req: TimelineChangePropertyRequest = {}; + if (newVisibility !== props.oldInfo.visibility) { + req.visibility = newVisibility as TimelineVisibility; + } + if (newDescription !== props.oldInfo.description) { + req.description = newDescription as string; + } + return props.onProcess(req); + }} + /> + ); +}; + +export default TimelinePropertyChangeDialog; diff --git a/FrontEnd/src/app/views/timeline-common/TimelineTop.tsx b/FrontEnd/src/app/views/timeline-common/TimelineTop.tsx new file mode 100644 index 00000000..93a2a32c --- /dev/null +++ b/FrontEnd/src/app/views/timeline-common/TimelineTop.tsx @@ -0,0 +1,21 @@ +import React from "react"; + +export interface TimelineTopProps { + height?: number | string; + children?: React.ReactElement; +} + +const TimelineTop: React.FC = ({ height, children }) => { + return ( +
+
+
+
+
+
+ {children} +
+ ); +}; + +export default TimelineTop; diff --git a/FrontEnd/src/app/views/timeline-common/timeline-common.sass b/FrontEnd/src/app/views/timeline-common/timeline-common.sass new file mode 100644 index 00000000..4151bfcc --- /dev/null +++ b/FrontEnd/src/app/views/timeline-common/timeline-common.sass @@ -0,0 +1,146 @@ +@use 'sass:color' + +.timeline + z-index: 0 + position: relative + + &-item + display: flex + +$timeline-line-width: 7px +$timeline-line-node-radius: 18px +$timeline-line-color: $primary +$timeline-line-color-current: #36c2e6 + +@keyframes timeline-line-node-noncurrent + from + background: $timeline-line-color + + to + background: color.adjust($timeline-line-color, $lightness: +10%) + box-shadow: 0 0 20px 3px color.adjust($timeline-line-color, $lightness: +10%, $alpha: -0.1) + +@keyframes timeline-line-node-current + from + background: $timeline-line-color-current + + to + background: color.adjust($timeline-line-color-current, $lightness: +10%) + box-shadow: 0 0 20px 3px color.adjust($timeline-line-color-current, $lightness: +10%, $alpha: -0.1) + +.timeline-line + &-area-container + display: flex + justify-content: flex-end + padding-right: 5px + + flex: 0 0 auto + width: 60px + + &-area + display: flex + flex-direction: column + align-items: center + width: 30px + + &-segment + width: $timeline-line-width + background: $timeline-line-color + + &.start + height: 14px + flex: 0 0 auto + + &.end + flex: 1 1 auto + + &.current-end + height: 20px + flex: 0 0 auto + background: linear-gradient($timeline-line-color-current, transparent) + + &-node-container + flex: 0 0 auto + position: relative + width: $timeline-line-node-radius + height: $timeline-line-node-radius + + &-node + width: $timeline-line-node-radius + 2 + height: $timeline-line-node-radius + 2 + position: absolute + left: -1px + top: -1px + border-radius: 50% + box-sizing: border-box + z-index: 1 + animation: 1s infinite alternate + animation-name: timeline-line-node-noncurrent + +.timeline-top + display: flex + justify-content: space-between + + .timeline-line-segment + flex: 1 1 auto + +.current + .timeline-line + &-segment + + &.start + background: linear-gradient($timeline-line-color, $timeline-line-color-current) + + &.end + background: $timeline-line-color-current + + &-node + animation-name: timeline-line-node-current + +.timeline-content-area + padding: 10px 0 + flex-grow: 1 + +.timeline-item-delete-button + position: absolute + right: 0 + bottom: 0 + +.timeline-content + white-space: pre-line + +.timeline-content-image + max-width: 60% + max-height: 200px + +.timeline-post-edit-image + max-width: 100px + max-height: 100px + +.mask + background: change-color($color: white, $alpha: 0.8) + z-index: 100 + +.timeline-page-top-space + transition: height 0.5s + +.timeline-sync-state-badge + font-size: 0.8em + padding: 3px 8px + border-radius: 5px + background: #e8fbff + +.timeline-sync-state-badge-pin + display: inline-block + width: 0.4em + height: 0.4em + border-radius: 50% + vertical-align: middle + margin-right: 0.6em + +.timeline-template-card + position: fixed + z-index: 1 + top: 56px + right: 0 + margin: 0.5em diff --git a/FrontEnd/src/app/views/timeline/TimelineDeleteDialog.tsx b/FrontEnd/src/app/views/timeline/TimelineDeleteDialog.tsx new file mode 100644 index 00000000..894b8195 --- /dev/null +++ b/FrontEnd/src/app/views/timeline/TimelineDeleteDialog.tsx @@ -0,0 +1,55 @@ +import React from "react"; +import { useHistory } from "react-router"; +import { Trans } from "react-i18next"; + +import { timelineService } from "@/services/timeline"; + +import OperationDialog from "../common/OperationDialog"; + +interface TimelineDeleteDialog { + open: boolean; + name: string; + close: () => void; +} + +const TimelineDeleteDialog: React.FC = (props) => { + const history = useHistory(); + + const { name } = props; + + return ( + { + return ( + + 0{{ name }}2 + + ); + }} + inputScheme={[ + { + type: "text", + validator: (value) => { + if (value !== name) { + return "timeline.deleteDialog.notMatch"; + } else { + return null; + } + }, + }, + ]} + onProcess={() => { + return timelineService.deleteTimeline(name).toPromise(); + }} + onSuccessAndClose={() => { + history.replace("/"); + }} + /> + ); +}; + +export default TimelineDeleteDialog; diff --git a/FrontEnd/src/app/views/timeline/TimelineInfoCard.tsx b/FrontEnd/src/app/views/timeline/TimelineInfoCard.tsx new file mode 100644 index 00000000..2d787709 --- /dev/null +++ b/FrontEnd/src/app/views/timeline/TimelineInfoCard.tsx @@ -0,0 +1,85 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { Dropdown, Button } from "react-bootstrap"; + +import { useAvatar } from "@/services/user"; +import { timelineVisibilityTooltipTranslationMap } from "@/services/timeline"; + +import BlobImage from "../common/BlobImage"; +import { TimelineCardComponentProps } from "../timeline-common/TimelinePageTemplateUI"; +import InfoCardTemplate from "../timeline-common/InfoCardTemplate"; + +export type OrdinaryTimelineManageItem = "delete"; + +export type TimelineInfoCardProps = TimelineCardComponentProps< + OrdinaryTimelineManageItem +>; + +const TimelineInfoCard: React.FC = (props) => { + const { + timeline, + collapse, + onMember, + onManage, + syncStatus, + toggleCollapse, + } = props; + + const { t } = useTranslation(); + + const avatar = useAvatar(timeline?.owner?.username); + + return ( + +

+ {timeline.name} +

+
+ + {timeline.owner.nickname} + + @{timeline.owner.username} + +
+

{timeline.description}

+ + {t(timelineVisibilityTooltipTranslationMap[timeline.visibility])} + +
+ {onManage != null ? ( + + + {t("timeline.manage")} + + + onManage("property")}> + {t("timeline.manageItem.property")} + + + {t("timeline.manageItem.member")} + + + onManage("delete")} + > + {t("timeline.manageItem.delete")} + + + + ) : ( + + )} +
+
+ ); +}; + +export default TimelineInfoCard; diff --git a/FrontEnd/src/app/views/timeline/TimelinePageUI.tsx b/FrontEnd/src/app/views/timeline/TimelinePageUI.tsx new file mode 100644 index 00000000..67ea699e --- /dev/null +++ b/FrontEnd/src/app/views/timeline/TimelinePageUI.tsx @@ -0,0 +1,20 @@ +import React from "react"; + +import TimelinePageTemplateUI, { + TimelinePageTemplateUIProps, +} from "../timeline-common/TimelinePageTemplateUI"; + +import TimelineInfoCard, { + OrdinaryTimelineManageItem, +} from "./TimelineInfoCard"; + +export type TimelinePageUIProps = Omit< + TimelinePageTemplateUIProps, + "CardComponent" +>; + +const TimelinePageUI: React.FC = (props) => { + return ; +}; + +export default TimelinePageUI; diff --git a/FrontEnd/src/app/views/timeline/index.tsx b/FrontEnd/src/app/views/timeline/index.tsx new file mode 100644 index 00000000..225a1a59 --- /dev/null +++ b/FrontEnd/src/app/views/timeline/index.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import { useParams } from "react-router"; + +import TimelinePageTemplate from "../timeline-common/TimelinePageTemplate"; + +import TimelinePageUI from "./TimelinePageUI"; +import { OrdinaryTimelineManageItem } from "./TimelineInfoCard"; +import TimelineDeleteDialog from "./TimelineDeleteDialog"; + +const TimelinePage: React.FC = (_) => { + const { name } = useParams<{ name: string }>(); + + const [dialog, setDialog] = React.useState( + null + ); + + let dialogElement: React.ReactElement | undefined; + if (dialog === "delete") { + dialogElement = ( + setDialog(null)} name={name} /> + ); + } + + return ( + <> + setDialog(item)} + notFoundI18nKey="timeline.timelineNotExist" + /> + {dialogElement} + + ); +}; + +export default TimelinePage; diff --git a/FrontEnd/src/app/views/timeline/timeline.sass b/FrontEnd/src/app/views/timeline/timeline.sass new file mode 100644 index 00000000..e69de29b diff --git a/FrontEnd/src/app/views/user/ChangeAvatarDialog.tsx b/FrontEnd/src/app/views/user/ChangeAvatarDialog.tsx new file mode 100644 index 00000000..ffa2218b --- /dev/null +++ b/FrontEnd/src/app/views/user/ChangeAvatarDialog.tsx @@ -0,0 +1,302 @@ +import React, { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { AxiosError } from "axios"; +import { Modal, Row, Button } from "react-bootstrap"; + +import { UiLogicError } from "@/common"; + +import ImageCropper, { Clip, applyClipToImage } from "../common/ImageCropper"; + +export interface ChangeAvatarDialogProps { + open: boolean; + close: () => void; + process: (blob: Blob) => Promise; +} + +const ChangeAvatarDialog: React.FC = (props) => { + const { t } = useTranslation(); + + const [file, setFile] = React.useState(null); + const [fileUrl, setFileUrl] = React.useState(null); + const [clip, setClip] = React.useState(null); + const [ + cropImgElement, + setCropImgElement, + ] = React.useState(null); + const [resultBlob, setResultBlob] = React.useState(null); + const [resultUrl, setResultUrl] = React.useState(null); + + const [state, setState] = React.useState< + | "select" + | "crop" + | "processcrop" + | "preview" + | "uploading" + | "success" + | "error" + >("select"); + + const [message, setMessage] = useState< + string | { type: "custom"; text: string } | null + >("userPage.dialogChangeAvatar.prompt.select"); + + const trueMessage = + message == null + ? null + : typeof message === "string" + ? t(message) + : message.text; + + const closeDialog = props.close; + + const close = React.useCallback((): void => { + if (!(state === "uploading")) { + closeDialog(); + } + }, [state, closeDialog]); + + useEffect(() => { + if (file != null) { + const url = URL.createObjectURL(file); + setClip(null); + setFileUrl(url); + setState("crop"); + return () => { + URL.revokeObjectURL(url); + }; + } else { + setFileUrl(null); + setState("select"); + } + }, [file]); + + React.useEffect(() => { + if (resultBlob != null) { + const url = URL.createObjectURL(resultBlob); + setResultUrl(url); + setState("preview"); + return () => { + URL.revokeObjectURL(url); + }; + } else { + setResultUrl(null); + } + }, [resultBlob]); + + const onSelectFile = React.useCallback( + (e: React.ChangeEvent): void => { + const files = e.target.files; + if (files == null || files.length === 0) { + setFile(null); + } else { + setFile(files[0]); + } + }, + [] + ); + + const onCropNext = React.useCallback(() => { + if ( + cropImgElement == null || + clip == null || + clip.width === 0 || + file == null + ) { + throw new UiLogicError(); + } + + setState("processcrop"); + void applyClipToImage(cropImgElement, clip, file.type).then((b) => { + setResultBlob(b); + }); + }, [cropImgElement, clip, file]); + + const onCropPrevious = React.useCallback(() => { + setFile(null); + setState("select"); + }, []); + + const onPreviewPrevious = React.useCallback(() => { + setResultBlob(null); + setState("crop"); + }, []); + + const process = props.process; + + const upload = React.useCallback(() => { + if (resultBlob == null) { + throw new UiLogicError(); + } + + setState("uploading"); + process(resultBlob).then( + () => { + setState("success"); + }, + (e: unknown) => { + setState("error"); + setMessage({ type: "custom", text: (e as AxiosError).message }); + } + ); + }, [resultBlob, process]); + + const createPreviewRow = (): React.ReactElement => { + if (resultUrl == null) { + throw new UiLogicError(); + } + return ( + + {t("userPage.dialogChangeAvatar.previewImgAlt")} + + ); + }; + + return ( + + + {t("userPage.dialogChangeAvatar.title")} + + {(() => { + if (state === "select") { + return ( + <> + + {t("userPage.dialogChangeAvatar.prompt.select")} + + + + + + + + + ); + } else if (state === "crop") { + if (fileUrl == null) { + throw new UiLogicError(); + } + return ( + <> + + + + + {t("userPage.dialogChangeAvatar.prompt.crop")} + + + + + + + + ); + } else if (state === "processcrop") { + return ( + <> + + + {t("userPage.dialogChangeAvatar.prompt.processingCrop")} + + + + + + + + ); + } else if (state === "preview") { + return ( + <> + + {createPreviewRow()} + {t("userPage.dialogChangeAvatar.prompt.preview")} + + + + + + + + ); + } else if (state === "uploading") { + return ( + <> + + {createPreviewRow()} + {t("userPage.dialogChangeAvatar.prompt.uploading")} + + + + ); + } else if (state === "success") { + return ( + <> + + + {t("operationDialog.success")} + + + + + + + ); + } else { + return ( + <> + + {createPreviewRow()} + {trueMessage} + + + + + + + ); + } + })()} + + ); +}; + +export default ChangeAvatarDialog; diff --git a/FrontEnd/src/app/views/user/ChangeNicknameDialog.tsx b/FrontEnd/src/app/views/user/ChangeNicknameDialog.tsx new file mode 100644 index 00000000..251b18c5 --- /dev/null +++ b/FrontEnd/src/app/views/user/ChangeNicknameDialog.tsx @@ -0,0 +1,28 @@ +import React from "react"; + +import OperationDialog from "../common/OperationDialog"; + +export interface ChangeNicknameDialogProps { + open: boolean; + close: () => void; + onProcess: (newNickname: string) => Promise; +} + +const ChangeNicknameDialog: React.FC = (props) => { + return ( + { + return props.onProcess(newNickname as string); + }} + close={props.close} + /> + ); +}; + +export default ChangeNicknameDialog; diff --git a/FrontEnd/src/app/views/user/UserInfoCard.tsx b/FrontEnd/src/app/views/user/UserInfoCard.tsx new file mode 100644 index 00000000..888fb18a --- /dev/null +++ b/FrontEnd/src/app/views/user/UserInfoCard.tsx @@ -0,0 +1,80 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { Dropdown, Button } from "react-bootstrap"; + +import { timelineVisibilityTooltipTranslationMap } from "@/services/timeline"; +import { useAvatar } from "@/services/user"; + +import BlobImage from "../common/BlobImage"; +import { TimelineCardComponentProps } from "../timeline-common/TimelinePageTemplateUI"; +import InfoCardTemplate from "../timeline-common/InfoCardTemplate"; + +export type PersonalTimelineManageItem = "avatar" | "nickname"; + +export type UserInfoCardProps = TimelineCardComponentProps< + PersonalTimelineManageItem +>; + +const UserInfoCard: React.FC = (props) => { + const { + timeline, + collapse, + onMember, + onManage, + syncStatus, + toggleCollapse, + } = props; + const { t } = useTranslation(); + + const avatar = useAvatar(timeline?.owner?.username); + + return ( + +
+ + {timeline.owner.nickname} + + @{timeline.owner.username} + +
+

{timeline.description}

+ + {t(timelineVisibilityTooltipTranslationMap[timeline.visibility])} + +
+ {onManage != null ? ( + + + {t("timeline.manage")} + + + onManage("nickname")}> + {t("timeline.manageItem.nickname")} + + onManage("avatar")}> + {t("timeline.manageItem.avatar")} + + onManage("property")}> + {t("timeline.manageItem.property")} + + + {t("timeline.manageItem.member")} + + + + ) : ( + + )} +
+
+ ); +}; + +export default UserInfoCard; diff --git a/FrontEnd/src/app/views/user/UserPageUI.tsx b/FrontEnd/src/app/views/user/UserPageUI.tsx new file mode 100644 index 00000000..d405399c --- /dev/null +++ b/FrontEnd/src/app/views/user/UserPageUI.tsx @@ -0,0 +1,18 @@ +import React from "react"; + +import TimelinePageTemplateUI, { + TimelinePageTemplateUIProps, +} from "../timeline-common/TimelinePageTemplateUI"; + +import UserInfoCard, { PersonalTimelineManageItem } from "./UserInfoCard"; + +export type UserPageUIProps = Omit< + TimelinePageTemplateUIProps, + "CardComponent" +>; + +const UserPageUI: React.FC = (props) => { + return ; +}; + +export default UserPageUI; diff --git a/FrontEnd/src/app/views/user/index.tsx b/FrontEnd/src/app/views/user/index.tsx new file mode 100644 index 00000000..7c0b1563 --- /dev/null +++ b/FrontEnd/src/app/views/user/index.tsx @@ -0,0 +1,72 @@ +import React, { useState } from "react"; +import { useParams } from "react-router"; + +import { UiLogicError } from "@/common"; +import { useUser, userInfoService } from "@/services/user"; + +import TimelinePageTemplate from "../timeline-common/TimelinePageTemplate"; + +import UserPageUI from "./UserPageUI"; +import { PersonalTimelineManageItem } from "./UserInfoCard"; +import ChangeNicknameDialog from "./ChangeNicknameDialog"; +import ChangeAvatarDialog from "./ChangeAvatarDialog"; + +const UserPage: React.FC = (_) => { + const { username } = useParams<{ username: string }>(); + + const user = useUser(); + + const [dialog, setDialog] = useState(null); + + let dialogElement: React.ReactElement | undefined; + + const closeDialogHandler = (): void => { + setDialog(null); + }; + + if (dialog === "nickname") { + if (user == null) { + throw new UiLogicError("Change nickname without login."); + } + + dialogElement = ( + + userInfoService.setNickname(username, newNickname) + } + /> + ); + } else if (dialog === "avatar") { + if (user == null) { + throw new UiLogicError("Change avatar without login."); + } + + dialogElement = ( + userInfoService.setAvatar(username, file)} + /> + ); + } + + const onManage = React.useCallback((item: PersonalTimelineManageItem) => { + setDialog(item); + }, []); + + return ( + <> + + {dialogElement} + + ); +}; + +export default UserPage; diff --git a/FrontEnd/src/app/views/user/user.sass b/FrontEnd/src/app/views/user/user.sass new file mode 100644 index 00000000..63a28e05 --- /dev/null +++ b/FrontEnd/src/app/views/user/user.sass @@ -0,0 +1,7 @@ +.change-avatar-cropper-row + max-height: 400px + +.change-avatar-img + min-width: 50% + max-width: 100% + max-height: 400px diff --git a/FrontEnd/src/sw/sw.ts b/FrontEnd/src/sw/sw.ts new file mode 100644 index 00000000..d6202f36 --- /dev/null +++ b/FrontEnd/src/sw/sw.ts @@ -0,0 +1,28 @@ +import { precacheAndRoute, matchPrecache } from "workbox-precaching"; +import { setDefaultHandler } from "workbox-routing"; +import { NetworkOnly } from "workbox-strategies"; + +declare let self: ServiceWorkerGlobalScope; + +self.addEventListener("message", (event) => { + if (event.data && (event.data as { type: string }).type === "SKIP_WAITING") { + void self.skipWaiting(); + } +}); + +precacheAndRoute(self.__WB_MANIFEST); + +const networkOnly = new NetworkOnly(); + +setDefaultHandler((options) => { + const { request, url } = options; + if (url && url.pathname.startsWith("/api/")) { + return networkOnly.handle(options); + } + + if (request instanceof Request && request.destination === "document") + return matchPrecache("/index.html").then((r) => + r == null ? Response.error() : r + ); + else return networkOnly.handle(options); +}); diff --git a/FrontEnd/src/sw/tsconfig.json b/FrontEnd/src/sw/tsconfig.json new file mode 100644 index 00000000..71fc0bcd --- /dev/null +++ b/FrontEnd/src/sw/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "lib": [ + "esnext", + "webworker" + ] + }, + "include": [ + "." + ] +} diff --git a/FrontEnd/src/tsconfig.json b/FrontEnd/src/tsconfig.json new file mode 100644 index 00000000..1855f5cd --- /dev/null +++ b/FrontEnd/src/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "esnext", + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "sourceMap": true, + "baseUrl": "./", + "paths": { + "@/*": [ + "app/*" + ] + } + } +} diff --git a/FrontEnd/webpack.common.js b/FrontEnd/webpack.common.js new file mode 100644 index 00000000..3779003e --- /dev/null +++ b/FrontEnd/webpack.common.js @@ -0,0 +1,86 @@ +const path = require("path"); +const HtmlWebpackPlugin = require("html-webpack-plugin"); +const postcssPresetEnv = require("postcss-preset-env"); +const Config = require("webpack-chain"); + +const config = new Config(); + +config.entry("index").add(path.resolve(__dirname, "src/app/index.tsx")); + +config.module + .rule("ts") + .test(/\.ts(x?)$/) + .exclude.add(/node_modules/) + .end() + .use("babel") + .loader("babel-loader") + .end() + .use("ts") + .loader("ts-loader") + .end(); + +config.module + .rule("js") + .test(/\.js(x?)$/) + .exclude.add(/node_modules/) + .end() + .use("babel") + .loader("babel-loader") + .end(); + +config.module + .rule("css") + .test(/\.css$/) + .use("css") + .loader("css-loader") + .end() + .use("postcss") + .loader("postcss-loader") + .end(); + +config.module + .rule("sass") + .test(/\.(scss|sass)$/) + .use("css") + .loader("css-loader") + .end() + .use("postcss") + .loader("postcss-loader") + .end() + .use("sass") + .loader("sass-loader") + .end(); + +config.module + .rule("file") + .test(/\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot)$/i) + .use("url") + .loader("url-loader") + .options({ + limit: 8192, + }); + +config.resolve.extensions + .add("*") + .add(".js") + .add(".jsx") + .add(".ts") + .add(".tsx") + .end(); + +config.resolve.alias.set("@", path.resolve(__dirname, "src/app")); + +config.output + .path(path.resolve(__dirname, "dist/")) + .filename("[name].[contenthash].js") + .chunkFilename("[name].[contenthash].js") + .publicPath("/"); + +config.plugin("html").use(HtmlWebpackPlugin, [ + { + template: "src/app/index.ejs", + title: "Timeline", + }, +]); + +module.exports = config; diff --git a/FrontEnd/webpack.config.dev.js b/FrontEnd/webpack.config.dev.js new file mode 100644 index 00000000..c88e1aaf --- /dev/null +++ b/FrontEnd/webpack.config.dev.js @@ -0,0 +1,52 @@ +const path = require("path"); +const webpack = require("webpack"); + +const config = require("./webpack.common"); + +config.mode("development"); + +config.entry("index").add("react-hot-loader/patch"); + +config.module + .rule("ts") + .use("babel") + .options({ + plugins: ["react-hot-loader/babel"], + }); + +config.module + .rule("js") + .use("babel") + .options({ + plugins: ["react-hot-loader/babel"], + }); + +config.module + .rule("css") + .use("style") + .before("css") + .loader("style-loader") + .end(); + +config.module + .rule("sass") + .use("style") + .before("css") + .loader("style-loader") + .end(); + +config.devtool("eval-cheap-module-source-map"); + +config.resolve.alias.set("react-dom", "@hot-loader/react-dom"); + +config.devServer + .contentBase(path.resolve(__dirname, "public/")) + .host("0.0.0.0") + .port(3000) + .historyApiFallback(true) + .hotOnly(true) + .allowedHosts.add(".myide.io"); + +config.plugin("hot").use(webpack.HotModuleReplacementPlugin); + +module.exports = config.toConfig(); diff --git a/FrontEnd/webpack.config.prod.js b/FrontEnd/webpack.config.prod.js new file mode 100644 index 00000000..188cb940 --- /dev/null +++ b/FrontEnd/webpack.config.prod.js @@ -0,0 +1,53 @@ +const path = require("path"); +const { CleanWebpackPlugin } = require("clean-webpack-plugin"); +const CopyPlugin = require("copy-webpack-plugin"); +const WorkboxPlugin = require("workbox-webpack-plugin"); +const MiniCssExtractPlugin = require("mini-css-extract-plugin"); + +const config = require("./webpack.common"); + +config.mode("production"); + +config + .entry("index") + .add(path.resolve(__dirname, "src/app/service-worker.tsx")); + +config.module + .rule("css") + .use("mini-css-extract") + .before("css") + .loader(MiniCssExtractPlugin.loader) + .end(); + +config.module + .rule("sass") + .use("mini-css-extract") + .before("css") + .loader(MiniCssExtractPlugin.loader) + .end(); + +config.devtool("source-map"); + +config.plugin("mini-css-extract").use(MiniCssExtractPlugin); + +config.plugin("clean").use(CleanWebpackPlugin); + +config.plugin("copy").use(CopyPlugin, [ + { + patterns: [ + { + from: path.resolve(__dirname, "public/"), + to: path.resolve(__dirname, "dist/"), + }, + ], + }, +]); + +config.plugin("workbox").use(WorkboxPlugin.InjectManifest, [ + { + swSrc: path.resolve(__dirname, "src/sw/sw.ts"), + maximumFileSizeToCacheInBytes: 15000000, + }, +]); + +module.exports = config.toConfig(); diff --git a/Nuget.Config b/Nuget.Config deleted file mode 100644 index e219de2b..00000000 --- a/Nuget.Config +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/Timeline.ErrorCodes.CodeGenerator/Program.cs b/Timeline.ErrorCodes.CodeGenerator/Program.cs deleted file mode 100644 index 84ab5908..00000000 --- a/Timeline.ErrorCodes.CodeGenerator/Program.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; -using System.Text; - -namespace Timeline.ErrorCodes.CodeGenerator -{ - class Program - { - static void Main(string[] args) - { - string Indent(int n) - { - const string indent = " "; - return string.Concat(Enumerable.Repeat(indent, n)); - } - - StringBuilder code = new StringBuilder(); - - code.AppendLine("using static Timeline.Resources.Messages;"); - code.AppendLine(); - code.AppendLine("namespace Timeline.Models.Http"); - code.AppendLine("{"); - - int depth = 1; - - void RecursiveAddErrorCode(Type type, bool root) - { - code.AppendLine($"{Indent(depth)}public static class {(root ? "ErrorResponse" : type.Name)}"); - code.AppendLine($"{Indent(depth)}{{"); - - foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy) - .Where(fi => fi.IsLiteral && !fi.IsInitOnly && fi.FieldType == typeof(int))) - { - var path = type.FullName.Replace("+", ".").Replace("Timeline.Models.Http.ErrorCodes.", "") + "." + field.Name; - - code.AppendLine($"{Indent(depth + 1)}public static CommonResponse {field.Name}(params object?[] formatArgs)"); - code.AppendLine($"{Indent(depth + 1)}{{"); - code.AppendLine($"{Indent(depth + 2)}return new CommonResponse({"ErrorCodes." + path}, string.Format({path.Replace(".", "_")}, formatArgs));"); - code.AppendLine($"{Indent(depth + 1)}}}"); - code.AppendLine(); - code.AppendLine($"{Indent(depth + 1)}public static CommonResponse CustomMessage_{field.Name}(string message, params object?[] formatArgs)"); - code.AppendLine($"{Indent(depth + 1)}{{"); - code.AppendLine($"{Indent(depth + 2)}return new CommonResponse({"ErrorCodes." + path}, string.Format(message, formatArgs));"); - code.AppendLine($"{Indent(depth + 1)}}}"); - code.AppendLine(); - } - - depth += 1; - - foreach (var nestedType in type.GetNestedTypes()) - { - RecursiveAddErrorCode(nestedType, false); - } - - depth -= 1; - - code.AppendLine($"{Indent(depth)}}}"); - code.AppendLine(); - } - - RecursiveAddErrorCode(typeof(Timeline.Models.Http.ErrorCodes), true); - - code.AppendLine("}"); - - var generatedCode = code.ToString(); - - Console.WriteLine(generatedCode); - - TextCopy.ClipboardService.SetText(generatedCode); - var oldColor = Console.ForegroundColor; - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine("Code has copied to clipboard!"); - Console.ForegroundColor = oldColor; - } - } -} diff --git a/Timeline.ErrorCodes.CodeGenerator/Timeline.ErrorCodes.CodeGenerator.csproj b/Timeline.ErrorCodes.CodeGenerator/Timeline.ErrorCodes.CodeGenerator.csproj deleted file mode 100644 index c8eb97f3..00000000 --- a/Timeline.ErrorCodes.CodeGenerator/Timeline.ErrorCodes.CodeGenerator.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - - Exe - netcoreapp3.1 - - - - - - - - - - - diff --git a/Timeline.ErrorCodes.CodeGenerator/packages.lock.json b/Timeline.ErrorCodes.CodeGenerator/packages.lock.json deleted file mode 100644 index 69cfee1e..00000000 --- a/Timeline.ErrorCodes.CodeGenerator/packages.lock.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "version": 1, - "dependencies": { - ".NETCoreApp,Version=v3.1": { - "TextCopy": { - "type": "Direct", - "requested": "[4.2.0, )", - "resolved": "4.2.0", - "contentHash": "NY2UAFIjBJj+3aABP5tyO6ooEdkJxIGtwRNqvMQKLmyIeZiyGvM4XYbkKNntyQlhyFhhfBww05C3D/0DdimfaQ==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.4" - } - }, - "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "Transitive", - "resolved": "3.1.4", - "contentHash": "AceHamXNKDMDwIoZqEoApLp8s3935wSC3VXrPaRWa0wWOaEcYdDlo1nWQ1zLiezoDmpJzV7FqDm53E0Ty/hEMg==" - }, - "timeline.errorcodes": { - "type": "Project" - } - } - } -} \ No newline at end of file diff --git a/Timeline.ErrorCodes/ErrorCodes.cs b/Timeline.ErrorCodes/ErrorCodes.cs deleted file mode 100644 index 91e0c1fd..00000000 --- a/Timeline.ErrorCodes/ErrorCodes.cs +++ /dev/null @@ -1,66 +0,0 @@ -namespace Timeline.Models.Http -{ - /// - /// All error code constants. - /// - /// - /// Format: 1bbbccdd - /// - public static class ErrorCodes - { - public static class Common - { - public const int InvalidModel = 1_000_0001; - public const int Forbid = 1_000_0002; - public const int UnknownEndpoint = 1_000_0003; - - public static class Header - { - public const int IfNonMatch_BadFormat = 1_000_01_01; - } - - public static class Content - { - public const int TooBig = 1_000_11_01; - } - } - - public static class UserCommon - { - public const int NotExist = 1_001_0001; - } - - public static class TokenController - { - public const int Create_BadCredential = 1_101_01_01; - public const int Verify_BadFormat = 1_101_02_01; - public const int Verify_UserNotExist = 1_101_02_02; - public const int Verify_OldVersion = 1_101_02_03; - public const int Verify_TimeExpired = 1_101_02_04; - } - - public static class UserController - { - public const int UsernameConflict = 1_102_01_01; - public const int ChangePassword_BadOldPassword = 1_102_02_01; - } - - public static class UserAvatar - { - public const int BadFormat_CantDecode = 1_103_00_01; - public const int BadFormat_UnmatchedFormat = 1_103_00_02; - public const int BadFormat_BadSize = 1_103_00_03; - } - - public static class TimelineController - { - public const int NameConflict = 1_104_01_01; - public const int NotExist = 1_104_02_01; - public const int MemberPut_NotExist = 1_104_03_01; - public const int QueryRelateNotExist = 1_104_04_01; - public const int PostNotExist = 1_104_05_01; - public const int PostNoData = 1_104_05_02; - } - } -} - diff --git a/Timeline.ErrorCodes/Timeline.ErrorCodes.csproj b/Timeline.ErrorCodes/Timeline.ErrorCodes.csproj deleted file mode 100644 index 01ca2568..00000000 --- a/Timeline.ErrorCodes/Timeline.ErrorCodes.csproj +++ /dev/null @@ -1,7 +0,0 @@ - - - - netcoreapp3.1 - - - diff --git a/Timeline.ErrorCodes/packages.lock.json b/Timeline.ErrorCodes/packages.lock.json deleted file mode 100644 index dabf86bc..00000000 --- a/Timeline.ErrorCodes/packages.lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "version": 1, - "dependencies": { - ".NETCoreApp,Version=v3.1": {} - } -} \ No newline at end of file diff --git a/Timeline.Tests/ErrorCodeTest.cs b/Timeline.Tests/ErrorCodeTest.cs deleted file mode 100644 index 258ebf4e..00000000 --- a/Timeline.Tests/ErrorCodeTest.cs +++ /dev/null @@ -1,53 +0,0 @@ -using FluentAssertions; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Timeline.Models.Http; -using Xunit; -using Xunit.Abstractions; - -namespace Timeline.Tests -{ - public class ErrorCodeTest - { - private readonly ITestOutputHelper _output; - - public ErrorCodeTest(ITestOutputHelper output) - { - _output = output; - } - - [Fact] - public void ShouldWork() - { - var errorCodes = new Dictionary(); - - void RecursiveCheckErrorCode(Type type) - { - foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy) - .Where(fi => fi.IsLiteral && !fi.IsInitOnly && fi.FieldType == typeof(int))) - { - var name = type.FullName + "." + field.Name; - var value = (int)field.GetRawConstantValue(); - _output.WriteLine($"Find error code {name} , value is {value}."); - - value.Should().BeInRange(1000_0000, 9999_9999, "Error code should have exactly 8 digits."); - - errorCodes.Should().NotContainKey(value, - "identical error codes are found and conflict paths are {0} and {1}", - name, errorCodes.GetValueOrDefault(value)); - - errorCodes.Add(value, name); - } - - foreach (var nestedType in type.GetNestedTypes()) - { - RecursiveCheckErrorCode(nestedType); - } - } - - RecursiveCheckErrorCode(typeof(ErrorCodes)); - } - } -} diff --git a/Timeline.Tests/GlobalSuppressions.cs b/Timeline.Tests/GlobalSuppressions.cs deleted file mode 100644 index 0f873033..00000000 --- a/Timeline.Tests/GlobalSuppressions.cs +++ /dev/null @@ -1,16 +0,0 @@ -// This file is used by Code Analysis to maintain SuppressMessage -// attributes that are applied to this project. -// Project-level suppressions either have no target or are given -// a specific target and scoped to a namespace, type, member, etc. - -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "This is not a UI application.")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Tests name have underscores.")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Test may catch all exceptions.")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "Test classes can be nested.")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "This is redundant.")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1063:Implement IDisposable Correctly", Justification = "Test classes do not need to implement it that way.")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize", Justification = "Test classes do not need to implement it that way.")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2234:Pass system uri objects instead of strings", Justification = "I really don't understand this rule.")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "Tests do not need make strings resources.")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1054:Uri parameters should not be strings", Justification = "That's unnecessary.")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1056:Uri properties should not be strings", Justification = "That's unnecessary.")] diff --git a/Timeline.Tests/Helpers/AsyncFunctionAssertionsExtensions.cs b/Timeline.Tests/Helpers/AsyncFunctionAssertionsExtensions.cs deleted file mode 100644 index b78309c0..00000000 --- a/Timeline.Tests/Helpers/AsyncFunctionAssertionsExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -using FluentAssertions; -using FluentAssertions.Primitives; -using FluentAssertions.Specialized; -using System; -using System.Threading.Tasks; - -namespace Timeline.Tests.Helpers -{ - public static class AsyncFunctionAssertionsExtensions - { - public static async Task> ThrowAsync(this AsyncFunctionAssertions assertions, Type exceptionType, string because = "", params object[] becauseArgs) - { - return (await assertions.ThrowAsync(because, becauseArgs)).Which.Should().BeAssignableTo(exceptionType); - } - } -} diff --git a/Timeline.Tests/Helpers/CacheTestHelper.cs b/Timeline.Tests/Helpers/CacheTestHelper.cs deleted file mode 100644 index b3709a28..00000000 --- a/Timeline.Tests/Helpers/CacheTestHelper.cs +++ /dev/null @@ -1,64 +0,0 @@ -using FluentAssertions; -using System; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading.Tasks; -using Timeline.Models.Http; - -namespace Timeline.Tests.Helpers -{ - public static class CacheTestHelper - { - public static async Task TestCache(HttpClient client, string getUrl) - { - EntityTagHeaderValue eTag; - { - var res = await client.GetAsync(getUrl); - res.Should().HaveStatusCode(200); - var cacheControlHeader = res.Headers.CacheControl; - cacheControlHeader.NoCache.Should().BeTrue(); - cacheControlHeader.NoStore.Should().BeFalse(); - cacheControlHeader.Private.Should().BeTrue(); - cacheControlHeader.Public.Should().BeFalse(); - cacheControlHeader.MustRevalidate.Should().BeTrue(); - cacheControlHeader.MaxAge.Should().NotBeNull().And.Be(TimeSpan.FromDays(14)); - eTag = res.Headers.ETag; - } - - { - using var request = new HttpRequestMessage() - { - RequestUri = new Uri(client.BaseAddress, getUrl), - Method = HttpMethod.Get, - }; - request.Headers.TryAddWithoutValidation("If-None-Match", "\"dsdfd"); - var res = await client.SendAsync(request); - res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.HaveCommonBody(ErrorCodes.Common.Header.IfNonMatch_BadFormat); - } - - { - using var request = new HttpRequestMessage() - { - RequestUri = new Uri(client.BaseAddress, getUrl), - Method = HttpMethod.Get, - }; - request.Headers.TryAddWithoutValidation("If-None-Match", "\"aaa\""); - var res = await client.SendAsync(request); - res.Should().HaveStatusCode(HttpStatusCode.OK); - } - - { - using var request = new HttpRequestMessage() - { - RequestUri = new Uri(client.BaseAddress, getUrl), - Method = HttpMethod.Get, - }; - request.Headers.Add("If-None-Match", eTag.ToString()); - var res = await client.SendAsync(request); - res.Should().HaveStatusCode(HttpStatusCode.NotModified); - } - } - } -} diff --git a/Timeline.Tests/Helpers/HttpClientExtensions.cs b/Timeline.Tests/Helpers/HttpClientExtensions.cs deleted file mode 100644 index 6513bbe7..00000000 --- a/Timeline.Tests/Helpers/HttpClientExtensions.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Net.Mime; -using System.Text; -using System.Threading.Tasks; - -namespace Timeline.Tests.Helpers -{ - public static class HttpClientExtensions - { - public static Task PatchAsJsonAsync(this HttpClient client, string url, T body) - { - return client.PatchAsJsonAsync(new Uri(url, UriKind.RelativeOrAbsolute), body); - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")] - public static Task PatchAsJsonAsync(this HttpClient client, Uri url, T body) - { - return client.PatchAsync(url, new StringContent( - JsonConvert.SerializeObject(body), Encoding.UTF8, MediaTypeNames.Application.Json)); - } - - public static Task PutByteArrayAsync(this HttpClient client, string url, byte[] body, string mimeType) - { - return client.PutByteArrayAsync(new Uri(url, UriKind.RelativeOrAbsolute), body, mimeType); - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")] - public static Task PutByteArrayAsync(this HttpClient client, Uri url, byte[] body, string mimeType) - { - var content = new ByteArrayContent(body); - content.Headers.ContentLength = body.Length; - content.Headers.ContentType = new MediaTypeHeaderValue(mimeType); - return client.PutAsync(url, content); - } - - public static Task PutStringAsync(this HttpClient client, string url, string body, string mimeType = null) - { - return client.PutStringAsync(new Uri(url, UriKind.RelativeOrAbsolute), body, mimeType); - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")] - public static Task PutStringAsync(this HttpClient client, Uri url, string body, string mimeType = null) - { - var content = new StringContent(body, Encoding.UTF8, mimeType ?? MediaTypeNames.Text.Plain); - return client.PutAsync(url, content); - } - } -} diff --git a/Timeline.Tests/Helpers/HttpResponseExtensions.cs b/Timeline.Tests/Helpers/HttpResponseExtensions.cs deleted file mode 100644 index 2bd497f1..00000000 --- a/Timeline.Tests/Helpers/HttpResponseExtensions.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Net.Http; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading.Tasks; -using Timeline.Models.Converters; -using Timeline.Models.Http; - -namespace Timeline.Tests.Helpers -{ - public static class HttpResponseExtensions - { - public static JsonSerializerOptions JsonSerializerOptions { get; } - - static HttpResponseExtensions() - { - JsonSerializerOptions = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }; - JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); - JsonSerializerOptions.Converters.Add(new JsonDateTimeConverter()); - } - - public static async Task ReadBodyAsJsonAsync(this HttpResponseMessage response) - { - var stream = await response.Content.ReadAsStreamAsync(); - return await JsonSerializer.DeserializeAsync(stream, JsonSerializerOptions); - } - - public static Task ReadBodyAsCommonResponseAsync(this HttpResponseMessage response) - { - return response.ReadBodyAsJsonAsync(); - } - } -} diff --git a/Timeline.Tests/Helpers/ImageHelper.cs b/Timeline.Tests/Helpers/ImageHelper.cs deleted file mode 100644 index 9bed0917..00000000 --- a/Timeline.Tests/Helpers/ImageHelper.cs +++ /dev/null @@ -1,26 +0,0 @@ -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.PixelFormats; -using System.IO; - -namespace Timeline.Tests.Helpers -{ - public static class ImageHelper - { - public static byte[] CreatePngWithSize(int width, int height) - { - using var image = new Image(width, height); - using var stream = new MemoryStream(); - image.SaveAsPng(stream); - return stream.ToArray(); - } - - public static byte[] CreateImageWithSize(int width, int height, IImageFormat format) - { - using var image = new Image(width, height); - using var stream = new MemoryStream(); - image.Save(stream, format); - return stream.ToArray(); - } - } -} diff --git a/Timeline.Tests/Helpers/ParameterInfoAssertions.cs b/Timeline.Tests/Helpers/ParameterInfoAssertions.cs deleted file mode 100644 index d3e5a41e..00000000 --- a/Timeline.Tests/Helpers/ParameterInfoAssertions.cs +++ /dev/null @@ -1,60 +0,0 @@ -using FluentAssertions; -using FluentAssertions.Execution; -using FluentAssertions.Formatting; -using FluentAssertions.Primitives; -using System; -using System.Reflection; - -namespace Timeline.Tests.Helpers -{ - public class ParameterInfoValueFormatter : IValueFormatter - { - public bool CanHandle(object value) - { - return value is ParameterInfo; - } - - public string Format(object value, FormattingContext context, FormatChild formatChild) - { - var param = (ParameterInfo)value; - return $"{param.Member.DeclaringType.FullName}.{param.Member.Name}#{param.Name}"; - } - } - - public class ParameterInfoAssertions : ReferenceTypeAssertions - { - static ParameterInfoAssertions() - { - Formatter.AddFormatter(new ParameterInfoValueFormatter()); - } - - public ParameterInfoAssertions(ParameterInfo parameterInfo) - { - Subject = parameterInfo; - } - - protected override string Identifier => "parameter"; - - public AndWhichConstraint BeDecoratedWith(string because = "", params object[] becauseArgs) - where TAttribute : Attribute - { - var attribute = Subject.GetCustomAttribute(false); - - Execute.Assertion - .BecauseOf(because, becauseArgs) - .ForCondition(attribute != null) - .FailWith("Expected {0} {1} to be decorated with {2}{reason}, but that attribute was not found.", - Identifier, Subject, typeof(TAttribute).FullName); - - return new AndWhichConstraint(this, attribute); - } - } - - public static class ParameterInfoAssertionExtensions - { - public static ParameterInfoAssertions Should(this ParameterInfo parameterInfo) - { - return new ParameterInfoAssertions(parameterInfo); - } - } -} diff --git a/Timeline.Tests/Helpers/ReflectionHelper.cs b/Timeline.Tests/Helpers/ReflectionHelper.cs deleted file mode 100644 index 3f6036e3..00000000 --- a/Timeline.Tests/Helpers/ReflectionHelper.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Linq; -using System.Reflection; - -namespace Timeline.Tests.Helpers -{ - public static class ReflectionHelper - { - public static ParameterInfo GetParameter(this MethodInfo methodInfo, string name) - { - return methodInfo.GetParameters().Where(p => p.Name == name).Single(); - } - } -} diff --git a/Timeline.Tests/Helpers/ResponseAssertions.cs b/Timeline.Tests/Helpers/ResponseAssertions.cs deleted file mode 100644 index 024732f5..00000000 --- a/Timeline.Tests/Helpers/ResponseAssertions.cs +++ /dev/null @@ -1,172 +0,0 @@ -using FluentAssertions; -using FluentAssertions.Execution; -using FluentAssertions.Formatting; -using FluentAssertions.Primitives; -using System; -using System.Globalization; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; -using Timeline.Models.Converters; -using Timeline.Models.Http; - -namespace Timeline.Tests.Helpers -{ - public class HttpResponseMessageValueFormatter : IValueFormatter - { - public bool CanHandle(object value) - { - return value is HttpResponseMessage; - } - - public string Format(object value, FormattingContext context, FormatChild formatChild) - { - string newline = context.UseLineBreaks ? Environment.NewLine : ""; - string padding = new string('\t', context.Depth); - - var res = (HttpResponseMessage)value; - - var builder = new StringBuilder(); - builder.Append($"{newline}{padding} Status Code: {res.StatusCode} ; Body: "); - - try - { - var task = res.Content.ReadAsStringAsync(); - task.Wait(); - var body = task.Result; - if (body.Length > 40) - { - body = body[0..40] + " ..."; - } - builder.Append(body); - } - catch (AggregateException) - { - builder.Append("NOT A STRING."); - } - - return builder.ToString(); - } - } - - public class HttpResponseMessageAssertions - : ReferenceTypeAssertions - { - static HttpResponseMessageAssertions() - { - Formatter.AddFormatter(new HttpResponseMessageValueFormatter()); - } - - public HttpResponseMessageAssertions(HttpResponseMessage instance) - { - Subject = instance; - } - - protected override string Identifier => "HttpResponseMessage"; - - public AndConstraint HaveStatusCode(int expected, string because = "", params object[] becauseArgs) - { - return HaveStatusCode((HttpStatusCode)expected, because, becauseArgs); - } - - public AndConstraint HaveStatusCode(HttpStatusCode expected, string because = "", params object[] becauseArgs) - { - Execute.Assertion.BecauseOf(because, becauseArgs) - .ForCondition(Subject.StatusCode == expected) - .FailWith("Expected status code of {context:HttpResponseMessage} to be {0}{reason}, but found {1}.", expected, Subject.StatusCode); - return new AndConstraint(this); - } - - public AndWhichConstraint HaveJsonBody(string because = "", params object[] becauseArgs) - { - var a = Execute.Assertion.BecauseOf(because, becauseArgs); - string body; - try - { - var task = Subject.Content.ReadAsStringAsync(); - task.Wait(); - body = task.Result; - } - catch (AggregateException e) - { - a.FailWith("Expected response body of {context:HttpResponseMessage} to be json string{reason}, but failed to read it or it was not a string. Exception is {0}.", e.InnerExceptions); - return new AndWhichConstraint(this, null); - } - - - try - { - var options = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }; - options.Converters.Add(new JsonStringEnumConverter()); - options.Converters.Add(new JsonDateTimeConverter()); - - var result = JsonSerializer.Deserialize(body, options); - - return new AndWhichConstraint(this, result); - } - catch (JsonException e) - { - a.FailWith("Expected response body of {context:HttpResponseMessage} to be json string{reason}, but failed to deserialize it. Exception is {0}.", e); - return new AndWhichConstraint(this, null); - } - } - } - - public static class AssertionResponseExtensions - { - public static HttpResponseMessageAssertions Should(this HttpResponseMessage instance) - { - return new HttpResponseMessageAssertions(instance); - } - - public static AndWhichConstraint HaveCommonBody(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs) - { - return assertions.HaveJsonBody(because, becauseArgs); - } - - public static void HaveCommonBody(this HttpResponseMessageAssertions assertions, int code, string message = null, params object[] messageArgs) - { - message = string.IsNullOrEmpty(message) ? "" : ", " + string.Format(CultureInfo.CurrentCulture, message, messageArgs); - var body = assertions.HaveCommonBody("Response body should be CommonResponse{0}", message).Which; - body.Code.Should().Be(code, "Response body code is not the specified one{0}", message); - } - - public static AndWhichConstraint> HaveCommonDataBody(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs) - { - return assertions.HaveJsonBody>(because, becauseArgs); - } - - public static void BePut(this HttpResponseMessageAssertions assertions, bool create, string because = "", params object[] becauseArgs) - { - var body = assertions.HaveStatusCode(create ? 201 : 200, because, becauseArgs) - .And.HaveJsonBody(because, becauseArgs) - .Which; - body.Code.Should().Be(0); - body.Data.Create.Should().Be(create); - } - - public static void BeDelete(this HttpResponseMessageAssertions assertions, bool delete, string because = "", params object[] becauseArgs) - { - var body = assertions.HaveStatusCode(200, because, becauseArgs) - .And.HaveJsonBody(because, becauseArgs) - .Which; - body.Code.Should().Be(0); - body.Data.Delete.Should().Be(delete); - } - - public static void BeInvalidModel(this HttpResponseMessageAssertions assertions, string message = null) - { - message = string.IsNullOrEmpty(message) ? "" : ", " + message; - assertions.HaveStatusCode(400, "Invalid Model Error must have 400 status code{0}", message) - .And.HaveCommonBody("Invalid Model Error must have CommonResponse body{0}", message) - .Which.Code.Should().Be(ErrorCodes.Common.InvalidModel, - "Invalid Model Error must have code {0} in body{1}", - ErrorCodes.Common.InvalidModel, message); - } - } -} diff --git a/Timeline.Tests/Helpers/TestApplication.cs b/Timeline.Tests/Helpers/TestApplication.cs deleted file mode 100644 index 684ffe2c..00000000 --- a/Timeline.Tests/Helpers/TestApplication.cs +++ /dev/null @@ -1,72 +0,0 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; -using Timeline.Configs; -using Timeline.Entities; -using Xunit; - -namespace Timeline.Tests.Helpers -{ - public class TestApplication : IAsyncLifetime - { - public TestDatabase Database { get; } - - public IHost Host { get; private set; } - - public string WorkDir { get; private set; } - - public TestApplication() - { - Database = new TestDatabase(false); - } - - public async Task InitializeAsync() - { - await Database.InitializeAsync(); - - WorkDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - Directory.CreateDirectory(WorkDir); - - Host = await Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder() - .ConfigureAppConfiguration((context, config) => - { - config.AddInMemoryCollection(new Dictionary - { - [ApplicationConfiguration.UseMockFrontEndKey] = "true", - ["WorkDir"] = WorkDir - }); - }) - .ConfigureServices(services => - { - services.AddDbContext(options => - { - options.UseSqlite(Database.Connection); - }); - }) - .ConfigureWebHost(webBuilder => - { - webBuilder - .UseTestServer() - .UseStartup(); - }) - .StartAsync(); - } - - public async Task DisposeAsync() - { - await Host.StopAsync(); - Host.Dispose(); - - Directory.Delete(WorkDir, true); - - await Database.DisposeAsync(); - } - } -} diff --git a/Timeline.Tests/Helpers/TestClock.cs b/Timeline.Tests/Helpers/TestClock.cs deleted file mode 100644 index 34adb245..00000000 --- a/Timeline.Tests/Helpers/TestClock.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Timeline.Services; - -namespace Timeline.Tests.Helpers -{ - public class TestClock : IClock - { - private DateTime? _currentTime; - - public DateTime GetCurrentTime() - { - return _currentTime ?? DateTime.UtcNow; - } - - public void SetCurrentTime(DateTime? mockTime) - { - _currentTime = mockTime; - } - - public DateTime SetMockCurrentTime() - { - var time = new DateTime(3000, 1, 1, 1, 1, 1, DateTimeKind.Utc); - _currentTime = time; - return time; - } - - public DateTime ForwardCurrentTime() - { - return ForwardCurrentTime(TimeSpan.FromDays(1)); - } - - public DateTime ForwardCurrentTime(TimeSpan timeSpan) - { - if (_currentTime == null) - return SetMockCurrentTime(); - _currentTime += timeSpan; - return _currentTime.Value; - } - } -} diff --git a/Timeline.Tests/Helpers/TestDatabase.cs b/Timeline.Tests/Helpers/TestDatabase.cs deleted file mode 100644 index f0c26180..00000000 --- a/Timeline.Tests/Helpers/TestDatabase.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging.Abstractions; -using System.Threading.Tasks; -using Timeline.Entities; -using Timeline.Migrations; -using Timeline.Models; -using Timeline.Services; -using Xunit; - -namespace Timeline.Tests.Helpers -{ - public class TestDatabase : IAsyncLifetime - { - private readonly bool _createUser; - - public TestDatabase(bool createUser = true) - { - _createUser = createUser; - Connection = new SqliteConnection("Data Source=:memory:;"); - } - - public async Task InitializeAsync() - { - await Connection.OpenAsync(); - - using (var context = CreateContext()) - { - await context.Database.EnsureCreatedAsync(); - context.JwtToken.Add(new JwtTokenEntity - { - Key = JwtTokenGenerateHelper.GenerateKey() - }); - await context.SaveChangesAsync(); - - if (_createUser) - { - var passwordService = new PasswordService(); - var userService = new UserService(NullLogger.Instance, context, passwordService, new Clock()); - - await userService.CreateUser(new User - { - Username = "admin", - Password = "adminpw", - Administrator = true, - Nickname = "administrator" - }); - - await userService.CreateUser(new User - { - Username = "user", - Password = "userpw", - Administrator = false, - Nickname = "imuser" - }); - } - } - } - - public async Task DisposeAsync() - { - await Connection.CloseAsync(); - await Connection.DisposeAsync(); - } - - public SqliteConnection Connection { get; } - - public DatabaseContext CreateContext() - { - var options = new DbContextOptionsBuilder() - .UseSqlite(Connection).Options; - - return new DatabaseContext(options); - } - } -} diff --git a/Timeline.Tests/IntegratedTests/AuthorizationTest.cs b/Timeline.Tests/IntegratedTests/AuthorizationTest.cs deleted file mode 100644 index 38071394..00000000 --- a/Timeline.Tests/IntegratedTests/AuthorizationTest.cs +++ /dev/null @@ -1,52 +0,0 @@ -using FluentAssertions; -using System.Net; -using System.Threading.Tasks; -using Timeline.Tests.Helpers; -using Xunit; - -namespace Timeline.Tests.IntegratedTests -{ - public class AuthorizationTest : IntegratedTestBase - { - private const string BaseUrl = "testing/auth/"; - private const string AuthorizeUrl = BaseUrl + "Authorize"; - private const string UserUrl = BaseUrl + "User"; - private const string AdminUrl = BaseUrl + "Admin"; - - [Fact] - public async Task UnauthenticationTest() - { - using var client = await CreateDefaultClient(); - var response = await client.GetAsync(AuthorizeUrl); - response.Should().HaveStatusCode(HttpStatusCode.Unauthorized); - } - - [Fact] - public async Task AuthenticationTest() - { - using var client = await CreateClientAsUser(); - var response = await client.GetAsync(AuthorizeUrl); - response.Should().HaveStatusCode(HttpStatusCode.OK); - } - - [Fact] - public async Task UserAuthorizationTest() - { - using var client = await CreateClientAsUser(); - var response1 = await client.GetAsync(UserUrl); - response1.Should().HaveStatusCode(HttpStatusCode.OK); - var response2 = await client.GetAsync(AdminUrl); - response2.Should().HaveStatusCode(HttpStatusCode.Forbidden); - } - - [Fact] - public async Task AdminAuthorizationTest() - { - using var client = await CreateClientAsAdministrator(); - var response1 = await client.GetAsync(UserUrl); - response1.Should().HaveStatusCode(HttpStatusCode.OK); - var response2 = await client.GetAsync(AdminUrl); - response2.Should().HaveStatusCode(HttpStatusCode.OK); - } - } -} diff --git a/Timeline.Tests/IntegratedTests/FrontEndTest.cs b/Timeline.Tests/IntegratedTests/FrontEndTest.cs deleted file mode 100644 index 39a6e545..00000000 --- a/Timeline.Tests/IntegratedTests/FrontEndTest.cs +++ /dev/null @@ -1,29 +0,0 @@ -using FluentAssertions; -using System.Net.Mime; -using System.Threading.Tasks; -using Timeline.Tests.Helpers; -using Xunit; - -namespace Timeline.Tests.IntegratedTests -{ - public class FrontEndTest : IntegratedTestBase - { - [Fact] - public async Task Index() - { - using var client = await CreateDefaultClient(false); - var res = await client.GetAsync("index.html"); - res.Should().HaveStatusCode(200); - res.Content.Headers.ContentType.MediaType.Should().Be(MediaTypeNames.Text.Html); - } - - [Fact] - public async Task Fallback() - { - using var client = await CreateDefaultClient(false); - var res = await client.GetAsync("aaaaaaaaaaaaaaa"); - res.Should().HaveStatusCode(200); - res.Content.Headers.ContentType.MediaType.Should().Be(MediaTypeNames.Text.Html); - } - } -} diff --git a/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs b/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs deleted file mode 100644 index 7cf27297..00000000 --- a/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs +++ /dev/null @@ -1,164 +0,0 @@ -using FluentAssertions; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.DependencyInjection; -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading.Tasks; -using Timeline.Models; -using Timeline.Models.Converters; -using Timeline.Models.Http; -using Timeline.Services; -using Timeline.Tests.Helpers; -using Xunit; - -namespace Timeline.Tests.IntegratedTests -{ - public abstract class IntegratedTestBase : IAsyncLifetime - { - protected TestApplication TestApp { get; } - - public IReadOnlyList UserInfos { get; private set; } - - private readonly int _userCount; - - public IntegratedTestBase() : this(1) - { - - } - - public IntegratedTestBase(int userCount) - { - if (userCount < 0) - throw new ArgumentOutOfRangeException(nameof(userCount), userCount, "User count can't be negative."); - - _userCount = userCount; - - TestApp = new TestApplication(); - } - - protected virtual Task OnInitializeAsync() - { - return Task.CompletedTask; - } - - protected virtual Task OnDisposeAsync() - { - return Task.CompletedTask; - } - - protected virtual void OnDispose() - { - - } - - public async Task InitializeAsync() - { - await TestApp.InitializeAsync(); - - using (var scope = TestApp.Host.Services.CreateScope()) - { - var users = new List() - { - new User - { - Username = "admin", - Password = "adminpw", - Administrator = true, - Nickname = "administrator" - } - }; - - for (int i = 1; i <= _userCount; i++) - { - users.Add(new User - { - Username = $"user{i}", - Password = $"user{i}pw", - Administrator = false, - Nickname = $"imuser{i}" - }); - } - - var userInfoList = new List(); - - var userService = scope.ServiceProvider.GetRequiredService(); - foreach (var user in users) - { - await userService.CreateUser(user); - } - - using var client = await CreateDefaultClient(); - var options = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }; - options.Converters.Add(new JsonStringEnumConverter()); - options.Converters.Add(new JsonDateTimeConverter()); - foreach (var user in users) - { - var s = await client.GetStringAsync($"users/{user.Username}"); - userInfoList.Add(JsonSerializer.Deserialize(s, options)); - } - - UserInfos = userInfoList; - } - - await OnInitializeAsync(); - } - - public async Task DisposeAsync() - { - await OnDisposeAsync(); - OnDispose(); - await TestApp.DisposeAsync(); - } - - public Task CreateDefaultClient(bool setApiBase = true) - { - var client = TestApp.Host.GetTestServer().CreateClient(); - if (setApiBase) - { - client.BaseAddress = new Uri(client.BaseAddress, "api/"); - } - return Task.FromResult(client); - } - - public async Task CreateClientWithCredential(string username, string password, bool setApiBase = true) - { - var client = TestApp.Host.GetTestServer().CreateClient(); - if (setApiBase) - { - client.BaseAddress = new Uri(client.BaseAddress, "api/"); - } - var response = await client.PostAsJsonAsync("token/create", - new CreateTokenRequest { Username = username, Password = password }); - var token = response.Should().HaveStatusCode(200) - .And.HaveJsonBody().Which.Token; - client.DefaultRequestHeaders.Add("Authorization", "Bearer " + token); - return client; - } - - public Task CreateClientAs(int userNumber, bool setApiBase = true) - { - if (userNumber < 0) - return CreateDefaultClient(setApiBase); - if (userNumber == 0) - return CreateClientWithCredential("admin", "adminpw", setApiBase); - else - return CreateClientWithCredential($"user{userNumber}", $"user{userNumber}pw", setApiBase); - } - - public Task CreateClientAsAdministrator(bool setApiBase = true) - { - return CreateClientAs(0, setApiBase); - } - - public Task CreateClientAsUser(bool setApiBase = true) - { - return CreateClientAs(1, setApiBase); - } - } -} diff --git a/Timeline.Tests/IntegratedTests/TimelineTest.cs b/Timeline.Tests/IntegratedTests/TimelineTest.cs deleted file mode 100644 index ec46b96a..00000000 --- a/Timeline.Tests/IntegratedTests/TimelineTest.cs +++ /dev/null @@ -1,1523 +0,0 @@ -using FluentAssertions; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats.Png; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; -using Timeline.Entities; -using Timeline.Models; -using Timeline.Models.Http; -using Timeline.Tests.Helpers; -using Xunit; - -namespace Timeline.Tests.IntegratedTests -{ - public static class TimelineHelper - { - public static TimelinePostContentInfo TextPostContent(string text) - { - return new TimelinePostContentInfo - { - Type = "text", - Text = text - }; - } - - public static TimelinePostCreateRequest TextPostCreateRequest(string text, DateTime? time = null) - { - return new TimelinePostCreateRequest - { - Content = new TimelinePostCreateRequestContent - { - Type = "text", - Text = text - }, - Time = time - }; - } - } - - public class TimelineTest : IntegratedTestBase - { - public TimelineTest() : base(3) - { - } - - protected override async Task OnInitializeAsync() - { - await CreateTestTimelines(); - } - - private List _testTimelines; - - private async Task CreateTestTimelines() - { - _testTimelines = new List(); - for (int i = 0; i <= 3; i++) - { - var client = await CreateClientAs(i); - var res = await client.PostAsJsonAsync("timelines", new TimelineCreateRequest { Name = $"t{i}" }); - var timelineInfo = res.Should().HaveStatusCode(200) - .And.HaveJsonBody().Which; - _testTimelines.Add(timelineInfo); - } - } - - private static string CalculateUrlTail(string subpath, ICollection> query) - { - StringBuilder result = new StringBuilder(); - if (subpath != null) - { - if (!subpath.StartsWith("/", StringComparison.OrdinalIgnoreCase)) - result.Append('/'); - result.Append(subpath); - } - - if (query != null && query.Count != 0) - { - result.Append('?'); - foreach (var (key, value, index) in query.Select((pair, index) => (pair.Key, pair.Value, index))) - { - result.Append(WebUtility.UrlEncode(key)); - result.Append('='); - result.Append(WebUtility.UrlEncode(value)); - if (index != query.Count - 1) - result.Append('&'); - } - } - - return result.ToString(); - } - - private static string GeneratePersonalTimelineUrl(int id, string subpath = null, ICollection> query = null) - { - return $"timelines/@{(id == 0 ? "admin" : ("user" + id))}{CalculateUrlTail(subpath, query)}"; - } - - private static string GenerateOrdinaryTimelineUrl(int id, string subpath = null, ICollection> query = null) - { - return $"timelines/t{id}{CalculateUrlTail(subpath, query)}"; - } - - public delegate string TimelineUrlGenerator(int userId, string subpath = null, ICollection> query = null); - - public static IEnumerable TimelineUrlGeneratorData() - { - yield return new[] { new TimelineUrlGenerator(GeneratePersonalTimelineUrl) }; - yield return new[] { new TimelineUrlGenerator(GenerateOrdinaryTimelineUrl) }; - } - - private static string GeneratePersonalTimelineUrlByName(string name, string subpath = null) - { - return $"timelines/@{name}{(subpath == null ? "" : "/" + subpath)}"; - } - - private static string GenerateOrdinaryTimelineUrlByName(string name, string subpath = null) - { - return $"timelines/{name}{(subpath == null ? "" : "/" + subpath)}"; - } - - public static IEnumerable TimelineUrlByNameGeneratorData() - { - yield return new[] { new Func(GeneratePersonalTimelineUrlByName) }; - yield return new[] { new Func(GenerateOrdinaryTimelineUrlByName) }; - } - - [Fact] - public async Task TimelineGet_Should_Work() - { - using var client = await CreateDefaultClient(); - { - var res = await client.GetAsync("timelines/@user1"); - var body = res.Should().HaveStatusCode(200) - .And.HaveJsonBody().Which; - body.Owner.Should().BeEquivalentTo(UserInfos[1]); - body.Visibility.Should().Be(TimelineVisibility.Register); - body.Description.Should().Be(""); - body.Members.Should().NotBeNull().And.BeEmpty(); - var links = body._links; - links.Should().NotBeNull(); - links.Self.Should().EndWith("timelines/@user1"); - links.Posts.Should().EndWith("timelines/@user1/posts"); - } - - { - var res = await client.GetAsync("timelines/t1"); - var body = res.Should().HaveStatusCode(200) - .And.HaveJsonBody().Which; - body.Owner.Should().BeEquivalentTo(UserInfos[1]); - body.Visibility.Should().Be(TimelineVisibility.Register); - body.Description.Should().Be(""); - body.Members.Should().NotBeNull().And.BeEmpty(); - var links = body._links; - links.Should().NotBeNull(); - links.Self.Should().EndWith("timelines/t1"); - links.Posts.Should().EndWith("timelines/t1/posts"); - } - } - - [Fact] - public async Task TimelineList() - { - TimelineInfo user1Timeline; - - var client = await CreateDefaultClient(); - - { - var res = await client.GetAsync("timelines/@user1"); - user1Timeline = res.Should().HaveStatusCode(200) - .And.HaveJsonBody().Which; - } - - { - var testResult = new List(); - testResult.Add(user1Timeline); - testResult.AddRange(_testTimelines); - - var res = await client.GetAsync("timelines"); - res.Should().HaveStatusCode(200) - .And.HaveJsonBody>() - .Which.Should().BeEquivalentTo(testResult); - } - } - - [Fact] - public async Task TimelineList_WithQuery() - { - var testResultRelate = new List(); - var testResultOwn = new List(); - var testResultJoin = new List(); - var testResultOwnPrivate = new List(); - var testResultRelatePublic = new List(); - var testResultRelateRegister = new List(); - var testResultJoinPrivate = new List(); - var testResultPublic = new List(); - - { - var client = await CreateClientAsUser(); - - { - var res = await client.PutAsync("timelines/@user1/members/user3", null); - res.Should().HaveStatusCode(200); - } - - { - var res = await client.PutAsync("timelines/t1/members/user3", null); - res.Should().HaveStatusCode(200); - } - - { - var res = await client.PatchAsJsonAsync("timelines/@user1", new TimelinePatchRequest { Visibility = TimelineVisibility.Public }); - res.Should().HaveStatusCode(200); - } - - { - var res = await client.PatchAsJsonAsync("timelines/t1", new TimelinePatchRequest { Visibility = TimelineVisibility.Register }); - res.Should().HaveStatusCode(200); - } - - { - var res = await client.GetAsync("timelines/@user1"); - var timeline = res.Should().HaveStatusCode(200) - .And.HaveJsonBody().Which; - testResultRelate.Add(timeline); - testResultJoin.Add(timeline); - testResultRelatePublic.Add(timeline); - testResultPublic.Add(timeline); - } - - { - var res = await client.GetAsync("timelines/t1"); - var timeline = res.Should().HaveStatusCode(200) - .And.HaveJsonBody().Which; - testResultRelate.Add(timeline); - testResultJoin.Add(timeline); - testResultRelateRegister.Add(timeline); - } - } - - { - var client = await CreateClientAs(2); - - { - var res = await client.PutAsync("timelines/@user2/members/user3", null); - res.Should().HaveStatusCode(200); - } - - { - var res = await client.PutAsync("timelines/t2/members/user3", null); - res.Should().HaveStatusCode(200); - } - - { - var res = await client.PatchAsJsonAsync("timelines/@user2", new TimelinePatchRequest { Visibility = TimelineVisibility.Register }); - res.Should().HaveStatusCode(200); - } - - { - var res = await client.PatchAsJsonAsync("timelines/t2", new TimelinePatchRequest { Visibility = TimelineVisibility.Private }); - res.Should().HaveStatusCode(200); - } - - { - var res = await client.GetAsync("timelines/@user2"); - var timeline = res.Should().HaveStatusCode(200) - .And.HaveJsonBody().Which; - testResultRelate.Add(timeline); - testResultJoin.Add(timeline); - testResultRelateRegister.Add(timeline); - } - - { - var res = await client.GetAsync("timelines/t2"); - var timeline = res.Should().HaveStatusCode(200) - .And.HaveJsonBody().Which; - testResultRelate.Add(timeline); - testResultJoin.Add(timeline); - testResultJoinPrivate.Add(timeline); - } - } - - { - var client = await CreateClientAs(3); - - { - var res = await client.PatchAsJsonAsync("timelines/@user3", new TimelinePatchRequest { Visibility = TimelineVisibility.Private }); - res.Should().HaveStatusCode(200); - } - - { - var res = await client.PatchAsJsonAsync("timelines/t3", new TimelinePatchRequest { Visibility = TimelineVisibility.Register }); - res.Should().HaveStatusCode(200); - } - - { - var res = await client.GetAsync("timelines/@user3"); - var timeline = res.Should().HaveStatusCode(200) - .And.HaveJsonBody().Which; - testResultRelate.Add(timeline); - testResultOwn.Add(timeline); - testResultOwnPrivate.Add(timeline); - } - - { - var res = await client.GetAsync("timelines/t3"); - var timeline = res.Should().HaveStatusCode(200) - .And.HaveJsonBody().Which; - testResultRelate.Add(timeline); - testResultOwn.Add(timeline); - testResultRelateRegister.Add(timeline); - } - } - - { - var client = await CreateClientAs(3); - { - var res = await client.GetAsync("timelines?relate=user3"); - var body = res.Should().HaveStatusCode(200) - .And.HaveJsonBody>() - .Which; - body.Should().BeEquivalentTo(testResultRelate); - } - - { - var res = await client.GetAsync("timelines?relate=user3&relateType=own"); - var body = res.Should().HaveStatusCode(200) - .And.HaveJsonBody>() - .Which; - body.Should().BeEquivalentTo(testResultOwn); - } - - { - var res = await client.GetAsync("timelines?relate=user3&visibility=public"); - var body = res.Should().HaveStatusCode(200) - .And.HaveJsonBody>() - .Which; - body.Should().BeEquivalentTo(testResultRelatePublic); - } - - { - var res = await client.GetAsync("timelines?relate=user3&visibility=register"); - var body = res.Should().HaveStatusCode(200) - .And.HaveJsonBody>() - .Which; - body.Should().BeEquivalentTo(testResultRelateRegister); - } - - { - var res = await client.GetAsync("timelines?relate=user3&relateType=join&visibility=private"); - var body = res.Should().HaveStatusCode(200) - .And.HaveJsonBody>() - .Which; - body.Should().BeEquivalentTo(testResultJoinPrivate); - } - - { - var res = await client.GetAsync("timelines?relate=user3&relateType=own&visibility=private"); - var body = res.Should().HaveStatusCode(200) - .And.HaveJsonBody>() - .Which; - body.Should().BeEquivalentTo(testResultOwnPrivate); - } - } - - { - var client = await CreateDefaultClient(); - { - var res = await client.GetAsync("timelines?visibility=public"); - var body = res.Should().HaveStatusCode(200) - .And.HaveJsonBody>() - .Which; - body.Should().BeEquivalentTo(testResultPublic); - } - } - } - - [Fact] - public async Task TimelineList_InvalidModel() - { - var client = await CreateClientAsUser(); - - { - var res = await client.GetAsync("timelines?relate=us!!"); - res.Should().BeInvalidModel(); - } - - { - var res = await client.GetAsync("timelines?relateType=aaa"); - res.Should().BeInvalidModel(); - } - - { - var res = await client.GetAsync("timelines?visibility=aaa"); - res.Should().BeInvalidModel(); - } - } - - [Fact] - public async Task TimelineCreate_Should_Work() - { - { - using var client = await CreateDefaultClient(); - var res = await client.PostAsJsonAsync("timelines", new TimelineCreateRequest { Name = "aaa" }); - res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); - } - - using (var client = await CreateClientAsUser()) - { - { - var res = await client.PostAsJsonAsync("timelines", new TimelineCreateRequest { Name = "!!!" }); - res.Should().BeInvalidModel(); - } - - TimelineInfo timelineInfo; - { - var res = await client.PostAsJsonAsync("timelines", new TimelineCreateRequest { Name = "aaa" }); - timelineInfo = res.Should().HaveStatusCode(200) - .And.HaveJsonBody().Which; - } - - { - var res = await client.GetAsync("timelines/aaa"); - res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.Should().BeEquivalentTo(timelineInfo); - } - - { - var res = await client.PostAsJsonAsync("timelines", new TimelineCreateRequest { Name = "aaa" }); - res.Should().HaveStatusCode(400) - .And.HaveCommonBody(ErrorCodes.TimelineController.NameConflict); - } - } - } - - [Fact] - public async Task TimelineDelete_Should_Work() - { - { - using var client = await CreateDefaultClient(); - var res = await client.DeleteAsync("timelines/t1"); - res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); - } - - { - using var client = await CreateClientAs(2); - var res = await client.DeleteAsync("timelines/t1"); - res.Should().HaveStatusCode(HttpStatusCode.Forbidden); - } - - { - using var client = await CreateClientAsAdministrator(); - - { - var res = await client.DeleteAsync("timelines/!!!"); - res.Should().BeInvalidModel(); - } - - { - var res = await client.DeleteAsync("timelines/t2"); - res.Should().BeDelete(true); - } - - { - var res = await client.DeleteAsync("timelines/t2"); - res.Should().BeDelete(false); - } - } - - { - using var client = await CreateClientAs(1); - - { - var res = await client.DeleteAsync("timelines/!!!"); - res.Should().BeInvalidModel(); - } - - { - var res = await client.DeleteAsync("timelines/t1"); - res.Should().BeDelete(true); - } - - { - var res = await client.DeleteAsync("timelines/t1"); - res.Should().HaveStatusCode(400); - } - } - } - - [Theory] - [MemberData(nameof(TimelineUrlByNameGeneratorData))] - public async Task InvalidModel_BadName(Func generator) - { - using var client = await CreateClientAsAdministrator(); - { - var res = await client.GetAsync(generator("aaa!!!", null)); - res.Should().BeInvalidModel(); - } - { - var res = await client.PatchAsJsonAsync(generator("aaa!!!", null), new TimelinePatchRequest { }); - res.Should().BeInvalidModel(); - } - { - var res = await client.PutAsync(generator("aaa!!!", "members/user1"), null); - res.Should().BeInvalidModel(); - } - { - var res = await client.DeleteAsync(generator("aaa!!!", "members/user1")); - res.Should().BeInvalidModel(); - } - { - var res = await client.GetAsync(generator("aaa!!!", "posts")); - res.Should().BeInvalidModel(); - } - { - var res = await client.PostAsJsonAsync(generator("aaa!!!", "posts"), TimelineHelper.TextPostCreateRequest("aaa")); - res.Should().BeInvalidModel(); - } - { - var res = await client.DeleteAsync(generator("aaa!!!", "posts/123")); - res.Should().BeInvalidModel(); - } - { - var res = await client.GetAsync(generator("aaa!!!", "posts/123/data")); - res.Should().BeInvalidModel(); - } - } - - [Theory] - [MemberData(nameof(TimelineUrlByNameGeneratorData))] - public async Task Ordinary_NotFound(Func generator) - { - var errorCode = generator == GenerateOrdinaryTimelineUrlByName ? ErrorCodes.TimelineController.NotExist : ErrorCodes.UserCommon.NotExist; - - using var client = await CreateClientAsAdministrator(); - { - var res = await client.GetAsync(generator("notexist", null)); - res.Should().HaveStatusCode(404).And.HaveCommonBody(errorCode); - } - { - var res = await client.PatchAsJsonAsync(generator("notexist", null), new TimelinePatchRequest { }); - res.Should().HaveStatusCode(400).And.HaveCommonBody(errorCode); - } - { - var res = await client.PutAsync(generator("notexist", "members/user1"), null); - res.Should().HaveStatusCode(400).And.HaveCommonBody(errorCode); - } - { - var res = await client.DeleteAsync(generator("notexist", "members/user1")); - res.Should().HaveStatusCode(400).And.HaveCommonBody(errorCode); - } - { - var res = await client.GetAsync(generator("notexist", "posts")); - res.Should().HaveStatusCode(404).And.HaveCommonBody(errorCode); - } - { - var res = await client.PostAsJsonAsync(generator("notexist", "posts"), TimelineHelper.TextPostCreateRequest("aaa")); - res.Should().HaveStatusCode(400).And.HaveCommonBody(errorCode); - } - { - var res = await client.DeleteAsync(generator("notexist", "posts/123")); - res.Should().HaveStatusCode(400).And.HaveCommonBody(errorCode); - } - { - var res = await client.GetAsync(generator("notexist", "posts/123/data")); - res.Should().HaveStatusCode(404).And.HaveCommonBody(errorCode); - } - } - - [Theory] - [MemberData(nameof(TimelineUrlGeneratorData))] - public async Task Description_Should_Work(TimelineUrlGenerator generator) - { - using var client = await CreateClientAsUser(); - - async Task AssertDescription(string description) - { - var res = await client.GetAsync(generator(1, null)); - var body = res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.Description.Should().Be(description); - } - - const string mockDescription = "haha"; - - await AssertDescription(""); - { - var res = await client.PatchAsJsonAsync(generator(1, null), - new TimelinePatchRequest { Description = mockDescription }); - res.Should().HaveStatusCode(200) - .And.HaveJsonBody().Which.Description.Should().Be(mockDescription); - await AssertDescription(mockDescription); - } - { - var res = await client.PatchAsJsonAsync(generator(1, null), - new TimelinePatchRequest { Description = null }); - res.Should().HaveStatusCode(200) - .And.HaveJsonBody().Which.Description.Should().Be(mockDescription); - await AssertDescription(mockDescription); - } - { - var res = await client.PatchAsJsonAsync(generator(1, null), - new TimelinePatchRequest { Description = "" }); - res.Should().HaveStatusCode(200) - .And.HaveJsonBody().Which.Description.Should().Be(""); - await AssertDescription(""); - } - } - - [Theory] - [MemberData(nameof(TimelineUrlGeneratorData))] - public async Task Member_Should_Work(TimelineUrlGenerator generator) - { - var getUrl = generator(1, null); - using var client = await CreateClientAsUser(); - - async Task AssertMembers(IList members) - { - var res = await client.GetAsync(getUrl); - res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.Members.Should().NotBeNull().And.BeEquivalentTo(members); - } - - async Task AssertEmptyMembers() - { - var res = await client.GetAsync(getUrl); - res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.Members.Should().NotBeNull().And.BeEmpty(); - } - - await AssertEmptyMembers(); - { - var res = await client.PutAsync(generator(1, "members/usernotexist"), null); - res.Should().HaveStatusCode(400) - .And.HaveCommonBody(ErrorCodes.TimelineController.MemberPut_NotExist); - } - await AssertEmptyMembers(); - { - var res = await client.PutAsync(generator(1, "members/user2"), null); - res.Should().HaveStatusCode(200); - } - await AssertMembers(new List { UserInfos[2] }); - { - var res = await client.DeleteAsync(generator(1, "members/user2")); - res.Should().BeDelete(true); - } - await AssertEmptyMembers(); - { - var res = await client.DeleteAsync(generator(1, "members/aaa")); - res.Should().BeDelete(false); - } - await AssertEmptyMembers(); - } - - public static IEnumerable Permission_Timeline_Data() - { - yield return new object[] { new TimelineUrlGenerator(GenerateOrdinaryTimelineUrl), -1, 200, 401, 401, 401, 401 }; - yield return new object[] { new TimelineUrlGenerator(GenerateOrdinaryTimelineUrl), 1, 200, 200, 403, 200, 403 }; - yield return new object[] { new TimelineUrlGenerator(GenerateOrdinaryTimelineUrl), 0, 200, 200, 200, 200, 200 }; - yield return new object[] { new TimelineUrlGenerator(GeneratePersonalTimelineUrl), -1, 200, 401, 401, 401, 401 }; - yield return new object[] { new TimelineUrlGenerator(GeneratePersonalTimelineUrl), 1, 200, 200, 403, 200, 403 }; - yield return new object[] { new TimelineUrlGenerator(GeneratePersonalTimelineUrl), 0, 200, 200, 200, 200, 200 }; - } - - [Theory] - [MemberData(nameof(Permission_Timeline_Data))] - public async Task Permission_Timeline(TimelineUrlGenerator generator, int userNumber, int get, int opPatchUser, int opPatchAdmin, int opMemberUser, int opMemberAdmin) - { - using var client = await CreateClientAs(userNumber); - { - var res = await client.GetAsync("timelines/t1"); - res.Should().HaveStatusCode(get); - } - - { - var res = await client.PatchAsJsonAsync(generator(1, null), new TimelinePatchRequest { Description = "hahaha" }); - res.Should().HaveStatusCode(opPatchUser); - } - - { - var res = await client.PatchAsJsonAsync(generator(0, null), new TimelinePatchRequest { Description = "hahaha" }); - res.Should().HaveStatusCode(opPatchAdmin); - } - - { - var res = await client.PutAsync(generator(1, "members/user2"), null); - res.Should().HaveStatusCode(opMemberUser); - } - - { - var res = await client.DeleteAsync(generator(1, "members/user2")); - res.Should().HaveStatusCode(opMemberUser); - } - - { - var res = await client.PutAsync(generator(0, "members/user2"), null); - res.Should().HaveStatusCode(opMemberAdmin); - } - - { - var res = await client.DeleteAsync(generator(0, "members/user2")); - res.Should().HaveStatusCode(opMemberAdmin); - } - } - - [Theory] - [MemberData(nameof(TimelineUrlGeneratorData))] - public async Task Visibility_Test(TimelineUrlGenerator generator) - { - var userUrl = generator(1, "posts"); - var adminUrl = generator(0, "posts"); - { - - using var client = await CreateClientAsUser(); - using var content = new StringContent(@"{""visibility"":""abcdefg""}", System.Text.Encoding.UTF8, System.Net.Mime.MediaTypeNames.Application.Json); - var res = await client.PatchAsync(generator(1, null), content); - res.Should().BeInvalidModel(); - } - { // default visibility is registered - { - using var client = await CreateDefaultClient(); - var res = await client.GetAsync(userUrl); - res.Should().HaveStatusCode(403); - } - - { - using var client = await CreateClientAsUser(); - var res = await client.GetAsync(adminUrl); - res.Should().HaveStatusCode(200); - } - } - - { // change visibility to public - { - using var client = await CreateClientAsUser(); - var res = await client.PatchAsJsonAsync(generator(1, null), - new TimelinePatchRequest { Visibility = TimelineVisibility.Public }); - res.Should().HaveStatusCode(200); - } - { - using var client = await CreateDefaultClient(); - var res = await client.GetAsync(userUrl); - res.Should().HaveStatusCode(200); - } - } - - { // change visibility to private - { - using var client = await CreateClientAsAdministrator(); - { - var res = await client.PatchAsJsonAsync(generator(1, null), - new TimelinePatchRequest { Visibility = TimelineVisibility.Private }); - res.Should().HaveStatusCode(200); - } - { - var res = await client.PatchAsJsonAsync(generator(0, null), - new TimelinePatchRequest { Visibility = TimelineVisibility.Private }); - res.Should().HaveStatusCode(200); - } - } - { - using var client = await CreateDefaultClient(); - var res = await client.GetAsync(userUrl); - res.Should().HaveStatusCode(403); - } - { // user can't read admin's - using var client = await CreateClientAsUser(); - var res = await client.GetAsync(adminUrl); - res.Should().HaveStatusCode(403); - } - { // admin can read user's - using var client = await CreateClientAsAdministrator(); - var res = await client.GetAsync(userUrl); - res.Should().HaveStatusCode(200); - } - { // add member - using var client = await CreateClientAsAdministrator(); - var res = await client.PutAsync(generator(0, "members/user1"), null); - res.Should().HaveStatusCode(200); - } - { // now user can read admin's - using var client = await CreateClientAsUser(); - var res = await client.GetAsync(adminUrl); - res.Should().HaveStatusCode(200); - } - } - } - - [Theory] - [MemberData(nameof(TimelineUrlGeneratorData))] - public async Task Permission_Post_Create(TimelineUrlGenerator generator) - { - using (var client = await CreateClientAsUser()) - { - var res = await client.PutAsync(generator(1, "members/user2"), null); - res.Should().HaveStatusCode(200); - } - - using (var client = await CreateDefaultClient()) - { - { // no auth should get 401 - var res = await client.PostAsJsonAsync(generator(1, "posts"), - TimelineHelper.TextPostCreateRequest("aaa")); - res.Should().HaveStatusCode(401); - } - } - - using (var client = await CreateClientAsUser()) - { - { // post self's - var res = await client.PostAsJsonAsync(generator(1, "posts"), - TimelineHelper.TextPostCreateRequest("aaa")); - res.Should().HaveStatusCode(200); - } - { // post other not as a member should get 403 - var res = await client.PostAsJsonAsync(generator(0, "posts"), - TimelineHelper.TextPostCreateRequest("aaa")); - res.Should().HaveStatusCode(403); - } - } - - using (var client = await CreateClientAsAdministrator()) - { - { // post as admin - var res = await client.PostAsJsonAsync(generator(1, "posts"), - TimelineHelper.TextPostCreateRequest("aaa")); - res.Should().HaveStatusCode(200); - } - } - - using (var client = await CreateClientAs(2)) - { - { // post as member - var res = await client.PostAsJsonAsync(generator(1, "posts"), - TimelineHelper.TextPostCreateRequest("aaa")); - res.Should().HaveStatusCode(200); - } - } - } - - [Theory] - [MemberData(nameof(TimelineUrlGeneratorData))] - public async Task Permission_Post_Delete(TimelineUrlGenerator generator) - { - async Task CreatePost(int userNumber) - { - using var client = await CreateClientAs(userNumber); - var res = await client.PostAsJsonAsync(generator(1, "posts"), - TimelineHelper.TextPostCreateRequest("aaa")); - return res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.Id; - } - - using (var client = await CreateClientAsUser()) - { - { - var res = await client.PutAsync(generator(1, "members/user2"), null); - res.Should().HaveStatusCode(200); - } - { - var res = await client.PutAsync(generator(1, "members/user3"), null); - res.Should().HaveStatusCode(200); - } - } - - { // no auth should get 401 - using var client = await CreateDefaultClient(); - var res = await client.DeleteAsync(generator(1, "posts/12")); - res.Should().HaveStatusCode(401); - } - - { // self can delete self - var postId = await CreatePost(1); - using var client = await CreateClientAsUser(); - var res = await client.DeleteAsync(generator(1, $"posts/{postId}")); - res.Should().HaveStatusCode(200); - } - - { // admin can delete any - var postId = await CreatePost(1); - using var client = await CreateClientAsAdministrator(); - var res = await client.DeleteAsync(generator(1, $"posts/{postId}")); - res.Should().HaveStatusCode(200); - } - - { // owner can delete other - var postId = await CreatePost(2); - using var client = await CreateClientAsUser(); - var res = await client.DeleteAsync(generator(1, $"posts/{postId}")); - res.Should().HaveStatusCode(200); - } - - { // author can delete self - var postId = await CreatePost(2); - using var client = await CreateClientAs(2); - var res = await client.DeleteAsync(generator(1, $"posts/{postId}")); - res.Should().HaveStatusCode(200); - } - - { // otherwise is forbidden - var postId = await CreatePost(2); - using var client = await CreateClientAs(3); - var res = await client.DeleteAsync(generator(1, $"posts/{postId}")); - res.Should().HaveStatusCode(403); - } - } - - [Theory] - [MemberData(nameof(TimelineUrlGeneratorData))] - public async Task TextPost_ShouldWork(TimelineUrlGenerator generator) - { - { - using var client = await CreateClientAsUser(); - { - var res = await client.GetAsync(generator(1, "posts")); - res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.Should().NotBeNull().And.BeEmpty(); - } - { - var res = await client.PostAsJsonAsync(generator(1, "posts"), - TimelineHelper.TextPostCreateRequest(null)); - res.Should().BeInvalidModel(); - } - const string mockContent = "aaa"; - TimelinePostInfo createRes; - { - var res = await client.PostAsJsonAsync(generator(1, "posts"), - TimelineHelper.TextPostCreateRequest(mockContent)); - var body = res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which; - body.Should().NotBeNull(); - body.Content.Should().BeEquivalentTo(TimelineHelper.TextPostContent(mockContent)); - body.Author.Should().BeEquivalentTo(UserInfos[1]); - body.Deleted.Should().BeFalse(); - createRes = body; - } - { - var res = await client.GetAsync(generator(1, "posts")); - res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.Should().NotBeNull().And.BeEquivalentTo(createRes); - } - const string mockContent2 = "bbb"; - var mockTime2 = DateTime.UtcNow.AddDays(-1); - TimelinePostInfo createRes2; - { - var res = await client.PostAsJsonAsync(generator(1, "posts"), - TimelineHelper.TextPostCreateRequest(mockContent2, mockTime2)); - var body = res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which; - body.Should().NotBeNull(); - body.Content.Should().BeEquivalentTo(TimelineHelper.TextPostContent(mockContent2)); - body.Author.Should().BeEquivalentTo(UserInfos[1]); - body.Time.Should().BeCloseTo(mockTime2, 1000); - body.Deleted.Should().BeFalse(); - createRes2 = body; - } - { - var res = await client.GetAsync(generator(1, "posts")); - res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.Should().NotBeNull().And.BeEquivalentTo(createRes, createRes2); - } - { - var res = await client.DeleteAsync(generator(1, $"posts/{createRes.Id}")); - res.Should().BeDelete(true); - } - { - var res = await client.DeleteAsync(generator(1, $"posts/{createRes.Id}")); - res.Should().BeDelete(false); - } - { - var res = await client.DeleteAsync(generator(1, "posts/30000")); - res.Should().BeDelete(false); - } - { - var res = await client.GetAsync(generator(1, "posts")); - res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.Should().NotBeNull().And.BeEquivalentTo(createRes2); - } - } - } - - [Theory] - [MemberData(nameof(TimelineUrlGeneratorData))] - public async Task GetPost_Should_Ordered(TimelineUrlGenerator generator) - { - using var client = await CreateClientAsUser(); - - async Task CreatePost(DateTime time) - { - var res = await client.PostAsJsonAsync(generator(1, "posts"), - TimelineHelper.TextPostCreateRequest("aaa", time)); - return res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.Id; - } - - var now = DateTime.UtcNow; - var id0 = await CreatePost(now.AddDays(1)); - var id1 = await CreatePost(now.AddDays(-1)); - var id2 = await CreatePost(now); - - { - var res = await client.GetAsync(generator(1, "posts")); - res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.Select(p => p.Id).Should().Equal(id1, id2, id0); - } - } - - [Theory] - [MemberData(nameof(TimelineUrlGeneratorData))] - public async Task CreatePost_InvalidModel(TimelineUrlGenerator generator) - { - using var client = await CreateClientAsUser(); - - { - var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = null }); - res.Should().BeInvalidModel(); - } - - { - var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = null } }); - res.Should().BeInvalidModel(); - } - - { - var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "hahaha" } }); - res.Should().BeInvalidModel(); - } - - { - var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "text", Text = null } }); - res.Should().BeInvalidModel(); - } - - { - var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "image", Data = null } }); - res.Should().BeInvalidModel(); - } - - { - // image not base64 - var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "image", Data = "!!!" } }); - res.Should().BeInvalidModel(); - } - - { - // image base64 not image - var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "image", Data = Convert.ToBase64String(new byte[] { 0x01, 0x02, 0x03 }) } }); - res.Should().BeInvalidModel(); - } - } - - [Theory] - [MemberData(nameof(TimelineUrlGeneratorData))] - public async Task ImagePost_ShouldWork(TimelineUrlGenerator generator) - { - var imageData = ImageHelper.CreatePngWithSize(100, 200); - - long postId; - string postImageUrl; - - void AssertPostContent(TimelinePostContentInfo content) - { - content.Type.Should().Be(TimelinePostContentTypes.Image); - content.Url.Should().EndWith(generator(1, $"posts/{postId}/data")); - content.Text.Should().Be(null); - } - - using var client = await CreateClientAsUser(); - - { - var res = await client.PostAsJsonAsync(generator(1, "posts"), - new TimelinePostCreateRequest - { - Content = new TimelinePostCreateRequestContent - { - Type = TimelinePostContentTypes.Image, - Data = Convert.ToBase64String(imageData) - } - }); - var body = res.Should().HaveStatusCode(200) - .And.HaveJsonBody().Which; - postId = body.Id; - postImageUrl = body.Content.Url; - AssertPostContent(body.Content); - } - - { - var res = await client.GetAsync(generator(1, "posts")); - var body = res.Should().HaveStatusCode(200) - .And.HaveJsonBody().Which; - body.Should().HaveCount(1); - var post = body[0]; - post.Id.Should().Be(postId); - AssertPostContent(post.Content); - } - - { - var res = await client.GetAsync(generator(1, $"posts/{postId}/data")); - res.Content.Headers.ContentType.MediaType.Should().Be("image/png"); - var data = await res.Content.ReadAsByteArrayAsync(); - var image = Image.Load(data, out var format); - image.Width.Should().Be(100); - image.Height.Should().Be(200); - format.Name.Should().Be(PngFormat.Instance.Name); - } - - { - await CacheTestHelper.TestCache(client, generator(1, $"posts/{postId}/data")); - } - - { - var res = await client.DeleteAsync(generator(1, $"posts/{postId}")); - res.Should().BeDelete(true); - } - - { - var res = await client.DeleteAsync(generator(1, $"posts/{postId}")); - res.Should().BeDelete(false); - } - - { - var res = await client.GetAsync(generator(1, "posts")); - res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.Should().BeEmpty(); - } - - { - using var scope = TestApp.Host.Services.CreateScope(); - var database = scope.ServiceProvider.GetRequiredService(); - var count = await database.Data.CountAsync(); - count.Should().Be(0); - } - } - - [Theory] - [MemberData(nameof(TimelineUrlGeneratorData))] - public async Task ImagePost_400(TimelineUrlGenerator generator) - { - using var client = await CreateClientAsUser(); - - { - var res = await client.GetAsync(generator(1, "posts/11234/data")); - res.Should().HaveStatusCode(404) - .And.HaveCommonBody(ErrorCodes.TimelineController.PostNotExist); - } - - long postId; - { - var res = await client.PostAsJsonAsync(generator(1, "posts"), - TimelineHelper.TextPostCreateRequest("aaa")); - var body = res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which; - postId = body.Id; - } - - { - var res = await client.GetAsync(generator(1, $"posts/{postId}/data")); - res.Should().HaveStatusCode(400) - .And.HaveCommonBody(ErrorCodes.TimelineController.PostNoData); - } - } - - [Theory] - [MemberData(nameof(TimelineUrlGeneratorData))] - public async Task Timeline_LastModified(TimelineUrlGenerator generator) - { - using var client = await CreateClientAsUser(); - - DateTime lastModified; - - { - var res = await client.GetAsync(generator(1)); - lastModified = res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.LastModified; - } - - await Task.Delay(1000); - - { - var res = await client.PatchAsJsonAsync(generator(1), new TimelinePatchRequest { Description = "123" }); - lastModified = res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.LastModified.Should().BeAfter(lastModified).And.Subject.Value; - } - - { - var res = await client.GetAsync(generator(1)); - res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.LastModified.Should().Be(lastModified); - } - - await Task.Delay(1000); - - { - var res = await client.PutAsync(generator(1, "members/user2"), null); - res.Should().HaveStatusCode(200); - } - - { - var res = await client.GetAsync(generator(1)); - res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.LastModified.Should().BeAfter(lastModified); - } - } - - [Theory] - [MemberData(nameof(TimelineUrlGeneratorData))] - public async Task Post_ModifiedSince(TimelineUrlGenerator generator) - { - using var client = await CreateClientAsUser(); - - var postContentList = new List { "a", "b", "c", "d" }; - var posts = new List(); - - foreach (var content in postContentList) - { - var res = await client.PostAsJsonAsync(generator(1, "posts"), - new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } }); - var post = res.Should().HaveStatusCode(200) - .And.HaveJsonBody().Which; - posts.Add(post); - await Task.Delay(1000); - } - - { - var res = await client.DeleteAsync(generator(1, $"posts/{posts[2].Id}")); - res.Should().BeDelete(true); - } - - { - var res = await client.GetAsync(generator(1, "posts", - new Dictionary { { "modifiedSince", posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture) } })); - res.Should().HaveStatusCode(200) - .And.HaveJsonBody>() - .Which.Should().HaveCount(2) - .And.Subject.Select(p => p.Content.Text).Should().Equal("b", "d"); - } - } - - [Theory] - [MemberData(nameof(TimelineUrlGeneratorData))] - public async Task PostList_IncludeDeleted(TimelineUrlGenerator urlGenerator) - { - using var client = await CreateClientAsUser(); - - var postContentList = new List { "a", "b", "c", "d" }; - var posts = new List(); - - foreach (var content in postContentList) - { - var res = await client.PostAsJsonAsync(urlGenerator(1, "posts"), - new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } }); - posts.Add(res.Should().HaveStatusCode(200) - .And.HaveJsonBody().Which); - } - - foreach (var id in new long[] { posts[0].Id, posts[2].Id }) - { - var res = await client.DeleteAsync(urlGenerator(1, $"posts/{id}")); - res.Should().BeDelete(true); - } - - { - var res = await client.GetAsync(urlGenerator(1, "posts", new Dictionary { ["includeDeleted"] = "true" })); - posts = res.Should().HaveStatusCode(200) - .And.HaveJsonBody>() - .Which; - posts.Should().HaveCount(4); - posts.Select(p => p.Deleted).Should().Equal(true, false, true, false); - posts.Select(p => p.Content == null).Should().Equal(true, false, true, false); - } - } - - [Theory] - [MemberData(nameof(TimelineUrlGeneratorData))] - public async Task Post_ModifiedSince_And_IncludeDeleted(TimelineUrlGenerator urlGenerator) - { - using var client = await CreateClientAsUser(); - - var postContentList = new List { "a", "b", "c", "d" }; - var posts = new List(); - - foreach (var (content, index) in postContentList.Select((v, i) => (v, i))) - { - var res = await client.PostAsJsonAsync(urlGenerator(1, "posts"), - new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } }); - var post = res.Should().HaveStatusCode(200) - .And.HaveJsonBody().Which; - posts.Add(post); - await Task.Delay(1000); - } - - { - var res = await client.DeleteAsync(urlGenerator(1, $"posts/{posts[2].Id}")); - res.Should().BeDelete(true); - } - - { - - var res = await client.GetAsync(urlGenerator(1, "posts", - new Dictionary { - { "modifiedSince", posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture) }, - { "includeDeleted", "true" } - })); - posts = res.Should().HaveStatusCode(200) - .And.HaveJsonBody>() - .Which; - posts.Should().HaveCount(3); - posts.Select(p => p.Deleted).Should().Equal(false, true, false); - posts.Select(p => p.Content == null).Should().Equal(false, true, false); - } - } - - [Theory] - [MemberData(nameof(TimelineUrlGeneratorData))] - public async Task Timeline_Get_IfModifiedSince_And_CheckUniqueId(TimelineUrlGenerator urlGenerator) - { - using var client = await CreateClientAsUser(); - - DateTime lastModifiedTime; - TimelineInfo timeline; - string uniqueId; - - { - var res = await client.GetAsync(urlGenerator(1)); - var body = res.Should().HaveStatusCode(200) - .And.HaveJsonBody().Which; - timeline = body; - lastModifiedTime = body.LastModified; - uniqueId = body.UniqueId; - } - - { - using var req = new HttpRequestMessage - { - RequestUri = new Uri(client.BaseAddress, urlGenerator(1)), - Method = HttpMethod.Get, - }; - req.Headers.IfModifiedSince = lastModifiedTime.AddSeconds(1); - var res = await client.SendAsync(req); - res.Should().HaveStatusCode(304); - } - - { - using var req = new HttpRequestMessage - { - RequestUri = new Uri(client.BaseAddress, urlGenerator(1)), - Method = HttpMethod.Get, - }; - req.Headers.IfModifiedSince = lastModifiedTime.AddSeconds(-1); - var res = await client.SendAsync(req); - res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.Should().BeEquivalentTo(timeline); - } - - { - var res = await client.GetAsync(urlGenerator(1, null, - new Dictionary { { "ifModifiedSince", lastModifiedTime.AddSeconds(1).ToString("s", CultureInfo.InvariantCulture) } })); - res.Should().HaveStatusCode(304); - } - - { - var res = await client.GetAsync(urlGenerator(1, null, - new Dictionary { { "ifModifiedSince", lastModifiedTime.AddSeconds(-1).ToString("s", CultureInfo.InvariantCulture) } })); - res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.Should().BeEquivalentTo(timeline); - } - - { - var res = await client.GetAsync(urlGenerator(1, null, - new Dictionary { { "ifModifiedSince", lastModifiedTime.AddSeconds(1).ToString("s", CultureInfo.InvariantCulture) }, - {"checkUniqueId", uniqueId } })); - res.Should().HaveStatusCode(304); - } - - { - var testUniqueId = (uniqueId[0] == 'a' ? "b" : "a") + uniqueId[1..]; - var res = await client.GetAsync(urlGenerator(1, null, - new Dictionary { { "ifModifiedSince", lastModifiedTime.AddSeconds(1).ToString("s", CultureInfo.InvariantCulture) }, - {"checkUniqueId", testUniqueId } })); - res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.Should().BeEquivalentTo(timeline); - } - } - - [Theory] - [MemberData(nameof(TimelineUrlGeneratorData))] - public async Task Title(TimelineUrlGenerator urlGenerator) - { - using var client = await CreateClientAsUser(); - - { - var res = await client.GetAsync(urlGenerator(1)); - var timeline = res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which; - timeline.Title.Should().Be(timeline.Name); - } - - { - var res = await client.PatchAsJsonAsync(urlGenerator(1), new TimelinePatchRequest { Title = "atitle" }); - res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.Title.Should().Be("atitle"); - } - - { - var res = await client.GetAsync(urlGenerator(1)); - res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.Title.Should().Be("atitle"); - } - } - - [Fact] - public async Task ChangeName() - { - { - using var client = await CreateDefaultClient(); - var res = await client.PostAsJsonAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "t1", NewName = "tttttttt" }); - res.Should().HaveStatusCode(401); - } - - { - using var client = await CreateClientAs(2); - var res = await client.PostAsJsonAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "t1", NewName = "tttttttt" }); - res.Should().HaveStatusCode(403); - } - - using (var client = await CreateClientAsUser()) - { - { - var res = await client.PostAsJsonAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "!!!", NewName = "tttttttt" }); - res.Should().BeInvalidModel(); - } - - { - var res = await client.PostAsJsonAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "ttt", NewName = "!!!!" }); - res.Should().BeInvalidModel(); - } - - { - var res = await client.PostAsJsonAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "ttttt", NewName = "tttttttt" }); - res.Should().HaveStatusCode(400).And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.TimelineController.NotExist); - } - - { - var res = await client.PostAsJsonAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "t1", NewName = "newt" }); - res.Should().HaveStatusCode(200).And.HaveJsonBody().Which.Name.Should().Be("newt"); - } - - { - var res = await client.GetAsync("timelines/t1"); - res.Should().HaveStatusCode(404); - } - - { - var res = await client.GetAsync("timelines/newt"); - res.Should().HaveStatusCode(200).And.HaveJsonBody().Which.Name.Should().Be("newt"); - } - } - } - - [Theory] - [MemberData(nameof(TimelineUrlGeneratorData))] - public async Task PostDataETag(TimelineUrlGenerator urlGenerator) - { - using var client = await CreateClientAsUser(); - - long id; - string etag; - - { - var res = await client.PostAsJsonAsync(urlGenerator(1, "posts"), new TimelinePostCreateRequest - { - Content = new TimelinePostCreateRequestContent - { - Type = TimelinePostContentTypes.Image, - Data = Convert.ToBase64String(ImageHelper.CreatePngWithSize(100, 50)) - } - }); - res.Should().HaveStatusCode(200); - var body = await res.ReadBodyAsJsonAsync(); - body.Content.ETag.Should().NotBeNullOrEmpty(); - - id = body.Id; - etag = body.Content.ETag; - } - - { - var res = await client.GetAsync(urlGenerator(1, $"posts/{id}/data")); - res.Should().HaveStatusCode(200); - res.Headers.ETag.Should().NotBeNull(); - res.Headers.ETag.ToString().Should().Be(etag); - } - } - } -} diff --git a/Timeline.Tests/IntegratedTests/TokenTest.cs b/Timeline.Tests/IntegratedTests/TokenTest.cs deleted file mode 100644 index 480d66cd..00000000 --- a/Timeline.Tests/IntegratedTests/TokenTest.cs +++ /dev/null @@ -1,165 +0,0 @@ -using FluentAssertions; -using Microsoft.Extensions.DependencyInjection; -using System.Collections.Generic; -using System.Net.Http; -using System.Threading.Tasks; -using Timeline.Models; -using Timeline.Models.Http; -using Timeline.Services; -using Timeline.Tests.Helpers; -using Xunit; - -namespace Timeline.Tests.IntegratedTests -{ - public class TokenTest : IntegratedTestBase - { - private const string CreateTokenUrl = "token/create"; - private const string VerifyTokenUrl = "token/verify"; - - private static async Task CreateUserTokenAsync(HttpClient client, string username, string password, int? expireOffset = null) - { - var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = username, Password = password, Expire = expireOffset }); - return response.Should().HaveStatusCode(200) - .And.HaveJsonBody().Which; - } - - public static IEnumerable CreateToken_InvalidModel_Data() - { - yield return new[] { null, "p", null }; - yield return new[] { "u", null, null }; - yield return new object[] { "u", "p", 2000 }; - yield return new object[] { "u", "p", -1 }; - } - - [Theory] - [MemberData(nameof(CreateToken_InvalidModel_Data))] - public async Task CreateToken_InvalidModel(string username, string password, int expire) - { - using var client = await CreateDefaultClient(); - (await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest - { - Username = username, - Password = password, - Expire = expire - })).Should().BeInvalidModel(); - } - - public static IEnumerable CreateToken_UserCredential_Data() - { - yield return new[] { "usernotexist", "p" }; - yield return new[] { "user1", "???" }; - } - - [Theory] - [MemberData(nameof(CreateToken_UserCredential_Data))] - public async void CreateToken_UserCredential(string username, string password) - { - using var client = await CreateDefaultClient(); - var response = await client.PostAsJsonAsync(CreateTokenUrl, - new CreateTokenRequest { Username = username, Password = password }); - response.Should().HaveStatusCode(400) - .And.HaveCommonBody() - .Which.Code.Should().Be(ErrorCodes.TokenController.Create_BadCredential); - } - - [Fact] - public async Task CreateToken_Success() - { - using var client = await CreateDefaultClient(); - var response = await client.PostAsJsonAsync(CreateTokenUrl, - new CreateTokenRequest { Username = "user1", Password = "user1pw" }); - var body = response.Should().HaveStatusCode(200) - .And.HaveJsonBody().Which; - body.Token.Should().NotBeNullOrWhiteSpace(); - body.User.Should().BeEquivalentTo(UserInfos[1]); - } - - [Fact] - public async Task VerifyToken_InvalidModel() - { - using var client = await CreateDefaultClient(); - (await client.PostAsJsonAsync(VerifyTokenUrl, - new VerifyTokenRequest { Token = null })).Should().BeInvalidModel(); - } - - [Fact] - public async Task VerifyToken_BadFormat() - { - using var client = await CreateDefaultClient(); - var response = await client.PostAsJsonAsync(VerifyTokenUrl, - new VerifyTokenRequest { Token = "bad token hahaha" }); - response.Should().HaveStatusCode(400) - .And.HaveCommonBody() - .Which.Code.Should().Be(ErrorCodes.TokenController.Verify_BadFormat); - } - - [Fact] - public async Task VerifyToken_OldVersion() - { - using var client = await CreateDefaultClient(); - var token = (await CreateUserTokenAsync(client, "user1", "user1pw")).Token; - - using (var scope = TestApp.Host.Services.CreateScope()) // UserService is scoped. - { - // create a user for test - var userService = scope.ServiceProvider.GetRequiredService(); - await userService.ModifyUser("user1", new User { Password = "user1pw" }); - } - - (await client.PostAsJsonAsync(VerifyTokenUrl, - new VerifyTokenRequest { Token = token })) - .Should().HaveStatusCode(400) - .And.HaveCommonBody() - .Which.Code.Should().Be(ErrorCodes.TokenController.Verify_OldVersion); - } - - [Fact] - public async Task VerifyToken_UserNotExist() - { - using var client = await CreateDefaultClient(); - var token = (await CreateUserTokenAsync(client, "user1", "user1pw")).Token; - - using (var scope = TestApp.Host.Services.CreateScope()) // UserDeleteService is scoped. - { - var userService = scope.ServiceProvider.GetRequiredService(); - await userService.DeleteUser("user1"); - } - - (await client.PostAsJsonAsync(VerifyTokenUrl, - new VerifyTokenRequest { Token = token })) - .Should().HaveStatusCode(400) - .And.HaveCommonBody() - .Which.Code.Should().Be(ErrorCodes.TokenController.Verify_UserNotExist); - } - - //[Fact] - //public async Task VerifyToken_Expired() - //{ - // using (var client = await CreateClientWithNoAuth()) - // { - // // I can only control the token expired time but not current time - // // because verify logic is encapsuled in other library. - // var mockClock = _factory.GetTestClock(); - // mockClock.MockCurrentTime = DateTime.Now - TimeSpan.FromDays(2); - // var token = (await client.CreateUserTokenAsync(MockUsers.UserUsername, MockUsers.UserPassword, 1)).Token; - // var response = await client.PostAsJsonAsync(VerifyTokenUrl, - // new VerifyTokenRequest { Token = token }); - // response.Should().HaveStatusCodeBadRequest() - // .And.Should().HaveBodyAsCommonResponseWithCode(TokenController.ErrorCodes.Verify_Expired); - // mockClock.MockCurrentTime = null; - // } - //} - - [Fact] - public async Task VerifyToken_Success() - { - using var client = await CreateDefaultClient(); - var createTokenResult = await CreateUserTokenAsync(client, "user1", "user1pw"); - var response = await client.PostAsJsonAsync(VerifyTokenUrl, - new VerifyTokenRequest { Token = createTokenResult.Token }); - response.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.User.Should().BeEquivalentTo(UserInfos[1]); - } - } -} diff --git a/Timeline.Tests/IntegratedTests/UnknownEndpointTest.cs b/Timeline.Tests/IntegratedTests/UnknownEndpointTest.cs deleted file mode 100644 index 732232e2..00000000 --- a/Timeline.Tests/IntegratedTests/UnknownEndpointTest.cs +++ /dev/null @@ -1,21 +0,0 @@ -using FluentAssertions; -using System.Threading.Tasks; -using Timeline.Models.Http; -using Timeline.Tests.Helpers; -using Xunit; - -namespace Timeline.Tests.IntegratedTests -{ - public class UnknownEndpointTest : IntegratedTestBase - { - [Fact] - public async Task UnknownEndpoint() - { - using var client = await CreateDefaultClient(); - var res = await client.GetAsync("unknownEndpoint"); - res.Should().HaveStatusCode(400) - .And.HaveCommonBody() - .Which.Code.Should().Be(ErrorCodes.Common.UnknownEndpoint); - } - } -} diff --git a/Timeline.Tests/IntegratedTests/UserAvatarTest.cs b/Timeline.Tests/IntegratedTests/UserAvatarTest.cs deleted file mode 100644 index f2796005..00000000 --- a/Timeline.Tests/IntegratedTests/UserAvatarTest.cs +++ /dev/null @@ -1,251 +0,0 @@ -using FluentAssertions; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.Formats.Gif; -using SixLabors.ImageSharp.Formats.Jpeg; -using SixLabors.ImageSharp.Formats.Png; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Net.Mime; -using System.Threading.Tasks; -using Timeline.Models.Http; -using Timeline.Services; -using Timeline.Tests.Helpers; -using Xunit; - -namespace Timeline.Tests.IntegratedTests -{ - public class UserAvatarTest : IntegratedTestBase - { - [Fact] - public async Task Test() - { - Avatar mockAvatar = new Avatar - { - Data = ImageHelper.CreatePngWithSize(100, 100), - Type = PngFormat.Instance.DefaultMimeType - }; - - using (var client = await CreateClientAsUser()) - { - { - var res = await client.GetAsync("users/usernotexist/avatar"); - res.Should().HaveStatusCode(404) - .And.HaveCommonBody() - .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); - } - - var env = TestApp.Host.Services.GetRequiredService(); - var defaultAvatarData = await File.ReadAllBytesAsync(Path.Combine(env.ContentRootPath, "default-avatar.png")); - - async Task GetReturnDefault(string username = "user1") - { - var res = await client.GetAsync($"users/{username}/avatar"); - res.Should().HaveStatusCode(200); - res.Content.Headers.ContentType.MediaType.Should().Be("image/png"); - var body = await res.Content.ReadAsByteArrayAsync(); - body.Should().Equal(defaultAvatarData); - } - - { - var res = await client.GetAsync("users/user1/avatar"); - res.Should().HaveStatusCode(200); - res.Content.Headers.ContentType.MediaType.Should().Be("image/png"); - var body = await res.Content.ReadAsByteArrayAsync(); - body.Should().Equal(defaultAvatarData); - } - - await CacheTestHelper.TestCache(client, "users/user1/avatar"); - - await GetReturnDefault("admin"); - - { - using var content = new ByteArrayContent(new[] { (byte)0x00 }); - content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); - var res = await client.PutAsync("users/user1/avatar", content); - res.Should().BeInvalidModel(); - } - - { - using var content = new ByteArrayContent(new[] { (byte)0x00 }); - content.Headers.ContentLength = 1; - var res = await client.PutAsync("users/user1/avatar", content); - res.Should().BeInvalidModel(); - } - - { - using var content = new ByteArrayContent(new[] { (byte)0x00 }); - content.Headers.ContentLength = 0; - content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); - var res = await client.PutAsync("users/user1/avatar", content); - res.Should().BeInvalidModel(); - } - - { - var res = await client.PutByteArrayAsync("users/user1/avatar", new[] { (byte)0x00 }, "image/notaccept"); - res.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); - } - - { - using var content = new ByteArrayContent(new[] { (byte)0x00 }); - content.Headers.ContentLength = 1000 * 1000 * 11; - content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); - var res = await client.PutAsync("users/user1/avatar", content); - res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Content.TooBig); - } - - { - using var content = new ByteArrayContent(new[] { (byte)0x00 }); - content.Headers.ContentLength = 2; - content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); - var res = await client.PutAsync("users/user1/avatar", content); - res.Should().BeInvalidModel(); - } - - { - using var content = new ByteArrayContent(new[] { (byte)0x00, (byte)0x01 }); - content.Headers.ContentLength = 1; - content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); - var res = await client.PutAsync("users/user1/avatar", content); - res.Should().BeInvalidModel(); - } - - { - var res = await client.PutByteArrayAsync("users/user1/avatar", new[] { (byte)0x00 }, "image/png"); - res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.UserAvatar.BadFormat_CantDecode); - } - - { - var res = await client.PutByteArrayAsync("users/user1/avatar", mockAvatar.Data, "image/jpeg"); - res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.UserAvatar.BadFormat_UnmatchedFormat); - } - - { - var res = await client.PutByteArrayAsync("users/user1/avatar", ImageHelper.CreatePngWithSize(100, 200), "image/png"); - res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.UserAvatar.BadFormat_BadSize); - } - - { - var res = await client.PutByteArrayAsync("users/user1/avatar", mockAvatar.Data, mockAvatar.Type); - res.Should().HaveStatusCode(HttpStatusCode.OK); - - var res2 = await client.GetAsync("users/user1/avatar"); - res2.Should().HaveStatusCode(200); - res2.Content.Headers.ContentType.MediaType.Should().Be(mockAvatar.Type); - var body = await res2.Content.ReadAsByteArrayAsync(); - body.Should().Equal(mockAvatar.Data); - } - - IEnumerable<(string, IImageFormat)> formats = new (string, IImageFormat)[] - { - ("image/jpeg", JpegFormat.Instance), - ("image/gif", GifFormat.Instance), - ("image/png", PngFormat.Instance), - }; - - foreach ((var mimeType, var format) in formats) - { - var res = await client.PutByteArrayAsync("users/user1/avatar", ImageHelper.CreateImageWithSize(100, 100, format), mimeType); - res.Should().HaveStatusCode(HttpStatusCode.OK); - } - - { - var res = await client.PutByteArrayAsync("users/admin/avatar", new[] { (byte)0x00 }, "image/png"); - res.Should().HaveStatusCode(HttpStatusCode.Forbidden) - .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Forbid); - } - - { - var res = await client.DeleteAsync("users/admin/avatar"); - res.Should().HaveStatusCode(HttpStatusCode.Forbidden) - .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Forbid); - } - - for (int i = 0; i < 2; i++) // double delete should work. - { - var res = await client.DeleteAsync("users/user1/avatar"); - res.Should().HaveStatusCode(200); - await GetReturnDefault(); - } - } - - // Authorization check. - using (var client = await CreateClientAsAdministrator()) - { - { - var res = await client.PutByteArrayAsync("users/user1/avatar", mockAvatar.Data, mockAvatar.Type); - res.Should().HaveStatusCode(HttpStatusCode.OK); - } - - { - var res = await client.DeleteAsync("users/user1/avatar"); - res.Should().HaveStatusCode(HttpStatusCode.OK); - } - - { - var res = await client.PutByteArrayAsync("users/usernotexist/avatar", new[] { (byte)0x00 }, "image/png"); - res.Should().HaveStatusCode(400) - .And.HaveCommonBody() - .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); - } - - { - var res = await client.DeleteAsync("users/usernotexist/avatar"); - res.Should().HaveStatusCode(400) - .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); - } - } - - // bad username check - using (var client = await CreateClientAsAdministrator()) - { - { - var res = await client.GetAsync("users/u!ser/avatar"); - res.Should().BeInvalidModel(); - } - - { - var res = await client.PutByteArrayAsync("users/u!ser/avatar", ImageHelper.CreatePngWithSize(100, 100), "image/png"); - res.Should().BeInvalidModel(); - } - - { - var res = await client.DeleteAsync("users/u!ser/avatar"); - res.Should().BeInvalidModel(); - } - } - } - - [Fact] - public async Task AvatarPutReturnETag() - { - using var client = await CreateClientAsUser(); - - EntityTagHeaderValue etag; - - { - var image = ImageHelper.CreatePngWithSize(100, 100); - var res = await client.PutByteArrayAsync("users/user1/avatar", image, PngFormat.Instance.DefaultMimeType); - res.Should().HaveStatusCode(200); - etag = res.Headers.ETag; - etag.Should().NotBeNull(); - etag.Tag.Should().NotBeNullOrEmpty(); - } - - { - var res = await client.GetAsync("users/user1/avatar"); - res.Should().HaveStatusCode(200); - res.Headers.ETag.Should().Be(etag); - res.Headers.ETag.Tag.Should().Be(etag.Tag); - } - } - } -} \ No newline at end of file diff --git a/Timeline.Tests/IntegratedTests/UserTest.cs b/Timeline.Tests/IntegratedTests/UserTest.cs deleted file mode 100644 index 9dfcc6a5..00000000 --- a/Timeline.Tests/IntegratedTests/UserTest.cs +++ /dev/null @@ -1,447 +0,0 @@ -using FluentAssertions; -using System.Collections.Generic; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using Timeline.Models.Http; -using Timeline.Tests.Helpers; -using Xunit; - -namespace Timeline.Tests.IntegratedTests -{ - public class UserTest : IntegratedTestBase - { - [Fact] - public void UserListShouldHaveUniqueId() - { - foreach (var user in UserInfos) - { - user.UniqueId.Should().NotBeNullOrWhiteSpace(); - } - } - - [Fact] - public async Task GetList_NoAuth() - { - using var client = await CreateDefaultClient(); - var res = await client.GetAsync("users"); - res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.Should().BeEquivalentTo(UserInfos); - } - - [Fact] - public async Task GetList_User() - { - using var client = await CreateClientAsUser(); - var res = await client.GetAsync("users"); - res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.Should().BeEquivalentTo(UserInfos); - } - - [Fact] - public async Task GetList_Admin() - { - using var client = await CreateClientAsAdministrator(); - var res = await client.GetAsync("users"); - res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.Should().BeEquivalentTo(UserInfos); - } - - [Fact] - public async Task Get_NoAuth() - { - using var client = await CreateDefaultClient(); - var res = await client.GetAsync($"users/admin"); - res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.Should().BeEquivalentTo(UserInfos[0]); - } - - [Fact] - public async Task Get_User() - { - using var client = await CreateClientAsUser(); - var res = await client.GetAsync($"users/admin"); - res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.Should().BeEquivalentTo(UserInfos[0]); - } - - [Fact] - public async Task Get_Admin() - { - using var client = await CreateClientAsAdministrator(); - var res = await client.GetAsync($"users/user1"); - res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.Should().BeEquivalentTo(UserInfos[1]); - } - - [Fact] - public async Task Get_InvalidModel() - { - using var client = await CreateClientAsUser(); - var res = await client.GetAsync("users/aaa!a"); - res.Should().BeInvalidModel(); - } - - [Fact] - public async Task Get_404() - { - using var client = await CreateClientAsUser(); - var res = await client.GetAsync("users/usernotexist"); - res.Should().HaveStatusCode(404) - .And.HaveCommonBody(ErrorCodes.UserCommon.NotExist); - } - - [Fact] - public async Task Patch_User() - { - using var client = await CreateClientAsUser(); - { - var res = await client.PatchAsJsonAsync("users/user1", - new UserPatchRequest { Nickname = "aaa" }); - res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.Nickname.Should().Be("aaa"); - } - - { - var res = await client.GetAsync("users/user1"); - res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.Nickname.Should().Be("aaa"); - } - } - - [Fact] - public async Task Patch_Admin() - { - using var client = await CreateClientAsAdministrator(); - using var userClient = await CreateClientAsUser(); - - { - var res = await client.PatchAsJsonAsync("users/user1", - new UserPatchRequest - { - Username = "newuser", - Password = "newpw", - Administrator = true, - Nickname = "aaa" - }); - var body = res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which; - body.Administrator.Should().Be(true); - body.Nickname.Should().Be("aaa"); - } - - { - var res = await client.GetAsync("users/newuser"); - var body = res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which; - body.Administrator.Should().Be(true); - body.Nickname.Should().Be("aaa"); - } - - { - // Token should expire. - var res = await userClient.GetAsync("testing/auth/Authorize"); - res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); - } - - { - // Check password. - (await CreateClientWithCredential("newuser", "newpw")).Dispose(); - } - } - - [Fact] - public async Task Patch_NotExist() - { - using var client = await CreateClientAsAdministrator(); - var res = await client.PatchAsJsonAsync("users/usernotexist", new UserPatchRequest { }); - res.Should().HaveStatusCode(404) - .And.HaveCommonBody() - .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); - } - - [Fact] - public async Task Patch_InvalidModel() - { - using var client = await CreateClientAsAdministrator(); - var res = await client.PatchAsJsonAsync("users/aaa!a", new UserPatchRequest { }); - res.Should().BeInvalidModel(); - } - - public static IEnumerable Patch_InvalidModel_Body_Data() - { - yield return new[] { new UserPatchRequest { Username = "aaa!a" } }; - yield return new[] { new UserPatchRequest { Password = "" } }; - yield return new[] { new UserPatchRequest { Nickname = new string('a', 50) } }; - } - - [Theory] - [MemberData(nameof(Patch_InvalidModel_Body_Data))] - public async Task Patch_InvalidModel_Body(UserPatchRequest body) - { - using var client = await CreateClientAsAdministrator(); - var res = await client.PatchAsJsonAsync("users/user1", body); - res.Should().BeInvalidModel(); - } - - [Fact] - public async Task Patch_UsernameConflict() - { - using var client = await CreateClientAsAdministrator(); - var res = await client.PatchAsJsonAsync("users/user1", new UserPatchRequest { Username = "admin" }); - res.Should().HaveStatusCode(400) - .And.HaveCommonBody(ErrorCodes.UserController.UsernameConflict); - } - - [Fact] - public async Task Patch_NoAuth_Unauthorized() - { - using var client = await CreateDefaultClient(); - var res = await client.PatchAsJsonAsync("users/user1", new UserPatchRequest { Nickname = "aaa" }); - res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); - } - - [Fact] - public async Task Patch_User_Forbid() - { - using var client = await CreateClientAsUser(); - var res = await client.PatchAsJsonAsync("users/admin", new UserPatchRequest { Nickname = "aaa" }); - res.Should().HaveStatusCode(HttpStatusCode.Forbidden); - } - - [Fact] - public async Task Patch_Username_Forbid() - { - using var client = await CreateClientAsUser(); - var res = await client.PatchAsJsonAsync("users/user1", new UserPatchRequest { Username = "aaa" }); - res.Should().HaveStatusCode(HttpStatusCode.Forbidden); - } - - [Fact] - public async Task Patch_Password_Forbid() - { - using var client = await CreateClientAsUser(); - var res = await client.PatchAsJsonAsync("users/user1", new UserPatchRequest { Password = "aaa" }); - res.Should().HaveStatusCode(HttpStatusCode.Forbidden); - } - - [Fact] - public async Task Patch_Administrator_Forbid() - { - using var client = await CreateClientAsUser(); - var res = await client.PatchAsJsonAsync("users/user1", new UserPatchRequest { Administrator = true }); - res.Should().HaveStatusCode(HttpStatusCode.Forbidden); - } - - [Fact] - public async Task Delete_Deleted() - { - using var client = await CreateClientAsAdministrator(); - { - var res = await client.DeleteAsync("users/user1"); - res.Should().BeDelete(true); - } - - { - var res = await client.GetAsync("users/user1"); - res.Should().HaveStatusCode(404); - } - } - - [Fact] - public async Task Delete_NotExist() - { - using var client = await CreateClientAsAdministrator(); - var res = await client.DeleteAsync("users/usernotexist"); - res.Should().BeDelete(false); - } - - [Fact] - public async Task Delete_InvalidModel() - { - using var client = await CreateClientAsAdministrator(); - var res = await client.DeleteAsync("users/aaa!a"); - res.Should().BeInvalidModel(); - } - - [Fact] - public async Task Delete_NoAuth_Unauthorized() - { - using var client = await CreateDefaultClient(); - var res = await client.DeleteAsync("users/aaa!a"); - res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); - } - - [Fact] - public async Task Delete_User_Forbid() - { - using var client = await CreateClientAsUser(); - var res = await client.DeleteAsync("users/aaa!a"); - res.Should().HaveStatusCode(HttpStatusCode.Forbidden); - } - - private const string createUserUrl = "userop/createuser"; - - [Fact] - public async Task Op_CreateUser() - { - using var client = await CreateClientAsAdministrator(); - { - var res = await client.PostAsJsonAsync(createUserUrl, new CreateUserRequest - { - Username = "aaa", - Password = "bbb", - Administrator = true, - Nickname = "ccc" - }); - var body = res.Should().HaveStatusCode(200) - .And.HaveJsonBody().Which; - body.Username.Should().Be("aaa"); - body.Nickname.Should().Be("ccc"); - body.Administrator.Should().BeTrue(); - } - { - var res = await client.GetAsync("users/aaa"); - var body = res.Should().HaveStatusCode(200) - .And.HaveJsonBody().Which; - body.Username.Should().Be("aaa"); - body.Nickname.Should().Be("ccc"); - body.Administrator.Should().BeTrue(); - } - { - // Test password. - (await CreateClientWithCredential("aaa", "bbb")).Dispose(); - } - } - - public static IEnumerable Op_CreateUser_InvalidModel_Data() - { - yield return new[] { new CreateUserRequest { Username = "aaa", Password = "bbb" } }; - yield return new[] { new CreateUserRequest { Username = "aaa", Administrator = true } }; - yield return new[] { new CreateUserRequest { Password = "bbb", Administrator = true } }; - yield return new[] { new CreateUserRequest { Username = "a!a", Password = "bbb", Administrator = true } }; - yield return new[] { new CreateUserRequest { Username = "aaa", Password = "", Administrator = true } }; - yield return new[] { new CreateUserRequest { Username = "aaa", Password = "bbb", Administrator = true, Nickname = new string('a', 40) } }; - } - - [Theory] - [MemberData(nameof(Op_CreateUser_InvalidModel_Data))] - public async Task Op_CreateUser_InvalidModel(CreateUserRequest body) - { - using var client = await CreateClientAsAdministrator(); - { - var res = await client.PostAsJsonAsync(createUserUrl, body); - res.Should().BeInvalidModel(); - } - } - - [Fact] - public async Task Op_CreateUser_UsernameConflict() - { - using var client = await CreateClientAsAdministrator(); - { - var res = await client.PostAsJsonAsync(createUserUrl, new CreateUserRequest - { - Username = "user1", - Password = "bbb", - Administrator = false - }); - res.Should().HaveStatusCode(400) - .And.HaveCommonBody(ErrorCodes.UserController.UsernameConflict); - } - } - - [Fact] - public async Task Op_CreateUser_NoAuth_Unauthorized() - { - using var client = await CreateDefaultClient(); - { - var res = await client.PostAsJsonAsync(createUserUrl, new CreateUserRequest - { - Username = "aaa", - Password = "bbb", - Administrator = false - }); - res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); - } - } - - [Fact] - public async Task Op_CreateUser_User_Forbid() - { - using var client = await CreateClientAsUser(); - { - var res = await client.PostAsJsonAsync(createUserUrl, new CreateUserRequest - { - Username = "aaa", - Password = "bbb", - Administrator = false - }); - res.Should().HaveStatusCode(HttpStatusCode.Forbidden); - } - } - - private const string changePasswordUrl = "userop/changepassword"; - - [Fact] - public async Task Op_ChangePassword() - { - using var client = await CreateClientAsUser(); - { - var res = await client.PostAsJsonAsync(changePasswordUrl, - new ChangePasswordRequest { OldPassword = "user1pw", NewPassword = "newpw" }); - res.Should().HaveStatusCode(200); - } - { - var res = await client.PatchAsJsonAsync("users/user1", new UserPatchRequest { }); - res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); - } - { - (await CreateClientWithCredential("user1", "newpw")).Dispose(); - } - } - - public static IEnumerable Op_ChangePassword_InvalidModel_Data() - { - yield return new[] { null, "ppp" }; - yield return new[] { "ppp", null }; - } - - [Theory] - [MemberData(nameof(Op_ChangePassword_InvalidModel_Data))] - public async Task Op_ChangePassword_InvalidModel(string oldPassword, string newPassword) - { - using var client = await CreateClientAsUser(); - var res = await client.PostAsJsonAsync(changePasswordUrl, - new ChangePasswordRequest { OldPassword = oldPassword, NewPassword = newPassword }); - res.Should().BeInvalidModel(); - } - - [Fact] - public async Task Op_ChangePassword_BadOldPassword() - { - using var client = await CreateClientAsUser(); - var res = await client.PostAsJsonAsync(changePasswordUrl, new ChangePasswordRequest { OldPassword = "???", NewPassword = "???" }); - res.Should().HaveStatusCode(400) - .And.HaveCommonBody(ErrorCodes.UserController.ChangePassword_BadOldPassword); - } - - [Fact] - public async Task Op_ChangePassword_NoAuth_Unauthorized() - { - using var client = await CreateDefaultClient(); - var res = await client.PostAsJsonAsync(changePasswordUrl, new ChangePasswordRequest { OldPassword = "???", NewPassword = "???" }); - res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); - } - } -} diff --git a/Timeline.Tests/PasswordGenerator.cs b/Timeline.Tests/PasswordGenerator.cs deleted file mode 100644 index 863439b5..00000000 --- a/Timeline.Tests/PasswordGenerator.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Timeline.Services; -using Xunit; -using Xunit.Abstractions; - -namespace Timeline.Tests -{ - public class PasswordGenerator - { - private readonly ITestOutputHelper _output; - - public PasswordGenerator(ITestOutputHelper output) - { - _output = output; - } - - [Fact] - public void Generate() - { - var service = new PasswordService(); - _output.WriteLine(service.HashPassword("crupest")); - } - } -} diff --git a/Timeline.Tests/Properties/launchSettings.json b/Timeline.Tests/Properties/launchSettings.json deleted file mode 100644 index f3ee419d..00000000 --- a/Timeline.Tests/Properties/launchSettings.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} diff --git a/Timeline.Tests/Services/TimelineServiceTest.cs b/Timeline.Tests/Services/TimelineServiceTest.cs deleted file mode 100644 index 5a774b78..00000000 --- a/Timeline.Tests/Services/TimelineServiceTest.cs +++ /dev/null @@ -1,329 +0,0 @@ -using FluentAssertions; -using Microsoft.Extensions.Logging.Abstractions; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Timeline.Entities; -using Timeline.Models; -using Timeline.Services; -using Timeline.Services.Exceptions; -using Timeline.Tests.Helpers; -using Xunit; - -namespace Timeline.Tests.Services -{ - public class TimelineServiceTest : IAsyncLifetime, IDisposable - { - private readonly TestDatabase _testDatabase = new TestDatabase(); - - private DatabaseContext _databaseContext; - - private readonly PasswordService _passwordService = new PasswordService(); - - private readonly ETagGenerator _eTagGenerator = new ETagGenerator(); - - private readonly ImageValidator _imageValidator = new ImageValidator(); - - private readonly TestClock _clock = new TestClock(); - - private DataManager _dataManager; - - private UserService _userService; - - private TimelineService _timelineService; - - private UserDeleteService _userDeleteService; - - public TimelineServiceTest() - { - } - - public async Task InitializeAsync() - { - await _testDatabase.InitializeAsync(); - _databaseContext = _testDatabase.CreateContext(); - _dataManager = new DataManager(_databaseContext, _eTagGenerator); - _userService = new UserService(NullLogger.Instance, _databaseContext, _passwordService, _clock); - _timelineService = new TimelineService(NullLogger.Instance, _databaseContext, _dataManager, _userService, _imageValidator, _clock); - _userDeleteService = new UserDeleteService(NullLogger.Instance, _databaseContext, _timelineService); - } - - public async Task DisposeAsync() - { - await _testDatabase.DisposeAsync(); - await _databaseContext.DisposeAsync(); - } - - public void Dispose() - { - _eTagGenerator.Dispose(); - } - - [Theory] - [InlineData("@user")] - [InlineData("tl")] - public async Task Timeline_GetLastModified(string timelineName) - { - var time = _clock.ForwardCurrentTime(); - - var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); - if (!isPersonal) - await _timelineService.CreateTimeline(timelineName, await _userService.GetUserIdByUsername("user")); - - var t = await _timelineService.GetTimelineLastModifiedTime(timelineName); - - t.Should().Be(time); - } - - [Theory] - [InlineData("@user")] - [InlineData("tl")] - public async Task Timeline_GetUnqiueId(string timelineName) - { - var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); - if (!isPersonal) - await _timelineService.CreateTimeline(timelineName, await _userService.GetUserIdByUsername("user")); - - var uniqueId = await _timelineService.GetTimelineUniqueId(timelineName); - - uniqueId.Should().NotBeNullOrEmpty(); - } - - [Theory] - [InlineData("@user")] - [InlineData("tl")] - public async Task Timeline_LastModified(string timelineName) - { - var initTime = _clock.ForwardCurrentTime(); - - void Check(Models.Timeline timeline) - { - timeline.NameLastModified.Should().Be(initTime); - timeline.LastModified.Should().Be(_clock.GetCurrentTime()); - } - - async Task GetAndCheck() - { - Check(await _timelineService.GetTimeline(timelineName)); - } - - var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); - if (!isPersonal) - Check(await _timelineService.CreateTimeline(timelineName, await _userService.GetUserIdByUsername("user"))); - - await GetAndCheck(); - - _clock.ForwardCurrentTime(); - await _timelineService.ChangeProperty(timelineName, new TimelineChangePropertyRequest { Visibility = TimelineVisibility.Public }); - await GetAndCheck(); - - _clock.ForwardCurrentTime(); - await _timelineService.ChangeMember(timelineName, new List { "admin" }, null); - await GetAndCheck(); - } - - [Theory] - [InlineData("@user")] - [InlineData("tl")] - public async Task GetPosts_ModifiedSince(string timelineName) - { - _clock.ForwardCurrentTime(); - - var userId = await _userService.GetUserIdByUsername("user"); - - var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); - if (!isPersonal) - await _timelineService.CreateTimeline(timelineName, userId); - - var postContentList = new string[] { "a", "b", "c", "d" }; - - DateTime testPoint = new DateTime(); - - foreach (var (content, index) in postContentList.Select((v, i) => (v, i))) - { - var t = _clock.ForwardCurrentTime(); - if (index == 1) - testPoint = t; - await _timelineService.CreateTextPost(timelineName, userId, content, null); - } - - var posts = await _timelineService.GetPosts(timelineName, testPoint); - posts.Should().HaveCount(3) - .And.Subject.Select(p => (p.Content as TextTimelinePostContent).Text).Should().Equal(postContentList.Skip(1)); - } - - [Theory] - [InlineData("@user")] - [InlineData("tl")] - public async Task GetPosts_IncludeDeleted(string timelineName) - { - var userId = await _userService.GetUserIdByUsername("user"); - - var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); - if (!isPersonal) - await _timelineService.CreateTimeline(timelineName, userId); - - var postContentList = new string[] { "a", "b", "c", "d" }; - - foreach (var content in postContentList) - { - await _timelineService.CreateTextPost(timelineName, userId, content, null); - } - - var posts = await _timelineService.GetPosts(timelineName); - posts.Should().HaveCount(4); - posts.Select(p => p.Deleted).Should().Equal(Enumerable.Repeat(false, posts.Count)); - posts.Select(p => ((TextTimelinePostContent)p.Content).Text).Should().Equal(postContentList); - - foreach (var id in new long[] { posts[0].Id, posts[2].Id }) - { - await _timelineService.DeletePost(timelineName, id); - } - - posts = await _timelineService.GetPosts(timelineName); - posts.Should().HaveCount(2); - posts.Select(p => p.Deleted).Should().Equal(Enumerable.Repeat(false, posts.Count)); - posts.Select(p => ((TextTimelinePostContent)p.Content).Text).Should().Equal(new string[] { "b", "d" }); - - posts = await _timelineService.GetPosts(timelineName, includeDeleted: true); - posts.Should().HaveCount(4); - posts.Select(p => p.Deleted).Should().Equal(new bool[] { true, false, true, false }); - posts.Where(p => !p.Deleted).Select(p => ((TextTimelinePostContent)p.Content).Text).Should().Equal(new string[] { "b", "d" }); - } - - [Theory] - [InlineData("@admin")] - [InlineData("tl")] - public async Task GetPosts_ModifiedSince_UsernameChange(string timelineName) - { - var time1 = _clock.ForwardCurrentTime(); - - var userId = await _userService.GetUserIdByUsername("user"); - - var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); - if (!isPersonal) - await _timelineService.CreateTimeline(timelineName, userId); - - var postContentList = new string[] { "a", "b", "c", "d" }; - - foreach (var (content, index) in postContentList.Select((v, i) => (v, i))) - { - await _timelineService.CreateTextPost(timelineName, userId, content, null); - } - - var time2 = _clock.ForwardCurrentTime(); - - { - var posts = await _timelineService.GetPosts(timelineName, time2); - posts.Should().HaveCount(0); - } - - { - await _userService.ModifyUser(userId, new User { Nickname = "haha" }); - var posts = await _timelineService.GetPosts(timelineName, time2); - posts.Should().HaveCount(0); - } - - { - await _userService.ModifyUser(userId, new User { Username = "haha" }); - var posts = await _timelineService.GetPosts(timelineName, time2); - posts.Should().HaveCount(4); - } - } - - [Theory] - [InlineData("@admin")] - [InlineData("tl")] - public async Task GetPosts_ModifiedSince_UserDelete(string timelineName) - { - var time1 = _clock.ForwardCurrentTime(); - - var userId = await _userService.GetUserIdByUsername("user"); - var adminId = await _userService.GetUserIdByUsername("admin"); - - var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); - if (!isPersonal) - await _timelineService.CreateTimeline(timelineName, adminId); - - var postContentList = new string[] { "a", "b", "c", "d" }; - - foreach (var (content, index) in postContentList.Select((v, i) => (v, i))) - { - await _timelineService.CreateTextPost(timelineName, userId, content, null); - } - - var time2 = _clock.ForwardCurrentTime(); - - { - var posts = await _timelineService.GetPosts(timelineName, time2); - posts.Should().HaveCount(0); - } - - await _userDeleteService.DeleteUser("user"); - - { - var posts = await _timelineService.GetPosts(timelineName, time2); - posts.Should().HaveCount(0); - } - - { - var posts = await _timelineService.GetPosts(timelineName, time2, true); - posts.Should().HaveCount(4); - } - } - - [Theory] - [InlineData("@admin")] - [InlineData("tl")] - public async Task Title(string timelineName) - { - var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); - if (!isPersonal) - await _timelineService.CreateTimeline(timelineName, await _userService.GetUserIdByUsername("user")); - - { - var timeline = await _timelineService.GetTimeline(timelineName); - timeline.Title.Should().Be(timelineName); - } - - { - await _timelineService.ChangeProperty(timelineName, new TimelineChangePropertyRequest { Title = null }); - var timeline = await _timelineService.GetTimeline(timelineName); - timeline.Title.Should().Be(timelineName); - } - - { - await _timelineService.ChangeProperty(timelineName, new TimelineChangePropertyRequest { Title = "atitle" }); - var timeline = await _timelineService.GetTimeline(timelineName); - timeline.Title.Should().Be("atitle"); - } - } - - [Fact] - public async Task ChangeName() - { - _clock.ForwardCurrentTime(); - - await _timelineService.Awaiting(s => s.ChangeTimelineName("!!!", "newtl")).Should().ThrowAsync(); - await _timelineService.Awaiting(s => s.ChangeTimelineName("tl", "!!!")).Should().ThrowAsync(); - await _timelineService.Awaiting(s => s.ChangeTimelineName("tl", "newtl")).Should().ThrowAsync(); - - await _timelineService.CreateTimeline("tl", await _userService.GetUserIdByUsername("user")); - await _timelineService.CreateTimeline("tl2", await _userService.GetUserIdByUsername("user")); - - await _timelineService.Awaiting(s => s.ChangeTimelineName("tl", "tl2")).Should().ThrowAsync(); - - var time = _clock.ForwardCurrentTime(); - - await _timelineService.ChangeTimelineName("tl", "newtl"); - - { - var timeline = await _timelineService.GetTimeline("newtl"); - timeline.Name.Should().Be("newtl"); - timeline.LastModified.Should().Be(time); - timeline.NameLastModified.Should().Be(time); - } - } - } -} diff --git a/Timeline.Tests/Timeline.Tests.csproj b/Timeline.Tests/Timeline.Tests.csproj deleted file mode 100644 index 973e0fc0..00000000 --- a/Timeline.Tests/Timeline.Tests.csproj +++ /dev/null @@ -1,34 +0,0 @@ - - - - netcoreapp3.1 - - 8.0 - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - all - runtime; build; native; contentfiles; analyzers - - - - - - - diff --git a/Timeline.Tests/UsernameValidatorUnitTest.cs b/Timeline.Tests/UsernameValidatorUnitTest.cs deleted file mode 100644 index 5b568adf..00000000 --- a/Timeline.Tests/UsernameValidatorUnitTest.cs +++ /dev/null @@ -1,78 +0,0 @@ -using FluentAssertions; -using Timeline.Models.Validation; -using Timeline.Tests.Helpers; -using Xunit; - -namespace Timeline.Tests -{ - public class UsernameValidatorUnitTest : IClassFixture - { - private readonly UsernameValidator _validator; - - public UsernameValidatorUnitTest(UsernameValidator validator) - { - _validator = validator; - } - - private string FailAndMessage(string username) - { - var (result, message) = _validator.Validate(username); - result.Should().BeFalse(); - return message; - } - - [Fact] - public void NotString() - { - var (result, message) = _validator.Validate(123); - result.Should().BeFalse(); - message.Should().ContainEquivalentOf("type"); - } - - [Fact] - public void Empty() - { - FailAndMessage("").Should().ContainEquivalentOf("empty"); - } - - [Theory] - [InlineData("!")] - [InlineData("!abc")] - [InlineData("ab c")] - [InlineData("ab c!")] // This is a chinese ! . - public void BadCharactor(string value) - { - FailAndMessage(value).Should().ContainEquivalentOf("invalid") - .And.ContainEquivalentOf("character"); - } - - [Fact] - public void TooLong() - { - FailAndMessage(new string('a', 40)).Should().ContainEquivalentOf("long"); - } - - [Fact(Skip = "Currently name can't be longer than 26. So this will print message of too long.")] - public void UniqueId() - { - FailAndMessage("e4c80127d092d9b2fc19c5e04612d4c0").Should().ContainEquivalentOf("unique id"); - } - - [Theory] - [InlineData(null)] - [InlineData("abc")] - [InlineData("-abc")] - [InlineData("_abc")] - [InlineData("abc-")] - [InlineData("abc_")] - [InlineData("a-bc")] - [InlineData("a-b-c")] - [InlineData("a-b_c")] - [InlineData("a-你好_c")] - public void Success(string value) - { - var (result, _) = _validator.Validate(value); - result.Should().BeTrue(); - } - } -} diff --git a/Timeline.Tests/coverletArgs.runsettings b/Timeline.Tests/coverletArgs.runsettings deleted file mode 100644 index 24cd1822..00000000 --- a/Timeline.Tests/coverletArgs.runsettings +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - [xunit.*]*,[Timeline]Timeline.Migrations.* - - - - - diff --git a/Timeline.Tests/packages.lock.json b/Timeline.Tests/packages.lock.json deleted file mode 100644 index 7150a222..00000000 --- a/Timeline.Tests/packages.lock.json +++ /dev/null @@ -1,2040 +0,0 @@ -{ - "version": 1, - "dependencies": { - ".NETCoreApp,Version=v3.1": { - "coverlet.collector": { - "type": "Direct", - "requested": "[1.3.0, )", - "resolved": "1.3.0", - "contentHash": "t8pnf5SX2ya0RX4vjoxsbhDMQCZJcpPun2neHKJ4FouMmObylo25FvoOydvf3Bl+l+IzWw7u2vjEeCBHnleB9g==" - }, - "FluentAssertions": { - "type": "Direct", - "requested": "[5.10.3, )", - "resolved": "5.10.3", - "contentHash": "gVPEVp1hLVqcv+7Q2wiDf7kqCNn7+bQcQ0jbJ2mcRT6CeRoZl1tNkqvzSIhvekyldDptk77j1b03MXTTRIqqpg==", - "dependencies": { - "System.Configuration.ConfigurationManager": "4.4.0" - } - }, - "JunitXml.TestLogger": { - "type": "Direct", - "requested": "[2.1.78, )", - "resolved": "2.1.78", - "contentHash": "4y4FSfKWxlked8ilQdqBBSeRMf5jD/Hkvyp744hc54yQcABLt4rR2Q+4hNqAqrSo+mhwAlusj2rpXpN/5TICCA==" - }, - "Microsoft.AspNet.WebApi.Client": { - "type": "Direct", - "requested": "[5.2.7, )", - "resolved": "5.2.7", - "contentHash": "/76fAHknzvFqbznS6Uj2sOyE9rJB3PltY+f53TH8dX9RiGhk02EhuFCWljSj5nnqKaTsmma8DFR50OGyQ4yJ1g==", - "dependencies": { - "Newtonsoft.Json": "10.0.1", - "Newtonsoft.Json.Bson": "1.0.1" - } - }, - "Microsoft.AspNetCore.TestHost": { - "type": "Direct", - "requested": "[3.1.9, )", - "resolved": "3.1.9", - "contentHash": "0DBtfgmM2yS4h0v+gS4JHRX4nuyQmW7Yi5/G4yB5KelA2dDXPsAiipw9z47B1jVEs9QZdOwSqPQm2R/owl2TnA==", - "dependencies": { - "System.IO.Pipelines": "4.7.3" - } - }, - "Microsoft.CodeAnalysis.FxCopAnalyzers": { - "type": "Direct", - "requested": "[3.3.0, )", - "resolved": "3.3.0", - "contentHash": "k3Icqx8kc+NrHImuiB8Jc/wd32Xeyd2B/7HOR5Qu9pyKzXQ4ikPeBAwzG2FSTuYhyIuNWvwL5k9yYBbbVz6w9w==", - "dependencies": { - "Microsoft.CodeAnalysis.VersionCheckAnalyzer": "[3.3.0]", - "Microsoft.CodeQuality.Analyzers": "[3.3.0]", - "Microsoft.NetCore.Analyzers": "[3.3.0]", - "Microsoft.NetFramework.Analyzers": "[3.3.0]" - } - }, - "Microsoft.NET.Test.Sdk": { - "type": "Direct", - "requested": "[16.7.1, )", - "resolved": "16.7.1", - "contentHash": "7T3XYuLT2CRMZXwlp8p4cEEf6y7VifxTdKwYNzCYp31CN4iyrcDKneIJvNTo0YVnTxJn+CSlGVlUnZHUlAwt9A==", - "dependencies": { - "Microsoft.CodeCoverage": "16.7.1", - "Microsoft.TestPlatform.TestHost": "16.7.1" - } - }, - "Moq": { - "type": "Direct", - "requested": "[4.14.7, )", - "resolved": "4.14.7", - "contentHash": "z1jwY3lL3d4l+92cdSnhRDUUco68HiRNfLKB9r9/PLP5lrN+ZL1Qtt3brVGVB8iY+ioBXhlFue2JtycBczE8Pw==", - "dependencies": { - "Castle.Core": "4.4.0", - "System.Threading.Tasks.Extensions": "4.5.1" - } - }, - "xunit": { - "type": "Direct", - "requested": "[2.4.1, )", - "resolved": "2.4.1", - "contentHash": "XNR3Yz9QTtec16O0aKcO6+baVNpXmOnPUxDkCY97J+8krUYxPvXT1szYYEUdKk4sB8GOI2YbAjRIOm8ZnXRfzQ==", - "dependencies": { - "xunit.analyzers": "0.10.0", - "xunit.assert": "[2.4.1]", - "xunit.core": "[2.4.1]" - } - }, - "xunit.runner.visualstudio": { - "type": "Direct", - "requested": "[2.4.3, )", - "resolved": "2.4.3", - "contentHash": "kZZSmOmKA8OBlAJaquPXnJJLM9RwQ27H7BMVqfMLUcTi9xHinWGJiWksa3D4NEtz0wZ/nxd2mogObvBgJKCRhQ==" - }, - "AutoMapper": { - "type": "Transitive", - "resolved": "10.1.1", - "contentHash": "uMgbqOdu9ZG5cIOty0C85hzzayBH2i9BthnS5FlMqKtMSHDv4ts81a2jS1VFaDBVhlBeIqJ/kQKjQY95BZde9w==", - "dependencies": { - "Microsoft.CSharp": "4.7.0", - "System.Reflection.Emit": "4.7.0" - } - }, - "AutoMapper.Extensions.Microsoft.DependencyInjection": { - "type": "Transitive", - "resolved": "8.1.0", - "contentHash": "dQyGCAYcHbGuimVvCMu4Ea2S1oYOlgO9XfVdClmY5wgygJMZoS57emPzH0qNfknmtzMm4QbDO9i237W5IDjU1A==", - "dependencies": { - "AutoMapper": "[10.1.0, 11.0.0)", - "Microsoft.Extensions.DependencyInjection.Abstractions": "3.0.0", - "Microsoft.Extensions.Options": "3.0.0" - } - }, - "Castle.Core": { - "type": "Transitive", - "resolved": "4.4.0", - "contentHash": "b5rRL5zeaau1y/5hIbI+6mGw3cwun16YjkHZnV9RRT5UyUIFsgLmNXJ0YnIN9p8Hw7K7AbG1q1UclQVU3DinAQ==", - "dependencies": { - "NETStandard.Library": "1.6.1", - "System.Collections.Specialized": "4.3.0", - "System.ComponentModel": "4.3.0", - "System.ComponentModel.TypeConverter": "4.3.0", - "System.Diagnostics.TraceSource": "4.3.0", - "System.Dynamic.Runtime": "4.3.0", - "System.Reflection": "4.3.0", - "System.Reflection.Emit": "4.3.0", - "System.Reflection.TypeExtensions": "4.3.0", - "System.Xml.XmlDocument": "4.3.0" - } - }, - "Microsoft.AspNetCore.Authorization": { - "type": "Transitive", - "resolved": "1.0.3", - "contentHash": "cN2KJkfHcKwh82c9WGx4Tqfd2h5HflU/Mu5vYLMHON8WahHU9hE32ciIXcEIoKLNpu+zs1u1cN/qxcKTdqu89w==", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "1.0.2", - "Microsoft.Extensions.Options": "1.0.2", - "System.Security.Claims": "4.0.1" - } - }, - "Microsoft.AspNetCore.Hosting.Abstractions": { - "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "ybY8FOkdNfBPB5PLv1JO+It/94ftBzGUI1WqU4XySbIWyhw2TPmmKAUuO9uvJoR0qpsFup8FJz6trsBcBITg9w==", - "dependencies": { - "Microsoft.AspNetCore.Hosting.Server.Abstractions": "1.0.4", - "Microsoft.AspNetCore.Http.Abstractions": "1.0.3", - "Microsoft.Extensions.Configuration.Abstractions": "1.0.2", - "Microsoft.Extensions.DependencyInjection.Abstractions": "1.0.2", - "Microsoft.Extensions.FileProviders.Abstractions": "1.0.1", - "Microsoft.Extensions.Logging.Abstractions": "1.0.2" - } - }, - "Microsoft.AspNetCore.Hosting.Server.Abstractions": { - "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "XUiQPe/CflK1i0Voo9S6/G1iQh00gQ6sMqi3LRtKeceBbO6AOostaAUdhjyME92MapI4VFNl+Z+/KXUlMAExJQ==", - "dependencies": { - "Microsoft.AspNetCore.Http.Features": "1.0.3", - "Microsoft.Extensions.Configuration.Abstractions": "1.0.2" - } - }, - "Microsoft.AspNetCore.Http": { - "type": "Transitive", - "resolved": "1.0.3", - "contentHash": "kfNOIGGgVtMzsSWZzXBqz5zsdo8ssBa90YHzZt95N8ARGXoolSaBHy6yBoMm/XcpbXM+m/x1fixTTMIWMgzJdQ==", - "dependencies": { - "Microsoft.AspNetCore.Http.Abstractions": "1.0.3", - "Microsoft.AspNetCore.WebUtilities": "1.0.3", - "Microsoft.Extensions.ObjectPool": "1.0.1", - "Microsoft.Extensions.Options": "1.0.2", - "Microsoft.Net.Http.Headers": "1.0.3", - "System.Buffers": "4.0.0", - "System.Threading": "4.0.11" - } - }, - "Microsoft.AspNetCore.Http.Abstractions": { - "type": "Transitive", - "resolved": "1.0.3", - "contentHash": "nnjvAf7ag6P0DyD/0nhRGjLpv+3DkPU0juF8aQh46X8uF4kzjJdrh65oL+4PVOu3K6BgSg6OVUs0QC0SE0FRtg==", - "dependencies": { - "Microsoft.AspNetCore.Http.Features": "1.0.3", - "System.Globalization.Extensions": "4.0.1", - "System.Linq.Expressions": "4.1.1", - "System.Reflection.TypeExtensions": "4.1.0", - "System.Runtime.InteropServices": "4.1.0", - "System.Text.Encodings.Web": "4.0.1" - } - }, - "Microsoft.AspNetCore.Http.Extensions": { - "type": "Transitive", - "resolved": "1.0.3", - "contentHash": "+7Sd+14nexIJqcB4S1Eur9kzeMZ5CBtrxkei+PNbD78fg8vO3+TcCgrl5SBNTsUB/VJAfD/s0fgs5t+hHRj2Pg==", - "dependencies": { - "Microsoft.AspNetCore.Http.Abstractions": "1.0.3", - "Microsoft.Extensions.FileProviders.Abstractions": "1.0.1", - "Microsoft.Net.Http.Headers": "1.0.3", - "System.Buffers": "4.0.0", - "System.IO.FileSystem": "4.0.1" - } - }, - "Microsoft.AspNetCore.Http.Features": { - "type": "Transitive", - "resolved": "1.0.3", - "contentHash": "Ihq57tseNyPbJTmFXY4jQ4JkxLP0lh45VRwocQci/sFx+qcJGvWB+sJJ2/YPLy/qTWFAEfNAcswuY3OsNH9Gwg==", - "dependencies": { - "Microsoft.Extensions.Primitives": "1.0.1", - "System.Collections": "4.0.11", - "System.ComponentModel": "4.0.1", - "System.Linq": "4.1.0", - "System.Net.Primitives": "4.0.11", - "System.Net.WebSockets": "4.0.0", - "System.Runtime.Extensions": "4.1.0", - "System.Security.Claims": "4.0.1", - "System.Security.Cryptography.X509Certificates": "4.1.0", - "System.Security.Principal": "4.0.1" - } - }, - "Microsoft.AspNetCore.JsonPatch": { - "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "WVaSVS+dDlWCR/qerHnBxU9tIeJ9GMA3M5tg4cxH7/cJYZZLnr2zvaFHGB+cRRNCKKTJ0pFRxT7ES8knhgAAaA==", - "dependencies": { - "Microsoft.CSharp": "4.0.1", - "Newtonsoft.Json": "9.0.1", - "System.Collections.Concurrent": "4.0.12", - "System.ComponentModel.TypeConverter": "4.1.0", - "System.Diagnostics.Debug": "4.0.11", - "System.Globalization": "4.0.11", - "System.Linq": "4.1.0", - "System.Reflection.Extensions": "4.0.1", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime.Extensions": "4.1.0", - "System.Runtime.Serialization.Primitives": "4.1.1", - "System.Text.Encoding.Extensions": "4.0.11" - } - }, - "Microsoft.AspNetCore.Mvc.Abstractions": { - "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "Isqgif1nuB+um86cEkpL8KnoxFCUCXBsbs9PuiuzElvlSiv4Ek3LvtrSUcbivekDDfys8CDbJhxwEI7WKJieAQ==", - "dependencies": { - "Microsoft.AspNetCore.Routing.Abstractions": "1.0.4", - "Microsoft.CSharp": "4.0.1", - "Microsoft.Net.Http.Headers": "1.0.3", - "System.ComponentModel.TypeConverter": "4.1.0", - "System.Reflection.Extensions": "4.0.1", - "System.Text.Encoding.Extensions": "4.0.11" - } - }, - "Microsoft.AspNetCore.Mvc.ApiExplorer": { - "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "ujCFTM42U2WKUBhdaoLoiI+wVHgYhrmDrkl5+hWJ7EJW4fhp42w4cRZ97tjuveWr+M6JZjpS0q+7PVofQzFUiw==", - "dependencies": { - "Microsoft.AspNetCore.Mvc.Core": "1.0.4" - } - }, - "Microsoft.AspNetCore.Mvc.Core": { - "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "1ukcttN1+T82hWXE8WS5kawkruolKI6LPVqVI4rTzN16kFszS/UqTrcwSUEnmTRpmWgFo665V3c2GpdQ9B6znw==", - "dependencies": { - "Microsoft.AspNetCore.Authorization": "1.0.3", - "Microsoft.AspNetCore.Hosting.Abstractions": "1.0.3", - "Microsoft.AspNetCore.Http": "1.0.3", - "Microsoft.AspNetCore.Mvc.Abstractions": "1.0.4", - "Microsoft.AspNetCore.Routing": "1.0.4", - "Microsoft.Extensions.DependencyModel": "1.0.0", - "Microsoft.Extensions.FileProviders.Abstractions": "1.0.1", - "Microsoft.Extensions.Logging.Abstractions": "1.0.2", - "Microsoft.Extensions.PlatformAbstractions": "1.0.0", - "System.Buffers": "4.0.0", - "System.Diagnostics.DiagnosticSource": "4.0.0", - "System.Text.Encoding": "4.0.11" - } - }, - "Microsoft.AspNetCore.Mvc.Formatters.Json": { - "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "i8WWK2GwlBHfOL+d+kknJWPks6DS9tbN6nfJZU4yb+/wfUAYd311B2CIHzdat3IewubnK1TYONwrhQcs2FbLeA==", - "dependencies": { - "Microsoft.AspNetCore.JsonPatch": "1.0.0", - "Microsoft.AspNetCore.Mvc.Core": "1.0.4" - } - }, - "Microsoft.AspNetCore.NodeServices": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "bbd3FlSPWiRQrIcBLa5TaOvo4gjmmiNMkxA8VmZ6u0eIpS0Yj35/eTopaGdtzqwlqj5jXbdRoib1MruXuPaW8A==", - "dependencies": { - "Microsoft.Extensions.Logging.Console": "3.1.9", - "Newtonsoft.Json": "12.0.2" - } - }, - "Microsoft.AspNetCore.Routing": { - "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "mdIF3ckRothHWuCSFkk6YXACj5zxi5qM+cEAHjcpP04/wCHUoV0gGVnW+HI+LyFXE6JUwu2zXn5tfsCpW0U+SA==", - "dependencies": { - "Microsoft.AspNetCore.Http.Extensions": "1.0.3", - "Microsoft.AspNetCore.Routing.Abstractions": "1.0.4", - "Microsoft.Extensions.Logging.Abstractions": "1.0.2", - "Microsoft.Extensions.ObjectPool": "1.0.1", - "Microsoft.Extensions.Options": "1.0.2", - "System.Collections": "4.0.11", - "System.Text.RegularExpressions": "4.1.0" - } - }, - "Microsoft.AspNetCore.Routing.Abstractions": { - "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "GHxVt6LlXHFsCUd2Un+/vY1tBTXxnogfbDO0b8G5EGmkapSK+dOGOLJviscxQkp338Uabs081JEIdkRymI5GXA==", - "dependencies": { - "Microsoft.AspNetCore.Http.Abstractions": "1.0.3", - "System.Collections.Concurrent": "4.0.12", - "System.Reflection.Extensions": "4.0.1", - "System.Threading.Tasks": "4.0.11" - } - }, - "Microsoft.AspNetCore.SpaServices": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "Fb+N2ZyF1wNrGeWggT+Ovv6W8AAVxfi4V/SnuEsBOR+nmkFhty9zyh6IDRRS98GJK6OE3adqqPbWMtJqbxYnNA==", - "dependencies": { - "Microsoft.AspNetCore.NodeServices": "3.1.9" - } - }, - "Microsoft.AspNetCore.SpaServices.Extensions": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "ciy2GCvRnh9C22laArLsaItS+72U6Hqf4nDYShdvFgcen2ZV+NNSitb/B3vsmFfIPM8m4mf2x4T+vZ6OlI5XaA==", - "dependencies": { - "Microsoft.AspNetCore.SpaServices": "3.1.9", - "Microsoft.Extensions.FileProviders.Physical": "3.1.9" - } - }, - "Microsoft.AspNetCore.StaticFiles": { - "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "2pNvwewAazhaaCdw2CGUvIcDrNQMlqP57JgBDf3v+pRj1rZ29HVnpvkX6a+TrmRYlJNmmxHOKEt468uE/gDcFw==", - "dependencies": { - "Microsoft.AspNetCore.Hosting.Abstractions": "1.0.4", - "Microsoft.AspNetCore.Http.Extensions": "1.0.3", - "Microsoft.Extensions.FileProviders.Abstractions": "1.0.1", - "Microsoft.Extensions.Logging.Abstractions": "1.0.2", - "Microsoft.Extensions.WebEncoders": "1.0.3" - } - }, - "Microsoft.AspNetCore.WebUtilities": { - "type": "Transitive", - "resolved": "1.0.3", - "contentHash": "snSGNs5EEisqivDjDiskFkFyu+DV2Ib9sMPOBQKtoFwI5H1W5YNB/rIVqDZQL16zj/uzdwwxrdE/5xhkVyf6gQ==", - "dependencies": { - "Microsoft.Extensions.Primitives": "1.0.1", - "System.Buffers": "4.0.0", - "System.Collections": "4.0.11", - "System.IO": "4.1.0", - "System.IO.FileSystem": "4.0.1", - "System.Text.Encodings.Web": "4.0.1" - } - }, - "Microsoft.Bcl.AsyncInterfaces": { - "type": "Transitive", - "resolved": "1.1.1", - "contentHash": "yuvf07qFWFqtK3P/MRkEKLhn5r2UbSpVueRziSqj0yJQIKFwG1pq9mOayK3zE5qZCTs0CbrwL9M6R8VwqyGy2w==" - }, - "Microsoft.Bcl.HashCode": { - "type": "Transitive", - "resolved": "1.1.0", - "contentHash": "J2G1k+u5unBV+aYcwxo94ip16Rkp65pgWFb0R6zwJipzWNMgvqlWeuI7/+R+e8bob66LnSG+llLJ+z8wI94cHg==" - }, - "Microsoft.CodeAnalysis.VersionCheckAnalyzer": { - "type": "Transitive", - "resolved": "3.3.0", - "contentHash": "xjLM3DRFZMan3nQyBQEM1mBw6VqQybi4iMJhMFW6Ic1E1GCvqJR3ABOwEL7WtQjDUzxyrGld9bASnAos7G/Xyg==" - }, - "Microsoft.CodeCoverage": { - "type": "Transitive", - "resolved": "16.7.1", - "contentHash": "PhSppbk+kvAyD9yGJIcBRJ/XYwY+21YK88l22PGTtixaxNdjnx1idVKh88LCGwKaTL8HhlnQ41VmBiBdZJzIQw==" - }, - "Microsoft.CodeQuality.Analyzers": { - "type": "Transitive", - "resolved": "3.3.0", - "contentHash": "zZ3miq6u22UFQKhfJyLnVEJ+DgeOopLh3eKJnKAcOetPP2hiv3wa7kHZlBDeTvtqJQiAQhAVbttket8XxjN1zw==" - }, - "Microsoft.CSharp": { - "type": "Transitive", - "resolved": "4.7.0", - "contentHash": "pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA==" - }, - "Microsoft.Data.Sqlite.Core": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "+u4PeT1npi2EzhxGc5r1Z2z73zuXw+TlKVZm44WQhNCUw4LtUVDaxGSpUhrjW+X4snBCBfr4kT/uJyKnL4R4og==", - "dependencies": { - "SQLitePCLRaw.core": "2.0.2" - } - }, - "Microsoft.DotNet.PlatformAbstractions": { - "type": "Transitive", - "resolved": "3.1.6", - "contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg==" - }, - "Microsoft.EntityFrameworkCore": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "u3A2W0BvAuAF2jgW+WX+C+Sh8sMGX5Kl1hdA0gu6A/XSrZQoW/BUP4a/q2n3iitDGndaorqjAKx+Spb9gBto+w==", - "dependencies": { - "Microsoft.Bcl.AsyncInterfaces": "1.1.1", - "Microsoft.Bcl.HashCode": "1.1.0", - "Microsoft.EntityFrameworkCore.Abstractions": "3.1.9", - "Microsoft.EntityFrameworkCore.Analyzers": "3.1.9", - "Microsoft.Extensions.Caching.Memory": "3.1.9", - "Microsoft.Extensions.DependencyInjection": "3.1.9", - "Microsoft.Extensions.Logging": "3.1.9", - "System.Collections.Immutable": "1.7.1", - "System.ComponentModel.Annotations": "4.7.0", - "System.Diagnostics.DiagnosticSource": "4.7.1" - } - }, - "Microsoft.EntityFrameworkCore.Abstractions": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "IR6Y4RJVlw0QXdWXjF3Kx9s1QLiicJus+BFBKr43lBtriV20j3yrWMoaZ9W1AUUgnicZXpXVcNfklqtmwb9Sxw==" - }, - "Microsoft.EntityFrameworkCore.Analyzers": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "eXGyx/Lb1fiiKtnIStdxGrfBSSQg8oZytE10f1T/2xAx12W9dKB9U9fg05cwNCDC0S2CXILsmZHYaGqCSXVAqQ==" - }, - "Microsoft.EntityFrameworkCore.Relational": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "7fhWuSfrCYlv/hvOX5OhbFJF/G9f8sifqTrJiYnAYLDOvNizwv7t9tFPD8JwaF3zM2S54O5/Vni2NxvwzSaW2w==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "3.1.9" - } - }, - "Microsoft.EntityFrameworkCore.Sqlite": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "sMFCWv/1UcsFQZeGQcbfPbEZKZ1oKZqWZXTbc7PEZVMIXu82nbavstdNQ84x5IBXJkxl8iW3zjChb/FRBr5uLQ==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Sqlite.Core": "3.1.9", - "SQLitePCLRaw.bundle_e_sqlite3": "2.0.2" - } - }, - "Microsoft.EntityFrameworkCore.Sqlite.Core": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "Da6h8LdpJwKc1az9DMWt2Mt6gHXPRZqwiumV1Zx0AuM3EThyokVDzBGy2sti0AcBhcQMLJHPEr5R9xuiWvaYYQ==", - "dependencies": { - "Microsoft.Data.Sqlite.Core": "3.1.9", - "Microsoft.DotNet.PlatformAbstractions": "3.1.6", - "Microsoft.EntityFrameworkCore.Relational": "3.1.9", - "Microsoft.Extensions.DependencyModel": "3.1.6" - } - }, - "Microsoft.Extensions.ApiDescription.Server": { - "type": "Transitive", - "resolved": "3.0.0", - "contentHash": "LH4OE/76F6sOCslif7+Xh3fS/wUUrE5ryeXAMcoCnuwOQGT5Smw0p57IgDh/pHgHaGz/e+AmEQb7pRgb++wt0w==" - }, - "Microsoft.Extensions.Caching.Abstractions": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "/2QsPAsUZD4qvftZkUKHRRRryPDXWh606/iNXPLrulwHLMr9JNsKBJWVqylT3qU92nJok5VoqSblkY9mSyxFyg==", - "dependencies": { - "Microsoft.Extensions.Primitives": "3.1.9" - } - }, - "Microsoft.Extensions.Caching.Memory": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "/JrVMVetX/kpJQUIlJ6NLQ3zbF0yyryXpo4+uFCqYIUZzgmWk8DS/zSKcyj1tQ3410+vhDEAPngxC+hg0IlJeg==", - "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "3.1.9", - "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.9", - "Microsoft.Extensions.Logging.Abstractions": "3.1.9", - "Microsoft.Extensions.Options": "3.1.9" - } - }, - "Microsoft.Extensions.Configuration": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "lqdkOGNeTMKG981Q7yWGlRiFbIlsRwTlMMiybT+WOzUCFBS/wc25tZgh7Wm/uRoBbWefgvokzmnea7ZjmFedmA==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "3.1.9" - } - }, - "Microsoft.Extensions.Configuration.Abstractions": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "vOJxPKczaHpXeZFrxARxYwsEulhEouXc5aZGgMdkhV/iEXX9/pfjqKk76rTG+4CsJjHV+G/4eMhvOIaQMHENNA==", - "dependencies": { - "Microsoft.Extensions.Primitives": "3.1.9" - } - }, - "Microsoft.Extensions.Configuration.Binder": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "BG6HcT7tARYakftqfQu+cLksgIWG1NdxMY+igI12hdZrUK+WjS973NiRyuao/U9yyTeM9NPwRnC61hCmG3G3jg==", - "dependencies": { - "Microsoft.Extensions.Configuration": "3.1.9" - } - }, - "Microsoft.Extensions.DependencyInjection": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "ORqfrAACcvTInie1oGola5uky344/PiNfgayTPuZWV4WnSfIQZJQm/ZLpGshJE3h7TqwYaYElGazK/yaM2bFLA==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.9" - } - }, - "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "8PkcaPwiTPOhqshoY4+rQUbz86X6YpLDLUqXOezh7L2A3pgpBmeBBByYIffofBlvQxDdQ0zB2DkWjbZWyCxRWg==" - }, - "Microsoft.Extensions.DependencyModel": { - "type": "Transitive", - "resolved": "3.1.6", - "contentHash": "/UlDKULIVkLQYn1BaHcy/rc91ApDxJb7T75HcCbGdqwvxhnRQRKM2di1E70iCPMF9zsr6f4EgQTotBGxFIfXmw==", - "dependencies": { - "System.Text.Json": "4.7.2" - } - }, - "Microsoft.Extensions.FileProviders.Abstractions": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "Q4SGwEFZKiZbpzPgdGbQUULxtcH1zXMOwCPKSm6QwVcOCGshf3QLfBh+O/GyFH4B0RfZ16nKyeW1mMONlRyjUw==", - "dependencies": { - "Microsoft.Extensions.Primitives": "3.1.9" - } - }, - "Microsoft.Extensions.FileProviders.Embedded": { - "type": "Transitive", - "resolved": "1.0.1", - "contentHash": "nSEa8bH3fVdTYGqK4twOKLxxgKIW3cz9g9mrzhPh/CmdvGJWKRTIlBIZi7lz+lqNQpxean5vbAo84R/mU+JpGA==", - "dependencies": { - "Microsoft.Extensions.FileProviders.Abstractions": "1.0.1", - "System.Runtime.Extensions": "4.1.0" - } - }, - "Microsoft.Extensions.FileProviders.Physical": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "HWDSsblTCQp7EEJJmnLzttIhFGzDu+DGqBbOvGCdFT0+pkCuBkn3EiWpEEcm5WMTO5njmsbLSK9ZuUUf2zPsFg==", - "dependencies": { - "Microsoft.Extensions.FileProviders.Abstractions": "3.1.9", - "Microsoft.Extensions.FileSystemGlobbing": "3.1.9" - } - }, - "Microsoft.Extensions.FileSystemGlobbing": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "5bnewG1aBiSESPNwcXGIxDDRN95uqdy+fqZZ8Z63Et5rRNlAwAfXHOrg+FTht7UjHobjvtjzquMCbAWhWEPHIw==" - }, - "Microsoft.Extensions.Logging": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "+V3i0jCQCO6IIOf6e+fL0SqrZd2x/Krug9EEL1JHa9R03RsbEpltCtjVY5hxedyuyuQKwvLoR12sCfu/9XEUAw==", - "dependencies": { - "Microsoft.Extensions.Configuration.Binder": "3.1.9", - "Microsoft.Extensions.DependencyInjection": "3.1.9", - "Microsoft.Extensions.Logging.Abstractions": "3.1.9", - "Microsoft.Extensions.Options": "3.1.9" - } - }, - "Microsoft.Extensions.Logging.Abstractions": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "W5fbF8qVR9SMVVJqDQLIR7meWbev6Pu/lbrm7LDNr4Sp7HOotr4k2UULTdFSXOi5aoDdkQZpWnq0ZSpjrR3tjg==" - }, - "Microsoft.Extensions.Logging.Configuration": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "hv6XsGgikrbkolBJdF1usl9R/nrliC5mifMqHMEY9zWcCLwNkXMJiS8p0lbosrnpVAMi4PbNx39DB51Dqscd0w==", - "dependencies": { - "Microsoft.Extensions.Logging": "3.1.9", - "Microsoft.Extensions.Options.ConfigurationExtensions": "3.1.9" - } - }, - "Microsoft.Extensions.Logging.Console": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "8Dusl1rkDivmvLrwj6QAo917xMHPiDBzG3IG3agiyDdtsC/fRp+1VN5iIN+O09PtEaMged2OLA6wCDwfSTSTZw==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "3.1.9", - "Microsoft.Extensions.Logging": "3.1.9", - "Microsoft.Extensions.Logging.Configuration": "3.1.9" - } - }, - "Microsoft.Extensions.ObjectPool": { - "type": "Transitive", - "resolved": "1.0.1", - "contentHash": "pJMOnxuqmG37OjccfvtqVoo3bQGoN+0EJUzzp7+2uxSdioER82caAk6Yi/z5aysapn5XENNIIa7SaYnYKSS69A==", - "dependencies": { - "System.Diagnostics.Debug": "4.0.11", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime.Extensions": "4.1.0", - "System.Threading": "4.0.11" - } - }, - "Microsoft.Extensions.Options": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "EIb3G1DL+Rl9MvJR7LjI1wCy2nfTN4y8MflbOftn1HLYQBj/Rwl8kUbGTrSFE01c99Wm4ETjWVsjqKcpFvhPng==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.9", - "Microsoft.Extensions.Primitives": "3.1.9" - } - }, - "Microsoft.Extensions.Options.ConfigurationExtensions": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "u5jh7RW+Ev81YqK1ZoBG0lftp2MA9xqXiTiRL46XzaPj2ScNUyiVbzcVY0fPbE27UOpT2hj+yPzRSOMIIo55UA==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "3.1.9", - "Microsoft.Extensions.Configuration.Binder": "3.1.9", - "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.9", - "Microsoft.Extensions.Options": "3.1.9" - } - }, - "Microsoft.Extensions.PlatformAbstractions": { - "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "zyjUzrOmuevOAJpIo3Mt5GmpALVYCVdLZ99keMbmCxxgQH7oxzU58kGHzE6hAgYEiWsdfMJLjVR7r+vSmaJmtg==", - "dependencies": { - "System.AppContext": "4.1.0", - "System.Reflection": "4.1.0", - "System.Reflection.Extensions": "4.0.1", - "System.Reflection.TypeExtensions": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime.Extensions": "4.1.0" - } - }, - "Microsoft.Extensions.Primitives": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "IrHecH0eGG7/XoeEtv++oLg/sJHRNyeCqlA9RhAo6ig4GpOTjtDr32sBMYuuLtUq8ALahneWkrOzoBAwJ4L4iA==" - }, - "Microsoft.Extensions.WebEncoders": { - "type": "Transitive", - "resolved": "1.0.3", - "contentHash": "TClNvczWRxF6bVPhn5EK3Y3QNi5jTP68Qur+5Fk+MQLPeBI18WN7X145DDJ6bFeNOwgdCHl73lHs5uZp9ish1A==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "1.0.2", - "Microsoft.Extensions.Options": "1.0.2", - "System.Text.Encodings.Web": "4.0.1" - } - }, - "Microsoft.IdentityModel.JsonWebTokens": { - "type": "Transitive", - "resolved": "6.8.0", - "contentHash": "+7JIww64PkMt7NWFxoe4Y/joeF7TAtA/fQ0b2GFGcagzB59sKkTt/sMZWR6aSZht5YC7SdHi3W6yM1yylRGJCQ==", - "dependencies": { - "Microsoft.IdentityModel.Tokens": "6.8.0" - } - }, - "Microsoft.IdentityModel.Logging": { - "type": "Transitive", - "resolved": "6.8.0", - "contentHash": "Rfh/p4MaN4gkmhPxwbu8IjrmoDncGfHHPh1sTnc0AcM/Oc39/fzC9doKNWvUAjzFb8LqA6lgZyblTrIsX/wDXg==" - }, - "Microsoft.IdentityModel.Tokens": { - "type": "Transitive", - "resolved": "6.8.0", - "contentHash": "gTqzsGcmD13HgtNePPcuVHZ/NXWmyV+InJgalW/FhWpII1D7V1k0obIseGlWMeA4G+tZfeGMfXr0klnWbMR/mQ==", - "dependencies": { - "Microsoft.CSharp": "4.5.0", - "Microsoft.IdentityModel.Logging": "6.8.0", - "System.Security.Cryptography.Cng": "4.5.0" - } - }, - "Microsoft.Net.Http.Headers": { - "type": "Transitive", - "resolved": "1.0.3", - "contentHash": "2F8USh4hR5xppvaxtw2EStX74Ih+HhRj7aQD1uaB9JmTGy478F7t4VU+IdZXauEDrvS7LYAyyhmOExsUFK3PAw==", - "dependencies": { - "System.Buffers": "4.0.0", - "System.Collections": "4.0.11", - "System.Diagnostics.Contracts": "4.0.1", - "System.Globalization": "4.0.11", - "System.Linq": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime.Extensions": "4.1.0", - "System.Text.Encoding": "4.0.11" - } - }, - "Microsoft.NetCore.Analyzers": { - "type": "Transitive", - "resolved": "3.3.0", - "contentHash": "6qptTHUu1Wfszuf83NhU0IoAb4j7YWOpJs6oc6S4G/nI6aGGWKH/Xi5Vs9L/8lrI74ijEEzPcIwafSQW5ASHtA==" - }, - "Microsoft.NETCore.Platforms": { - "type": "Transitive", - "resolved": "1.1.0", - "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" - }, - "Microsoft.NETCore.Targets": { - "type": "Transitive", - "resolved": "1.1.0", - "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==" - }, - "Microsoft.NetFramework.Analyzers": { - "type": "Transitive", - "resolved": "3.3.0", - "contentHash": "JTfMic5fEFWICePbr7GXOGPranqS9Qxu2U/BZEcnnGbK1SFW8TxRyGp6O1L52xsbfOdqmzjc0t5ubhDrjj+Xpg==" - }, - "Microsoft.TestPlatform.ObjectModel": { - "type": "Transitive", - "resolved": "16.7.1", - "contentHash": "FL+VpAC/nCCzj80MwX6L8gJD06u2m1SKcQQLAymDLFqNtgtI9h3J5n0mVN+s18qcMzybsmO9GK7rMuHYx11KMg==", - "dependencies": { - "NuGet.Frameworks": "5.0.0" - } - }, - "Microsoft.TestPlatform.TestHost": { - "type": "Transitive", - "resolved": "16.7.1", - "contentHash": "mv7MnBDtqwQAjoH+AphE+Tu0dsF6x/c7Zs8umkb2McbvNALJdfBuWJQbiXGWqhNq7k8eMmnkNO6klJz4pkgekw==", - "dependencies": { - "Microsoft.TestPlatform.ObjectModel": "16.7.1", - "Newtonsoft.Json": "9.0.1" - } - }, - "Microsoft.Win32.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "Namotion.Reflection": { - "type": "Transitive", - "resolved": "1.0.14", - "contentHash": "wuJGiFvGfehH2w7jAhMbCJt0/rvUuHyqSZn0sMhNTviDfBZRyX8LFlR/ndQcofkGWulPDfH5nKYTeGXE8xBHPA==", - "dependencies": { - "Microsoft.CSharp": "4.3.0" - } - }, - "NETStandard.Library": { - "type": "Transitive", - "resolved": "1.6.1", - "contentHash": "WcSp3+vP+yHNgS8EV5J7pZ9IRpeDuARBPN28by8zqff1wJQXm26PVU8L3/fYLBJVU7BtDyqNVWq2KlCVvSSR4A==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.Win32.Primitives": "4.3.0", - "System.AppContext": "4.3.0", - "System.Collections": "4.3.0", - "System.Collections.Concurrent": "4.3.0", - "System.Console": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Diagnostics.Tools": "4.3.0", - "System.Diagnostics.Tracing": "4.3.0", - "System.Globalization": "4.3.0", - "System.Globalization.Calendars": "4.3.0", - "System.IO": "4.3.0", - "System.IO.Compression": "4.3.0", - "System.IO.Compression.ZipFile": "4.3.0", - "System.IO.FileSystem": "4.3.0", - "System.IO.FileSystem.Primitives": "4.3.0", - "System.Linq": "4.3.0", - "System.Linq.Expressions": "4.3.0", - "System.Net.Http": "4.3.0", - "System.Net.Primitives": "4.3.0", - "System.Net.Sockets": "4.3.0", - "System.ObjectModel": "4.3.0", - "System.Reflection": "4.3.0", - "System.Reflection.Extensions": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Runtime.InteropServices.RuntimeInformation": "4.3.0", - "System.Runtime.Numerics": "4.3.0", - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Security.Cryptography.X509Certificates": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Text.Encoding.Extensions": "4.3.0", - "System.Text.RegularExpressions": "4.3.0", - "System.Threading": "4.3.0", - "System.Threading.Tasks": "4.3.0", - "System.Threading.Timer": "4.3.0", - "System.Xml.ReaderWriter": "4.3.0", - "System.Xml.XDocument": "4.3.0" - } - }, - "Newtonsoft.Json": { - "type": "Transitive", - "resolved": "12.0.2", - "contentHash": "rTK0s2EKlfHsQsH6Yx2smvcTCeyoDNgCW7FEYyV01drPlh2T243PR2DiDXqtC5N4GDm4Ma/lkxfW5a/4793vbA==" - }, - "Newtonsoft.Json.Bson": { - "type": "Transitive", - "resolved": "1.0.1", - "contentHash": "5PYT/IqQ+UK31AmZiSS102R6EsTo+LGTSI8bp7WAUqDKaF4wHXD8U9u4WxTI1vc64tYi++8p3dk3WWNqPFgldw==", - "dependencies": { - "NETStandard.Library": "1.6.1", - "Newtonsoft.Json": "10.0.1" - } - }, - "NJsonSchema": { - "type": "Transitive", - "resolved": "10.2.1", - "contentHash": "/BtWbYTusyoSgQkCB4eYijMfZotB/rfASDsl1k9evlkm5vlOP4s4Y09TOzBChU77d/qUABVYL1Xf+TB8E0Wfpw==", - "dependencies": { - "Namotion.Reflection": "1.0.14", - "Newtonsoft.Json": "9.0.1" - } - }, - "NSwag.Annotations": { - "type": "Transitive", - "resolved": "13.8.2", - "contentHash": "/GO+35CjPYQTPS5/Q8udM5JAMEWVo8JsrkV2Uw3OW4/AJU9iOS7t6WJid6ZlkpLMjnW7oex9mvJ2EZNE4eOG/Q==" - }, - "NSwag.AspNetCore": { - "type": "Transitive", - "resolved": "13.8.2", - "contentHash": "SNGlVSZoMyywBWueZBxl3B/nfaIM0fAcuNhTD/cfMKUn3Cn/Oi8d45HZY5vAPqczvppTbk4cZXyVwWDOfgiPbA==", - "dependencies": { - "Microsoft.AspNetCore.Mvc.Core": "1.0.4", - "Microsoft.AspNetCore.Mvc.Formatters.Json": "1.0.4", - "Microsoft.AspNetCore.StaticFiles": "1.0.4", - "Microsoft.Extensions.ApiDescription.Server": "3.0.0", - "Microsoft.Extensions.FileProviders.Embedded": "1.0.1", - "NSwag.Annotations": "13.8.2", - "NSwag.Core": "13.8.2", - "NSwag.Generation": "13.8.2", - "NSwag.Generation.AspNetCore": "13.8.2", - "System.IO.FileSystem": "4.3.0", - "System.Xml.XPath.XDocument": "4.0.1" - } - }, - "NSwag.Core": { - "type": "Transitive", - "resolved": "13.8.2", - "contentHash": "Hm6pU9qFJuXLo3b27+JTXztfeuI/15Ob1sDsfUu4rchN0+bMogtn8Lia8KVbcalw/M+hXc0rWTFp5ueP23e+iA==", - "dependencies": { - "NJsonSchema": "10.2.1", - "Newtonsoft.Json": "9.0.1" - } - }, - "NSwag.Generation": { - "type": "Transitive", - "resolved": "13.8.2", - "contentHash": "LBIrpHFRZeMMbqL1hdyGb7r8v+T52aOCARxwfAmzE+MlOHVpjsIxyNSXht9EzBFMbSH0tj7CK2Ugo7bm+zUssg==", - "dependencies": { - "NJsonSchema": "10.2.1", - "NSwag.Core": "13.8.2", - "Newtonsoft.Json": "9.0.1" - } - }, - "NSwag.Generation.AspNetCore": { - "type": "Transitive", - "resolved": "13.8.2", - "contentHash": "0ydVv6OidspZ/MS6qmU8hswGtXwq5YZPg+2a2PHGD6jNp2Fef4j1wC3xa3hplDAq7cK+BgpyDKtvj9+X01+P5g==", - "dependencies": { - "Microsoft.AspNetCore.Mvc.ApiExplorer": "1.0.4", - "Microsoft.AspNetCore.Mvc.Core": "1.0.4", - "Microsoft.AspNetCore.Mvc.Formatters.Json": "1.0.4", - "NJsonSchema": "10.2.1", - "NSwag.Generation": "13.8.2" - } - }, - "NuGet.Frameworks": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "c5JVjuVAm4f7E9Vj+v09Z9s2ZsqFDjBpcsyS3M9xRo0bEdm/LVZSzLxxNvfvAwRiiE8nwe1h2G4OwiwlzFKXlA==" - }, - "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "HdSSp5MnJSsg08KMfZThpuLPJpPwE5hBXvHwoKWosyHHfe8Mh5WKT0ylEOf6yNzX6Ngjxe4Whkafh5q7Ymac4Q==" - }, - "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "+yH1a49wJMy8Zt4yx5RhJrxO/DBDByAiCzNwiETI+1S4mPdCu0OY4djdciC7Vssk0l22wQaDLrXxXkp+3+7bVA==" - }, - "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "c3YNH1GQJbfIPJeCnr4avseugSqPrxwIqzthYyZDN6EuOyNOzq+y2KSUfRcXauya1sF4foESTgwM5e1A8arAKw==" - }, - "runtime.native.System": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "c/qWt2LieNZIj1jGnVNsE2Kl23Ya2aSTBuXMD6V7k9KWr6l16Tqdwq+hJScEpWER9753NWC8h96PaVNY5Ld7Jw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0" - } - }, - "runtime.native.System.IO.Compression": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "INBPonS5QPEgn7naufQFXJEp3zX6L4bwHgJ/ZH78aBTpeNfQMtf7C6VrAFhlq2xxWBveIOWyFzQjJ8XzHMhdOQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0" - } - }, - "runtime.native.System.Net.Http": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "ZVuZJqnnegJhd2k/PtAbbIcZ3aZeITq3sj06oKfMBSfphW3HDmk/t4ObvbOk/JA/swGR0LNqMksAh/f7gpTROg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0" - } - }, - "runtime.native.System.Security.Cryptography.Apple": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "DloMk88juo0OuOWr56QG7MNchmafTLYWvABy36izkrLI5VledI0rq28KGs1i9wbpeT9NPQrx/wTf8U2vazqQ3Q==", - "dependencies": { - "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": "4.3.0" - } - }, - "runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "NS1U+700m4KFRHR5o4vo9DSlTmlCKu/u7dtE5sUHVIPB+xpXxYQvgBgA6wEIeCz6Yfn0Z52/72WYsToCEPJnrw==", - "dependencies": { - "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" - } - }, - "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "b3pthNgxxFcD+Pc0WSEoC0+md3MyhRS6aCEeenvNE3Fdw1HyJ18ZhRFVJJzIeR/O/jpxPboB805Ho0T3Ul7w8A==" - }, - "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "KeLz4HClKf+nFS7p/6Fi/CqyLXh81FpiGzcmuS8DGi9lUqSnZ6Es23/gv2O+1XVGfrbNmviF7CckBpavkBoIFQ==" - }, - "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "kVXCuMTrTlxq4XOOMAysuNwsXWpYeboGddNGpIgNSZmv1b6r/s/DPk0fYMB7Q5Qo4bY68o48jt4T4y5BVecbCQ==" - }, - "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "X7IdhILzr4ROXd8mI1BUCQMSHSQwelUlBjF1JyTKCjXaOGn2fB4EKBxQbCK2VjO3WaWIdlXZL3W6TiIVnrhX4g==" - }, - "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "nyFNiCk/r+VOiIqreLix8yN+q3Wga9+SE8BCgkf+2BwEKiNx6DyvFjCgkfV743/grxv8jHJ8gUK4XEQw7yzRYg==" - }, - "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "ytoewC6wGorL7KoCAvRfsgoJPJbNq+64k2SqW6JcOAebWsFUvCCYgfzQMrnpvPiEl4OrblUlhF2ji+Q1+SVLrQ==" - }, - "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "I8bKw2I8k58Wx7fMKQJn2R8lamboCAiHfHeV/pS65ScKWMMI0+wJkLYlEKvgW1D/XvSl/221clBoR2q9QNNM7A==" - }, - "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "VB5cn/7OzUfzdnC8tqAIMQciVLiq2epm2NrAm1E9OjNRyG4lVhfR61SMcLizejzQP8R8Uf/0l5qOIbUEi+RdEg==" - }, - "SixLabors.ImageSharp": { - "type": "Transitive", - "resolved": "1.0.1", - "contentHash": "DjLoFNdUfsDP7RhPpr5hcUhl1XiejqBML9uDWuOUwCkc0Y+sG9IJLLbqSOi9XeoWqPviwdcDm1F8nKdF0qTYIQ==" - }, - "SQLitePCLRaw.bundle_e_sqlite3": { - "type": "Transitive", - "resolved": "2.0.2", - "contentHash": "OVPI/nh5AqfLCIKhAYqjCa6AHhc7oKApGcGM3UhMRSerFiBx58nSpGwxVFdMgjOCWZR+fA49nzsnKlWp5hFo8w==", - "dependencies": { - "SQLitePCLRaw.core": "2.0.2", - "SQLitePCLRaw.lib.e_sqlite3": "2.0.2", - "SQLitePCLRaw.provider.dynamic_cdecl": "2.0.2" - } - }, - "SQLitePCLRaw.core": { - "type": "Transitive", - "resolved": "2.0.2", - "contentHash": "TFSBX426OelS1tkaVC254NVVlrJIe9YLhWPkEvuqJj2104QpmDmEYOhfdfDJD1E/2SmqDhoRw1ek5cQHj8olcQ==", - "dependencies": { - "System.Memory": "4.5.3" - } - }, - "SQLitePCLRaw.lib.e_sqlite3": { - "type": "Transitive", - "resolved": "2.0.2", - "contentHash": "S+Tsqe/M7wsc+9HeediI6UHtBKf2X586aRwhi1aBVLGe0WxkAo52O9ZxwEy/v8XMLefcrEMupd2e9CDlIT6QCw==" - }, - "SQLitePCLRaw.provider.dynamic_cdecl": { - "type": "Transitive", - "resolved": "2.0.2", - "contentHash": "ZSwacbKJUsxJEZxwT23uZVrGbaIvXcADZDz5Sr66fikO5eehdcceDncjzwzTzWfW13di8gpTpstx3WJSt/Ci5Q==", - "dependencies": { - "SQLitePCLRaw.core": "2.0.2" - } - }, - "System.AppContext": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "fKC+rmaLfeIzUhagxY17Q9siv/sPrjjKcfNg1Ic8IlQkZLipo8ljcaZQu4VtI4Jqbzjc2VTjzGLF6WmsRXAEgA==", - "dependencies": { - "System.Runtime": "4.3.0" - } - }, - "System.Buffers": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "ratu44uTIHgeBeI0dE8DWvmXVBSo4u7ozRZZHOMmK/JPpYyo0dAfgSiHlpiObMQ5lEtEyIXA40sKRYg5J6A8uQ==", - "dependencies": { - "System.Diagnostics.Debug": "4.3.0", - "System.Diagnostics.Tracing": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Threading": "4.3.0" - } - }, - "System.Collections": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Collections.Concurrent": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "ztl69Xp0Y/UXCL+3v3tEU+lIy+bvjKNUmopn1wep/a291pVPK7dxBd6T7WnlQqRog+d1a/hSsgRsmFnIBKTPLQ==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Diagnostics.Tracing": "4.3.0", - "System.Globalization": "4.3.0", - "System.Reflection": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Threading": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.Collections.Immutable": { - "type": "Transitive", - "resolved": "1.7.1", - "contentHash": "B43Zsz5EfMwyEbnObwRxW5u85fzJma3lrDeGcSAV1qkhSRTNY5uXAByTn9h9ddNdhM+4/YoLc/CI43umjwIl9Q==" - }, - "System.Collections.NonGeneric": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "prtjIEMhGUnQq6RnPEYLpFt8AtLbp9yq2zxOSrY7KJJZrw25Fi97IzBqY7iqssbM61Ek5b8f3MG/sG1N2sN5KA==", - "dependencies": { - "System.Diagnostics.Debug": "4.3.0", - "System.Globalization": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Threading": "4.3.0" - } - }, - "System.Collections.Specialized": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "Epx8PoVZR0iuOnJJDzp7pWvdfMMOAvpUo95pC4ScH2mJuXkKA2Y4aR3cG9qt2klHgSons1WFh4kcGW7cSXvrxg==", - "dependencies": { - "System.Collections.NonGeneric": "4.3.0", - "System.Globalization": "4.3.0", - "System.Globalization.Extensions": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Threading": "4.3.0" - } - }, - "System.ComponentModel": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "VyGn1jGRZVfxnh8EdvDCi71v3bMXrsu8aYJOwoV7SNDLVhiEqwP86pPMyRGsDsxhXAm2b3o9OIqeETfN5qfezw==", - "dependencies": { - "System.Runtime": "4.3.0" - } - }, - "System.ComponentModel.Annotations": { - "type": "Transitive", - "resolved": "4.7.0", - "contentHash": "0YFqjhp/mYkDGpU0Ye1GjE53HMp9UVfGN7seGpAMttAC0C40v5gw598jCgpbBLMmCo0E5YRLBv5Z2doypO49ZQ==" - }, - "System.ComponentModel.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "j8GUkCpM8V4d4vhLIIoBLGey2Z5bCkMVNjEZseyAlm4n5arcsJOeI3zkUP+zvZgzsbLTYh4lYeP/ZD/gdIAPrw==", - "dependencies": { - "System.ComponentModel": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.ComponentModel.TypeConverter": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "16pQ6P+EdhcXzPiEK4kbA953Fu0MNG2ovxTZU81/qsCd1zPRsKc3uif5NgvllCY598k6bI0KUyKW8fanlfaDQg==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Collections.NonGeneric": "4.3.0", - "System.Collections.Specialized": "4.3.0", - "System.ComponentModel": "4.3.0", - "System.ComponentModel.Primitives": "4.3.0", - "System.Globalization": "4.3.0", - "System.Linq": "4.3.0", - "System.Reflection": "4.3.0", - "System.Reflection.Extensions": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Reflection.TypeExtensions": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Threading": "4.3.0" - } - }, - "System.Configuration.ConfigurationManager": { - "type": "Transitive", - "resolved": "4.4.0", - "contentHash": "gWwQv/Ug1qWJmHCmN17nAbxJYmQBM/E94QxKLksvUiiKB1Ld3Sc/eK1lgmbSjDFxkQhVuayI/cGFZhpBSodLrg==", - "dependencies": { - "System.Security.Cryptography.ProtectedData": "4.4.0" - } - }, - "System.Console": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "DHDrIxiqk1h03m6khKWV2X8p/uvN79rgSqpilL6uzpmSfxfU5ng8VcPtW4qsDsQDHiTv6IPV9TmD5M/vElPNLg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.IO": "4.3.0", - "System.Runtime": "4.3.0", - "System.Text.Encoding": "4.3.0" - } - }, - "System.Diagnostics.Contracts": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "HvQQjy712vnlpPxaloZYkuE78Gn353L0SJLJVeLcNASeg9c4qla2a1Xq8I7B3jZoDzKPtHTkyVO7AZ5tpeQGuA==", - "dependencies": { - "System.Runtime": "4.1.0" - } - }, - "System.Diagnostics.Debug": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Diagnostics.DiagnosticSource": { - "type": "Transitive", - "resolved": "4.7.1", - "contentHash": "j81Lovt90PDAq8kLpaJfJKV/rWdWuEk6jfV+MBkee33vzYLEUsy4gXK8laa9V2nZlLM9VM9yA/OOQxxPEJKAMw==" - }, - "System.Diagnostics.Tools": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "UUvkJfSYJMM6x527dJg2VyWPSRqIVB0Z7dbjHst1zmwTXz5CcXSYJFWRpuigfbO1Lf7yfZiIaEUesfnl/g5EyA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Diagnostics.TraceSource": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "VnYp1NxGx8Ww731y2LJ1vpfb/DKVNKEZ8Jsh5SgQTZREL/YpWRArgh9pI8CDLmgHspZmLL697CaLvH85qQpRiw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Globalization": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Threading": "4.3.0", - "runtime.native.System": "4.3.0" - } - }, - "System.Diagnostics.Tracing": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "rswfv0f/Cqkh78rA5S8eN8Neocz234+emGCtTF3lxPY96F+mmmUen6tbn0glN6PMvlKQb9bPAY5e9u7fgPTkKw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Dynamic.Runtime": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "SNVi1E/vfWUAs/WYKhE9+qlS6KqK0YVhnlT0HQtr8pMIA8YX3lwy3uPMownDwdYISBdmAF/2holEIldVp85Wag==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Linq": "4.3.0", - "System.Linq.Expressions": "4.3.0", - "System.ObjectModel": "4.3.0", - "System.Reflection": "4.3.0", - "System.Reflection.Emit": "4.3.0", - "System.Reflection.Emit.ILGeneration": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Reflection.TypeExtensions": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Threading": "4.3.0" - } - }, - "System.Globalization": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Globalization.Calendars": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "GUlBtdOWT4LTV3I+9/PJW+56AnnChTaOqqTLFtdmype/L500M2LIyXgmtd9X2P2VOkmJd5c67H5SaC2QcL1bFA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Globalization": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Globalization.Extensions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "FhKmdR6MPG+pxow6wGtNAWdZh7noIOpdD5TwQ3CprzgIE1bBBoim0vbR1+AWsWjQmU7zXHgQo4TWSP6lCeiWcQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Globalization": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.InteropServices": "4.3.0" - } - }, - "System.IdentityModel.Tokens.Jwt": { - "type": "Transitive", - "resolved": "6.8.0", - "contentHash": "5tBCjAub2Bhd5qmcd0WhR5s354e4oLYa//kOWrkX+6/7ZbDDJjMTfwLSOiZ/MMpWdE4DWPLOfTLOq/juj9CKzA==", - "dependencies": { - "Microsoft.IdentityModel.JsonWebTokens": "6.8.0", - "Microsoft.IdentityModel.Tokens": "6.8.0" - } - }, - "System.IO": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.IO.Compression": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "YHndyoiV90iu4iKG115ibkhrG+S3jBm8Ap9OwoUAzO5oPDAWcr0SFwQFm0HjM8WkEZWo0zvLTyLmbvTkW1bXgg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Buffers": "4.3.0", - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.IO": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading": "4.3.0", - "System.Threading.Tasks": "4.3.0", - "runtime.native.System": "4.3.0", - "runtime.native.System.IO.Compression": "4.3.0" - } - }, - "System.IO.Compression.ZipFile": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "G4HwjEsgIwy3JFBduZ9quBkAu+eUwjIdJleuNSgmUojbH6O3mlvEIme+GHx/cLlTAPcrnnL7GqvB9pTlWRfhOg==", - "dependencies": { - "System.Buffers": "4.3.0", - "System.IO": "4.3.0", - "System.IO.Compression": "4.3.0", - "System.IO.FileSystem": "4.3.0", - "System.IO.FileSystem.Primitives": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Text.Encoding": "4.3.0" - } - }, - "System.IO.FileSystem": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "3wEMARTnuio+ulnvi+hkRNROYwa1kylvYahhcLk4HSoVdl+xxTFVeVlYOfLwrDPImGls0mDqbMhrza8qnWPTdA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.IO": "4.3.0", - "System.IO.FileSystem.Primitives": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.IO.FileSystem.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "6QOb2XFLch7bEc4lIcJH49nJN2HV+OC3fHDgsLVsBVBk3Y4hFAnOBGzJ2lUu7CyDDFo9IBWkSsnbkT6IBwwiMw==", - "dependencies": { - "System.Runtime": "4.3.0" - } - }, - "System.IO.Pipelines": { - "type": "Transitive", - "resolved": "4.7.3", - "contentHash": "zykThu9scJyg2Yeg27GMZCbjzniIsmjtNP5x6kQCd/8rEeKXRy20fP2NOMS7xQ+0pS/E85LZQA+K1aoQLxiUdw==" - }, - "System.Linq": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "5DbqIUpsDp0dFftytzuMmc0oeMdQwjcP/EWxsksIz/w1TcFRkZ3yKKz0PqiYFMmEwPSWw+qNVqD7PJ889JzHbw==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0" - } - }, - "System.Linq.Expressions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "PGKkrd2khG4CnlyJwxwwaWWiSiWFNBGlgXvJpeO0xCXrZ89ODrQ6tjEWS/kOqZ8GwEOUATtKtzp1eRgmYNfclg==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Globalization": "4.3.0", - "System.IO": "4.3.0", - "System.Linq": "4.3.0", - "System.ObjectModel": "4.3.0", - "System.Reflection": "4.3.0", - "System.Reflection.Emit": "4.3.0", - "System.Reflection.Emit.ILGeneration": "4.3.0", - "System.Reflection.Emit.Lightweight": "4.3.0", - "System.Reflection.Extensions": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Reflection.TypeExtensions": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Threading": "4.3.0" - } - }, - "System.Memory": { - "type": "Transitive", - "resolved": "4.5.3", - "contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA==" - }, - "System.Net.Http": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "sYg+FtILtRQuYWSIAuNOELwVuVsxVyJGWQyOnlAzhV4xvhyFnON1bAzYYC+jjRW8JREM45R0R5Dgi8MTC5sEwA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Diagnostics.DiagnosticSource": "4.3.0", - "System.Diagnostics.Tracing": "4.3.0", - "System.Globalization": "4.3.0", - "System.Globalization.Extensions": "4.3.0", - "System.IO": "4.3.0", - "System.IO.FileSystem": "4.3.0", - "System.Net.Primitives": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.OpenSsl": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Security.Cryptography.X509Certificates": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading": "4.3.0", - "System.Threading.Tasks": "4.3.0", - "runtime.native.System": "4.3.0", - "runtime.native.System.Net.Http": "4.3.0", - "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" - } - }, - "System.Net.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "qOu+hDwFwoZPbzPvwut2qATe3ygjeQBDQj91xlsaqGFQUI5i4ZnZb8yyQuLGpDGivEPIt8EJkd1BVzVoP31FXA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "System.Runtime.Handles": "4.3.0" - } - }, - "System.Net.Sockets": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "m6icV6TqQOAdgt5N/9I5KNpjom/5NFtkmGseEH+AK/hny8XrytLH3+b5M8zL/Ycg3fhIocFpUMyl/wpFnVRvdw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.IO": "4.3.0", - "System.Net.Primitives": "4.3.0", - "System.Runtime": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.Net.WebSockets": { - "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "2KJo8hir6Edi9jnMDAMhiJoI691xRBmKcbNpwjrvpIMOCTYOtBpSsSEGBxBDV7PKbasJNaFp1+PZz1D7xS41Hg==", - "dependencies": { - "Microsoft.Win32.Primitives": "4.0.1", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Threading.Tasks": "4.0.11" - } - }, - "System.ObjectModel": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "bdX+80eKv9bN6K4N+d77OankKHGn6CH711a6fcOpMQu2Fckp/Ft4L/kW9WznHpyR0NRAvJutzOMHNNlBGvxQzQ==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Threading": "4.3.0" - } - }, - "System.Reflection": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.IO": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Reflection.Emit": { - "type": "Transitive", - "resolved": "4.7.0", - "contentHash": "VR4kk8XLKebQ4MZuKuIni/7oh+QGFmZW3qORd1GvBq/8026OpW501SzT/oypwiQl4TvT8ErnReh/NzY9u+C6wQ==" - }, - "System.Reflection.Emit.ILGeneration": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==", - "dependencies": { - "System.Reflection": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Reflection.Emit.Lightweight": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "oadVHGSMsTmZsAF864QYN1t1QzZjIcuKU3l2S9cZOwDdDueNTrqq1yRj7koFfIGEnKpt6NjpL3rOzRhs4ryOgA==", - "dependencies": { - "System.Reflection": "4.3.0", - "System.Reflection.Emit.ILGeneration": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Reflection.Extensions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "rJkrJD3kBI5B712aRu4DpSIiHRtr6QlfZSQsb0hYHrDCZORXCFjQfoipo2LaMUHoT9i1B7j7MnfaEKWDFmFQNQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Reflection": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Reflection.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Reflection.TypeExtensions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "7u6ulLcZbyxB5Gq0nMkQttcdBTx57ibzw+4IOXEfR+sXYQoHvjW5LTLyNr8O22UIMrqYbchJQJnos4eooYzYJA==", - "dependencies": { - "System.Reflection": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Resources.ResourceManager": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Globalization": "4.3.0", - "System.Reflection": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Runtime": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0" - } - }, - "System.Runtime.Extensions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Runtime.Handles": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Runtime.InteropServices": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "uv1ynXqiMK8mp1GM3jDqPCFN66eJ5w5XNomaK2XD+TuCroNTLFGeZ+WCmBMcBDyTFKou3P6cR6J/QsaqDp7fGQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Reflection": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Handles": "4.3.0" - } - }, - "System.Runtime.InteropServices.RuntimeInformation": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "cbz4YJMqRDR7oLeMRbdYv7mYzc++17lNhScCX0goO2XpGWdvAt60CGN+FHdePUEHCe/Jy9jUlvNAiNdM+7jsOw==", - "dependencies": { - "System.Reflection": "4.3.0", - "System.Reflection.Extensions": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Threading": "4.3.0", - "runtime.native.System": "4.3.0" - } - }, - "System.Runtime.Numerics": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "yMH+MfdzHjy17l2KESnPiF2dwq7T+xLnSJar7slyimAkUh/gTrS9/UQOtv7xarskJ2/XDSNvfLGOBQPjL7PaHQ==", - "dependencies": { - "System.Globalization": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0" - } - }, - "System.Runtime.Serialization.Primitives": { - "type": "Transitive", - "resolved": "4.1.1", - "contentHash": "HZ6Du5QrTG8MNJbf4e4qMO3JRAkIboGT5Fk804uZtg3Gq516S7hAqTm2UZKUHa7/6HUGdVy3AqMQKbns06G/cg==", - "dependencies": { - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Security.Claims": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "4Jlp0OgJLS/Voj1kyFP6MJlIYp3crgfH8kNQk2p7+4JYfc1aAmh9PZyAMMbDhuoolGNtux9HqSOazsioRiDvCw==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Globalization": "4.0.11", - "System.IO": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Security.Principal": "4.0.1" - } - }, - "System.Security.Cryptography.Algorithms": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Collections": "4.3.0", - "System.IO": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Runtime.Numerics": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Text.Encoding": "4.3.0", - "runtime.native.System.Security.Cryptography.Apple": "4.3.0", - "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" - } - }, - "System.Security.Cryptography.Cng": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "WG3r7EyjUe9CMPFSs6bty5doUqT+q9pbI80hlNzo2SkPkZ4VTuZkGWjpp77JB8+uaL4DFPRdBsAY+DX3dBK92A==" - }, - "System.Security.Cryptography.Csp": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "X4s/FCkEUnRGnwR3aSfVIkldBmtURMhmexALNTwpjklzxWU7yjMk7GHLKOZTNkgnWnE0q7+BCf9N2LVRWxewaA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.IO": "4.3.0", - "System.Reflection": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading": "4.3.0" - } - }, - "System.Security.Cryptography.Encoding": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Collections": "4.3.0", - "System.Collections.Concurrent": "4.3.0", - "System.Linq": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Text.Encoding": "4.3.0", - "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" - } - }, - "System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "h4CEgOgv5PKVF/HwaHzJRiVboL2THYCou97zpmhjghx5frc7fIvlkY1jL+lnIQyChrJDMNEXS6r7byGif8Cy4w==", - "dependencies": { - "System.Collections": "4.3.0", - "System.IO": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Runtime.Numerics": "4.3.0", - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Text.Encoding": "4.3.0", - "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" - } - }, - "System.Security.Cryptography.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==", - "dependencies": { - "System.Diagnostics.Debug": "4.3.0", - "System.Globalization": "4.3.0", - "System.IO": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Threading": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.Security.Cryptography.ProtectedData": { - "type": "Transitive", - "resolved": "4.4.0", - "contentHash": "cJV7ScGW7EhatRsjehfvvYVBvtiSMKgN8bOVI0bQhnF5bU7vnHVIsH49Kva7i7GWaWYvmEzkYVk1TC+gZYBEog==" - }, - "System.Security.Cryptography.X509Certificates": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Globalization": "4.3.0", - "System.Globalization.Calendars": "4.3.0", - "System.IO": "4.3.0", - "System.IO.FileSystem": "4.3.0", - "System.IO.FileSystem.Primitives": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Runtime.Numerics": "4.3.0", - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Cng": "4.3.0", - "System.Security.Cryptography.Csp": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.OpenSsl": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading": "4.3.0", - "runtime.native.System": "4.3.0", - "runtime.native.System.Net.Http": "4.3.0", - "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" - } - }, - "System.Security.Principal": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "On+SKhXY5rzxh/S8wlH1Rm0ogBlu7zyHNxeNBiXauNrhHRXAe9EuX8Yl5IOzLPGU5Z4kLWHMvORDOCG8iu9hww==", - "dependencies": { - "System.Runtime": "4.1.0" - } - }, - "System.Text.Encoding": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Text.Encoding.Extensions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "YVMK0Bt/A43RmwizJoZ22ei2nmrhobgeiYwFzC4YAN+nue8RF6djXDMog0UCn+brerQoYVyaS+ghy9P/MUVcmw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "System.Text.Encoding": "4.3.0" - } - }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "GgJDO6/1bW6kkttxIiPK2jsqllQ3ifaeeBAJJrcoJq0lAclIZsAZZdEqi6JHq+QLZXL2UsjyWb8K8EOH7nOSPw==", - "dependencies": { - "System.Diagnostics.Debug": "4.0.11", - "System.IO": "4.1.0", - "System.Reflection": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Threading": "4.0.11" - } - }, - "System.Text.Json": { - "type": "Transitive", - "resolved": "4.7.2", - "contentHash": "TcMd95wcrubm9nHvJEQs70rC0H/8omiSGGpU4FQ/ZA1URIqD4pjmFJh2Mfv1yH1eHgJDWTi2hMDXwTET+zOOyg==" - }, - "System.Text.RegularExpressions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "RpT2DA+L660cBt1FssIE9CAGpLFdFPuheB7pLpKpn6ZXNby7jDERe8Ua/Ne2xGiwLVG2JOqziiaVCGDon5sKFA==", - "dependencies": { - "System.Runtime": "4.3.0" - } - }, - "System.Threading": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "VkUS0kOBcUf3Wwm0TSbrevDDZ6BlM+b/HRiapRFWjM5O0NS0LviG0glKmFK+hhPDd1XFeSdU1GmlLhb2CoVpIw==", - "dependencies": { - "System.Runtime": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.Threading.Tasks": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Threading.Tasks.Extensions": { - "type": "Transitive", - "resolved": "4.5.1", - "contentHash": "WSKUTtLhPR8gllzIWO2x6l4lmAIfbyMAiTlyXAis4QBDonXK4b4S6F8zGARX4/P8wH3DH+sLdhamCiHn+fTU1A==" - }, - "System.Threading.Timer": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "Z6YfyYTCg7lOZjJzBjONJTFKGN9/NIYKSxhU5GRd+DTwHSZyvWp1xuI5aR+dLg+ayyC5Xv57KiY4oJ0tMO89fQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Xml.ReaderWriter": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "GrprA+Z0RUXaR4N7/eW71j1rgMnEnEVlgii49GZyAjTH7uliMnrOU3HNFBr6fEDBCJCIdlVNq9hHbaDR621XBA==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Globalization": "4.3.0", - "System.IO": "4.3.0", - "System.IO.FileSystem": "4.3.0", - "System.IO.FileSystem.Primitives": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Text.Encoding.Extensions": "4.3.0", - "System.Text.RegularExpressions": "4.3.0", - "System.Threading.Tasks": "4.3.0", - "System.Threading.Tasks.Extensions": "4.3.0" - } - }, - "System.Xml.XDocument": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "5zJ0XDxAIg8iy+t4aMnQAu0MqVbqyvfoUVl1yDV61xdo3Vth45oA2FoY4pPkxYAH5f8ixpmTqXeEIya95x0aCQ==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Diagnostics.Tools": "4.3.0", - "System.Globalization": "4.3.0", - "System.IO": "4.3.0", - "System.Reflection": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading": "4.3.0", - "System.Xml.ReaderWriter": "4.3.0" - } - }, - "System.Xml.XmlDocument": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "lJ8AxvkX7GQxpC6GFCeBj8ThYVyQczx2+f/cWHJU8tjS7YfI6Cv6bon70jVEgs2CiFbmmM8b9j1oZVx0dSI2Ww==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Globalization": "4.3.0", - "System.IO": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading": "4.3.0", - "System.Xml.ReaderWriter": "4.3.0" - } - }, - "System.Xml.XPath": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "UWd1H+1IJ9Wlq5nognZ/XJdyj8qPE4XufBUkAW59ijsCPjZkZe0MUzKKJFBr+ZWBe5Wq1u1d5f2CYgE93uH7DA==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Diagnostics.Debug": "4.0.11", - "System.Globalization": "4.0.11", - "System.IO": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Threading": "4.0.11", - "System.Xml.ReaderWriter": "4.0.11" - } - }, - "System.Xml.XPath.XDocument": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "FLhdYJx4331oGovQypQ8JIw2kEmNzCsjVOVYY/16kZTUoquZG85oVn7yUhBE2OZt1yGPSXAL0HTEfzjlbNpM7Q==", - "dependencies": { - "System.Diagnostics.Debug": "4.0.11", - "System.Linq": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Threading": "4.0.11", - "System.Xml.ReaderWriter": "4.0.11", - "System.Xml.XDocument": "4.0.11", - "System.Xml.XPath": "4.0.1" - } - }, - "xunit.abstractions": { - "type": "Transitive", - "resolved": "2.0.3", - "contentHash": "pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg==" - }, - "xunit.analyzers": { - "type": "Transitive", - "resolved": "0.10.0", - "contentHash": "4/IDFCJfIeg6bix9apmUtIMwvOsiwqdEexeO/R2D4GReIGPLIRODTpId/l4LRSrAJk9lEO3Zx1H0Zx6uohJDNg==" - }, - "xunit.assert": { - "type": "Transitive", - "resolved": "2.4.1", - "contentHash": "O/Oe0BS5RmSsM+LQOb041TzuPo5MdH2Rov+qXGS37X+KFG1Hxz7kopYklM5+1Y+tRGeXrOx5+Xne1RuqLFQoyQ==", - "dependencies": { - "NETStandard.Library": "1.6.1" - } - }, - "xunit.core": { - "type": "Transitive", - "resolved": "2.4.1", - "contentHash": "Zsj5OMU6JasNGERXZy8s72+pcheG6Q15atS5XpZXqAtULuyQiQ6XNnUsp1gyfC6WgqScqMvySiEHmHcOG6Eg0Q==", - "dependencies": { - "xunit.extensibility.core": "[2.4.1]", - "xunit.extensibility.execution": "[2.4.1]" - } - }, - "xunit.extensibility.core": { - "type": "Transitive", - "resolved": "2.4.1", - "contentHash": "yKZKm/8QNZnBnGZFD9SewkllHBiK0DThybQD/G4PiAmQjKtEZyHi6ET70QPU9KtSMJGRYS6Syk7EyR2EVDU4Kg==", - "dependencies": { - "NETStandard.Library": "1.6.1", - "xunit.abstractions": "2.0.3" - } - }, - "xunit.extensibility.execution": { - "type": "Transitive", - "resolved": "2.4.1", - "contentHash": "7e/1jqBpcb7frLkB6XDrHCGXAbKN4Rtdb88epYxCSRQuZDRW8UtTfdTEVpdTl8s4T56e07hOBVd4G0OdCxIY2A==", - "dependencies": { - "NETStandard.Library": "1.6.1", - "xunit.extensibility.core": "[2.4.1]" - } - }, - "timeline": { - "type": "Project", - "dependencies": { - "AutoMapper": "10.1.1", - "AutoMapper.Extensions.Microsoft.DependencyInjection": "8.1.0", - "Microsoft.AspNetCore.SpaServices.Extensions": "3.1.9", - "Microsoft.EntityFrameworkCore": "3.1.9", - "Microsoft.EntityFrameworkCore.Analyzers": "3.1.9", - "Microsoft.EntityFrameworkCore.Sqlite": "3.1.9", - "NSwag.AspNetCore": "13.8.2", - "SixLabors.ImageSharp": "1.0.1", - "System.IdentityModel.Tokens.Jwt": "6.8.0", - "Timeline.ErrorCodes": "1.0.0" - } - }, - "timeline.errorcodes": { - "type": "Project" - } - } - } -} \ No newline at end of file diff --git a/Timeline.sln b/Timeline.sln deleted file mode 100644 index 40a32ee9..00000000 --- a/Timeline.sln +++ /dev/null @@ -1,42 +0,0 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29709.97 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Timeline", "Timeline\Timeline.csproj", "{A34D323C-5233-4754-B14F-4819CE9C27CA}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Timeline.Tests", "Timeline.Tests\Timeline.Tests.csproj", "{3D76D578-37BC-43C2-97BF-9C6DD3825F10}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Timeline.ErrorCodes", "Timeline.ErrorCodes\Timeline.ErrorCodes.csproj", "{1044E3B0-1010-47CA-956E-B6E8FE87055B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Timeline.ErrorCodes.CodeGenerator", "Timeline.ErrorCodes.CodeGenerator\Timeline.ErrorCodes.CodeGenerator.csproj", "{D0263FD3-DC6A-4676-A746-FDAFCDACC5F2}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {A34D323C-5233-4754-B14F-4819CE9C27CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A34D323C-5233-4754-B14F-4819CE9C27CA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A34D323C-5233-4754-B14F-4819CE9C27CA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A34D323C-5233-4754-B14F-4819CE9C27CA}.Release|Any CPU.Build.0 = Release|Any CPU - {3D76D578-37BC-43C2-97BF-9C6DD3825F10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3D76D578-37BC-43C2-97BF-9C6DD3825F10}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3D76D578-37BC-43C2-97BF-9C6DD3825F10}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3D76D578-37BC-43C2-97BF-9C6DD3825F10}.Release|Any CPU.Build.0 = Release|Any CPU - {1044E3B0-1010-47CA-956E-B6E8FE87055B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1044E3B0-1010-47CA-956E-B6E8FE87055B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1044E3B0-1010-47CA-956E-B6E8FE87055B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1044E3B0-1010-47CA-956E-B6E8FE87055B}.Release|Any CPU.Build.0 = Release|Any CPU - {D0263FD3-DC6A-4676-A746-FDAFCDACC5F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D0263FD3-DC6A-4676-A746-FDAFCDACC5F2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D0263FD3-DC6A-4676-A746-FDAFCDACC5F2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D0263FD3-DC6A-4676-A746-FDAFCDACC5F2}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {9A7526E5-E68F-465C-9E0F-88BF6E040F14} - EndGlobalSection -EndGlobal diff --git a/Timeline/Auth/Attribute.cs b/Timeline/Auth/Attribute.cs deleted file mode 100644 index 86d0109b..00000000 --- a/Timeline/Auth/Attribute.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Timeline.Entities; - -namespace Timeline.Auth -{ - public class AdminAuthorizeAttribute : AuthorizeAttribute - { - public AdminAuthorizeAttribute() - { - Roles = UserRoles.Admin; - } - } - - public class UserAuthorizeAttribute : AuthorizeAttribute - { - public UserAuthorizeAttribute() - { - Roles = UserRoles.User; - } - } -} diff --git a/Timeline/Auth/MyAuthenticationHandler.cs b/Timeline/Auth/MyAuthenticationHandler.cs deleted file mode 100644 index 3c97c329..00000000 --- a/Timeline/Auth/MyAuthenticationHandler.cs +++ /dev/null @@ -1,100 +0,0 @@ -using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Net.Http.Headers; -using System; -using System.Globalization; -using System.Linq; -using System.Security.Claims; -using System.Text.Encodings.Web; -using System.Threading.Tasks; -using Timeline.Services; -using static Timeline.Resources.Authentication.AuthHandler; - -namespace Timeline.Auth -{ - public static class AuthenticationConstants - { - public const string Scheme = "Bearer"; - public const string DisplayName = "My Jwt Auth Scheme"; - } - - public class MyAuthenticationOptions : AuthenticationSchemeOptions - { - /// - /// The query param key to search for token. If null then query params are not searched for token. Default to "token". - /// - public string TokenQueryParamKey { get; set; } = "token"; - } - - public class MyAuthenticationHandler : AuthenticationHandler - { - private readonly ILogger _logger; - private readonly IUserTokenManager _userTokenManager; - - public MyAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IUserTokenManager userTokenManager) - : base(options, logger, encoder, clock) - { - _logger = logger.CreateLogger(); - _userTokenManager = userTokenManager; - } - - // return null if no token is found - private string? ExtractToken() - { - // check the authorization header - string header = Request.Headers[HeaderNames.Authorization]; - if (!string.IsNullOrEmpty(header) && header.StartsWith("Bearer ", StringComparison.InvariantCultureIgnoreCase)) - { - var token = header.Substring("Bearer ".Length).Trim(); - _logger.LogInformation(LogTokenFoundInHeader, token); - return token; - } - - // check the query params - var paramQueryKey = Options.TokenQueryParamKey; - if (!string.IsNullOrEmpty(paramQueryKey)) - { - string token = Request.Query[paramQueryKey]; - if (!string.IsNullOrEmpty(token)) - { - _logger.LogInformation(LogTokenFoundInQuery, paramQueryKey, token); - return token; - } - } - - // not found anywhere then return null - return null; - } - - protected override async Task HandleAuthenticateAsync() - { - var token = ExtractToken(); - if (string.IsNullOrEmpty(token)) - { - _logger.LogInformation(LogTokenNotFound); - return AuthenticateResult.NoResult(); - } - - try - { - var userInfo = await _userTokenManager.VerifyToken(token); - - var identity = new ClaimsIdentity(AuthenticationConstants.Scheme); - identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userInfo.Id!.Value.ToString(CultureInfo.InvariantCulture), ClaimValueTypes.Integer64)); - identity.AddClaim(new Claim(identity.NameClaimType, userInfo.Username, ClaimValueTypes.String)); - identity.AddClaims(UserRoleConvert.ToArray(userInfo.Administrator!.Value).Select(role => new Claim(identity.RoleClaimType, role, ClaimValueTypes.String))); - - var principal = new ClaimsPrincipal(); - principal.AddIdentity(identity); - - return AuthenticateResult.Success(new AuthenticationTicket(principal, AuthenticationConstants.Scheme)); - } - catch (Exception e) when (!(e is ArgumentException)) - { - _logger.LogInformation(e, LogTokenValidationFail); - return AuthenticateResult.Fail(e); - } - } - } -} diff --git a/Timeline/Auth/PrincipalExtensions.cs b/Timeline/Auth/PrincipalExtensions.cs deleted file mode 100644 index ad7a887f..00000000 --- a/Timeline/Auth/PrincipalExtensions.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Security.Principal; -using Timeline.Entities; - -namespace Timeline.Auth -{ - internal static class PrincipalExtensions - { - internal static bool IsAdministrator(this IPrincipal principal) - { - return principal.IsInRole(UserRoles.Admin); - } - } -} diff --git a/Timeline/ClientApp/.babelrc b/Timeline/ClientApp/.babelrc deleted file mode 100644 index 092f2f73..00000000 --- a/Timeline/ClientApp/.babelrc +++ /dev/null @@ -1,27 +0,0 @@ -{ - "presets": [ - "@babel/env", - "@babel/preset-react" - ], - "plugins": [ - "@babel/plugin-syntax-dynamic-import", - "@babel/plugin-proposal-class-properties", - "@babel/plugin-proposal-optional-chaining", - "@babel/plugin-proposal-nullish-coalescing-operator", - [ - "@babel/plugin-proposal-decorators", - { - "decoratorsBeforeExport": true - } - ], - [ - "babel-plugin-transform-builtin-extend", - { - "globals": [ - "Error", - "Array" - ] - } - ] - ] -} \ No newline at end of file diff --git a/Timeline/ClientApp/.editorconfig b/Timeline/ClientApp/.editorconfig deleted file mode 100644 index 779719e0..00000000 --- a/Timeline/ClientApp/.editorconfig +++ /dev/null @@ -1,14 +0,0 @@ -root = true -end_of_line = lf - -[*.ts] -tab_width = 2 - -[*.tsx] -tab_width = 2 - -[*.css] -tab_width = 2 - -[*.sass] -tab_width = 2 diff --git a/Timeline/ClientApp/.eslintignore b/Timeline/ClientApp/.eslintignore deleted file mode 100644 index f29f7466..00000000 --- a/Timeline/ClientApp/.eslintignore +++ /dev/null @@ -1,6 +0,0 @@ -.yarn -node_modules -dist -webpack.*.js -.eslintrc.js -postcss.config.js diff --git a/Timeline/ClientApp/.eslintrc.js b/Timeline/ClientApp/.eslintrc.js deleted file mode 100644 index 900489ed..00000000 --- a/Timeline/ClientApp/.eslintrc.js +++ /dev/null @@ -1,47 +0,0 @@ -module.exports = { - env: { - browser: true, - es2020: true, - }, - extends: [ - "eslint:recommended", - "plugin:react/recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended", - "plugin:@typescript-eslint/recommended-requiring-type-checking", - "plugin:prettier/recommended", - "prettier/react", - "prettier/@typescript-eslint", - "plugin:react-hooks/recommended", - ], - globals: { - Atomics: "readonly", - SharedArrayBuffer: "readonly", - }, - parser: "@typescript-eslint/parser", - parserOptions: { - project: ["./src/app/tsconfig.json", "./src/sw/tsconfig.json"], - ecmaFeatures: { - jsx: true, - }, - sourceType: "module", - }, - plugins: ["react", "@typescript-eslint", "react-hooks"], - settings: { - react: { - version: "detect", - }, - }, - rules: { - "react/prop-types": "off", - "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], - "@typescript-eslint/explicit-function-return-type": [ - "warn", - { - allowExpressions: true, - allowTypedFunctionExpressions: true, - allowHigherOrderFunctions: true, - }, - ], - }, -}; diff --git a/Timeline/ClientApp/.gitattributes b/Timeline/ClientApp/.gitattributes deleted file mode 100644 index c1aa21ac..00000000 --- a/Timeline/ClientApp/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -.yarn/** linguist-vendored \ No newline at end of file diff --git a/Timeline/ClientApp/.gitignore b/Timeline/ClientApp/.gitignore deleted file mode 100644 index 1de0b58f..00000000 --- a/Timeline/ClientApp/.gitignore +++ /dev/null @@ -1,32 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules - -.yarn/* -!.yarn/cache -!.yarn/releases -!.yarn/plugins -!.yarn/sdks -!.yarn/versions - -# testing -/coverage - -# production -/build - -# misc -.DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local - -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -.vscode/launch.json - -/dist \ No newline at end of file diff --git a/Timeline/ClientApp/.vscode/extensions.json b/Timeline/ClientApp/.vscode/extensions.json deleted file mode 100644 index be640996..00000000 --- a/Timeline/ClientApp/.vscode/extensions.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "recommendations": [ - "dbaeumer.vscode-eslint", - "esbenp.prettier-vscode", - "arcanis.vscode-zipfs", - "syler.sass-indented", - "editorconfig.editorconfig" - ] -} diff --git a/Timeline/ClientApp/.vscode/preview.yml b/Timeline/ClientApp/.vscode/preview.yml deleted file mode 100644 index eb5e452c..00000000 --- a/Timeline/ClientApp/.vscode/preview.yml +++ /dev/null @@ -1,10 +0,0 @@ - -# .vscode/preview.yml -autoOpen: false # 打开工作空间时是否自动开启所有应用的预览 -apps: - - port: 3000 # 应用的端口 - run: yarn start:mock # 应用的启动命令 - root: . # 应用的启动目录 - name: timeline # 应用名称 - description: Timeline App # 应用描述 - autoOpen: false # 打开工作空间时是否自动开启预览(优先级高于根级 autoOpen) diff --git a/Timeline/ClientApp/.vscode/settings.json b/Timeline/ClientApp/.vscode/settings.json deleted file mode 100644 index 3db658ba..00000000 --- a/Timeline/ClientApp/.vscode/settings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "eslint.nodePath": ".yarn/sdks", - "prettier.prettierPath": ".yarn/sdks/prettier/index.js", - "typescript.tsdk": ".yarn/sdks/typescript/lib", - "typescript.enablePromptUseWorkspaceTsdk": true -} diff --git a/Timeline/ClientApp/.yarnrc.yml b/Timeline/ClientApp/.yarnrc.yml deleted file mode 100644 index 2e4e43a7..00000000 --- a/Timeline/ClientApp/.yarnrc.yml +++ /dev/null @@ -1,5 +0,0 @@ -plugins: - - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs - spec: "@yarnpkg/plugin-interactive-tools" - -yarnPath: .yarn/releases/yarn-2.1.1.cjs diff --git a/Timeline/ClientApp/LICENSE b/Timeline/ClientApp/LICENSE deleted file mode 100644 index 238cd2d9..00000000 --- a/Timeline/ClientApp/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2020 杨宇千 - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/Timeline/ClientApp/package.json b/Timeline/ClientApp/package.json deleted file mode 100644 index 65c5cbe2..00000000 --- a/Timeline/ClientApp/package.json +++ /dev/null @@ -1,111 +0,0 @@ -{ - "name": "timeline", - "version": "0.1.0", - "private": true, - "homepage": "https://crupest.xyz", - "keywords": [], - "description": "Timeline app.", - "dependencies": { - "axios": "^0.21.0", - "bootstrap": "^4.5.3", - "bootstrap-icons": "^1.0.0", - "classnames": "^2.2.6", - "clsx": "^1.1.1", - "core-js": "^3.6.5", - "i18next": "^19.8.3", - "i18next-browser-languagedetector": "^6.0.1", - "localforage": "^1.9.0", - "lodash": "^4.17.20", - "pepjs": "^0.5.2", - "react": "^17.0.1", - "react-bootstrap": "^1.4.0", - "react-dom": "^17.0.1", - "react-hot-loader": "^4.13.0", - "react-i18next": "^11.7.3", - "react-inlinesvg": "^2.1.1", - "react-responsive": "^8.1.0", - "react-router": "^5.2.0", - "react-router-bootstrap": "^0.25.0", - "react-router-dom": "^5.2.0", - "regenerator-runtime": "^0.13.7", - "rxjs": "^6.6.3", - "workbox-precaching": "^5.1.4", - "workbox-routing": "^5.1.4", - "workbox-strategies": "^5.1.4", - "workbox-window": "^5.1.4", - "xregexp": "^4.3.0" - }, - "scripts": { - "start": "webpack-dev-server --config ./webpack.config.dev.js", - "build": "webpack --config ./webpack.config.prod.js", - "lint": "eslint src/ --ext .js --ext .jsx --ext .ts --ext .tsx" - }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - }, - "devDependencies": { - "@babel/core": "^7.12.3", - "@babel/plugin-proposal-class-properties": "^7.12.1", - "@babel/plugin-proposal-decorators": "^7.12.1", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1", - "@babel/plugin-proposal-optional-chaining": "^7.12.1", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/preset-env": "^7.12.1", - "@babel/preset-react": "^7.12.1", - "@babel/preset-typescript": "^7.12.1", - "@hot-loader/react-dom": "^17.0.0", - "@types/classnames": "^2.2.10", - "@types/lodash": "^4.14.162", - "@types/node": "^14.14.5", - "@types/react": "^16.9.53", - "@types/react-dom": "^16.9.8", - "@types/react-responsive": "^8.0.2", - "@types/react-router": "^5.1.8", - "@types/react-router-bootstrap": "^0.24.5", - "@types/react-router-dom": "^5.1.6", - "@types/webpack-env": "^1.15.3", - "@types/xregexp": "^4.3.0", - "@typescript-eslint/eslint-plugin": "^4.6.0", - "@typescript-eslint/parser": "^4.6.0", - "@yarnpkg/pnpify": "^2.3.3", - "babel-loader": "^8.1.0", - "babel-plugin-transform-builtin-extend": "^1.1.2", - "clean-webpack-plugin": "^3.0.0", - "copy-webpack-plugin": "^6.2.1", - "css-loader": "^5.0.0", - "eslint": "^7.12.1", - "eslint-config-prettier": "^6.14.0", - "eslint-plugin-import": "^2.22.1", - "eslint-plugin-prettier": "^3.1.4", - "eslint-plugin-react": "^7.21.5", - "eslint-plugin-react-hooks": "^4.2.0", - "file-loader": "^6.1.1", - "html-webpack-plugin": "^4.5.0", - "http-server": "^0.12.3", - "mini-css-extract-plugin": "^1.2.0", - "postcss": "^8.1.4", - "postcss-loader": "^4.0.4", - "postcss-preset-env": "^6.7.0", - "prettier": "^2.1.2", - "sass": "^1.27.0", - "sass-loader": "^10.0.4", - "style-loader": "^2.0.0", - "ts-loader": "^8.0.7", - "typescript": "^4.0.5", - "url-loader": "^4.1.1", - "webpack": "^5.2.0", - "webpack-chain": "^6.5.1", - "webpack-cli": "^4.1.0", - "webpack-dev-server": "^3.11.0", - "workbox-webpack-plugin": "^5.1.4" - } -} diff --git a/Timeline/ClientApp/postcss.config.js b/Timeline/ClientApp/postcss.config.js deleted file mode 100644 index 74ee8155..00000000 --- a/Timeline/ClientApp/postcss.config.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - plugins: [ - [ - "postcss-preset-env", - { - // Options - }, - ], - ], -}; diff --git a/Timeline/ClientApp/public/android-chrome-192x192.png b/Timeline/ClientApp/public/android-chrome-192x192.png deleted file mode 100644 index da9b6b81..00000000 Binary files a/Timeline/ClientApp/public/android-chrome-192x192.png and /dev/null differ diff --git a/Timeline/ClientApp/public/android-chrome-512x512.png b/Timeline/ClientApp/public/android-chrome-512x512.png deleted file mode 100644 index fa84e055..00000000 Binary files a/Timeline/ClientApp/public/android-chrome-512x512.png and /dev/null differ diff --git a/Timeline/ClientApp/public/apple-touch-icon.png b/Timeline/ClientApp/public/apple-touch-icon.png deleted file mode 100644 index d5a3fb45..00000000 Binary files a/Timeline/ClientApp/public/apple-touch-icon.png and /dev/null differ diff --git a/Timeline/ClientApp/public/browserconfig.xml b/Timeline/ClientApp/public/browserconfig.xml deleted file mode 100644 index f2c89409..00000000 --- a/Timeline/ClientApp/public/browserconfig.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - #2d89ef - - - diff --git a/Timeline/ClientApp/public/favicon-16x16.png b/Timeline/ClientApp/public/favicon-16x16.png deleted file mode 100644 index 6c978995..00000000 Binary files a/Timeline/ClientApp/public/favicon-16x16.png and /dev/null differ diff --git a/Timeline/ClientApp/public/favicon-32x32.png b/Timeline/ClientApp/public/favicon-32x32.png deleted file mode 100644 index bbde902f..00000000 Binary files a/Timeline/ClientApp/public/favicon-32x32.png and /dev/null differ diff --git a/Timeline/ClientApp/public/favicon.ico b/Timeline/ClientApp/public/favicon.ico deleted file mode 100644 index d4cd3db6..00000000 Binary files a/Timeline/ClientApp/public/favicon.ico and /dev/null differ diff --git a/Timeline/ClientApp/public/mstile-144x144.png b/Timeline/ClientApp/public/mstile-144x144.png deleted file mode 100644 index 61eaaf43..00000000 Binary files a/Timeline/ClientApp/public/mstile-144x144.png and /dev/null differ diff --git a/Timeline/ClientApp/public/mstile-150x150.png b/Timeline/ClientApp/public/mstile-150x150.png deleted file mode 100644 index 85fa83ee..00000000 Binary files a/Timeline/ClientApp/public/mstile-150x150.png and /dev/null differ diff --git a/Timeline/ClientApp/public/mstile-310x150.png b/Timeline/ClientApp/public/mstile-310x150.png deleted file mode 100644 index 41889953..00000000 Binary files a/Timeline/ClientApp/public/mstile-310x150.png and /dev/null differ diff --git a/Timeline/ClientApp/public/mstile-310x310.png b/Timeline/ClientApp/public/mstile-310x310.png deleted file mode 100644 index cddce02e..00000000 Binary files a/Timeline/ClientApp/public/mstile-310x310.png and /dev/null differ diff --git a/Timeline/ClientApp/public/mstile-70x70.png b/Timeline/ClientApp/public/mstile-70x70.png deleted file mode 100644 index 52f59d43..00000000 Binary files a/Timeline/ClientApp/public/mstile-70x70.png and /dev/null differ diff --git a/Timeline/ClientApp/public/safari-pinned-tab.svg b/Timeline/ClientApp/public/safari-pinned-tab.svg deleted file mode 100644 index e91f046a..00000000 --- a/Timeline/ClientApp/public/safari-pinned-tab.svg +++ /dev/null @@ -1,25 +0,0 @@ - - - - -Created by potrace 1.11, written by Peter Selinger 2001-2013 - - - - - diff --git a/Timeline/ClientApp/public/site.webmanifest b/Timeline/ClientApp/public/site.webmanifest deleted file mode 100644 index 74f0901a..00000000 --- a/Timeline/ClientApp/public/site.webmanifest +++ /dev/null @@ -1,22 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/web-manifest", - - "name": "Timeline", - "short_name": "Timeline", - "description": "Record your life in Timeline! Created by crupest.", - "icons": [ - { - "src": "/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" -} diff --git a/Timeline/ClientApp/sandbox.config.json b/Timeline/ClientApp/sandbox.config.json deleted file mode 100644 index 42b540ce..00000000 --- a/Timeline/ClientApp/sandbox.config.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "infiniteLoopProtection": true, - "hardReloadOnChange": false, - "view": "browser", - "container": { - "port": 3000, - "startScript": "start:mock" - }, - "port": 3000, - "startScript": "start:mock" -} diff --git a/Timeline/ClientApp/src/app/App.tsx b/Timeline/ClientApp/src/app/App.tsx deleted file mode 100644 index b68eddb6..00000000 --- a/Timeline/ClientApp/src/app/App.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import React from "react"; -import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; -import { hot } from "react-hot-loader/root"; - -import AppBar from "./views/common/AppBar"; -import LoadingPage from "./views/common/LoadingPage"; -import Home from "./views/home"; -import Login from "./views/login"; -import Settings from "./views/settings"; -import About from "./views/about"; -import User from "./views/user"; -import TimelinePage from "./views/timeline"; -import AlertHost from "./views/common/alert/AlertHost"; - -import { dataStorage } from "./services/common"; -import { userService, useRawUser } from "./services/user"; - -const NoMatch: React.FC = () => { - return ( - <> - -
-
Ah-oh, 404!
- - ); -}; - -const LazyAdmin = React.lazy( - () => import(/* webpackChunkName: "admin" */ "./views/admin/Admin") -); - -const App: React.FC = () => { - const [loading, setLoading] = React.useState(true); - - const user = useRawUser(); - - React.useEffect(() => { - void userService.checkLoginState(); - void dataStorage.ready().then(() => setLoading(false)); - }, []); - - if (user === undefined || loading) { - return ; - } else { - return ( - }> - - - - - - - - - - - - - - - - - - - - - - {user && user.administrator && ( - - - - )} - - - - - - - - ); - } -}; - -export default hot(App); diff --git a/Timeline/ClientApp/src/app/common.ts b/Timeline/ClientApp/src/app/common.ts deleted file mode 100644 index 0a2d345f..00000000 --- a/Timeline/ClientApp/src/app/common.ts +++ /dev/null @@ -1,44 +0,0 @@ -import React from "react"; -import { Observable, Subject } from "rxjs"; - -// This error is thrown when ui goes wrong with bad logic. -// Such as a variable should not be null, but it does. -// This error should never occur. If it does, it indicates there is some logic bug in codes. -export class UiLogicError extends Error {} - -export function useEventEmiiter(): [() => Observable, () => void] { - const ref = React.useRef | null>(null); - - return React.useMemo(() => { - const getter = (): Subject => { - if (ref.current == null) { - ref.current = new Subject(); - } - return ref.current; - }; - const trigger = (): void => { - getter().next(null); - }; - return [getter, trigger]; - }, []); -} - -export function useValueEventEmiiter(): [ - () => Observable, - (value: T) => void -] { - const ref = React.useRef | null>(null); - - return React.useMemo(() => { - const getter = (): Subject => { - if (ref.current == null) { - ref.current = new Subject(); - } - return ref.current; - }; - const trigger = (value: T): void => { - getter().next(value); - }; - return [getter, trigger]; - }, []); -} diff --git a/Timeline/ClientApp/src/app/http/common.ts b/Timeline/ClientApp/src/app/http/common.ts deleted file mode 100644 index 54203d1a..00000000 --- a/Timeline/ClientApp/src/app/http/common.ts +++ /dev/null @@ -1,161 +0,0 @@ -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/timeline.ts b/Timeline/ClientApp/src/app/http/timeline.ts deleted file mode 100644 index eb7d5065..00000000 --- a/Timeline/ClientApp/src/app/http/timeline.ts +++ /dev/null @@ -1,544 +0,0 @@ -import axios, { AxiosError } from "axios"; - -import { updateQueryString, applyQueryParameters } from "../utilities/url"; - -import { - apiBaseUrl, - extractResponseData, - convertToNetworkError, - base64, - convertToIfStatusCodeIs, - convertToIfErrorCodeIs, - BlobWithEtag, - NotModified, - convertToNotModified, - convertToForbiddenError, - convertToBlobWithEtag, -} from "./common"; -import { HttpUser } from "./user"; - -export const kTimelineVisibilities = ["Public", "Register", "Private"] as const; - -export type TimelineVisibility = typeof kTimelineVisibilities[number]; - -export interface HttpTimelineInfo { - uniqueId: string; - name: string; - description: string; - owner: HttpUser; - visibility: TimelineVisibility; - lastModified: Date; - members: HttpUser[]; -} - -export interface HttpTimelineListQuery { - visibility?: TimelineVisibility; - relate?: string; - relateType?: "own" | "join"; -} - -export interface HttpTimelinePostRequest { - name: string; -} - -export interface HttpTimelinePostTextContent { - type: "text"; - text: string; -} - -export interface HttpTimelinePostImageContent { - type: "image"; -} - -export type HttpTimelinePostContent = - | HttpTimelinePostTextContent - | HttpTimelinePostImageContent; - -export interface HttpTimelinePostInfo { - id: number; - content: HttpTimelinePostContent; - time: Date; - lastUpdated: Date; - author: HttpUser; - deleted: false; -} - -export interface HttpTimelineDeletedPostInfo { - id: number; - time: Date; - lastUpdated: Date; - author?: HttpUser; - deleted: true; -} - -export type HttpTimelineGenericPostInfo = - | HttpTimelinePostInfo - | HttpTimelineDeletedPostInfo; - -export interface HttpTimelinePostPostRequestTextContent { - type: "text"; - text: string; -} - -export interface HttpTimelinePostPostRequestImageContent { - type: "image"; - data: Blob; -} - -export type HttpTimelinePostPostRequestContent = - | HttpTimelinePostPostRequestTextContent - | HttpTimelinePostPostRequestImageContent; - -export interface HttpTimelinePostPostRequest { - content: HttpTimelinePostPostRequestContent; - time?: Date; -} - -export interface HttpTimelinePatchRequest { - visibility?: TimelineVisibility; - description?: string; -} - -export class HttpTimelineNotExistError extends Error { - constructor(public innerError?: AxiosError) { - super(); - } -} - -export class HttpTimelinePostNotExistError extends Error { - constructor(public innerError?: AxiosError) { - super(); - } -} - -export class HttpTimelineNameConflictError extends Error { - constructor(public innerError?: AxiosError) { - super(); - } -} - -//-------------------- begin: internal model -------------------- - -interface RawTimelineInfo { - uniqueId: string; - name: string; - description: string; - owner: HttpUser; - visibility: TimelineVisibility; - lastModified: string; - members: HttpUser[]; -} - -interface RawTimelinePostTextContent { - type: "text"; - text: string; -} - -interface RawTimelinePostImageContent { - type: "image"; - url: string; -} - -type RawTimelinePostContent = - | RawTimelinePostTextContent - | RawTimelinePostImageContent; - -interface RawTimelinePostInfo { - id: number; - content: RawTimelinePostContent; - time: string; - lastUpdated: string; - author: HttpUser; - deleted: false; -} - -interface RawTimelineDeletedPostInfo { - id: number; - time: string; - lastUpdated: string; - author: HttpUser; - deleted: true; -} - -type RawTimelineGenericPostInfo = - | RawTimelinePostInfo - | RawTimelineDeletedPostInfo; - -interface RawTimelinePostPostRequestTextContent { - type: "text"; - text: string; -} - -interface RawTimelinePostPostRequestImageContent { - type: "image"; - data: string; -} - -type RawTimelinePostPostRequestContent = - | RawTimelinePostPostRequestTextContent - | RawTimelinePostPostRequestImageContent; - -interface RawTimelinePostPostRequest { - content: RawTimelinePostPostRequestContent; - time?: string; -} - -//-------------------- end: internal model -------------------- - -function processRawTimelineInfo(raw: RawTimelineInfo): HttpTimelineInfo { - return { - ...raw, - lastModified: new Date(raw.lastModified), - }; -} - -function processRawTimelinePostInfo( - raw: RawTimelinePostInfo -): HttpTimelinePostInfo; -function processRawTimelinePostInfo( - raw: RawTimelineGenericPostInfo -): HttpTimelineGenericPostInfo; -function processRawTimelinePostInfo( - raw: RawTimelineGenericPostInfo -): HttpTimelineGenericPostInfo { - return { - ...raw, - time: new Date(raw.time), - lastUpdated: new Date(raw.lastUpdated), - }; -} - -export interface IHttpTimelineClient { - listTimeline(query: HttpTimelineListQuery): Promise; - 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 deleted file mode 100644 index ae0cf3f6..00000000 --- a/Timeline/ClientApp/src/app/http/token.ts +++ /dev/null @@ -1,72 +0,0 @@ -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 deleted file mode 100644 index a0a02cce..00000000 --- a/Timeline/ClientApp/src/app/http/user.ts +++ /dev/null @@ -1,134 +0,0 @@ -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; -} diff --git a/Timeline/ClientApp/src/app/i18n.ts b/Timeline/ClientApp/src/app/i18n.ts deleted file mode 100644 index cdced7bf..00000000 --- a/Timeline/ClientApp/src/app/i18n.ts +++ /dev/null @@ -1,79 +0,0 @@ -import i18n, { BackendModule, ResourceKey } from "i18next"; -import LanguageDetector from "i18next-browser-languagedetector"; -import { initReactI18next } from "react-i18next"; - -const backend: BackendModule = { - type: "backend", - async read(language, namespace, callback) { - function error(message: string): void { - callback(new Error(message), false); - } - - function success(result: ResourceKey): void { - callback(null, result); - } - - if (namespace !== "translation") { - error("Namespace must be 'translation'."); - } - - if (language === "en") { - const res = ( - await import( - /* webpackChunkName: "locales-en" */ "./locales/en/translation" - ) - ).default; - success(res); - } else if (language === "zh-cn" || language === "zh") { - const res = ( - await import( - /* webpackChunkName: "locales-zh" */ "./locales/zh/translation" - ) - ).default; - success(res); - } else { - error(`Language ${language} is not supported.`); - } - }, - init() {}, // eslint-disable-line @typescript-eslint/no-empty-function - create() {}, // eslint-disable-line @typescript-eslint/no-empty-function -}; - -export const i18nPromise = i18n - .use(LanguageDetector) - .use(backend) - .use(initReactI18next) // bind react-i18next to the instance - .init({ - fallbackLng: false, - lowerCaseLng: true, - - debug: process.env.NODE_ENV === "development", - - interpolation: { - escapeValue: false, // not needed for react!! - }, - - // react i18next special options (optional) - // override if needed - omit if ok with defaults - /* - react: { - bindI18n: 'languageChanged', - bindI18nStore: '', - transEmptyNodeValue: '', - transSupportBasicHtmlNodes: true, - transKeepBasicHtmlNodesFor: ['br', 'strong', 'i'], - useSuspense: true, - } - */ - }); - -if (module.hot) { - module.hot.accept( - ["./locales/en/translation", "./locales/zh/translation"], - () => { - void i18n.reloadResources(); - } - ); -} - -export default i18n; diff --git a/Timeline/ClientApp/src/app/index.ejs b/Timeline/ClientApp/src/app/index.ejs deleted file mode 100644 index 49306786..00000000 --- a/Timeline/ClientApp/src/app/index.ejs +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - <%= htmlWebpackPlugin.options.title %> - - - -
- - - diff --git a/Timeline/ClientApp/src/app/index.sass b/Timeline/ClientApp/src/app/index.sass deleted file mode 100644 index 08e03bac..00000000 --- a/Timeline/ClientApp/src/app/index.sass +++ /dev/null @@ -1,66 +0,0 @@ -@import '~bootstrap/scss/bootstrap' - -@import './views/common/common' -@import './views/common/alert/alert' -@import './views/home/home' -@import './views/about/about' -@import './views/login/login' -@import './views/timeline-common/timeline-common' -@import './views/timeline/timeline' -@import './views/user/user' - -body - margin: 0 - -small - line-height: 1.2 - -.flex-fix-length - flex-grow: 0 - flex-shrink: 0 - -.position-lt - left: 0 - top: 0 - -.avatar - width: 60px - &.large - width: 100px - &.small - width: 40px - -.mt-appbar - margin-top: 56px - -.icon-button - font-size: 1.4em - &.large - font-size: 1.6em - -.cursor-pointer - cursor: pointer - -textarea - resize: none - -.white-space-no-wrap - white-space: nowrap - -.cru-card - @extend .shadow - @extend .border - @extend .border-primary - @extend .rounded - @extend .bg-light - -.full-viewport-center-child - position: fixed - width: 100vw - height: 100vh - display: flex - justify-content: center - align-items: center - -.text-orange - color: $orange diff --git a/Timeline/ClientApp/src/app/index.tsx b/Timeline/ClientApp/src/app/index.tsx deleted file mode 100644 index 00a75a4a..00000000 --- a/Timeline/ClientApp/src/app/index.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import "regenerator-runtime"; -import "core-js/modules/es.promise"; -import "core-js/modules/es.array.iterator"; -import "pepjs"; - -import React from "react"; -import ReactDOM from "react-dom"; - -import "./index.sass"; - -import "./i18n"; - -import App from "./App"; - -ReactDOM.render(, document.getElementById("app")); diff --git a/Timeline/ClientApp/src/app/locales/en/translation.ts b/Timeline/ClientApp/src/app/locales/en/translation.ts deleted file mode 100644 index c7f33d1e..00000000 --- a/Timeline/ClientApp/src/app/locales/en/translation.ts +++ /dev/null @@ -1,202 +0,0 @@ -import TranslationResource from "../scheme"; - -const translation: TranslationResource = { - welcome: "Welcome!", - search: "Search", - loadFailReload: "Load failed, click <1>here to reload.", - serviceWorker: { - availableOffline: - "Timeline is now cached in your computer and you can use it offline. 🎉🎉🎉", - upgradePrompt: "App is getting a new version!", - upgradeNow: "Update Now", - upgradeSuccess: - "Congratulations! App update succeeded! Still you can use it offline. 🎉🎉🎉", - externalActivatedPrompt: - "A new version of app is activated. Please refresh the page. Or it may be broken.", - reloadNow: "Refresh Now", - }, - nav: { - settings: "Settings", - login: "Login", - about: "About", - }, - chooseImage: "Choose a image", - loadImageError: "Failed to load image.", - home: { - go: "Go!", - allTimeline: "All Timelines", - joinTimeline: "Joined Timelines", - ownTimeline: "Owned Timelines", - offlinePrompt: - "Oh oh, it seems you are offline. Here list some timelines cached locally. You can view them or click <1>here to refresh.", - createButton: "Create Timeline", - createDialog: { - title: "Create Timeline!", - name: "Name", - nameFormat: - "Name must consist of only letter including non-English letter, digit, hyphen(-) and underline(_) and be no longer than 26.", - badFormat: "Bad format.", - noEmpty: "Empty is not allowed.", - tooLong: "Too long.", - }, - }, - operationDialog: { - retry: "Retry", - nextStep: "Next", - previousStep: "Previous", - confirm: "Confirm", - cancel: "Cancel", - ok: "OK!", - processing: "Processing...", - success: "Success!", - error: "An error occured.", - }, - timeline: { - messageCantSee: "Sorry, you are not allowed to see this timeline.😅", - userNotExist: "The user does not exist!", - timelineNotExist: "The timeline does not exist!", - manage: "Manage", - memberButton: "Member", - send: "Send", - deletePostFailed: "Failed to delete post.", - sendPostFailed: "Failed to send post.", - visibility: { - public: "public to everyone", - register: "only registed people can see", - private: "only members can see", - }, - visibilityTooltip: { - public: - "Everyone including those without accounts can see content of the timeline.", - register: - "Only those who have an account and logined can see content of the timeline.", - private: "Only members of this timeline can see content of the timeline.", - }, - dialogChangeProperty: { - title: "Change Timeline Properties", - visibility: "Visibility", - description: "Description", - }, - member: { - alreadyMember: "The user is already a member.", - add: "Add", - remove: "Remove", - }, - manageItem: { - nickname: "Nickname", - avatar: "Avatar", - property: "Timeline Property", - member: "Timeline Member", - delete: "Delete Timeline", - }, - deleteDialog: { - title: "Delete Timeline", - inputPrompt: - "This is a dangerous action. If you are sure to delete timeline<1>{{name}}, please input its name below and click confirm button.", - notMatch: "Name does not match.", - }, - postSyncState: { - syncing: "Syncing", - synced: "Synced", - offline: "Offline", - }, - post: { - deleteDialog: { - title: "Confirm Delete", - prompt: - "Are you sure to delete the post? This operation is not recoverable.", - }, - }, - }, - user: { - username: "username", - password: "password", - login: "login", - rememberMe: "Remember Me", - welcomeBack: "Welcome back!", - verifyTokenFailed: "User login info is expired. Please login again!", - verifyTokenFailedNetwork: - "Verifying user login info failed. Please check your network and refresh page!", - }, - login: { - emptyUsername: "Username can't be empty.", - emptyPassword: "Password can't be empty.", - badCredential: "Username or password is invalid.", - alreadyLogin: "Already login! Redirect to home page in 3s!", - }, - userPage: { - dialogChangeNickname: { - title: "Change Nickname", - inputLabel: "New nickname", - }, - dialogChangeAvatar: { - title: "Change Avatar", - previewImgAlt: "preview", - prompt: { - select: "Please select a picture.", - crop: "Please crop the picture.", - processingCrop: "Cropping picture...", - uploading: "Uploading...", - preview: "Please preview avatar", - }, - upload: "upload", - }, - }, - settings: { - subheaders: { - account: "Account", - customization: "Customization", - }, - languagePrimary: "Choose display language.", - languageSecondary: - "You language preference will be saved locally. Next time you visit this page, last language option will be used.", - changePassword: "Change account's password.", - logout: "Log out this account.", - gotoSelf: - "Click here to go to timeline of myself to change nickname and avatar.", - dialogChangePassword: { - title: "Change Password", - prompt: - "You are changing your password. You need to input the correct old password. After change, you need to login again and all old login will be invalid.", - inputOldPassword: "Old password", - inputNewPassword: "New password", - inputRetypeNewPassword: "Retype new password", - errorEmptyOldPassword: "Old password can't be empty.", - errorEmptyNewPassword: "New password can't be empty.", - errorRetypeNotMatch: "Password retyped does not match.", - }, - dialogConfirmLogout: { - title: "Confirm Logout", - prompt: - "Are you sure to log out? All cached data in the browser will be deleted.", - }, - }, - about: { - author: { - title: "Site Developer", - fullname: "Fullname: ", - nickname: "Nickname: ", - introduction: "Introduction: ", - introductionContent: "A programmer coding based on coincidence", - links: "Links: ", - }, - site: { - title: "Site Information", - content: - "The name of this site is <1>Timeline, which is a Web App with <3>timeline as its core concept. Its frontend and backend are both developed by <5>me, and open source on GitHub. It is relatively easy to deploy it on your own server, which is also one of my goals. Welcome to comment anything in GitHub repository.", - repo: "GitHub Repo", - }, - credits: { - title: "Credits", - content: - "Timeline is works standing on shoulders of gaints. Special appreciation for many open source projects listed below or not. Related licenses could be found in GitHub repository.", - frontend: "Frontend: ", - backend: "Backend: ", - }, - }, - admin: { - title: "admin", - }, -}; - -export default translation; diff --git a/Timeline/ClientApp/src/app/locales/scheme.ts b/Timeline/ClientApp/src/app/locales/scheme.ts deleted file mode 100644 index 9e3534ac..00000000 --- a/Timeline/ClientApp/src/app/locales/scheme.ts +++ /dev/null @@ -1,182 +0,0 @@ -export default interface TranslationResource { - welcome: string; - search: string; - chooseImage: string; - loadImageError: string; - loadFailReload: string; - serviceWorker: { - availableOffline: string; - upgradePrompt: string; - upgradeNow: string; - upgradeSuccess: string; - externalActivatedPrompt: string; - reloadNow: string; - }; - nav: { - settings: string; - login: string; - about: string; - }; - home: { - go: string; - allTimeline: string; - joinTimeline: string; - ownTimeline: string; - offlinePrompt: string; - createButton: string; - createDialog: { - title: string; - name: string; - nameFormat: string; - badFormat: string; - noEmpty: string; - tooLong: string; - }; - }; - operationDialog: { - retry: string; - nextStep: string; - previousStep: string; - confirm: string; - cancel: string; - ok: string; - processing: string; - success: string; - error: string; - }; - timeline: { - messageCantSee: string; - userNotExist: string; - timelineNotExist: string; - manage: string; - memberButton: string; - send: string; - deletePostFailed: string; - sendPostFailed: string; - visibility: { - public: string; - register: string; - private: string; - }; - visibilityTooltip: { - public: string; - register: string; - private: string; - }; - dialogChangeProperty: { - title: string; - visibility: string; - description: string; - }; - member: { - alreadyMember: string; - add: string; - remove: string; - }; - manageItem: { - nickname: string; - avatar: string; - property: string; - member: string; - delete: string; - }; - deleteDialog: { - title: string; - inputPrompt: string; - notMatch: string; - }; - postSyncState: { - syncing: string; - synced: string; - offline: string; - }; - post: { - deleteDialog: { - title: string; - prompt: string; - }; - }; - }; - user: { - username: string; - password: string; - login: string; - rememberMe: string; - welcomeBack: string; - verifyTokenFailed: string; - verifyTokenFailedNetwork: string; - }; - login: { - emptyUsername: string; - emptyPassword: string; - badCredential: string; - alreadyLogin: string; - }; - userPage: { - dialogChangeNickname: { - title: string; - inputLabel: string; - }; - dialogChangeAvatar: { - title: string; - previewImgAlt: string; - prompt: { - select: string; - crop: string; - processingCrop: string; - preview: string; - uploading: string; - }; - upload: string; - }; - }; - settings: { - subheaders: { - account: string; - customization: string; - }; - languagePrimary: string; - languageSecondary: string; - changePassword: string; - logout: string; - gotoSelf: string; - dialogChangePassword: { - title: string; - prompt: string; - inputOldPassword: string; - inputNewPassword: string; - inputRetypeNewPassword: string; - errorEmptyOldPassword: string; - errorEmptyNewPassword: string; - errorRetypeNotMatch: string; - }; - dialogConfirmLogout: { - title: string; - prompt: string; - }; - }; - about: { - author: { - title: string; - fullname: string; - nickname: string; - introduction: string; - introductionContent: string; - links: string; - }; - site: { - title: string; - content: string; - repo: string; - }; - credits: { - title: string; - content: string; - frontend: string; - backend: string; - }; - }; - admin: { - title: string; - }; -} diff --git a/Timeline/ClientApp/src/app/locales/zh/translation.ts b/Timeline/ClientApp/src/app/locales/zh/translation.ts deleted file mode 100644 index df316366..00000000 --- a/Timeline/ClientApp/src/app/locales/zh/translation.ts +++ /dev/null @@ -1,195 +0,0 @@ -import TranslationResource from "../scheme"; - -const translation: TranslationResource = { - welcome: "欢迎!", - search: "搜索", - loadFailReload: "加载失败,<1>点击重试。", - serviceWorker: { - availableOffline: "Timeline 已经缓存在本地,你可以离线使用它。🎉🎉🎉", - upgradePrompt: "App 有新版本!", - upgradeNow: "现在升级", - upgradeSuccess: "App 升级成功,当然,你仍可以离线使用它。 🎉🎉🎉", - externalActivatedPrompt: - "一个新的 App 版本已经激活,请刷新页面使用,否则页面可能会出现故障。", - reloadNow: "立刻刷新", - }, - nav: { - settings: "设置", - login: "登陆", - about: "关于", - }, - chooseImage: "选择一个图片", - loadImageError: "加载图片失败", - home: { - go: "冲!", - allTimeline: "所有的时间线", - joinTimeline: "加入的时间线", - ownTimeline: "拥有的时间线", - offlinePrompt: - "你好像处于离线状态。以下是一些缓存在本地的时间线。你可以查看它们或者<1>点击重新获取在线信息。", - createButton: "创建时间线", - createDialog: { - title: "创建时间线!", - name: "名字", - nameFormat: - "名字只能由字母、汉字、数字、下划线(_)和连字符(-)构成,且长度不能超过26.", - badFormat: "格式错误", - noEmpty: "不能为空", - tooLong: "太长了", - }, - }, - operationDialog: { - retry: "重试", - nextStep: "下一步", - previousStep: "上一步", - confirm: "确定", - cancel: "取消", - ok: "好的!", - processing: "处理中...", - success: "成功!", - error: "出错啦!", - }, - timeline: { - messageCantSee: "不好意思,你没有权限查看这个时间线。😅", - userNotExist: "该用户不存在!", - timelineNotExist: "该时间线不存在!", - manage: "管理", - memberButton: "成员", - send: "发送", - deletePostFailed: "删除消息失败。", - sendPostFailed: "发送消息失败。", - visibility: { - public: "对所有人公开", - register: "仅注册可见", - private: "仅成员可见", - }, - visibilityTooltip: { - public: "所有人都可以看到这个时间线的内容,包括没有注册的人。", - register: "只有拥有本网站的账号且登陆了的人才能看到这个时间线的内容。", - private: "只有这个时间线的成员可以看到这个时间线的内容。", - }, - dialogChangeProperty: { - title: "修改时间线属性", - visibility: "可见性", - description: "描述", - }, - member: { - alreadyMember: "该用户已经是一个成员。", - add: "添加", - remove: "移除", - }, - manageItem: { - nickname: "昵称", - avatar: "头像", - property: "时间线属性", - member: "时间线成员", - delete: "删除时间线", - }, - deleteDialog: { - title: "删除时间线", - inputPrompt: - "这是一个危险的操作。如果您确认要删除时间线<1>{{name}},请在下面输入它的名字并点击确认。", - notMatch: "名字不匹配", - }, - postSyncState: { - syncing: "同步中", - synced: "同步成功", - offline: "离线", - }, - post: { - deleteDialog: { - title: "确认删除", - prompt: "确定删除这个消息?这个操作不可撤销。", - }, - }, - }, - user: { - username: "用户名", - password: "密码", - login: "登录", - rememberMe: "记住我", - welcomeBack: "欢迎回来!", - verifyTokenFailed: "用户登录信息已过期,请重新登陆!", - verifyTokenFailedNetwork: - "验证用户登录信息失败,请检查网络连接并刷新页面!", - }, - login: { - emptyUsername: "用户名不能为空。", - emptyPassword: "密码不能为空。", - badCredential: "用户名或密码错误。", - alreadyLogin: "已经登陆,三秒后导航到首页!", - }, - userPage: { - dialogChangeNickname: { - title: "更改昵称", - inputLabel: "新昵称", - }, - dialogChangeAvatar: { - title: "修改头像", - previewImgAlt: "预览", - prompt: { - select: "请选择一个图片", - crop: "请裁剪图片", - processingCrop: "正在裁剪图片", - uploading: "正在上传", - preview: "请预览图片", - }, - upload: "上传", - }, - }, - settings: { - subheaders: { - account: "账户", - customization: "个性化", - }, - languagePrimary: "选择显示的语言。", - languageSecondary: - "您的语言偏好将会存储在本地,下次浏览时将自动使用上次保存的语言选项。", - changePassword: "更改账号的密码。", - logout: "注销此账号。", - gotoSelf: "点击前往个人时间线修改昵称和头像!", - dialogChangePassword: { - title: "修改密码", - prompt: - "您正在修改密码,您需要输入正确的旧密码。成功修改后您需要重新登陆,而且以前所有的登录都会失效。", - inputOldPassword: "旧密码", - inputNewPassword: "新密码", - inputRetypeNewPassword: "再次输入新密码", - errorEmptyOldPassword: "旧密码不能为空。", - errorEmptyNewPassword: "新密码不能为空", - errorRetypeNotMatch: "两次输入的密码不一致", - }, - dialogConfirmLogout: { - title: "确定注销", - prompt: "您确定注销此账号?这将删除所有已经缓存在浏览器的数据。", - }, - }, - about: { - author: { - title: "网站作者", - fullname: "姓名:", - nickname: "昵称:", - introduction: "简介:", - introductionContent: "一个基于巧合编程的代码爱好者。", - links: "链接:", - }, - site: { - title: "网站信息", - content: - "这个网站的名字叫 <1>Timeline,是一个以<3>时间线为核心概念的 Web App . 它的前端和后端都是由<5>我开发,并且在 GitHub 上开源。大家可以相对轻松的把它们部署在自己的服务器上,这也是我的目标之一。欢迎大家前往 GitHub 仓库提出任何意见。", - repo: "GitHub 仓库", - }, - credits: { - title: "鸣谢", - content: - "Timeline 是站在巨人肩膀上的作品,感谢以下列出的和其他未列出的许多开源项目,相关 License 请在 GitHub 仓库中查看。", - frontend: "前端:", - backend: "后端:", - }, - }, - admin: { - title: "管理", - }, -}; - -export default translation; diff --git a/Timeline/ClientApp/src/app/service-worker.tsx b/Timeline/ClientApp/src/app/service-worker.tsx deleted file mode 100644 index 3be54bc1..00000000 --- a/Timeline/ClientApp/src/app/service-worker.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import React from "react"; -import { useTranslation } from "react-i18next"; -import { Button } from "react-bootstrap"; - -import { pushAlert } from "./services/alert"; - -if ("serviceWorker" in navigator) { - let isThisTriggerUpgrade = false; - - const upgradeSuccessLocalStorageKey = "TIMELINE_UPGRADE_SUCCESS"; - - if (window.localStorage.getItem(upgradeSuccessLocalStorageKey)) { - pushAlert({ - message: { - type: "i18n", - key: "serviceWorker.upgradeSuccess", - }, - type: "success", - }); - window.localStorage.removeItem(upgradeSuccessLocalStorageKey); - } - - void import("workbox-window").then(({ Workbox, messageSW }) => { - const wb = new Workbox("/sw.js"); - let registration: ServiceWorkerRegistration | undefined; - - // externalactivated is not usable but I still use its name. - wb.addEventListener("controlling", () => { - const upgradeReload = (): void => { - window.localStorage.setItem(upgradeSuccessLocalStorageKey, "true"); - window.location.reload(); - }; - - if (isThisTriggerUpgrade) { - upgradeReload(); - } else { - const Message: React.FC = () => { - const { t } = useTranslation(); - return ( - <> - {t("serviceWorker.externalActivatedPrompt")} - - - ); - }; - - pushAlert({ - message: Message, - dismissTime: "never", - type: "warning", - }); - } - }); - - wb.addEventListener("activated", (event) => { - if (!event.isUpdate) { - pushAlert({ - message: { - type: "i18n", - key: "serviceWorker.availableOffline", - }, - type: "success", - }); - } - }); - - const showSkipWaitingPrompt = (): void => { - const upgrade = (): void => { - isThisTriggerUpgrade = true; - if (registration && registration.waiting) { - // Send a message to the waiting service worker, - // instructing it to activate. - // Note: for this to work, you have to add a message - // listener in your service worker. See below. - void messageSW(registration.waiting, { type: "SKIP_WAITING" }); - } - }; - - const UpgradeMessage: React.FC = () => { - const { t } = useTranslation(); - return ( - <> - {t("serviceWorker.upgradePrompt")} - - - ); - }; - - pushAlert({ - message: UpgradeMessage, - dismissTime: "never", - type: "success", - }); - }; - - // Add an event listener to detect when the registered - // service worker has installed but is waiting to activate. - wb.addEventListener("waiting", showSkipWaitingPrompt); - wb.addEventListener("externalwaiting", showSkipWaitingPrompt); - - void wb.register().then((reg) => { - registration = reg; - }); - }); -} diff --git a/Timeline/ClientApp/src/app/services/DataHub.ts b/Timeline/ClientApp/src/app/services/DataHub.ts deleted file mode 100644 index 93a9b41f..00000000 --- a/Timeline/ClientApp/src/app/services/DataHub.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { pull } from "lodash"; -import { Observable, BehaviorSubject, combineLatest } from "rxjs"; -import { map } from "rxjs/operators"; - -export type Subscriber = (data: TData) => void; - -export type WithSyncStatus = T & { syncing: boolean }; - -export class DataLine { - private _current: TData | undefined = undefined; - - private _syncPromise: Promise | null = null; - private _syncingSubject = new BehaviorSubject(false); - - private _observers: Subscriber[] = []; - - constructor( - private config: { - sync: () => Promise; - destroyable?: (value: TData | undefined) => boolean; - disableInitSync?: boolean; - } - ) { - if (config.disableInitSync !== true) { - setImmediate(() => void this.sync()); - } - } - - private subscribe(subscriber: Subscriber): void { - this._observers.push(subscriber); - if (this._current !== undefined) { - subscriber(this._current); - } - } - - private unsubscribe(subscriber: Subscriber): void { - if (!this._observers.includes(subscriber)) return; - pull(this._observers, subscriber); - } - - getObservable(): Observable { - return new Observable((observer) => { - const f = (data: TData): void => { - observer.next(data); - }; - this.subscribe(f); - - return () => { - this.unsubscribe(f); - }; - }); - } - - getSyncStatusObservable(): Observable { - return this._syncingSubject.asObservable(); - } - - getDataWithSyncStatusObservable(): Observable> { - return combineLatest([ - this.getObservable(), - this.getSyncStatusObservable(), - ]).pipe( - map(([data, syncing]) => ({ - ...data, - syncing, - })) - ); - } - - get value(): TData | undefined { - return this._current; - } - - next(value: TData): void { - this._current = value; - this._observers.forEach((observer) => observer(value)); - } - - get isSyncing(): boolean { - return this._syncPromise != null; - } - - sync(): Promise { - if (this._syncPromise == null) { - this._syncingSubject.next(true); - this._syncPromise = this.config.sync().then(() => { - this._syncingSubject.next(false); - this._syncPromise = null; - }); - } - - return this._syncPromise; - } - - syncWithAction( - syncAction: (line: DataLine) => Promise - ): Promise { - if (this._syncPromise == null) { - this._syncingSubject.next(true); - this._syncPromise = syncAction(this).then(() => { - this._syncingSubject.next(false); - this._syncPromise = null; - }); - } - - return this._syncPromise; - } - - get destroyable(): boolean { - const customDestroyable = this.config?.destroyable; - - return ( - this._observers.length === 0 && - !this.isSyncing && - (customDestroyable != null ? customDestroyable(this._current) : true) - ); - } -} - -export class DataHub { - private sync: (key: TKey, line: DataLine) => Promise; - private keyToString: (key: TKey) => string; - private destroyable?: (key: TKey, value: TData | undefined) => boolean; - - private readonly subscriptionLineMap = new Map>(); - - private cleanTimerId = 0; - - // setup is called after creating line and if it returns a function as destroyer, then when the line is destroyed the destroyer will be called. - constructor(config: { - sync: (key: TKey, line: DataLine) => Promise; - keyToString?: (key: TKey) => string; - destroyable?: (key: TKey, value: TData | undefined) => boolean; - }) { - this.sync = config.sync; - this.keyToString = - config.keyToString ?? - ((value): string => { - if (typeof value === "string") return value; - else - throw new Error( - "Default keyToString function only pass string value." - ); - }); - - this.destroyable = config.destroyable; - } - - private cleanLines(): void { - const toDelete: string[] = []; - for (const [key, line] of this.subscriptionLineMap.entries()) { - if (line.destroyable) { - toDelete.push(key); - } - } - - if (toDelete.length === 0) return; - - for (const key of toDelete) { - this.subscriptionLineMap.delete(key); - } - - if (this.subscriptionLineMap.size === 0) { - window.clearInterval(this.cleanTimerId); - this.cleanTimerId = 0; - } - } - - private createLine(key: TKey, disableInitSync = false): DataLine { - const keyString = this.keyToString(key); - const { destroyable } = this; - const newLine: DataLine = new DataLine({ - sync: () => this.sync(key, newLine), - destroyable: - destroyable != null ? (value) => destroyable(key, value) : undefined, - disableInitSync: disableInitSync, - }); - this.subscriptionLineMap.set(keyString, newLine); - if (this.subscriptionLineMap.size === 1) { - this.cleanTimerId = window.setInterval(this.cleanLines.bind(this), 20000); - } - return newLine; - } - - getObservable(key: TKey): Observable { - return this.getLineOrCreate(key).getObservable(); - } - - getSyncStatusObservable(key: TKey): Observable { - return this.getLineOrCreate(key).getSyncStatusObservable(); - } - - getDataWithSyncStatusObservable( - key: TKey - ): Observable> { - return this.getLineOrCreate(key).getDataWithSyncStatusObservable(); - } - - getLine(key: TKey): DataLine | null { - const keyString = this.keyToString(key); - return this.subscriptionLineMap.get(keyString) ?? null; - } - - getLineOrCreate(key: TKey): DataLine { - const keyString = this.keyToString(key); - return this.subscriptionLineMap.get(keyString) ?? this.createLine(key); - } - - getLineOrCreateWithoutInitSync(key: TKey): DataLine { - const keyString = this.keyToString(key); - return ( - this.subscriptionLineMap.get(keyString) ?? this.createLine(key, true) - ); - } - - optionalInitLineWithSyncAction( - key: TKey, - syncAction: (line: DataLine) => Promise - ): Promise { - const optionalLine = this.getLine(key); - if (optionalLine != null) return Promise.resolve(); - const line = this.createLine(key, true); - return line.syncWithAction(syncAction); - } -} diff --git a/Timeline/ClientApp/src/app/services/alert.ts b/Timeline/ClientApp/src/app/services/alert.ts deleted file mode 100644 index e4c0e653..00000000 --- a/Timeline/ClientApp/src/app/services/alert.ts +++ /dev/null @@ -1,61 +0,0 @@ -import React from "react"; -import pull from "lodash/pull"; - -export interface AlertInfo { - type?: "primary" | "secondary" | "success" | "danger" | "warning" | "info"; - message: string | React.FC | { type: "i18n"; key: string }; - dismissTime?: number | "never"; -} - -export interface AlertInfoEx extends AlertInfo { - id: number; -} - -export type AlertConsumer = (alerts: AlertInfoEx) => void; - -export class AlertService { - private consumers: AlertConsumer[] = []; - private savedAlerts: AlertInfoEx[] = []; - private currentId = 1; - - private produce(alert: AlertInfoEx): void { - for (const consumer of this.consumers) { - consumer(alert); - } - } - - registerConsumer(consumer: AlertConsumer): void { - this.consumers.push(consumer); - if (this.savedAlerts.length !== 0) { - for (const alert of this.savedAlerts) { - this.produce(alert); - } - this.savedAlerts = []; - } - } - - unregisterConsumer(consumer: AlertConsumer): void { - pull(this.consumers, consumer); - } - - push(alert: AlertInfo): void { - const newAlert: AlertInfoEx = { ...alert, id: this.currentId++ }; - if (this.consumers.length === 0) { - this.savedAlerts.push(newAlert); - } else { - this.produce(newAlert); - } - } -} - -export const alertService = new AlertService(); - -export function pushAlert(alert: AlertInfo): void { - alertService.push(alert); -} - -export const kAlertHostId = "alert-host"; - -export function getAlertHost(): HTMLElement | null { - return document.getElementById(kAlertHostId); -} diff --git a/Timeline/ClientApp/src/app/services/common.ts b/Timeline/ClientApp/src/app/services/common.ts deleted file mode 100644 index 3bb6b9d7..00000000 --- a/Timeline/ClientApp/src/app/services/common.ts +++ /dev/null @@ -1,23 +0,0 @@ -import localforage from "localforage"; - -import { HttpNetworkError } from "@/http/common"; - -export const dataStorage = localforage.createInstance({ - name: "data", - description: "Database for offline data.", - driver: localforage.INDEXEDDB, -}); - -export class ForbiddenError extends Error { - constructor(message?: string) { - super(message); - } -} - -export function throwIfNotNetworkError(e: unknown): void { - if (!(e instanceof HttpNetworkError)) { - throw e; - } -} - -export type BlobOrStatus = Blob | "loading" | "error"; diff --git a/Timeline/ClientApp/src/app/services/timeline.ts b/Timeline/ClientApp/src/app/services/timeline.ts deleted file mode 100644 index 9db76281..00000000 --- a/Timeline/ClientApp/src/app/services/timeline.ts +++ /dev/null @@ -1,702 +0,0 @@ -import React from "react"; -import XRegExp from "xregexp"; -import { Observable, from, combineLatest, of } from "rxjs"; -import { map, switchMap, startWith } from "rxjs/operators"; -import { uniqBy } from "lodash"; - -import { convertError } from "@/utilities/rxjs"; -import { - TimelineVisibility, - HttpTimelineInfo, - HttpTimelinePatchRequest, - HttpTimelinePostPostRequest, - HttpTimelinePostPostRequestContent, - HttpTimelinePostPostRequestTextContent, - HttpTimelinePostPostRequestImageContent, - HttpTimelinePostInfo, - HttpTimelinePostTextContent, - getHttpTimelineClient, - HttpTimelineNotExistError, - HttpTimelineNameConflictError, -} from "@/http/timeline"; -import { BlobWithEtag, NotModified, HttpForbiddenError } from "@/http/common"; -import { HttpUser } from "@/http/user"; - -export { kTimelineVisibilities } from "@/http/timeline"; - -export type { TimelineVisibility } from "@/http/timeline"; - -import { dataStorage, throwIfNotNetworkError, BlobOrStatus } from "./common"; -import { DataHub, WithSyncStatus } from "./DataHub"; -import { UserAuthInfo, checkLogin, userService, userInfoService } from "./user"; - -export type TimelineInfo = HttpTimelineInfo; -export type TimelineChangePropertyRequest = HttpTimelinePatchRequest; -export type TimelineCreatePostRequest = HttpTimelinePostPostRequest; -export type TimelineCreatePostContent = HttpTimelinePostPostRequestContent; -export type TimelineCreatePostTextContent = HttpTimelinePostPostRequestTextContent; -export type TimelineCreatePostImageContent = HttpTimelinePostPostRequestImageContent; - -export type TimelinePostTextContent = HttpTimelinePostTextContent; - -export interface TimelinePostImageContent { - type: "image"; - data: BlobOrStatus; -} - -export type TimelinePostContent = - | TimelinePostTextContent - | TimelinePostImageContent; - -export interface TimelinePostInfo { - id: number; - content: TimelinePostContent; - time: Date; - lastUpdated: Date; - author: HttpUser; -} - -export const timelineVisibilityTooltipTranslationMap: Record< - TimelineVisibility, - string -> = { - Public: "timeline.visibilityTooltip.public", - Register: "timeline.visibilityTooltip.register", - Private: "timeline.visibilityTooltip.private", -}; - -export class TimelineNotExistError extends Error {} -export class TimelineNameConflictError extends Error {} - -export type TimelineWithSyncStatus = WithSyncStatus< - | { - type: "cache"; - timeline: TimelineInfo; - } - | { - type: "offline" | "synced"; - timeline: TimelineInfo | null; - } ->; - -export type TimelinePostsWithSyncState = WithSyncStatus<{ - type: - | "cache" - | "offline" // Sync failed and use cache. - | "synced" // Sync succeeded. - | "forbid" // The list is forbidden to see. - | "notexist"; // The timeline does not exist. - posts: TimelinePostInfo[]; -}>; - -type TimelineData = Omit & { - owner: string; - members: string[]; -}; - -type TimelinePostData = Omit & { - author: string; -}; - -export class TimelineService { - private getCachedTimeline( - timelineName: string - ): Promise { - return dataStorage.getItem(`timeline.${timelineName}`); - } - - private saveTimeline( - timelineName: string, - data: TimelineData - ): Promise { - return dataStorage - .setItem(`timeline.${timelineName}`, data) - .then(); - } - - private async clearTimelineData(timelineName: string): Promise { - const keys = (await dataStorage.keys()).filter((k) => - k.startsWith(`timeline.${timelineName}`) - ); - await Promise.all(keys.map((k) => dataStorage.removeItem(k))); - } - - private convertHttpTimelineToData(timeline: HttpTimelineInfo): TimelineData { - return { - ...timeline, - owner: timeline.owner.username, - members: timeline.members.map((m) => m.username), - }; - } - - private _timelineHub = new DataHub< - string, - | { - type: "cache"; - timeline: TimelineData; - } - | { - type: "offline" | "synced"; - timeline: TimelineData | null; - } - >({ - sync: async (key, line) => { - const cache = await this.getCachedTimeline(key); - - if (line.value == undefined) { - if (cache != null) { - line.next({ type: "cache", timeline: cache }); - } - } - - try { - const httpTimeline = await getHttpTimelineClient().getTimeline(key); - - userInfoService.saveUsers([ - httpTimeline.owner, - ...httpTimeline.members, - ]); - - const timeline = this.convertHttpTimelineToData(httpTimeline); - - if (cache != null && timeline.uniqueId !== cache.uniqueId) { - console.log( - `Timeline with name ${key} has changed to a new one. Clear old data.` - ); - await this.clearTimelineData(key); // If timeline has changed, clear all old data. - } - - await this.saveTimeline(key, timeline); - - line.next({ type: "synced", timeline }); - } catch (e) { - if (e instanceof HttpTimelineNotExistError) { - line.next({ type: "synced", timeline: null }); - } else { - if (cache == null) { - line.next({ type: "offline", timeline: null }); - } else { - line.next({ type: "offline", timeline: cache }); - } - throwIfNotNetworkError(e); - } - } - }, - }); - - syncTimeline(timelineName: string): Promise { - return this._timelineHub.getLineOrCreate(timelineName).sync(); - } - - getTimeline$(timelineName: string): Observable { - return this._timelineHub.getDataWithSyncStatusObservable(timelineName).pipe( - switchMap((state) => { - const { timeline } = state; - if (timeline != null) { - return combineLatest( - [timeline.owner, ...timeline.members].map((u) => - userInfoService.getUser$(u) - ) - ).pipe( - map((users) => { - return { - ...state, - timeline: { - ...timeline, - owner: users[0], - members: users.slice(1), - }, - }; - }) - ); - } else { - return of(state as TimelineWithSyncStatus); - } - }) - ); - } - - createTimeline(timelineName: string): Observable { - const user = checkLogin(); - return from( - getHttpTimelineClient().postTimeline( - { - name: timelineName, - }, - user.token - ) - ).pipe( - convertError(HttpTimelineNameConflictError, TimelineNameConflictError) - ); - } - - changeTimelineProperty( - timelineName: string, - req: TimelineChangePropertyRequest - ): Observable { - const user = checkLogin(); - return from( - getHttpTimelineClient() - .patchTimeline(timelineName, req, user.token) - .then((timeline) => { - void this.syncTimeline(timelineName); - return timeline; - }) - ); - } - - deleteTimeline(timelineName: string): Observable { - const user = checkLogin(); - return from( - getHttpTimelineClient().deleteTimeline(timelineName, user.token) - ); - } - - addMember(timelineName: string, username: string): Observable { - const user = checkLogin(); - return from( - getHttpTimelineClient() - .memberPut(timelineName, username, user.token) - .then(() => { - void this.syncTimeline(timelineName); - }) - ); - } - - removeMember(timelineName: string, username: string): Observable { - const user = checkLogin(); - return from( - getHttpTimelineClient() - .memberDelete(timelineName, username, user.token) - .then(() => { - void this.syncTimeline(timelineName); - }) - ); - } - - private convertHttpPostToData(post: HttpTimelinePostInfo): TimelinePostData { - return { - ...post, - author: post.author.username, - }; - } - - private convertHttpPostToDataList( - posts: HttpTimelinePostInfo[] - ): TimelinePostData[] { - return posts.map((post) => this.convertHttpPostToData(post)); - } - - private getCachedPosts( - timelineName: string - ): Promise { - return dataStorage.getItem( - `timeline.${timelineName}.posts` - ); - } - - private savePosts( - timelineName: string, - data: TimelinePostData[] - ): Promise { - return dataStorage - .setItem(`timeline.${timelineName}.posts`, data) - .then(); - } - - private syncPosts(timelineName: string): Promise { - return this._postsHub.getLineOrCreate(timelineName).sync(); - } - - private _postsHub = new DataHub< - string, - { - type: "cache" | "offline" | "synced" | "forbid" | "notexist"; - posts: TimelinePostData[]; - } - >({ - sync: async (key, line) => { - // Wait for timeline synced. In case the timeline has changed to another and old data has been cleaned. - await this.syncTimeline(key); - - if (line.value == null) { - const cache = await this.getCachedPosts(key); - if (cache != null) { - line.next({ type: "cache", posts: cache }); - } - } - - const now = new Date(); - - const lastUpdatedTime = await dataStorage.getItem( - `timeline.${key}.lastUpdated` - ); - - try { - if (lastUpdatedTime == null) { - const httpPosts = await getHttpTimelineClient().listPost( - key, - userService.currentUser?.token - ); - - userInfoService.saveUsers( - uniqBy( - httpPosts.map((post) => post.author), - "username" - ) - ); - - const posts = this.convertHttpPostToDataList(httpPosts); - await this.savePosts(key, posts); - await dataStorage.setItem(`timeline.${key}.lastUpdated`, now); - - line.next({ type: "synced", posts }); - } else { - const httpPosts = await getHttpTimelineClient().listPost( - key, - userService.currentUser?.token, - { - modifiedSince: lastUpdatedTime, - includeDeleted: true, - } - ); - - const deletedIds = httpPosts - .filter((p) => p.deleted) - .map((p) => p.id); - const changed = httpPosts.filter( - (p): p is HttpTimelinePostInfo => !p.deleted - ); - - userInfoService.saveUsers( - uniqBy( - httpPosts - .map((post) => post.author) - .filter((u): u is HttpUser => u != null), - "username" - ) - ); - - const cache = (await this.getCachedPosts(key)) ?? []; - - const posts = cache.filter((p) => !deletedIds.includes(p.id)); - - for (const changedPost of changed) { - const savedChangedPostIndex = posts.findIndex( - (p) => p.id === changedPost.id - ); - if (savedChangedPostIndex === -1) { - posts.push(this.convertHttpPostToData(changedPost)); - } else { - posts[savedChangedPostIndex] = this.convertHttpPostToData( - changedPost - ); - } - } - - await this.savePosts(key, posts); - await dataStorage.setItem(`timeline.${key}.lastUpdated`, now); - line.next({ type: "synced", posts }); - } - } catch (e) { - if (e instanceof HttpTimelineNotExistError) { - line.next({ type: "notexist", posts: [] }); - } else if (e instanceof HttpForbiddenError) { - line.next({ type: "forbid", posts: [] }); - } else { - const cache = await this.getCachedPosts(key); - if (cache == null) { - line.next({ type: "offline", posts: [] }); - } else { - line.next({ type: "offline", posts: cache }); - } - throwIfNotNetworkError(e); - } - } - }, - }); - - getPosts$(timelineName: string): Observable { - return this._postsHub.getDataWithSyncStatusObservable(timelineName).pipe( - switchMap((state) => { - if (state.posts.length === 0) { - return of({ - ...state, - posts: [], - }); - } - - return combineLatest([ - combineLatest( - state.posts.map((post) => userInfoService.getUser$(post.author)) - ), - combineLatest( - state.posts.map((post) => { - if (post.content.type === "image") { - return this.getPostData$(timelineName, post.id); - } else { - return of(null); - } - }) - ), - ]).pipe( - map(([authors, datas]) => { - return { - ...state, - posts: state.posts.map((post, i) => { - const { content } = post; - - return { - ...post, - author: authors[i], - content: (() => { - if (content.type === "text") return content; - else - return { - type: "image", - data: datas[i], - } as TimelinePostImageContent; - })(), - }; - }), - }; - }) - ); - }) - ); - } - - private getCachedPostData(key: { - timelineName: string; - postId: number; - }): Promise { - return dataStorage.getItem( - `timeline.${key.timelineName}.post.${key.postId}.data` - ); - } - - private savePostData( - key: { - timelineName: string; - postId: number; - }, - data: BlobWithEtag - ): Promise { - return dataStorage - .setItem( - `timeline.${key.timelineName}.post.${key.postId}.data`, - data - ) - .then(); - } - - private syncPostData(key: { - timelineName: string; - postId: number; - }): Promise { - return this._postDataHub.getLineOrCreate(key).sync(); - } - - private _postDataHub = new DataHub< - { timelineName: string; postId: number }, - | { data: Blob; type: "cache" | "synced" | "offline" } - | { data?: undefined; type: "notexist" | "offline" } - >({ - keyToString: (key) => `${key.timelineName}.${key.postId}`, - sync: async (key, line) => { - const cache = await this.getCachedPostData(key); - if (line.value == null) { - if (cache != null) { - line.next({ type: "cache", data: cache.data }); - } - } - - if (cache == null) { - try { - const res = await getHttpTimelineClient().getPostData( - key.timelineName, - key.postId - ); - await this.savePostData(key, res); - line.next({ data: res.data, type: "synced" }); - } catch (e) { - line.next({ type: "offline" }); - throwIfNotNetworkError(e); - } - } else { - try { - const res = await getHttpTimelineClient().getPostData( - key.timelineName, - key.postId, - cache.etag - ); - if (res instanceof NotModified) { - line.next({ data: cache.data, type: "synced" }); - } else { - await this.savePostData(key, res); - line.next({ data: res.data, type: "synced" }); - } - } catch (e) { - line.next({ data: cache.data, type: "offline" }); - throwIfNotNetworkError(e); - } - } - }, - }); - - getPostData$(timelineName: string, postId: number): Observable { - return this._postDataHub.getObservable({ timelineName, postId }).pipe( - map((state): BlobOrStatus => state.data ?? "error"), - startWith("loading") - ); - } - - createPost( - timelineName: string, - request: TimelineCreatePostRequest - ): Observable { - const user = checkLogin(); - return from( - getHttpTimelineClient() - .postPost(timelineName, request, user.token) - .then(() => { - void this.syncPosts(timelineName); - }) - ); - } - - deletePost(timelineName: string, postId: number): Observable { - const user = checkLogin(); - return from( - getHttpTimelineClient() - .deletePost(timelineName, postId, user.token) - .then(() => { - void this.syncPosts(timelineName); - }) - ); - } - - isMemberOf(username: string, timeline: TimelineInfo): boolean { - return timeline.members.findIndex((m) => m.username == username) >= 0; - } - - hasReadPermission( - user: UserAuthInfo | null | undefined, - timeline: TimelineInfo - ): boolean { - if (user != null && user.administrator) return true; - - const { visibility } = timeline; - if (visibility === "Public") { - return true; - } else if (visibility === "Register") { - if (user != null) return true; - } else if (visibility === "Private") { - if ( - user != null && - (user.username === timeline.owner.username || - this.isMemberOf(user.username, timeline)) - ) { - return true; - } - } - return false; - } - - hasPostPermission( - user: UserAuthInfo | null | undefined, - timeline: TimelineInfo - ): boolean { - if (user != null && user.administrator) return true; - - return ( - user != null && - (timeline.owner.username === user.username || - this.isMemberOf(user.username, timeline)) - ); - } - - hasManagePermission( - user: UserAuthInfo | null | undefined, - timeline: TimelineInfo - ): boolean { - if (user != null && user.administrator) return true; - - return user != null && user.username == timeline.owner.username; - } - - hasModifyPostPermission( - user: UserAuthInfo | null | undefined, - timeline: TimelineInfo, - post: TimelinePostInfo - ): boolean { - if (user != null && user.administrator) return true; - - return ( - user != null && - (user.username === timeline.owner.username || - user.username === post.author.username) - ); - } -} - -export const timelineService = new TimelineService(); - -const timelineNameReg = XRegExp("^[-_\\p{L}]*$", "u"); - -export function validateTimelineName(name: string): boolean { - return timelineNameReg.test(name); -} - -export function useTimelineInfo( - timelineName: string -): TimelineWithSyncStatus | undefined { - const [state, setState] = React.useState( - undefined - ); - React.useEffect(() => { - const subscription = timelineService - .getTimeline$(timelineName) - .subscribe((data) => { - setState(data); - }); - return () => { - subscription.unsubscribe(); - }; - }, [timelineName]); - return state; -} - -export function usePostList( - timelineName: string | null | undefined -): TimelinePostsWithSyncState | undefined { - const [state, setState] = React.useState< - TimelinePostsWithSyncState | undefined - >(undefined); - React.useEffect(() => { - if (timelineName == null) { - setState(undefined); - return; - } - - const subscription = timelineService - .getPosts$(timelineName) - .subscribe((data) => { - setState(data); - }); - return () => { - subscription.unsubscribe(); - }; - }, [timelineName]); - return state; -} - -export async function getAllCachedTimelineNames(): Promise { - const keys = await dataStorage.keys(); - return keys - .filter( - (key) => - key.startsWith("timeline.") && (key.match(/\./g) ?? []).length === 1 - ) - .map((key) => key.substr("timeline.".length)); -} diff --git a/Timeline/ClientApp/src/app/services/user.ts b/Timeline/ClientApp/src/app/services/user.ts deleted file mode 100644 index f253fc19..00000000 --- a/Timeline/ClientApp/src/app/services/user.ts +++ /dev/null @@ -1,393 +0,0 @@ -import React, { useState, useEffect } from "react"; -import { BehaviorSubject, Observable, from } from "rxjs"; -import { map, filter } from "rxjs/operators"; - -import { UiLogicError } from "@/common"; -import { convertError } from "@/utilities/rxjs"; - -import { HttpNetworkError, BlobWithEtag, NotModified } from "@/http/common"; -import { - getHttpTokenClient, - HttpCreateTokenBadCredentialError, -} from "@/http/token"; -import { - getHttpUserClient, - HttpUserNotExistError, - HttpUser, -} from "@/http/user"; - -import { dataStorage, throwIfNotNetworkError } from "./common"; -import { DataHub } from "./DataHub"; -import { pushAlert } from "./alert"; - -export type User = HttpUser; - -export interface UserAuthInfo { - username: string; - administrator: boolean; -} - -export interface UserWithToken extends User { - token: string; -} - -export interface LoginCredentials { - username: string; - password: string; -} - -export class BadCredentialError { - message = "login.badCredential"; -} - -const USER_STORAGE_KEY = "currentuser"; - -export class UserService { - private userSubject = new BehaviorSubject( - undefined - ); - - get user$(): Observable { - return this.userSubject; - } - - get currentUser(): UserWithToken | null | undefined { - return this.userSubject.value; - } - - async checkLoginState(): Promise { - if (this.currentUser !== undefined) { - console.warn("Already checked user. Can't check twice."); - } - - const savedUser = await dataStorage.getItem( - USER_STORAGE_KEY - ); - - if (savedUser == null) { - this.userSubject.next(null); - return null; - } - - this.userSubject.next(savedUser); - - const savedToken = savedUser.token; - try { - const res = await getHttpTokenClient().verify({ token: savedToken }); - const user: UserWithToken = { ...res.user, token: savedToken }; - await dataStorage.setItem(USER_STORAGE_KEY, user); - this.userSubject.next(user); - pushAlert({ - type: "success", - message: { - type: "i18n", - key: "user.welcomeBack", - }, - }); - return user; - } catch (error) { - if (error instanceof HttpNetworkError) { - pushAlert({ - type: "danger", - message: { type: "i18n", key: "user.verifyTokenFailedNetwork" }, - }); - return savedUser; - } else { - await dataStorage.removeItem(USER_STORAGE_KEY); - this.userSubject.next(null); - pushAlert({ - type: "danger", - message: { type: "i18n", key: "user.verifyTokenFailed" }, - }); - return null; - } - } - } - - async login( - credentials: LoginCredentials, - rememberMe: boolean - ): Promise { - if (this.currentUser) { - throw new UiLogicError("Already login."); - } - try { - const res = await getHttpTokenClient().create({ - ...credentials, - expire: 30, - }); - const user: UserWithToken = { - ...res.user, - token: res.token, - }; - if (rememberMe) { - await dataStorage.setItem(USER_STORAGE_KEY, user); - } - this.userSubject.next(user); - } catch (e) { - if (e instanceof HttpCreateTokenBadCredentialError) { - throw new BadCredentialError(); - } else { - throw e; - } - } - } - - async logout(): Promise { - if (this.currentUser === undefined) { - throw new UiLogicError("Please check user first."); - } - if (this.currentUser === null) { - throw new UiLogicError("No login."); - } - await dataStorage.removeItem(USER_STORAGE_KEY); - this.userSubject.next(null); - } - - changePassword( - oldPassword: string, - newPassword: string - ): Observable { - if (this.currentUser == undefined) { - throw new UiLogicError("Not login or checked now, can't log out."); - } - const $ = from( - getHttpUserClient().changePassword( - { - oldPassword, - newPassword, - }, - this.currentUser.token - ) - ); - $.subscribe(() => { - void this.logout(); - }); - return $; - } -} - -export const userService = new UserService(); - -export function useRawUser(): UserWithToken | null | undefined { - const [user, setUser] = useState( - userService.currentUser - ); - useEffect(() => { - const subscription = userService.user$.subscribe((u) => setUser(u)); - return () => { - subscription.unsubscribe(); - }; - }); - return user; -} - -export function useUser(): UserWithToken | null { - const [user, setUser] = useState(() => { - const initUser = userService.currentUser; - if (initUser === undefined) { - throw new UiLogicError( - "This is a logic error in user module. Current user can't be undefined in useUser." - ); - } - return initUser; - }); - useEffect(() => { - const sub = userService.user$.subscribe((u) => { - if (u === undefined) { - throw new UiLogicError( - "This is a logic error in user module. User emitted can't be undefined later." - ); - } - setUser(u); - }); - return () => { - sub.unsubscribe(); - }; - }); - return user; -} - -export function useUserLoggedIn(): UserWithToken { - const user = useUser(); - if (user == null) { - throw new UiLogicError("You assert user has logged in but actually not."); - } - return user; -} - -export function checkLogin(): UserWithToken { - const user = userService.currentUser; - if (user == null) { - throw new UiLogicError("You must login to perform the operation."); - } - return user; -} - -export class UserNotExistError extends Error {} - -export class UserInfoService { - saveUser(user: HttpUser): void { - const key = user.username; - void this._userHub.optionalInitLineWithSyncAction(key, async (line) => { - await this.doSaveUser(user); - line.next({ user, type: "synced" }); - }); - } - - saveUsers(users: HttpUser[]): void { - return users.forEach((user) => this.saveUser(user)); - } - - private getCachedUser(username: string): Promise { - return dataStorage.getItem(`user.${username}`); - } - - private doSaveUser(user: HttpUser): Promise { - return dataStorage.setItem(`user.${user.username}`, user).then(); - } - - syncUser(username: string): Promise { - return this._userHub.getLineOrCreate(username).sync(); - } - - private _userHub = new DataHub< - string, - | { user: User; type: "cache" | "synced" | "offline" } - | { user?: undefined; type: "notexist" | "offline" } - >({ - sync: async (key, line) => { - if (line.value == undefined) { - const cache = await this.getCachedUser(key); - if (cache != null) { - line.next({ user: cache, type: "cache" }); - } - } - - try { - const res = await getHttpUserClient().get(key); - await this.doSaveUser(res); - line.next({ user: res, type: "synced" }); - } catch (e) { - if (e instanceof HttpUserNotExistError) { - line.next({ type: "notexist" }); - } else { - const cache = await this.getCachedUser(key); - line.next({ user: cache ?? undefined, type: "offline" }); - throwIfNotNetworkError(e); - } - } - }, - }); - - getUser$(username: string): Observable { - return this._userHub.getObservable(username).pipe( - map((state) => state?.user), - filter((user): user is User => user != null) - ); - } - - private getCachedAvatar(username: string): Promise { - return dataStorage.getItem(`user.${username}.avatar`); - } - - private saveAvatar(username: string, data: BlobWithEtag): Promise { - return dataStorage - .setItem(`user.${username}.avatar`, data) - .then(); - } - - syncAvatar(username: string): Promise { - return this._avatarHub.getLineOrCreate(username).sync(); - } - - private _avatarHub = new DataHub< - string, - | { data: Blob; type: "cache" | "synced" | "offline" } - | { data?: undefined; type: "notexist" | "offline" } - >({ - sync: async (key, line) => { - const cache = await this.getCachedAvatar(key); - if (line.value == null) { - if (cache != null) { - line.next({ data: cache.data, type: "cache" }); - } - } - - if (cache == null) { - try { - const avatar = await getHttpUserClient().getAvatar(key); - await this.saveAvatar(key, avatar); - line.next({ data: avatar.data, type: "synced" }); - } catch (e) { - line.next({ type: "offline" }); - throwIfNotNetworkError(e); - } - } else { - try { - const res = await getHttpUserClient().getAvatar(key, cache.etag); - if (res instanceof NotModified) { - line.next({ data: cache.data, type: "synced" }); - } else { - const avatar = res; - await this.saveAvatar(key, avatar); - line.next({ data: avatar.data, type: "synced" }); - } - } catch (e) { - line.next({ data: cache.data, type: "offline" }); - throwIfNotNetworkError(e); - } - } - }, - }); - - getAvatar$(username: string): Observable { - return this._avatarHub.getObservable(username).pipe( - map((state) => state.data), - filter((blob): blob is Blob => blob != null) - ); - } - - getUserInfo(username: string): Observable { - return from(getHttpUserClient().get(username)).pipe( - convertError(HttpUserNotExistError, UserNotExistError) - ); - } - - async setAvatar(username: string, blob: Blob): Promise { - const user = checkLogin(); - await getHttpUserClient().putAvatar(username, blob, user.token); - this._avatarHub.getLine(username)?.next({ data: blob, type: "synced" }); - } - - async setNickname(username: string, nickname: string): Promise { - const user = checkLogin(); - return getHttpUserClient() - .patch(username, { nickname }, user.token) - .then((user) => { - this.saveUser(user); - }); - } -} - -export const userInfoService = new UserInfoService(); - -export function useAvatar(username?: string): Blob | undefined { - const [state, setState] = React.useState(undefined); - React.useEffect(() => { - if (username == null) { - setState(undefined); - return; - } - - const subscription = userInfoService - .getAvatar$(username) - .subscribe((blob) => { - setState(blob); - }); - return () => { - subscription.unsubscribe(); - }; - }, [username]); - return state; -} diff --git a/Timeline/ClientApp/src/app/tsconfig.json b/Timeline/ClientApp/src/app/tsconfig.json deleted file mode 100644 index 14e6327f..00000000 --- a/Timeline/ClientApp/src/app/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "lib": [ - "dom", - "dom.iterable", - "esnext" - ] - }, - "include": [ - "." - ] -} diff --git a/Timeline/ClientApp/src/app/typings.d.ts b/Timeline/ClientApp/src/app/typings.d.ts deleted file mode 100644 index 34381682..00000000 --- a/Timeline/ClientApp/src/app/typings.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -declare module "*.png" { - const content: string; - export default content; -} - -declare module "*.jpeg" { - const content: string; - export default content; -} - -declare module "*.jpg" { - const content: string; - export default content; -} - -declare module "*.gif" { - const content: string; - export default content; -} - -declare module "*.svg" { - const content: string; - export default content; -} diff --git a/Timeline/ClientApp/src/app/utilities/rxjs.ts b/Timeline/ClientApp/src/app/utilities/rxjs.ts deleted file mode 100644 index 0730b899..00000000 --- a/Timeline/ClientApp/src/app/utilities/rxjs.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { OperatorFunction } from "rxjs"; -import { catchError } from "rxjs/operators"; - -export function convertError( - oldErrorType: { new (...args: never[]): unknown }, - newErrorType: { new (): NewError } -): OperatorFunction { - return catchError((error) => { - if (error instanceof oldErrorType) { - throw new newErrorType(); - } - throw error; - }); -} diff --git a/Timeline/ClientApp/src/app/utilities/url.ts b/Timeline/ClientApp/src/app/utilities/url.ts deleted file mode 100644 index 17ead5b2..00000000 --- a/Timeline/ClientApp/src/app/utilities/url.ts +++ /dev/null @@ -1,52 +0,0 @@ -//copied from https://stackoverflow.com/questions/5999118/how-can-i-add-or-update-a-query-string-parameter -export function updateQueryString( - key: string, - value: undefined | string | null, - url: string -): string { - const re = new RegExp("([?&])" + key + "=.*?(&|#|$)(.*)", "gi"); - let hash; - - if (re.test(url)) { - if (typeof value !== "undefined" && value !== null) { - return url.replace(re, "$1" + key + "=" + value + "$2$3"); - } else { - hash = url.split("#"); - url = hash[0].replace(re, "$1$3").replace(/(&|\?)$/, ""); - if (typeof hash[1] !== "undefined" && hash[1] !== null) { - url += "#" + hash[1]; - } - return url; - } - } else { - if (typeof value !== "undefined" && value !== null) { - const separator = url.includes("?") ? "&" : "?"; - hash = url.split("#"); - url = hash[0] + separator + key + "=" + value; - if (typeof hash[1] !== "undefined" && hash[1] !== null) { - url += "#" + hash[1]; - } - return url; - } else { - return url; - } - } -} - -export function applyQueryParameters(url: string, query: T): string { - if (query == null) return url; - - for (const [key, value] of Object.entries(query)) { - if (typeof value === "string") url = updateQueryString(key, value, url); - else if (typeof value === "number") - url = updateQueryString(key, String(value), url); - else if (typeof value === "boolean") - url = updateQueryString(key, value ? "true" : "false", url); - else if (value instanceof Date) - url = updateQueryString(key, value.toISOString(), url); - else { - console.error("Unknown query parameter type. Param: ", value); - } - } - return url; -} diff --git a/Timeline/ClientApp/src/app/views/about/about.sass b/Timeline/ClientApp/src/app/views/about/about.sass deleted file mode 100644 index 3b5840cd..00000000 --- a/Timeline/ClientApp/src/app/views/about/about.sass +++ /dev/null @@ -1,4 +0,0 @@ -.about-link-icon - @extend .mx-2 - width: 1.2em - height: 1.2em diff --git a/Timeline/ClientApp/src/app/views/about/author-avatar.png b/Timeline/ClientApp/src/app/views/about/author-avatar.png deleted file mode 100644 index d890d8d0..00000000 Binary files a/Timeline/ClientApp/src/app/views/about/author-avatar.png and /dev/null differ diff --git a/Timeline/ClientApp/src/app/views/about/github.png b/Timeline/ClientApp/src/app/views/about/github.png deleted file mode 100644 index ea6ff545..00000000 Binary files a/Timeline/ClientApp/src/app/views/about/github.png and /dev/null differ diff --git a/Timeline/ClientApp/src/app/views/about/index.tsx b/Timeline/ClientApp/src/app/views/about/index.tsx deleted file mode 100644 index e7771cec..00000000 --- a/Timeline/ClientApp/src/app/views/about/index.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import React from "react"; -import { useTranslation, Trans } from "react-i18next"; - -import authorAvatarUrl from "./author-avatar.png"; -import githubLogoUrl from "./github.png"; - -const frontendCredits: { - name: string; - url: string; -}[] = [ - { - name: "reactjs", - url: "https://reactjs.org", - }, - { - name: "typescript", - url: "https://www.typescriptlang.org", - }, - { - name: "bootstrap", - url: "https://getbootstrap.com", - }, - { - name: "react-bootstrap", - url: "https://react-bootstrap.github.io", - }, - { - name: "babeljs", - url: "https://babeljs.io", - }, - { - name: "webpack", - url: "https://webpack.js.org", - }, - { - name: "sass", - url: "https://sass-lang.com", - }, - { - name: "eslint", - url: "https://eslint.org", - }, - { - name: "prettier", - url: "https://prettier.io", - }, - { - name: "pepjs", - url: "https://github.com/jquery/PEP", - }, - { - name: "react-inlinesvg", - url: "https://github.com/gilbarbara/react-inlinesvg", - }, -]; - -const backendCredits: { - name: string; - url: string; -}[] = [ - { - name: "ASP.NET Core", - url: "https://dotnet.microsoft.com/learn/aspnet/what-is-aspnet-core", - }, - { name: "sqlite", url: "https://sqlite.org" }, - { - name: "ImageSharp", - url: "https://github.com/SixLabors/ImageSharp", - }, -]; - -const AboutPage: React.FC = () => { - const { t } = useTranslation(); - - return ( -
-
-

{t("about.author.title")}

-
-
- -
-

- {t("about.author.fullname")} - 杨宇千 -

-

- {t("about.author.nickname")} - crupest -

-

- {t("about.author.introduction")} - {t("about.author.introductionContent")} -

-
-
-

- {t("about.author.links")} - - - -

-
-
-
-

{t("about.site.title")}

-

- - 01234 - 56 - -

-

- - {t("about.site.repo")} - -

-
-
-

{t("about.credits.title")}

-

{t("about.credits.content")}

-

{t("about.credits.frontend")}

-
    - {frontendCredits.map((item, index) => { - return ( -
  • - - {item.name} - -
  • - ); - })} -
  • ...
  • -
-

{t("about.credits.backend")}

-
    - {backendCredits.map((item, index) => { - return ( -
  • - - {item.name} - -
  • - ); - })} -
  • ...
  • -
-
-
- ); -}; - -export default AboutPage; diff --git a/Timeline/ClientApp/src/app/views/admin/Admin.tsx b/Timeline/ClientApp/src/app/views/admin/Admin.tsx deleted file mode 100644 index 9c0250e7..00000000 --- a/Timeline/ClientApp/src/app/views/admin/Admin.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React, { Fragment } from "react"; -import { - Redirect, - Route, - Switch, - useRouteMatch, - useHistory, -} from "react-router"; -import { Nav } from "react-bootstrap"; - -import { UserWithToken } from "@/services/user"; - -import UserAdmin from "./UserAdmin"; - -interface AdminProps { - user: UserWithToken; -} - -const Admin: React.FC = (props) => { - const match = useRouteMatch(); - const history = useHistory(); - type TabNames = "users" | "more"; - - const tabName = history.location.pathname.replace(match.path + "/", ""); - - function toggle(newTab: TabNames): void { - history.push(`${match.url}/${newTab}`); - } - - const createRoute = ( - name: string, - body: React.ReactNode - ): React.ReactNode => { - return ( - -
- - {body} - - ); - }; - - return ( - - - - {createRoute("users", )} - {createRoute("more",
More Page Works
)} -
-
- ); -}; - -export default Admin; diff --git a/Timeline/ClientApp/src/app/views/admin/UserAdmin.tsx b/Timeline/ClientApp/src/app/views/admin/UserAdmin.tsx deleted file mode 100644 index 18b77ca8..00000000 --- a/Timeline/ClientApp/src/app/views/admin/UserAdmin.tsx +++ /dev/null @@ -1,460 +0,0 @@ -import React, { useState, useEffect } from "react"; -import axios from "axios"; -import { - ListGroup, - Row, - Col, - Dropdown, - Spinner, - Button, -} from "react-bootstrap"; - -import OperationDialog from "../common/OperationDialog"; -import { User, UserWithToken } from "@/services/user"; - -const apiBaseUrl = "/api"; - -async function fetchUserList(_token: string): Promise { - const res = await axios.get(`${apiBaseUrl}/users`); - return res.data; -} - -interface CreateUserInfo { - username: string; - password: string; - administrator: boolean; -} - -async function createUser(user: CreateUserInfo, token: string): Promise { - const res = await axios.post( - `${apiBaseUrl}/userop/createuser?token=${token}`, - user - ); - return res.data; -} - -function deleteUser(username: string, token: string): Promise { - return axios.delete(`${apiBaseUrl}/users/${username}?token=${token}`); -} - -function changeUsername( - oldUsername: string, - newUsername: string, - token: string -): Promise { - return axios.patch(`${apiBaseUrl}/users/${oldUsername}?token=${token}`, { - username: newUsername, - }); -} - -function changePassword( - username: string, - newPassword: string, - token: string -): Promise { - return axios.patch(`${apiBaseUrl}/users/${username}?token=${token}`, { - password: newPassword, - }); -} - -function changePermission( - username: string, - newPermission: boolean, - token: string -): Promise { - return axios.patch(`${apiBaseUrl}/users/${username}?token=${token}`, { - administrator: newPermission, - }); -} - -const kChangeUsername = "changeusername"; -const kChangePassword = "changepassword"; -const kChangePermission = "changepermission"; -const kDelete = "delete"; - -type TChangeUsername = typeof kChangeUsername; -type TChangePassword = typeof kChangePassword; -type TChangePermission = typeof kChangePermission; -type TDelete = typeof kDelete; - -type ContextMenuItem = - | TChangeUsername - | TChangePassword - | TChangePermission - | TDelete; - -interface UserCardProps { - onContextMenu: (item: ContextMenuItem) => void; - user: User; -} - -const UserItem: React.FC = (props) => { - const user = props.user; - - const createClickCallback = (item: ContextMenuItem): (() => void) => { - return () => { - props.onContextMenu(item); - }; - }; - - return ( - - - -

{user.username}

- - {user.administrator ? "administrator" : "user"} - - - - - - Manage - - - - Change Username - - - Change Password - - - Change Permission - - - Delete - - - - -
-
- ); -}; - -interface DialogProps { - open: boolean; - close: () => void; -} - -interface CreateUserDialogProps extends DialogProps { - process: (user: CreateUserInfo) => Promise; -} - -const CreateUserDialog: React.FC = (props) => { - return ( - - props.process({ - username: username as string, - password: password as string, - administrator: administrator as boolean, - }) - } - close={props.close} - open={props.open} - /> - ); -}; - -const UsernameLabel: React.FC = (props) => { - return {props.children}; -}; - -interface UserDeleteDialogProps extends DialogProps { - username: string; - process: () => Promise; -} - -const UserDeleteDialog: React.FC = (props) => { - return ( - ( - <> - {"You are deleting user "} - {props.username} - {" !"} - - )} - onProcess={props.process} - /> - ); -}; - -interface UserModifyDialogProps extends DialogProps { - username: string; - process: (value: T) => Promise; -} - -const UserChangeUsernameDialog: React.FC> = ( - props -) => { - return ( - ( - <> - {"You are change the username of user "} - {props.username} - {" !"} - - )} - inputScheme={[{ type: "text", label: "New Username" }]} - onProcess={([newUsername]) => { - return props.process(newUsername as string); - }} - /> - ); -}; - -const UserChangePasswordDialog: React.FC> = ( - props -) => { - return ( - ( - <> - {"You are change the password of user "} - {props.username} - {" !"} - - )} - inputScheme={[{ type: "text", label: "New Password" }]} - onProcess={([newPassword]) => { - return props.process(newPassword as string); - }} - /> - ); -}; - -interface UserChangePermissionDialogProps extends DialogProps { - username: string; - newPermission: boolean; - process: () => Promise; -} - -const UserChangePermissionDialog: React.FC = ( - props -) => { - return ( - ( - <> - {"You are change user "} - {props.username} - {" to "} - - {props.newPermission ? "administrator" : "normal user"} - - {" !"} - - )} - onProcess={props.process} - /> - ); -}; - -interface UserAdminProps { - user: UserWithToken; -} - -const UserAdmin: React.FC = (props) => { - type DialogInfo = - | null - | { - type: "create"; - } - | { type: TDelete; username: string } - | { - type: TChangeUsername; - username: string; - } - | { - type: TChangePassword; - username: string; - } - | { - type: TChangePermission; - username: string; - newPermission: boolean; - }; - - const [users, setUsers] = useState(null); - const [dialog, setDialog] = useState(null); - - const token = props.user.token; - - useEffect(() => { - let subscribe = true; - void fetchUserList(props.user.token).then((us) => { - if (subscribe) { - setUsers(us); - } - }); - return () => { - subscribe = false; - }; - }, [props.user]); - - let dialogNode: React.ReactNode; - if (dialog) - switch (dialog.type) { - case "create": - dialogNode = ( - setDialog(null)} - process={async (user) => { - const u = await createUser(user, token); - setUsers((oldUsers) => [...(oldUsers ?? []), u]); - }} - /> - ); - break; - case "delete": - dialogNode = ( - setDialog(null)} - username={dialog.username} - process={async () => { - await deleteUser(dialog.username, token); - setUsers((oldUsers) => - (oldUsers ?? []).filter((u) => u.username !== dialog.username) - ); - }} - /> - ); - break; - case kChangeUsername: - dialogNode = ( - setDialog(null)} - username={dialog.username} - process={async (newUsername) => { - await changeUsername(dialog.username, newUsername, token); - setUsers((oldUsers) => { - const users = (oldUsers ?? []).slice(); - const findedUser = users.find( - (u) => u.username === dialog.username - ); - if (findedUser) findedUser.username = newUsername; - return users; - }); - }} - /> - ); - break; - case kChangePassword: - dialogNode = ( - setDialog(null)} - username={dialog.username} - process={async (newPassword) => { - await changePassword(dialog.username, newPassword, token); - }} - /> - ); - break; - case kChangePermission: { - const newPermission = dialog.newPermission; - dialogNode = ( - setDialog(null)} - username={dialog.username} - newPermission={newPermission} - process={async () => { - await changePermission(dialog.username, newPermission, token); - setUsers((oldUsers) => { - const users = (oldUsers ?? []).slice(); - const findedUser = users.find( - (u) => u.username === dialog.username - ); - if (findedUser) findedUser.administrator = newPermission; - return users; - }); - }} - /> - ); - break; - } - } - - if (users) { - const userComponents = users.map((user) => { - return ( - { - setDialog( - item === kChangePermission - ? { - type: kChangePermission, - username: user.username, - newPermission: !user.administrator, - } - : { - type: item, - username: user.username, - } - ); - }} - /> - ); - }); - - return ( - <> - - {userComponents} - {dialogNode} - - ); - } else { - return ; - } -}; - -export default UserAdmin; diff --git a/Timeline/ClientApp/src/app/views/common/AppBar.tsx b/Timeline/ClientApp/src/app/views/common/AppBar.tsx deleted file mode 100644 index ee4ead8f..00000000 --- a/Timeline/ClientApp/src/app/views/common/AppBar.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import React from "react"; -import { useTranslation } from "react-i18next"; -import { LinkContainer } from "react-router-bootstrap"; -import { Navbar, Nav } from "react-bootstrap"; - -import { useUser, useAvatar } from "@/services/user"; - -import TimelineLogo from "./TimelineLogo"; -import BlobImage from "./BlobImage"; - -const AppBar: React.FC = (_) => { - const user = useUser(); - const avatar = useAvatar(user?.username); - - const { t } = useTranslation(); - - const isAdministrator = user && user.administrator; - - return ( - - - - - Timeline - - - - - - - - - - ); -}; - -export default AppBar; diff --git a/Timeline/ClientApp/src/app/views/common/BlobImage.tsx b/Timeline/ClientApp/src/app/views/common/BlobImage.tsx deleted file mode 100644 index 0dd25c52..00000000 --- a/Timeline/ClientApp/src/app/views/common/BlobImage.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from "react"; - -const BlobImage: React.FC< - Omit, "src"> & { - blob?: Blob | unknown; - } -> = (props) => { - const { blob, ...otherProps } = props; - - const [url, setUrl] = React.useState(undefined); - - React.useEffect(() => { - if (blob instanceof Blob) { - const url = URL.createObjectURL(blob); - setUrl(url); - return () => { - URL.revokeObjectURL(url); - }; - } else { - setUrl(undefined); - } - }, [blob]); - - return ; -}; - -export default BlobImage; diff --git a/Timeline/ClientApp/src/app/views/common/ImageCropper.tsx b/Timeline/ClientApp/src/app/views/common/ImageCropper.tsx deleted file mode 100644 index b9db8b99..00000000 --- a/Timeline/ClientApp/src/app/views/common/ImageCropper.tsx +++ /dev/null @@ -1,306 +0,0 @@ -import React from "react"; -import clsx from "clsx"; - -import { UiLogicError } from "@/common"; - -export interface Clip { - left: number; - top: number; - width: number; -} - -interface NormailizedClip extends Clip { - height: number; -} - -interface ImageInfo { - width: number; - height: number; - landscape: boolean; - ratio: number; - maxClipWidth: number; - maxClipHeight: number; -} - -interface ImageCropperSavedState { - clip: NormailizedClip; - x: number; - y: number; - pointerId: number; -} - -export interface ImageCropperProps { - clip: Clip | null; - imageUrl: string; - onChange: (clip: Clip) => void; - imageElementCallback?: (element: HTMLImageElement | null) => void; - className?: string; -} - -const ImageCropper = (props: ImageCropperProps): React.ReactElement => { - const { clip, imageUrl, onChange, imageElementCallback, className } = props; - - const [oldState, setOldState] = React.useState( - null - ); - const [imageInfo, setImageInfo] = React.useState(null); - - const normalizeClip = (c: Clip | null | undefined): NormailizedClip => { - if (c == null) { - return { left: 0, top: 0, width: 0, height: 0 }; - } - - return { - left: c.left || 0, - top: c.top || 0, - width: c.width || 0, - height: imageInfo != null ? (c.width || 0) / imageInfo.ratio : 0, - }; - }; - - const c = normalizeClip(clip); - - const imgElementRef = React.useRef(null); - - const onImageRef = React.useCallback( - (e: HTMLImageElement | null) => { - imgElementRef.current = e; - if (imageElementCallback != null && e == null) { - imageElementCallback(null); - } - }, - [imageElementCallback] - ); - - const onImageLoad = React.useCallback( - (e: React.SyntheticEvent) => { - const img = e.currentTarget; - const landscape = img.naturalWidth >= img.naturalHeight; - - const info = { - width: img.naturalWidth, - height: img.naturalHeight, - landscape, - ratio: img.naturalHeight / img.naturalWidth, - maxClipWidth: landscape ? img.naturalHeight / img.naturalWidth : 1, - maxClipHeight: landscape ? 1 : img.naturalWidth / img.naturalHeight, - }; - setImageInfo(info); - onChange({ left: 0, top: 0, width: info.maxClipWidth }); - if (imageElementCallback != null) { - imageElementCallback(img); - } - }, - [onChange, imageElementCallback] - ); - - const onPointerDown = React.useCallback( - (e: React.PointerEvent) => { - if (oldState != null) return; - e.currentTarget.setPointerCapture(e.pointerId); - setOldState({ - x: e.clientX, - y: e.clientY, - clip: c, - pointerId: e.pointerId, - }); - }, - [oldState, c] - ); - - const onPointerUp = React.useCallback( - (e: React.PointerEvent) => { - if (oldState == null || oldState.pointerId !== e.pointerId) return; - e.currentTarget.releasePointerCapture(e.pointerId); - setOldState(null); - }, - [oldState] - ); - - const onPointerMove = React.useCallback( - (e: React.PointerEvent) => { - if (oldState == null) return; - - const oldClip = oldState.clip; - - const movement = { x: e.clientX - oldState.x, y: e.clientY - oldState.y }; - - const { current: imgElement } = imgElementRef; - - if (imgElement == null) throw new UiLogicError("Image element is null."); - - const moveRatio = { - x: movement.x / imgElement.width, - y: movement.y / imgElement.height, - }; - - const newRatio = { - x: oldClip.left + moveRatio.x, - y: oldClip.top + moveRatio.y, - }; - if (newRatio.x < 0) { - newRatio.x = 0; - } else if (newRatio.x > 1 - oldClip.width) { - newRatio.x = 1 - oldClip.width; - } - if (newRatio.y < 0) { - newRatio.y = 0; - } else if (newRatio.y > 1 - oldClip.height) { - newRatio.y = 1 - oldClip.height; - } - - onChange({ left: newRatio.x, top: newRatio.y, width: oldClip.width }); - }, - [oldState, onChange] - ); - - const onHandlerPointerMove = React.useCallback( - (e: React.PointerEvent) => { - if (oldState == null) return; - - const oldClip = oldState.clip; - - const movement = { x: e.clientX - oldState.x, y: e.clientY - oldState.y }; - - const ratio = imageInfo == null ? 1 : imageInfo.ratio; - - const { current: imgElement } = imgElementRef; - - if (imgElement == null) throw new UiLogicError("Image element is null."); - - const moveRatio = { - x: movement.x / imgElement.width, - y: movement.x / imgElement.width / ratio, - }; - - const newRatio = { - x: oldClip.width + moveRatio.x, - y: oldClip.height + moveRatio.y, - }; - - const maxRatio = { - x: Math.min(1 - oldClip.left, newRatio.x), - y: Math.min(1 - oldClip.top, newRatio.y), - }; - - const maxWidthRatio = Math.min(maxRatio.x, maxRatio.y * ratio); - - let newWidth; - if (newRatio.x < 0) { - newWidth = 0; - } else if (newRatio.x > maxWidthRatio) { - newWidth = maxWidthRatio; - } else { - newWidth = newRatio.x; - } - - onChange({ left: oldClip.left, top: oldClip.top, width: newWidth }); - }, - [imageInfo, oldState, onChange] - ); - - const toPercentage = (n: number): string => `${n}%`; - - // fuck!!! I just can't find a better way to implement this in pure css - const containerStyle: React.CSSProperties = (() => { - if (imageInfo == null) { - return { width: "100%", paddingTop: "100%", height: 0 }; - } else { - if (imageInfo.ratio > 1) { - return { - width: toPercentage(100 / imageInfo.ratio), - paddingTop: "100%", - height: 0, - }; - } else { - return { - width: "100%", - paddingTop: toPercentage(100 * imageInfo.ratio), - height: 0, - }; - } - } - })(); - - return ( -
- to crop -
-
-
-
-
- ); -}; - -export default ImageCropper; - -export function applyClipToImage( - image: HTMLImageElement, - clip: Clip, - mimeType: string -): Promise { - return new Promise((resolve, reject) => { - const naturalSize = { - width: image.naturalWidth, - height: image.naturalHeight, - }; - const clipArea = { - x: naturalSize.width * clip.left, - y: naturalSize.height * clip.top, - length: naturalSize.width * clip.width, - }; - - const canvas = document.createElement("canvas"); - canvas.width = clipArea.length; - canvas.height = clipArea.length; - const context = canvas.getContext("2d"); - - if (context == null) throw new Error("Failed to create context."); - - context.drawImage( - image, - clipArea.x, - clipArea.y, - clipArea.length, - clipArea.length, - 0, - 0, - clipArea.length, - clipArea.length - ); - - canvas.toBlob((blob) => { - if (blob == null) { - reject(new Error("canvas.toBlob returns null")); - } else { - resolve(blob); - } - }, mimeType); - }); -} diff --git a/Timeline/ClientApp/src/app/views/common/LoadingButton.tsx b/Timeline/ClientApp/src/app/views/common/LoadingButton.tsx deleted file mode 100644 index 154334a7..00000000 --- a/Timeline/ClientApp/src/app/views/common/LoadingButton.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from "react"; -import { Button, ButtonProps, Spinner } from "react-bootstrap"; - -const LoadingButton: React.FC<{ loading?: boolean } & ButtonProps> = ({ - loading, - variant, - disabled, - ...otherProps -}) => { - return ( - - ); -}; - -export default LoadingButton; diff --git a/Timeline/ClientApp/src/app/views/common/LoadingPage.tsx b/Timeline/ClientApp/src/app/views/common/LoadingPage.tsx deleted file mode 100644 index 590fafa0..00000000 --- a/Timeline/ClientApp/src/app/views/common/LoadingPage.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from "react"; -import { Spinner } from "react-bootstrap"; - -const LoadingPage: React.FC = () => { - return ( -
- -
- ); -}; - -export default LoadingPage; diff --git a/Timeline/ClientApp/src/app/views/common/OperationDialog.tsx b/Timeline/ClientApp/src/app/views/common/OperationDialog.tsx deleted file mode 100644 index 841392a6..00000000 --- a/Timeline/ClientApp/src/app/views/common/OperationDialog.tsx +++ /dev/null @@ -1,364 +0,0 @@ -import React, { useState } from "react"; -import { useTranslation } from "react-i18next"; -import { Form, Button, Modal } from "react-bootstrap"; - -import { UiLogicError } from "@/common"; - -import LoadingButton from "./LoadingButton"; - -interface DefaultErrorPromptProps { - error?: string; -} - -const DefaultErrorPrompt: React.FC = (props) => { - const { t } = useTranslation(); - - let result =

{t("operationDialog.error")}

; - - if (props.error != null) { - result = ( - <> - {result} -

{props.error}

- - ); - } - - return result; -}; - -export type OperationInputOptionalError = undefined | null | string; - -export interface OperationInputErrorInfo { - [index: number]: OperationInputOptionalError; -} - -export type OperationInputValidator = ( - value: TValue, - values: (string | boolean)[] -) => OperationInputOptionalError | OperationInputErrorInfo; - -export interface OperationTextInputInfo { - type: "text"; - password?: boolean; - label?: string; - initValue?: string; - textFieldProps?: Omit< - React.InputHTMLAttributes, - "type" | "value" | "onChange" | "aria-relevant" - >; - helperText?: string; - validator?: OperationInputValidator; -} - -export interface OperationBoolInputInfo { - type: "bool"; - label: string; - initValue?: boolean; -} - -export interface OperationSelectInputInfoOption { - value: string; - label: string; - icon?: React.ReactElement; -} - -export interface OperationSelectInputInfo { - type: "select"; - label: string; - options: OperationSelectInputInfoOption[]; - initValue?: string; -} - -export type OperationInputInfo = - | OperationTextInputInfo - | OperationBoolInputInfo - | OperationSelectInputInfo; - -interface OperationResult { - type: "success" | "failure"; - data: unknown; -} - -interface OperationDialogProps { - open: boolean; - close: () => void; - title: React.ReactNode; - titleColor?: "default" | "dangerous" | "create" | string; - onProcess: (inputs: (string | boolean)[]) => Promise; - inputScheme?: OperationInputInfo[]; - inputPrompt?: string | (() => React.ReactNode); - processPrompt?: () => React.ReactNode; - successPrompt?: (data: unknown) => React.ReactNode; - failurePrompt?: (error: unknown) => React.ReactNode; - onSuccessAndClose?: () => void; -} - -const OperationDialog: React.FC = (props) => { - const inputScheme = props.inputScheme ?? []; - - const { t } = useTranslation(); - - type Step = "input" | "process" | OperationResult; - const [step, setStep] = useState("input"); - const [values, setValues] = useState<(boolean | string)[]>( - inputScheme.map((i) => { - if (i.type === "bool") { - return i.initValue ?? false; - } else if (i.type === "text" || i.type === "select") { - return i.initValue ?? ""; - } else { - throw new UiLogicError("Unknown input scheme."); - } - }) - ); - const [inputError, setInputError] = useState({}); - - const close = (): void => { - if (step !== "process") { - props.close(); - if ( - typeof step === "object" && - step.type === "success" && - props.onSuccessAndClose - ) { - props.onSuccessAndClose(); - } - } else { - console.log("Attempt to close modal when processing."); - } - }; - - const onConfirm = (): void => { - setStep("process"); - props.onProcess(values).then( - (d: unknown) => { - setStep({ - type: "success", - data: d, - }); - }, - (e: unknown) => { - setStep({ - type: "failure", - data: e, - }); - } - ); - }; - - let body: React.ReactNode; - if (step === "input" || step === "process") { - const process = step === "process"; - - let inputPrompt = - typeof props.inputPrompt === "function" - ? props.inputPrompt() - : props.inputPrompt; - inputPrompt =
{inputPrompt}
; - - const updateValue = ( - index: number, - newValue: string | boolean - ): (string | boolean)[] => { - const oldValues = values; - const newValues = oldValues.slice(); - newValues[index] = newValue; - setValues(newValues); - return newValues; - }; - - const testErrorInfo = (errorInfo: OperationInputErrorInfo): boolean => { - for (let i = 0; i < inputScheme.length; i++) { - if (inputScheme[i].type === "text" && errorInfo[i] != null) { - return true; - } - } - return false; - }; - - const calculateError = ( - oldError: OperationInputErrorInfo, - index: number, - newError: OperationInputOptionalError | OperationInputErrorInfo - ): OperationInputErrorInfo => { - if (newError === undefined) { - return oldError; - } else if (newError === null || typeof newError === "string") { - return { ...oldError, [index]: newError }; - } else { - const newInputError: OperationInputErrorInfo = { ...oldError }; - for (const [index, error] of Object.entries(newError)) { - if (error !== undefined) { - newInputError[+index] = error as OperationInputOptionalError; - } - } - return newInputError; - } - }; - - const validateAll = (): boolean => { - let newInputError = inputError; - for (let i = 0; i < inputScheme.length; i++) { - const item = inputScheme[i]; - if (item.type === "text") { - newInputError = calculateError( - newInputError, - i, - item.validator?.(values[i] as string, values) - ); - } - } - const result = !testErrorInfo(newInputError); - setInputError(newInputError); - return result; - }; - - body = ( - <> - - {inputPrompt} - {inputScheme.map((item, index) => { - const value = values[index]; - const error: string | undefined = ((e) => - typeof e === "string" ? t(e) : undefined)(inputError?.[index]); - - if (item.type === "text") { - return ( - - {item.label && {t(item.label)}} - { - const v = e.target.value; - const newValues = updateValue(index, v); - setInputError( - calculateError( - inputError, - index, - item.validator?.(v, newValues) - ) - ); - }} - isInvalid={error != null} - disabled={process} - /> - {error != null && ( - - {error} - - )} - {item.helperText && ( - {t(item.helperText)} - )} - - ); - } else if (item.type === "bool") { - return ( - - - type="checkbox" - checked={value as boolean} - onChange={(event) => { - updateValue(index, event.currentTarget.checked); - }} - label={t(item.label)} - disabled={process} - /> - - ); - } else if (item.type === "select") { - return ( - - {t(item.label)} - { - updateValue(index, event.target.value); - }} - disabled={process} - > - {item.options.map((option, i) => { - return ( - - ); - })} - - - ); - } - })} - - - - { - if (validateAll()) { - onConfirm(); - } - }} - > - {t("operationDialog.confirm")} - - - - ); - } else { - let content: React.ReactNode; - const result = step; - if (result.type === "success") { - content = - props.successPrompt?.(result.data) ?? t("operationDialog.success"); - if (typeof content === "string") - content =

{content}

; - } else { - content = props.failurePrompt?.(result.data) ?? ; - if (typeof content === "string") - content = ; - } - body = ( - <> - {content} - - - - - ); - } - - const title = typeof props.title === "string" ? t(props.title) : props.title; - - return ( - - - {title} - - {body} - - ); -}; - -export default OperationDialog; diff --git a/Timeline/ClientApp/src/app/views/common/SearchInput.tsx b/Timeline/ClientApp/src/app/views/common/SearchInput.tsx deleted file mode 100644 index 9833d515..00000000 --- a/Timeline/ClientApp/src/app/views/common/SearchInput.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React, { useCallback } from "react"; -import clsx from "clsx"; -import { useTranslation } from "react-i18next"; -import { Spinner, Form, Button } from "react-bootstrap"; - -export interface SearchInputProps { - value: string; - onChange: (value: string) => void; - onButtonClick: () => void; - className?: string; - loading?: boolean; - buttonText?: string; - placeholder?: string; - additionalButton?: React.ReactNode; -} - -const SearchInput: React.FC = (props) => { - const { onChange, onButtonClick } = props; - - const { t } = useTranslation(); - - const onInputChange = useCallback( - (event: React.ChangeEvent): void => { - onChange(event.currentTarget.value); - }, - [onChange] - ); - - const onInputKeyPress = useCallback( - (event: React.KeyboardEvent): void => { - if (event.key === "Enter") { - onButtonClick(); - } - }, - [onButtonClick] - ); - - return ( -
- -
- {props.additionalButton} -
-
- {props.loading ? ( - - ) : ( - - )} -
- - ); -}; - -export default SearchInput; diff --git a/Timeline/ClientApp/src/app/views/common/TimelineLogo.tsx b/Timeline/ClientApp/src/app/views/common/TimelineLogo.tsx deleted file mode 100644 index 27d188fc..00000000 --- a/Timeline/ClientApp/src/app/views/common/TimelineLogo.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React, { SVGAttributes } from "react"; - -export interface TimelineLogoProps extends SVGAttributes { - color?: string; -} - -const TimelineLogo: React.FC = (props) => { - const { color, ...forwardProps } = props; - const coercedColor = color ?? "currentcolor"; - return ( - - - - - - ); -}; - -export default TimelineLogo; diff --git a/Timeline/ClientApp/src/app/views/common/UserTimelineLogo.tsx b/Timeline/ClientApp/src/app/views/common/UserTimelineLogo.tsx deleted file mode 100644 index 29f6a69f..00000000 --- a/Timeline/ClientApp/src/app/views/common/UserTimelineLogo.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React, { SVGAttributes } from "react"; - -export interface UserTimelineLogoProps extends SVGAttributes { - color?: string; -} - -const UserTimelineLogo: React.FC = (props) => { - const { color, ...forwardProps } = props; - const coercedColor = color ?? "currentcolor"; - - return ( - - - - - - - - - - - - ); -}; - -export default UserTimelineLogo; diff --git a/Timeline/ClientApp/src/app/views/common/alert/AlertHost.tsx b/Timeline/ClientApp/src/app/views/common/alert/AlertHost.tsx deleted file mode 100644 index c74f18e2..00000000 --- a/Timeline/ClientApp/src/app/views/common/alert/AlertHost.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import React, { useCallback } from "react"; -import without from "lodash/without"; -import concat from "lodash/concat"; -import { useTranslation } from "react-i18next"; -import { Alert } from "react-bootstrap"; - -import { - alertService, - AlertInfoEx, - kAlertHostId, - AlertInfo, -} from "@/services/alert"; - -interface AutoCloseAlertProps { - alert: AlertInfo; - close: () => void; -} - -export const AutoCloseAlert: React.FC = (props) => { - const { alert } = props; - const { dismissTime } = alert; - - const { t } = useTranslation(); - - React.useEffect(() => { - const tag = - dismissTime === "never" - ? null - : typeof dismissTime === "number" - ? window.setTimeout(props.close, dismissTime) - : window.setTimeout(props.close, 5000); - return () => { - if (tag != null) { - window.clearTimeout(tag); - } - }; - }, [dismissTime, props.close]); - - return ( - - {(() => { - const { message } = alert; - if (typeof message === "function") { - const Message = message; - return ; - } else if (typeof message === "object" && message.type === "i18n") { - return t(message.key); - } else return alert.message; - })()} - - ); -}; - -// oh what a bad name! -interface AlertInfoExEx extends AlertInfoEx { - close: () => void; -} - -const AlertHost: React.FC = () => { - const [alerts, setAlerts] = React.useState([]); - - // react guarantee that state setters are stable, so we don't need to add it to dependency list - - const consume = useCallback((alert: AlertInfoEx): void => { - const alertEx: AlertInfoExEx = { - ...alert, - close: () => { - setAlerts((oldAlerts) => { - return without(oldAlerts, alertEx); - }); - }, - }; - setAlerts((oldAlerts) => { - return concat(oldAlerts, alertEx); - }); - }, []); - - React.useEffect(() => { - alertService.registerConsumer(consume); - return () => { - alertService.unregisterConsumer(consume); - }; - }, [consume]); - - return ( -
- {alerts.map((alert) => { - return ( - - ); - })} -
- ); -}; - -export default AlertHost; diff --git a/Timeline/ClientApp/src/app/views/common/alert/alert.sass b/Timeline/ClientApp/src/app/views/common/alert/alert.sass deleted file mode 100644 index 5b6e65c2..00000000 --- a/Timeline/ClientApp/src/app/views/common/alert/alert.sass +++ /dev/null @@ -1,15 +0,0 @@ -.alert-container - position: fixed - z-index: $zindex-popover - -@include media-breakpoint-up(sm) - .alert-container - bottom: 0 - right: 0 - -@include media-breakpoint-down(sm) - .alert-container - bottom: 0 - right: 0 - left: 0 - text-align: center diff --git a/Timeline/ClientApp/src/app/views/common/common.sass b/Timeline/ClientApp/src/app/views/common/common.sass deleted file mode 100644 index 15d34d7c..00000000 --- a/Timeline/ClientApp/src/app/views/common/common.sass +++ /dev/null @@ -1,33 +0,0 @@ -.image-cropper-container - position: relative - box-sizing: border-box - user-select: none - -.image-cropper-container img - position: absolute - left: 0 - top: 0 - width: 100% - height: 100% - -.image-cropper-mask-container - position: absolute - left: 0 - top: 0 - right: 0 - bottom: 0 - overflow: hidden - -.image-cropper-mask - position: absolute - box-shadow: 0 0 0 10000px rgba(255, 255, 255, 80%) - touch-action: none - -.image-cropper-handler - position: absolute - width: 26px - height: 26px - border: black solid 2px - border-radius: 50% - background: white - touch-action: none diff --git a/Timeline/ClientApp/src/app/views/home/BoardWithUser.tsx b/Timeline/ClientApp/src/app/views/home/BoardWithUser.tsx deleted file mode 100644 index dcd39cbe..00000000 --- a/Timeline/ClientApp/src/app/views/home/BoardWithUser.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import React from "react"; -import { Row, Col } from "react-bootstrap"; -import { useTranslation } from "react-i18next"; - -import { UserWithToken } from "@/services/user"; -import { TimelineInfo } from "@/services/timeline"; -import { getHttpTimelineClient } from "@/http/timeline"; - -import TimelineBoard from "./TimelineBoard"; -import OfflineBoard from "./OfflineBoard"; - -const BoardWithUser: React.FC<{ user: UserWithToken }> = ({ user }) => { - const { t } = useTranslation(); - - const [ownTimelines, setOwnTimelines] = React.useState< - TimelineInfo[] | "offline" | "loading" - >("loading"); - const [joinTimelines, setJoinTimelines] = React.useState< - TimelineInfo[] | "offline" | "loading" - >("loading"); - - React.useEffect(() => { - let subscribe = true; - if (ownTimelines === "loading") { - void getHttpTimelineClient() - .listTimeline({ relate: user.username, relateType: "own" }) - .then( - (timelines) => { - if (subscribe) { - setOwnTimelines(timelines); - } - }, - () => { - setOwnTimelines("offline"); - } - ); - } - return () => { - subscribe = false; - }; - }, [user, ownTimelines]); - - React.useEffect(() => { - let subscribe = true; - if (joinTimelines === "loading") { - void getHttpTimelineClient() - .listTimeline({ relate: user.username, relateType: "join" }) - .then( - (timelines) => { - if (subscribe) { - setJoinTimelines(timelines); - } - }, - () => { - setJoinTimelines("offline"); - } - ); - } - return () => { - subscribe = false; - }; - }, [user, joinTimelines]); - - return ( - - {ownTimelines === "offline" && joinTimelines === "offline" ? ( - - { - setOwnTimelines("loading"); - setJoinTimelines("loading"); - }} - /> - - ) : ( - <> - - { - setOwnTimelines("loading"); - }} - /> - - - { - setJoinTimelines("loading"); - }} - /> - - - )} - - ); -}; - -export default BoardWithUser; diff --git a/Timeline/ClientApp/src/app/views/home/BoardWithoutUser.tsx b/Timeline/ClientApp/src/app/views/home/BoardWithoutUser.tsx deleted file mode 100644 index ebfddb50..00000000 --- a/Timeline/ClientApp/src/app/views/home/BoardWithoutUser.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import React from "react"; -import { Row, Col } from "react-bootstrap"; - -import { TimelineInfo } from "@/services/timeline"; -import { getHttpTimelineClient } from "@/http/timeline"; - -import TimelineBoard from "./TimelineBoard"; -import OfflineBoard from "./OfflineBoard"; - -const BoardWithoutUser: React.FC = () => { - const [publicTimelines, setPublicTimelines] = React.useState< - TimelineInfo[] | "offline" | "loading" - >("loading"); - - React.useEffect(() => { - let subscribe = true; - if (publicTimelines === "loading") { - void getHttpTimelineClient() - .listTimeline({ visibility: "Public" }) - .then( - (timelines) => { - if (subscribe) { - setPublicTimelines(timelines); - } - }, - () => { - setPublicTimelines("offline"); - } - ); - } - return () => { - subscribe = false; - }; - }, [publicTimelines]); - - return ( - - {publicTimelines === "offline" ? ( - - { - setPublicTimelines("loading"); - }} - /> - - ) : ( - - { - setPublicTimelines("loading"); - }} - /> - - )} - - ); -}; - -export default BoardWithoutUser; diff --git a/Timeline/ClientApp/src/app/views/home/OfflineBoard.tsx b/Timeline/ClientApp/src/app/views/home/OfflineBoard.tsx deleted file mode 100644 index fc05bd74..00000000 --- a/Timeline/ClientApp/src/app/views/home/OfflineBoard.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import React from "react"; -import { Link } from "react-router-dom"; -import { Trans } from "react-i18next"; - -import { getAllCachedTimelineNames } from "@/services/timeline"; -import UserTimelineLogo from "../common/UserTimelineLogo"; -import TimelineLogo from "../common/TimelineLogo"; - -export interface OfflineBoardProps { - onReload: () => void; -} - -const OfflineBoard: React.FC = ({ onReload }) => { - const [timelines, setTimelines] = React.useState([]); - - React.useEffect(() => { - let subscribe = true; - void getAllCachedTimelineNames().then((t) => { - if (subscribe) setTimelines(t); - }); - return () => { - subscribe = false; - }; - }); - - return ( - <> - - 0 - { - onReload(); - e.preventDefault(); - }} - > - 1 - - 2 - - {timelines.map((timeline) => { - const isPersonal = timeline.startsWith("@"); - const url = isPersonal - ? `/users/${timeline.slice(1)}` - : `/timelines/${timeline}`; - return ( -
- {isPersonal ? ( - - ) : ( - - )} - {timeline} -
- ); - })} - - ); -}; - -export default OfflineBoard; diff --git a/Timeline/ClientApp/src/app/views/home/TimelineBoard.tsx b/Timeline/ClientApp/src/app/views/home/TimelineBoard.tsx deleted file mode 100644 index a3d176e1..00000000 --- a/Timeline/ClientApp/src/app/views/home/TimelineBoard.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React from "react"; -import clsx from "clsx"; -import { Link } from "react-router-dom"; -import { Trans } from "react-i18next"; -import { Spinner } from "react-bootstrap"; - -import { TimelineInfo } from "@/services/timeline"; -import TimelineLogo from "../common/TimelineLogo"; -import UserTimelineLogo from "../common/UserTimelineLogo"; - -export interface TimelineBoardProps { - title?: string; - timelines: TimelineInfo[] | "offline" | "loading"; - onReload: () => void; - className?: string; -} - -const TimelineBoard: React.FC = (props) => { - const { title, timelines, className } = props; - - return ( -
- {title != null &&

{title}

} - {(() => { - if (timelines === "loading") { - return ( -
- -
- ); - } else if (timelines === "offline") { - return ( - - ); - } else { - return timelines.map((timeline) => { - const { name } = timeline; - const isPersonal = name.startsWith("@"); - const url = isPersonal - ? `/users/${timeline.owner.username}` - : `/timelines/${name}`; - return ( -
- {isPersonal ? ( - - ) : ( - - )} - {name} -
- ); - }); - } - })()} -
- ); -}; - -export default TimelineBoard; diff --git a/Timeline/ClientApp/src/app/views/home/TimelineCreateDialog.tsx b/Timeline/ClientApp/src/app/views/home/TimelineCreateDialog.tsx deleted file mode 100644 index d9467719..00000000 --- a/Timeline/ClientApp/src/app/views/home/TimelineCreateDialog.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from "react"; -import { useHistory } from "react-router"; - -import { validateTimelineName, timelineService } from "@/services/timeline"; -import OperationDialog from "../common/OperationDialog"; - -interface TimelineCreateDialogProps { - open: boolean; - close: () => void; -} - -const TimelineCreateDialog: React.FC = (props) => { - const history = useHistory(); - - let nameSaved: string; - - return ( - { - if (name.length === 0) { - return "home.createDialog.noEmpty"; - } else if (name.length > 26) { - return "home.createDialog.tooLong"; - } else if (!validateTimelineName(name)) { - return "home.createDialog.badFormat"; - } else { - return null; - } - }, - }, - ]} - onProcess={([name]) => { - nameSaved = name as string; - return timelineService.createTimeline(nameSaved).toPromise(); - }} - onSuccessAndClose={() => { - history.push(`timelines/${nameSaved}`); - }} - failurePrompt={(e) => `${e as string}`} - /> - ); -}; - -export default TimelineCreateDialog; diff --git a/Timeline/ClientApp/src/app/views/home/home.sass b/Timeline/ClientApp/src/app/views/home/home.sass deleted file mode 100644 index f5d6ffc3..00000000 --- a/Timeline/ClientApp/src/app/views/home/home.sass +++ /dev/null @@ -1,13 +0,0 @@ -.timeline-board-item - font-size: 1.1em - @extend .my-2 - .icon - height: 1.3em - @extend .mr-2 - -.timeline-board - @extend .cru-card - @extend .d-flex - @extend .flex-column - @extend .p-3 - min-height: 200px diff --git a/Timeline/ClientApp/src/app/views/home/index.tsx b/Timeline/ClientApp/src/app/views/home/index.tsx deleted file mode 100644 index 760adcea..00000000 --- a/Timeline/ClientApp/src/app/views/home/index.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import React from "react"; -import { useHistory } from "react-router"; -import { useTranslation } from "react-i18next"; -import { Row, Container, Button, Col } from "react-bootstrap"; - -import { useUser } from "@/services/user"; -import SearchInput from "../common/SearchInput"; - -import BoardWithoutUser from "./BoardWithoutUser"; -import BoardWithUser from "./BoardWithUser"; -import TimelineCreateDialog from "./TimelineCreateDialog"; - -const HomePage: React.FC = () => { - const history = useHistory(); - - const { t } = useTranslation(); - - const user = useUser(); - - const [navText, setNavText] = React.useState(""); - - const [dialog, setDialog] = React.useState<"create" | null>(null); - - const goto = React.useCallback((): void => { - if (navText === "") { - history.push("users/crupest"); - } else if (navText.startsWith("@")) { - history.push(`users/${navText.slice(1)}`); - } else { - history.push(`timelines/${navText}`); - } - }, [navText, history]); - - return ( - <> - - - - { - setDialog("create"); - }} - > - {t("home.createButton")} - - ) - } - /> - - - {(() => { - if (user == null) { - return ; - } else { - return ; - } - })()} - - - {dialog === "create" && ( - { - setDialog(null); - }} - /> - )} - - ); -}; - -export default HomePage; diff --git a/Timeline/ClientApp/src/app/views/login/index.tsx b/Timeline/ClientApp/src/app/views/login/index.tsx deleted file mode 100644 index 61b9a525..00000000 --- a/Timeline/ClientApp/src/app/views/login/index.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import React from "react"; -import { useHistory } from "react-router"; -import { useTranslation } from "react-i18next"; -import { Container, Form } from "react-bootstrap"; - -import { useUser, userService } from "@/services/user"; - -import AppBar from "../common/AppBar"; -import LoadingButton from "../common/LoadingButton"; - -const LoginPage: React.FC = (_) => { - const { t } = useTranslation(); - const history = useHistory(); - const [username, setUsername] = React.useState(""); - const [usernameDirty, setUsernameDirty] = React.useState(false); - const [password, setPassword] = React.useState(""); - const [passwordDirty, setPasswordDirty] = React.useState(false); - const [rememberMe, setRememberMe] = React.useState(true); - const [process, setProcess] = React.useState(false); - const [error, setError] = React.useState(null); - - const user = useUser(); - - React.useEffect(() => { - if (user != null) { - const id = setTimeout(() => history.push("/"), 3000); - return () => { - clearTimeout(id); - }; - } - }, [history, user]); - - if (user != null) { - return ( - <> - -

{t("login.alreadyLogin")}

- - ); - } - - const submit = (): void => { - if (username === "" || password === "") { - setUsernameDirty(true); - setPasswordDirty(true); - return; - } - - setProcess(true); - userService - .login( - { - username: username, - password: password, - }, - rememberMe - ) - .then( - () => { - if (history.length === 0) { - history.push("/"); - } else { - history.goBack(); - } - }, - (e: Error) => { - setProcess(false); - setError(e.message); - } - ); - }; - - const onEnterPressInPassword: React.KeyboardEventHandler = (e) => { - if (e.key === "Enter") { - submit(); - } - }; - - return ( - -

{t("welcome")}

-
- - {t("user.username")} - { - setUsername(e.target.value); - setUsernameDirty(true); - }} - value={username} - isInvalid={usernameDirty && username === ""} - /> - {usernameDirty && username === "" && ( - - {t("login.emptyUsername")} - - )} - - - {t("user.password")} - { - setPassword(e.target.value); - setPasswordDirty(true); - }} - value={password} - onKeyDown={onEnterPressInPassword} - isInvalid={passwordDirty && password === ""} - /> - {passwordDirty && password === "" && ( - - {t("login.emptyPassword")} - - )} - - - - id="remember-me" - type="checkbox" - checked={rememberMe} - onChange={(e) => { - setRememberMe(e.currentTarget.checked); - }} - label={t("user.rememberMe")} - /> - - {error ?

{t(error)}

: null} -
- { - submit(); - e.preventDefault(); - }} - disabled={username === "" || password === "" ? true : undefined} - > - {t("user.login")} - -
-
-
- ); -}; - -export default LoginPage; diff --git a/Timeline/ClientApp/src/app/views/login/login.sass b/Timeline/ClientApp/src/app/views/login/login.sass deleted file mode 100644 index 0bf385f5..00000000 --- a/Timeline/ClientApp/src/app/views/login/login.sass +++ /dev/null @@ -1,2 +0,0 @@ -.login-container - max-width: 600px diff --git a/Timeline/ClientApp/src/app/views/settings/index.tsx b/Timeline/ClientApp/src/app/views/settings/index.tsx deleted file mode 100644 index 964e7442..00000000 --- a/Timeline/ClientApp/src/app/views/settings/index.tsx +++ /dev/null @@ -1,209 +0,0 @@ -import React, { useState } from "react"; -import { useHistory } from "react-router"; -import { useTranslation } from "react-i18next"; -import { Form, Container, Row, Col, Button, Modal } from "react-bootstrap"; - -import { useUser, userService } from "@/services/user"; -import OperationDialog, { - OperationInputErrorInfo, -} from "../common/OperationDialog"; - -interface ChangePasswordDialogProps { - open: boolean; - close: () => void; -} - -const ChangePasswordDialog: React.FC = (props) => { - const history = useHistory(); - const { t } = useTranslation(); - - const [redirect, setRedirect] = useState(false); - - return ( - - v === "" - ? "settings.dialogChangePassword.errorEmptyOldPassword" - : null, - }, - { - type: "text", - label: t("settings.dialogChangePassword.inputNewPassword"), - password: true, - validator: (v, values) => { - const error: OperationInputErrorInfo = {}; - error[1] = - v === "" - ? "settings.dialogChangePassword.errorEmptyNewPassword" - : null; - if (v === values[2]) { - error[2] = null; - } else { - if (values[2] !== "") { - error[2] = "settings.dialogChangePassword.errorRetypeNotMatch"; - } - } - return error; - }, - }, - { - type: "text", - label: t("settings.dialogChangePassword.inputRetypeNewPassword"), - password: true, - validator: (v, values) => - v !== values[1] - ? "settings.dialogChangePassword.errorRetypeNotMatch" - : null, - }, - ]} - onProcess={async ([oldPassword, newPassword]) => { - await userService - .changePassword(oldPassword as string, newPassword as string) - .toPromise(); - await userService.logout(); - setRedirect(true); - }} - close={() => { - props.close(); - if (redirect) { - history.push("/login"); - } - }} - /> - ); -}; - -const ConfirmLogoutDialog: React.FC<{ - toggle: () => void; - onConfirm: () => void; -}> = ({ toggle, onConfirm }) => { - const { t } = useTranslation(); - - return ( - - - - {t("settings.dialogConfirmLogout.title")} - - - {t("settings.dialogConfirmLogout.prompt")} - - - - - - ); -}; - -const SettingsPage: React.FC = (_) => { - const { i18n, t } = useTranslation(); - const user = useUser(); - const history = useHistory(); - - const [dialog, setDialog] = useState( - null - ); - - const language = i18n.language.slice(0, 2); - - return ( - - {user ? ( - <> - - -
{ - history.push(`/users/${user.username}`); - }} - > - {t("settings.gotoSelf")} -
- -
- - -
setDialog("changepassword")} - > - {t("settings.changePassword")} -
- -
- - -
{ - setDialog("logout"); - }} - > - {t("settings.logout")} -
- -
- - ) : null} - - -
{t("settings.languagePrimary")}
-

{t("settings.languageSecondary")}

- - - { - void i18n.changeLanguage(e.target.value); - }} - > - - - - -
- {(() => { - switch (dialog) { - case "changepassword": - return ( - { - setDialog(null); - }} - /> - ); - case "logout": - return ( - setDialog(null)} - onConfirm={() => { - void userService.logout().then(() => { - history.push("/"); - }); - }} - /> - ); - default: - return null; - } - })()} -
- ); -}; - -export default SettingsPage; diff --git a/Timeline/ClientApp/src/app/views/timeline-common/CollapseButton.tsx b/Timeline/ClientApp/src/app/views/timeline-common/CollapseButton.tsx deleted file mode 100644 index 3c52150f..00000000 --- a/Timeline/ClientApp/src/app/views/timeline-common/CollapseButton.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from "react"; -import clsx from "clsx"; -import Svg from "react-inlinesvg"; -import arrowsAngleContractIcon from "bootstrap-icons/icons/arrows-angle-contract.svg"; -import arrowsAngleExpandIcon from "bootstrap-icons/icons/arrows-angle-expand.svg"; - -const CollapseButton: React.FC<{ - collapse: boolean; - onClick: () => void; - className?: string; - style?: React.CSSProperties; -}> = ({ collapse, onClick, className, style }) => { - return ( - - ); -}; - -export default CollapseButton; diff --git a/Timeline/ClientApp/src/app/views/timeline-common/InfoCardTemplate.tsx b/Timeline/ClientApp/src/app/views/timeline-common/InfoCardTemplate.tsx deleted file mode 100644 index a8de20aa..00000000 --- a/Timeline/ClientApp/src/app/views/timeline-common/InfoCardTemplate.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from "react"; -import clsx from "clsx"; - -import { TimelineCardComponentProps } from "../timeline-common/TimelinePageTemplateUI"; -import SyncStatusBadge from "../timeline-common/SyncStatusBadge"; -import CollapseButton from "../timeline-common/CollapseButton"; - -const InfoCardTemplate: React.FC< - Pick< - TimelineCardComponentProps<"">, - "collapse" | "toggleCollapse" | "syncStatus" | "className" - > & { children: React.ReactElement[] } -> = ({ collapse, toggleCollapse, syncStatus, className, children }) => { - return ( -
-
- - -
- -
{children}
-
- ); -}; - -export default InfoCardTemplate; diff --git a/Timeline/ClientApp/src/app/views/timeline-common/SyncStatusBadge.tsx b/Timeline/ClientApp/src/app/views/timeline-common/SyncStatusBadge.tsx deleted file mode 100644 index e67cfb43..00000000 --- a/Timeline/ClientApp/src/app/views/timeline-common/SyncStatusBadge.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React from "react"; -import clsx from "clsx"; -import { useTranslation } from "react-i18next"; - -import { UiLogicError } from "@/common"; - -export type TimelineSyncStatus = "syncing" | "synced" | "offline"; - -const SyncStatusBadge: React.FC<{ - status: TimelineSyncStatus; - style?: React.CSSProperties; - className?: string; -}> = ({ status, style, className }) => { - const { t } = useTranslation(); - - return ( -
- {(() => { - switch (status) { - case "syncing": { - return ( - <> - - - {t("timeline.postSyncState.syncing")} - - - ); - } - case "synced": { - return ( - <> - - - {t("timeline.postSyncState.synced")} - - - ); - } - case "offline": { - return ( - <> - - - {t("timeline.postSyncState.offline")} - - - ); - } - default: - throw new UiLogicError("Unknown sync state."); - } - })()} -
- ); -}; - -export default SyncStatusBadge; diff --git a/Timeline/ClientApp/src/app/views/timeline-common/Timeline.tsx b/Timeline/ClientApp/src/app/views/timeline-common/Timeline.tsx deleted file mode 100644 index fd051d45..00000000 --- a/Timeline/ClientApp/src/app/views/timeline-common/Timeline.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import React from "react"; -import clsx from "clsx"; - -import { TimelinePostInfo } from "@/services/timeline"; - -import TimelineItem from "./TimelineItem"; - -export interface TimelinePostInfoEx extends TimelinePostInfo { - deletable: boolean; -} - -export type TimelineDeleteCallback = (index: number, id: number) => void; - -export interface TimelineProps { - className?: string; - posts: TimelinePostInfoEx[]; - onDelete: TimelineDeleteCallback; - onResize?: () => void; - containerRef?: React.Ref; -} - -const Timeline: React.FC = (props) => { - const { posts, onDelete, onResize } = props; - - const [indexShowDeleteButton, setIndexShowDeleteButton] = React.useState< - number - >(-1); - - const onItemClick = React.useCallback(() => { - setIndexShowDeleteButton(-1); - }, []); - - const onToggleDelete = React.useMemo(() => { - return posts.map((post, i) => { - return post.deletable - ? () => { - setIndexShowDeleteButton((oldIndexShowDeleteButton) => { - return oldIndexShowDeleteButton !== i ? i : -1; - }); - } - : undefined; - }); - }, [posts]); - - const onItemDelete = React.useMemo(() => { - return posts.map((post, i) => { - return () => { - onDelete(i, post.id); - }; - }); - }, [posts, onDelete]); - - return ( -
- {(() => { - const length = posts.length; - return posts.map((post, i) => { - const toggleMore = onToggleDelete[i]; - - return ( - - ); - }); - })()} -
- ); -}; - -export default Timeline; diff --git a/Timeline/ClientApp/src/app/views/timeline-common/TimelineItem.tsx b/Timeline/ClientApp/src/app/views/timeline-common/TimelineItem.tsx deleted file mode 100644 index 4db23371..00000000 --- a/Timeline/ClientApp/src/app/views/timeline-common/TimelineItem.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import React from "react"; -import clsx from "clsx"; -import { Link } from "react-router-dom"; -import { useTranslation } from "react-i18next"; -import Svg from "react-inlinesvg"; -import chevronDownIcon from "bootstrap-icons/icons/chevron-down.svg"; -import trashIcon from "bootstrap-icons/icons/trash.svg"; -import { Modal, Button } from "react-bootstrap"; - -import { useAvatar } from "@/services/user"; -import { TimelinePostInfo } from "@/services/timeline"; - -import BlobImage from "../common/BlobImage"; - -const TimelinePostDeleteConfirmDialog: React.FC<{ - toggle: () => void; - onConfirm: () => void; -}> = ({ toggle, onConfirm }) => { - const { t } = useTranslation(); - - return ( - - - - {t("timeline.post.deleteDialog.title")} - - - {t("timeline.post.deleteDialog.prompt")} - - - - - - ); -}; - -export interface TimelineItemProps { - post: TimelinePostInfo; - current?: boolean; - more?: { - isOpen: boolean; - toggle: () => void; - onDelete: () => void; - }; - onClick?: () => void; - onResize?: () => void; - className?: string; - style?: React.CSSProperties; -} - -const TimelineItem: React.FC = (props) => { - const { i18n } = useTranslation(); - - const current = props.current === true; - - const { more, onResize } = props; - - const avatar = useAvatar(props.post.author.username); - - const [deleteDialog, setDeleteDialog] = React.useState(false); - const toggleDeleteDialog = React.useCallback( - () => setDeleteDialog((old) => !old), - [] - ); - - return ( -
-
-
-
-
-
-
-
- {current &&
} -
-
-
-
- - - {props.post.time.toLocaleString(i18n.languages)} - - {props.post.author.nickname} - - {more != null ? ( - { - more.toggle(); - e.stopPropagation(); - }} - /> - ) : null} -
-
- - - - {(() => { - const { content } = props.post; - if (content.type === "text") { - return content.text; - } else { - return ( - - ); - } - })()} -
-
- {more != null && more.isOpen ? ( - <> -
- { - toggleDeleteDialog(); - e.stopPropagation(); - }} - /> -
- {deleteDialog ? ( - { - toggleDeleteDialog(); - more.toggle(); - }} - onConfirm={more.onDelete} - /> - ) : null} - - ) : null} -
- ); -}; - -export default TimelineItem; diff --git a/Timeline/ClientApp/src/app/views/timeline-common/TimelineMember.tsx b/Timeline/ClientApp/src/app/views/timeline-common/TimelineMember.tsx deleted file mode 100644 index 67a8543a..00000000 --- a/Timeline/ClientApp/src/app/views/timeline-common/TimelineMember.tsx +++ /dev/null @@ -1,211 +0,0 @@ -import React, { useState } from "react"; -import { useTranslation } from "react-i18next"; -import { Container, ListGroup, Modal, Row, Col, Button } from "react-bootstrap"; - -import { User, useAvatar } from "@/services/user"; - -import SearchInput from "../common/SearchInput"; -import BlobImage from "../common/BlobImage"; - -const TimelineMemberItem: React.FC<{ - user: User; - owner: boolean; - onRemove?: (username: string) => void; -}> = ({ user, owner, onRemove }) => { - const { t } = useTranslation(); - - const avatar = useAvatar(user.username); - - return ( - - - - - - - {user.nickname} - - {"@" + user.username} - - - {(() => { - if (owner) { - return null; - } - if (onRemove == null) { - return null; - } - return ( - - ); - })()} - - - ); -}; - -export interface TimelineMemberCallbacks { - onCheckUser: (username: string) => Promise; - onAddUser: (user: User) => Promise; - onRemoveUser: (username: string) => void; -} - -export interface TimelineMemberProps { - members: User[]; - edit: TimelineMemberCallbacks | null | undefined; -} - -const TimelineMember: React.FC = (props) => { - const { t } = useTranslation(); - - const [userSearchText, setUserSearchText] = useState(""); - const [userSearchState, setUserSearchState] = useState< - | { - type: "user"; - data: User; - } - | { type: "error"; data: string } - | { type: "loading" } - | { type: "init" } - >({ type: "init" }); - - const userSearchAvatar = useAvatar( - userSearchState.type === "user" ? userSearchState.data.username : undefined - ); - - const members = props.members; - - return ( - - - {members.map((member, index) => ( - - ))} - - {(() => { - const edit = props.edit; - if (edit != null) { - return ( - <> - { - setUserSearchText(v); - }} - loading={userSearchState.type === "loading"} - onButtonClick={() => { - if (userSearchText === "") { - setUserSearchState({ - type: "error", - data: "login.emptyUsername", - }); - return; - } - - setUserSearchState({ type: "loading" }); - edit.onCheckUser(userSearchText).then( - (u) => { - if (u == null) { - setUserSearchState({ - type: "error", - data: "timeline.userNotExist", - }); - } else { - setUserSearchState({ type: "user", data: u }); - } - }, - (e) => { - setUserSearchState({ - type: "error", - data: `${e as string}`, - }); - } - ); - }} - /> - {(() => { - if (userSearchState.type === "user") { - const u = userSearchState.data; - const addable = - members.findIndex((m) => m.username === u.username) === -1; - return ( - <> - {!addable ? ( -

{t("timeline.member.alreadyMember")}

- ) : null} - - - - - - - {u.nickname} - - {"@" + u.username} - - - - - - - ); - } else if (userSearchState.type === "error") { - return ( -

{t(userSearchState.data)}

- ); - } - })()} - - ); - } else { - return null; - } - })()} -
- ); -}; - -export default TimelineMember; - -export interface TimelineMemberDialogProps extends TimelineMemberProps { - open: boolean; - onClose: () => void; -} - -export const TimelineMemberDialog: React.FC = ( - props -) => { - return ( - - - - ); -}; diff --git a/Timeline/ClientApp/src/app/views/timeline-common/TimelinePageTemplate.tsx b/Timeline/ClientApp/src/app/views/timeline-common/TimelinePageTemplate.tsx deleted file mode 100644 index d5c91622..00000000 --- a/Timeline/ClientApp/src/app/views/timeline-common/TimelinePageTemplate.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import React from "react"; -import { useTranslation } from "react-i18next"; -import { of } from "rxjs"; -import { catchError } from "rxjs/operators"; - -import { UiLogicError } from "@/common"; -import { pushAlert } from "@/services/alert"; -import { useUser, userInfoService, UserNotExistError } from "@/services/user"; -import { - timelineService, - usePostList, - useTimelineInfo, -} from "@/services/timeline"; - -import { TimelineDeleteCallback } from "./Timeline"; -import { TimelineMemberDialog } from "./TimelineMember"; -import TimelinePropertyChangeDialog from "./TimelinePropertyChangeDialog"; -import { TimelinePageTemplateUIProps } from "./TimelinePageTemplateUI"; -import { TimelinePostSendCallback } from "./TimelinePostEdit"; - -export interface TimelinePageTemplateProps { - name: string; - onManage: (item: TManageItem) => void; - UiComponent: React.ComponentType< - Omit, "CardComponent"> - >; - notFoundI18nKey: string; -} - -export default function TimelinePageTemplate( - props: TimelinePageTemplateProps -): React.ReactElement | null { - const { t } = useTranslation(); - - const { name } = props; - - const service = timelineService; - - const user = useUser(); - - const [dialog, setDialog] = React.useState( - null - ); - - const timelineState = useTimelineInfo(name); - - const timeline = timelineState?.timeline; - - const postListState = usePostList(name); - - const error: string | undefined = (() => { - if (timelineState != null) { - const { type, timeline } = timelineState; - if (type === "offline" && timeline == null) return "Network Error"; - if (type === "synced" && timeline == null) - return t(props.notFoundI18nKey); - } - return undefined; - })(); - - const closeDialog = React.useCallback((): void => { - setDialog(null); - }, []); - - let dialogElement: React.ReactElement | undefined; - - if (dialog === "property") { - if (timeline == null) { - throw new UiLogicError( - "Timeline is null but attempt to open change property dialog." - ); - } - - dialogElement = ( - { - return service.changeTimelineProperty(name, req).toPromise().then(); - }} - /> - ); - } else if (dialog === "member") { - if (timeline == null) { - throw new UiLogicError( - "Timeline is null but attempt to open change property dialog." - ); - } - - dialogElement = ( - { - return userInfoService - .getUserInfo(u) - .pipe( - catchError((e) => { - if (e instanceof UserNotExistError) { - return of(null); - } else { - throw e; - } - }) - ) - .toPromise(); - }, - onAddUser: (u) => { - return service.addMember(name, u.username).toPromise().then(); - }, - onRemoveUser: (u) => { - service.removeMember(name, u); - }, - } - : null - } - /> - ); - } - - const { UiComponent } = props; - - const onDelete: TimelineDeleteCallback = React.useCallback( - (index, id) => { - service.deletePost(name, id).subscribe(null, () => { - pushAlert({ - type: "danger", - message: t("timeline.deletePostFailed"), - }); - }); - }, - [service, name, t] - ); - - const onPost: TimelinePostSendCallback = React.useCallback( - (req) => { - return service.createPost(name, req).toPromise().then(); - }, - [service, name] - ); - - const onManageProp = props.onManage; - - const onManage = React.useCallback( - (item: "property" | TManageItem) => { - if (item === "property") { - setDialog(item); - } else { - onManageProp(item); - } - }, - [onManageProp] - ); - - return ( - <> - setDialog("member")} - /> - {dialogElement} - - ); -} diff --git a/Timeline/ClientApp/src/app/views/timeline-common/TimelinePageTemplateUI.tsx b/Timeline/ClientApp/src/app/views/timeline-common/TimelinePageTemplateUI.tsx deleted file mode 100644 index 6c2c43c1..00000000 --- a/Timeline/ClientApp/src/app/views/timeline-common/TimelinePageTemplateUI.tsx +++ /dev/null @@ -1,243 +0,0 @@ -import React from "react"; -import { useTranslation } from "react-i18next"; -import { fromEvent } from "rxjs"; -import { Spinner } from "react-bootstrap"; - -import { getAlertHost } from "@/services/alert"; -import { useEventEmiiter, UiLogicError } from "@/common"; -import { - TimelineInfo, - TimelinePostsWithSyncState, - timelineService, -} from "@/services/timeline"; -import { userService } from "@/services/user"; - -import Timeline, { - TimelinePostInfoEx, - TimelineDeleteCallback, -} from "./Timeline"; -import TimelineTop from "./TimelineTop"; -import TimelinePostEdit, { TimelinePostSendCallback } from "./TimelinePostEdit"; -import { TimelineSyncStatus } from "./SyncStatusBadge"; - -export interface TimelineCardComponentProps { - timeline: TimelineInfo; - onManage?: (item: TManageItems | "property") => void; - onMember: () => void; - className?: string; - collapse: boolean; - syncStatus: TimelineSyncStatus; - toggleCollapse: () => void; -} - -export interface TimelinePageTemplateUIProps { - timeline?: TimelineInfo; - postListState?: TimelinePostsWithSyncState; - CardComponent: React.ComponentType>; - onMember: () => void; - onManage?: (item: TManageItems | "property") => void; - onPost?: TimelinePostSendCallback; - onDelete: TimelineDeleteCallback; - error?: string; -} - -export default function TimelinePageTemplateUI( - props: TimelinePageTemplateUIProps -): React.ReactElement | null { - const { timeline, postListState } = props; - - const { t } = useTranslation(); - - const bottomSpaceRef = React.useRef(null); - - const onPostEditHeightChange = React.useCallback((height: number): void => { - const { current: bottomSpaceDiv } = bottomSpaceRef; - if (bottomSpaceDiv != null) { - bottomSpaceDiv.style.height = `${height}px`; - } - if (height === 0) { - const alertHost = getAlertHost(); - if (alertHost != null) { - alertHost.style.removeProperty("margin-bottom"); - } - } else { - const alertHost = getAlertHost(); - if (alertHost != null) { - alertHost.style.marginBottom = `${height}px`; - } - } - }, []); - - const timelineRef = React.useRef(null); - - const [getResizeEvent, triggerResizeEvent] = useEventEmiiter(); - - React.useEffect(() => { - const { current: timelineElement } = timelineRef; - if (timelineElement != null) { - let loadingScrollToBottom = true; - let pinBottom = false; - - const isAtBottom = (): boolean => - window.innerHeight + window.scrollY + 10 >= document.body.scrollHeight; - - const disableLoadingScrollToBottom = (): void => { - loadingScrollToBottom = false; - if (isAtBottom()) pinBottom = true; - }; - - const checkAndScrollToBottom = (): void => { - if (loadingScrollToBottom || pinBottom) { - window.scrollTo(0, document.body.scrollHeight); - } - }; - - const subscriptions = [ - fromEvent(timelineElement, "wheel").subscribe( - disableLoadingScrollToBottom - ), - fromEvent(timelineElement, "pointerdown").subscribe( - disableLoadingScrollToBottom - ), - fromEvent(timelineElement, "keydown").subscribe( - disableLoadingScrollToBottom - ), - fromEvent(window, "scroll").subscribe(() => { - if (loadingScrollToBottom) return; - - if (isAtBottom()) { - pinBottom = true; - } else { - pinBottom = false; - } - }), - fromEvent(window, "resize").subscribe(checkAndScrollToBottom), - getResizeEvent().subscribe(checkAndScrollToBottom), - ]; - - return () => { - subscriptions.forEach((s) => s.unsubscribe()); - }; - } - }, [getResizeEvent, triggerResizeEvent, timeline, postListState]); - - const genCardCollapseLocalStorageKey = (uniqueId: string): string => - `timeline.${uniqueId}.cardCollapse`; - - const cardCollapseLocalStorageKey = - timeline != null ? genCardCollapseLocalStorageKey(timeline.uniqueId) : null; - - const [cardCollapse, setCardCollapse] = React.useState(true); - React.useEffect(() => { - if (cardCollapseLocalStorageKey != null) { - const savedCollapse = - window.localStorage.getItem(cardCollapseLocalStorageKey) === "true"; - setCardCollapse(savedCollapse); - } - }, [cardCollapseLocalStorageKey]); - - const toggleCardCollapse = (): void => { - const newState = !cardCollapse; - setCardCollapse(newState); - if (timeline != null) { - window.localStorage.setItem( - genCardCollapseLocalStorageKey(timeline.uniqueId), - newState.toString() - ); - } - }; - - let body: React.ReactElement; - - if (props.error != null) { - body =

{t(props.error)}

; - } else { - if (timeline != null) { - let timelineBody: React.ReactElement; - if (postListState != null) { - if (postListState.type === "notexist") { - throw new UiLogicError( - "Timeline is not null but post list state is notexist." - ); - } - if (postListState.type === "forbid") { - timelineBody = ( -

{t("timeline.messageCantSee")}

- ); - } else { - const posts: TimelinePostInfoEx[] = postListState.posts.map( - (post) => ({ - ...post, - deletable: timelineService.hasModifyPostPermission( - userService.currentUser, - timeline, - post - ), - }) - ); - - timelineBody = ( - - ); - if (props.onPost != null) { - timelineBody = ( - <> - {timelineBody} -
- - - ); - } - } - } else { - timelineBody = ( -
- -
- ); - } - - const { CardComponent } = props; - const syncStatus: TimelineSyncStatus = - postListState == null || postListState.syncing - ? "syncing" - : postListState.type === "synced" - ? "synced" - : "offline"; - - body = ( - <> - - - {timelineBody} - - ); - } else { - body = ( -
- -
- ); - } - } - - return body; -} diff --git a/Timeline/ClientApp/src/app/views/timeline-common/TimelinePostEdit.tsx b/Timeline/ClientApp/src/app/views/timeline-common/TimelinePostEdit.tsx deleted file mode 100644 index dfa2f879..00000000 --- a/Timeline/ClientApp/src/app/views/timeline-common/TimelinePostEdit.tsx +++ /dev/null @@ -1,241 +0,0 @@ -import React from "react"; -import clsx from "clsx"; -import { useTranslation } from "react-i18next"; -import Svg from "react-inlinesvg"; -import { Button, Spinner, Row, Col, Form } from "react-bootstrap"; -import textIcon from "bootstrap-icons/icons/card-text.svg"; -import imageIcon from "bootstrap-icons/icons/image.svg"; - -import { UiLogicError } from "@/common"; - -import { pushAlert } from "@/services/alert"; -import { TimelineCreatePostRequest } from "@/services/timeline"; - -interface TimelinePostEditImageProps { - onSelect: (blob: Blob | null) => void; -} - -const TimelinePostEditImage: React.FC = (props) => { - const { onSelect } = props; - const { t } = useTranslation(); - - const [file, setFile] = React.useState(null); - const [fileUrl, setFileUrl] = React.useState(null); - const [error, setError] = React.useState(null); - - React.useEffect(() => { - if (file != null) { - const url = URL.createObjectURL(file); - setFileUrl(url); - return () => { - URL.revokeObjectURL(url); - }; - } - }, [file]); - - const onInputChange: React.ChangeEventHandler = React.useCallback( - (e) => { - const files = e.target.files; - if (files == null || files.length === 0) { - setFile(null); - setFileUrl(null); - } else { - setFile(files[0]); - } - onSelect(null); - setError(null); - }, - [onSelect] - ); - - const onImgLoad = React.useCallback(() => { - onSelect(file); - }, [onSelect, file]); - - const onImgError = React.useCallback(() => { - setError("loadImageError"); - }, []); - - return ( - <> - - {fileUrl && error == null && ( - - )} - {error != null &&
{t(error)}
} - - ); -}; - -export type TimelinePostSendCallback = ( - content: TimelineCreatePostRequest -) => Promise; - -export interface TimelinePostEditProps { - className?: string; - onPost: TimelinePostSendCallback; - onHeightChange?: (height: number) => void; - timelineUniqueId: string; -} - -const TimelinePostEdit: React.FC = (props) => { - const { onPost } = props; - - const { t } = useTranslation(); - - const [state, setState] = React.useState<"input" | "process">("input"); - const [kind, setKind] = React.useState<"text" | "image">("text"); - const [text, setText] = React.useState(""); - const [imageBlob, setImageBlob] = React.useState(null); - - const draftLocalStorageKey = `timeline.${props.timelineUniqueId}.postDraft`; - - React.useEffect(() => { - setText(window.localStorage.getItem(draftLocalStorageKey) ?? ""); - }, [draftLocalStorageKey]); - - const canSend = kind === "text" || (kind === "image" && imageBlob != null); - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const containerRef = React.useRef(null!); - - const notifyHeightChange = (): void => { - if (props.onHeightChange) { - props.onHeightChange(containerRef.current.clientHeight); - } - }; - - React.useEffect(() => { - if (props.onHeightChange) { - props.onHeightChange(containerRef.current.clientHeight); - } - return () => { - if (props.onHeightChange) { - props.onHeightChange(0); - } - }; - }); - - const toggleKind = React.useCallback(() => { - setKind((oldKind) => (oldKind === "text" ? "image" : "text")); - setImageBlob(null); - }, []); - - const onSend = React.useCallback(() => { - setState("process"); - - const req: TimelineCreatePostRequest = (() => { - switch (kind) { - case "text": - return { - content: { - type: "text", - text: text, - }, - } as TimelineCreatePostRequest; - case "image": - if (imageBlob == null) { - throw new UiLogicError( - "Content type is image but image blob is null." - ); - } - return { - content: { - type: "image", - data: imageBlob, - }, - } as TimelineCreatePostRequest; - default: - throw new UiLogicError("Unknown content type."); - } - })(); - - onPost(req).then( - (_) => { - if (kind === "text") { - setText(""); - window.localStorage.removeItem(draftLocalStorageKey); - } - setState("input"); - setKind("text"); - }, - (_) => { - pushAlert({ - type: "danger", - message: t("timeline.sendPostFailed"), - }); - setState("input"); - } - ); - }, [onPost, kind, text, imageBlob, t, draftLocalStorageKey]); - - const onImageSelect = React.useCallback((blob: Blob | null) => { - setImageBlob(blob); - }, []); - - return ( -
- - - {kind === "text" ? ( - ) => { - const value = event.currentTarget.value; - setText(value); - window.localStorage.setItem(draftLocalStorageKey, value); - }} - /> - ) : ( - - )} - - - {(() => { - if (state === "input") { - return ( - <> -
- -
- - - ); - } else { - return ; - } - })()} - -
-
- ); -}; - -export default TimelinePostEdit; diff --git a/Timeline/ClientApp/src/app/views/timeline-common/TimelinePropertyChangeDialog.tsx b/Timeline/ClientApp/src/app/views/timeline-common/TimelinePropertyChangeDialog.tsx deleted file mode 100644 index 87638f31..00000000 --- a/Timeline/ClientApp/src/app/views/timeline-common/TimelinePropertyChangeDialog.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React from "react"; - -import { - TimelineVisibility, - kTimelineVisibilities, - TimelineChangePropertyRequest, -} from "@/services/timeline"; - -import OperationDialog, { - OperationSelectInputInfoOption, -} from "../common/OperationDialog"; - -export interface TimelinePropertyInfo { - visibility: TimelineVisibility; - description: string; -} - -export interface TimelinePropertyChangeDialogProps { - open: boolean; - close: () => void; - oldInfo: TimelinePropertyInfo; - onProcess: (request: TimelineChangePropertyRequest) => Promise; -} - -const labelMap: { [key in TimelineVisibility]: string } = { - Private: "timeline.visibility.private", - Public: "timeline.visibility.public", - Register: "timeline.visibility.register", -}; - -const TimelinePropertyChangeDialog: React.FC = ( - props -) => { - return ( - ( - (v) => ({ - label: labelMap[v], - value: v, - }) - ), - initValue: props.oldInfo.visibility, - }, - { - type: "text", - label: "timeline.dialogChangeProperty.description", - initValue: props.oldInfo.description, - }, - ]} - open={props.open} - close={props.close} - onProcess={([newVisibility, newDescription]) => { - const req: TimelineChangePropertyRequest = {}; - if (newVisibility !== props.oldInfo.visibility) { - req.visibility = newVisibility as TimelineVisibility; - } - if (newDescription !== props.oldInfo.description) { - req.description = newDescription as string; - } - return props.onProcess(req); - }} - /> - ); -}; - -export default TimelinePropertyChangeDialog; diff --git a/Timeline/ClientApp/src/app/views/timeline-common/TimelineTop.tsx b/Timeline/ClientApp/src/app/views/timeline-common/TimelineTop.tsx deleted file mode 100644 index 93a2a32c..00000000 --- a/Timeline/ClientApp/src/app/views/timeline-common/TimelineTop.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from "react"; - -export interface TimelineTopProps { - height?: number | string; - children?: React.ReactElement; -} - -const TimelineTop: React.FC = ({ height, children }) => { - return ( -
-
-
-
-
-
- {children} -
- ); -}; - -export default TimelineTop; diff --git a/Timeline/ClientApp/src/app/views/timeline-common/timeline-common.sass b/Timeline/ClientApp/src/app/views/timeline-common/timeline-common.sass deleted file mode 100644 index 4151bfcc..00000000 --- a/Timeline/ClientApp/src/app/views/timeline-common/timeline-common.sass +++ /dev/null @@ -1,146 +0,0 @@ -@use 'sass:color' - -.timeline - z-index: 0 - position: relative - - &-item - display: flex - -$timeline-line-width: 7px -$timeline-line-node-radius: 18px -$timeline-line-color: $primary -$timeline-line-color-current: #36c2e6 - -@keyframes timeline-line-node-noncurrent - from - background: $timeline-line-color - - to - background: color.adjust($timeline-line-color, $lightness: +10%) - box-shadow: 0 0 20px 3px color.adjust($timeline-line-color, $lightness: +10%, $alpha: -0.1) - -@keyframes timeline-line-node-current - from - background: $timeline-line-color-current - - to - background: color.adjust($timeline-line-color-current, $lightness: +10%) - box-shadow: 0 0 20px 3px color.adjust($timeline-line-color-current, $lightness: +10%, $alpha: -0.1) - -.timeline-line - &-area-container - display: flex - justify-content: flex-end - padding-right: 5px - - flex: 0 0 auto - width: 60px - - &-area - display: flex - flex-direction: column - align-items: center - width: 30px - - &-segment - width: $timeline-line-width - background: $timeline-line-color - - &.start - height: 14px - flex: 0 0 auto - - &.end - flex: 1 1 auto - - &.current-end - height: 20px - flex: 0 0 auto - background: linear-gradient($timeline-line-color-current, transparent) - - &-node-container - flex: 0 0 auto - position: relative - width: $timeline-line-node-radius - height: $timeline-line-node-radius - - &-node - width: $timeline-line-node-radius + 2 - height: $timeline-line-node-radius + 2 - position: absolute - left: -1px - top: -1px - border-radius: 50% - box-sizing: border-box - z-index: 1 - animation: 1s infinite alternate - animation-name: timeline-line-node-noncurrent - -.timeline-top - display: flex - justify-content: space-between - - .timeline-line-segment - flex: 1 1 auto - -.current - .timeline-line - &-segment - - &.start - background: linear-gradient($timeline-line-color, $timeline-line-color-current) - - &.end - background: $timeline-line-color-current - - &-node - animation-name: timeline-line-node-current - -.timeline-content-area - padding: 10px 0 - flex-grow: 1 - -.timeline-item-delete-button - position: absolute - right: 0 - bottom: 0 - -.timeline-content - white-space: pre-line - -.timeline-content-image - max-width: 60% - max-height: 200px - -.timeline-post-edit-image - max-width: 100px - max-height: 100px - -.mask - background: change-color($color: white, $alpha: 0.8) - z-index: 100 - -.timeline-page-top-space - transition: height 0.5s - -.timeline-sync-state-badge - font-size: 0.8em - padding: 3px 8px - border-radius: 5px - background: #e8fbff - -.timeline-sync-state-badge-pin - display: inline-block - width: 0.4em - height: 0.4em - border-radius: 50% - vertical-align: middle - margin-right: 0.6em - -.timeline-template-card - position: fixed - z-index: 1 - top: 56px - right: 0 - margin: 0.5em diff --git a/Timeline/ClientApp/src/app/views/timeline/TimelineDeleteDialog.tsx b/Timeline/ClientApp/src/app/views/timeline/TimelineDeleteDialog.tsx deleted file mode 100644 index 894b8195..00000000 --- a/Timeline/ClientApp/src/app/views/timeline/TimelineDeleteDialog.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from "react"; -import { useHistory } from "react-router"; -import { Trans } from "react-i18next"; - -import { timelineService } from "@/services/timeline"; - -import OperationDialog from "../common/OperationDialog"; - -interface TimelineDeleteDialog { - open: boolean; - name: string; - close: () => void; -} - -const TimelineDeleteDialog: React.FC = (props) => { - const history = useHistory(); - - const { name } = props; - - return ( - { - return ( - - 0{{ name }}2 - - ); - }} - inputScheme={[ - { - type: "text", - validator: (value) => { - if (value !== name) { - return "timeline.deleteDialog.notMatch"; - } else { - return null; - } - }, - }, - ]} - onProcess={() => { - return timelineService.deleteTimeline(name).toPromise(); - }} - onSuccessAndClose={() => { - history.replace("/"); - }} - /> - ); -}; - -export default TimelineDeleteDialog; diff --git a/Timeline/ClientApp/src/app/views/timeline/TimelineInfoCard.tsx b/Timeline/ClientApp/src/app/views/timeline/TimelineInfoCard.tsx deleted file mode 100644 index 2d787709..00000000 --- a/Timeline/ClientApp/src/app/views/timeline/TimelineInfoCard.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import React from "react"; -import { useTranslation } from "react-i18next"; -import { Dropdown, Button } from "react-bootstrap"; - -import { useAvatar } from "@/services/user"; -import { timelineVisibilityTooltipTranslationMap } from "@/services/timeline"; - -import BlobImage from "../common/BlobImage"; -import { TimelineCardComponentProps } from "../timeline-common/TimelinePageTemplateUI"; -import InfoCardTemplate from "../timeline-common/InfoCardTemplate"; - -export type OrdinaryTimelineManageItem = "delete"; - -export type TimelineInfoCardProps = TimelineCardComponentProps< - OrdinaryTimelineManageItem ->; - -const TimelineInfoCard: React.FC = (props) => { - const { - timeline, - collapse, - onMember, - onManage, - syncStatus, - toggleCollapse, - } = props; - - const { t } = useTranslation(); - - const avatar = useAvatar(timeline?.owner?.username); - - return ( - -

- {timeline.name} -

-
- - {timeline.owner.nickname} - - @{timeline.owner.username} - -
-

{timeline.description}

- - {t(timelineVisibilityTooltipTranslationMap[timeline.visibility])} - -
- {onManage != null ? ( - - - {t("timeline.manage")} - - - onManage("property")}> - {t("timeline.manageItem.property")} - - - {t("timeline.manageItem.member")} - - - onManage("delete")} - > - {t("timeline.manageItem.delete")} - - - - ) : ( - - )} -
-
- ); -}; - -export default TimelineInfoCard; diff --git a/Timeline/ClientApp/src/app/views/timeline/TimelinePageUI.tsx b/Timeline/ClientApp/src/app/views/timeline/TimelinePageUI.tsx deleted file mode 100644 index 67ea699e..00000000 --- a/Timeline/ClientApp/src/app/views/timeline/TimelinePageUI.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from "react"; - -import TimelinePageTemplateUI, { - TimelinePageTemplateUIProps, -} from "../timeline-common/TimelinePageTemplateUI"; - -import TimelineInfoCard, { - OrdinaryTimelineManageItem, -} from "./TimelineInfoCard"; - -export type TimelinePageUIProps = Omit< - TimelinePageTemplateUIProps, - "CardComponent" ->; - -const TimelinePageUI: React.FC = (props) => { - return ; -}; - -export default TimelinePageUI; diff --git a/Timeline/ClientApp/src/app/views/timeline/index.tsx b/Timeline/ClientApp/src/app/views/timeline/index.tsx deleted file mode 100644 index 225a1a59..00000000 --- a/Timeline/ClientApp/src/app/views/timeline/index.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from "react"; -import { useParams } from "react-router"; - -import TimelinePageTemplate from "../timeline-common/TimelinePageTemplate"; - -import TimelinePageUI from "./TimelinePageUI"; -import { OrdinaryTimelineManageItem } from "./TimelineInfoCard"; -import TimelineDeleteDialog from "./TimelineDeleteDialog"; - -const TimelinePage: React.FC = (_) => { - const { name } = useParams<{ name: string }>(); - - const [dialog, setDialog] = React.useState( - null - ); - - let dialogElement: React.ReactElement | undefined; - if (dialog === "delete") { - dialogElement = ( - setDialog(null)} name={name} /> - ); - } - - return ( - <> - setDialog(item)} - notFoundI18nKey="timeline.timelineNotExist" - /> - {dialogElement} - - ); -}; - -export default TimelinePage; diff --git a/Timeline/ClientApp/src/app/views/timeline/timeline.sass b/Timeline/ClientApp/src/app/views/timeline/timeline.sass deleted file mode 100644 index e69de29b..00000000 diff --git a/Timeline/ClientApp/src/app/views/user/ChangeAvatarDialog.tsx b/Timeline/ClientApp/src/app/views/user/ChangeAvatarDialog.tsx deleted file mode 100644 index ffa2218b..00000000 --- a/Timeline/ClientApp/src/app/views/user/ChangeAvatarDialog.tsx +++ /dev/null @@ -1,302 +0,0 @@ -import React, { useState, useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { AxiosError } from "axios"; -import { Modal, Row, Button } from "react-bootstrap"; - -import { UiLogicError } from "@/common"; - -import ImageCropper, { Clip, applyClipToImage } from "../common/ImageCropper"; - -export interface ChangeAvatarDialogProps { - open: boolean; - close: () => void; - process: (blob: Blob) => Promise; -} - -const ChangeAvatarDialog: React.FC = (props) => { - const { t } = useTranslation(); - - const [file, setFile] = React.useState(null); - const [fileUrl, setFileUrl] = React.useState(null); - const [clip, setClip] = React.useState(null); - const [ - cropImgElement, - setCropImgElement, - ] = React.useState(null); - const [resultBlob, setResultBlob] = React.useState(null); - const [resultUrl, setResultUrl] = React.useState(null); - - const [state, setState] = React.useState< - | "select" - | "crop" - | "processcrop" - | "preview" - | "uploading" - | "success" - | "error" - >("select"); - - const [message, setMessage] = useState< - string | { type: "custom"; text: string } | null - >("userPage.dialogChangeAvatar.prompt.select"); - - const trueMessage = - message == null - ? null - : typeof message === "string" - ? t(message) - : message.text; - - const closeDialog = props.close; - - const close = React.useCallback((): void => { - if (!(state === "uploading")) { - closeDialog(); - } - }, [state, closeDialog]); - - useEffect(() => { - if (file != null) { - const url = URL.createObjectURL(file); - setClip(null); - setFileUrl(url); - setState("crop"); - return () => { - URL.revokeObjectURL(url); - }; - } else { - setFileUrl(null); - setState("select"); - } - }, [file]); - - React.useEffect(() => { - if (resultBlob != null) { - const url = URL.createObjectURL(resultBlob); - setResultUrl(url); - setState("preview"); - return () => { - URL.revokeObjectURL(url); - }; - } else { - setResultUrl(null); - } - }, [resultBlob]); - - const onSelectFile = React.useCallback( - (e: React.ChangeEvent): void => { - const files = e.target.files; - if (files == null || files.length === 0) { - setFile(null); - } else { - setFile(files[0]); - } - }, - [] - ); - - const onCropNext = React.useCallback(() => { - if ( - cropImgElement == null || - clip == null || - clip.width === 0 || - file == null - ) { - throw new UiLogicError(); - } - - setState("processcrop"); - void applyClipToImage(cropImgElement, clip, file.type).then((b) => { - setResultBlob(b); - }); - }, [cropImgElement, clip, file]); - - const onCropPrevious = React.useCallback(() => { - setFile(null); - setState("select"); - }, []); - - const onPreviewPrevious = React.useCallback(() => { - setResultBlob(null); - setState("crop"); - }, []); - - const process = props.process; - - const upload = React.useCallback(() => { - if (resultBlob == null) { - throw new UiLogicError(); - } - - setState("uploading"); - process(resultBlob).then( - () => { - setState("success"); - }, - (e: unknown) => { - setState("error"); - setMessage({ type: "custom", text: (e as AxiosError).message }); - } - ); - }, [resultBlob, process]); - - const createPreviewRow = (): React.ReactElement => { - if (resultUrl == null) { - throw new UiLogicError(); - } - return ( - - {t("userPage.dialogChangeAvatar.previewImgAlt")} - - ); - }; - - return ( - - - {t("userPage.dialogChangeAvatar.title")} - - {(() => { - if (state === "select") { - return ( - <> - - {t("userPage.dialogChangeAvatar.prompt.select")} - - - - - - - - - ); - } else if (state === "crop") { - if (fileUrl == null) { - throw new UiLogicError(); - } - return ( - <> - - - - - {t("userPage.dialogChangeAvatar.prompt.crop")} - - - - - - - - ); - } else if (state === "processcrop") { - return ( - <> - - - {t("userPage.dialogChangeAvatar.prompt.processingCrop")} - - - - - - - - ); - } else if (state === "preview") { - return ( - <> - - {createPreviewRow()} - {t("userPage.dialogChangeAvatar.prompt.preview")} - - - - - - - - ); - } else if (state === "uploading") { - return ( - <> - - {createPreviewRow()} - {t("userPage.dialogChangeAvatar.prompt.uploading")} - - - - ); - } else if (state === "success") { - return ( - <> - - - {t("operationDialog.success")} - - - - - - - ); - } else { - return ( - <> - - {createPreviewRow()} - {trueMessage} - - - - - - - ); - } - })()} - - ); -}; - -export default ChangeAvatarDialog; diff --git a/Timeline/ClientApp/src/app/views/user/ChangeNicknameDialog.tsx b/Timeline/ClientApp/src/app/views/user/ChangeNicknameDialog.tsx deleted file mode 100644 index 251b18c5..00000000 --- a/Timeline/ClientApp/src/app/views/user/ChangeNicknameDialog.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from "react"; - -import OperationDialog from "../common/OperationDialog"; - -export interface ChangeNicknameDialogProps { - open: boolean; - close: () => void; - onProcess: (newNickname: string) => Promise; -} - -const ChangeNicknameDialog: React.FC = (props) => { - return ( - { - return props.onProcess(newNickname as string); - }} - close={props.close} - /> - ); -}; - -export default ChangeNicknameDialog; diff --git a/Timeline/ClientApp/src/app/views/user/UserInfoCard.tsx b/Timeline/ClientApp/src/app/views/user/UserInfoCard.tsx deleted file mode 100644 index 888fb18a..00000000 --- a/Timeline/ClientApp/src/app/views/user/UserInfoCard.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import React from "react"; -import { useTranslation } from "react-i18next"; -import { Dropdown, Button } from "react-bootstrap"; - -import { timelineVisibilityTooltipTranslationMap } from "@/services/timeline"; -import { useAvatar } from "@/services/user"; - -import BlobImage from "../common/BlobImage"; -import { TimelineCardComponentProps } from "../timeline-common/TimelinePageTemplateUI"; -import InfoCardTemplate from "../timeline-common/InfoCardTemplate"; - -export type PersonalTimelineManageItem = "avatar" | "nickname"; - -export type UserInfoCardProps = TimelineCardComponentProps< - PersonalTimelineManageItem ->; - -const UserInfoCard: React.FC = (props) => { - const { - timeline, - collapse, - onMember, - onManage, - syncStatus, - toggleCollapse, - } = props; - const { t } = useTranslation(); - - const avatar = useAvatar(timeline?.owner?.username); - - return ( - -
- - {timeline.owner.nickname} - - @{timeline.owner.username} - -
-

{timeline.description}

- - {t(timelineVisibilityTooltipTranslationMap[timeline.visibility])} - -
- {onManage != null ? ( - - - {t("timeline.manage")} - - - onManage("nickname")}> - {t("timeline.manageItem.nickname")} - - onManage("avatar")}> - {t("timeline.manageItem.avatar")} - - onManage("property")}> - {t("timeline.manageItem.property")} - - - {t("timeline.manageItem.member")} - - - - ) : ( - - )} -
-
- ); -}; - -export default UserInfoCard; diff --git a/Timeline/ClientApp/src/app/views/user/UserPageUI.tsx b/Timeline/ClientApp/src/app/views/user/UserPageUI.tsx deleted file mode 100644 index d405399c..00000000 --- a/Timeline/ClientApp/src/app/views/user/UserPageUI.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from "react"; - -import TimelinePageTemplateUI, { - TimelinePageTemplateUIProps, -} from "../timeline-common/TimelinePageTemplateUI"; - -import UserInfoCard, { PersonalTimelineManageItem } from "./UserInfoCard"; - -export type UserPageUIProps = Omit< - TimelinePageTemplateUIProps, - "CardComponent" ->; - -const UserPageUI: React.FC = (props) => { - return ; -}; - -export default UserPageUI; diff --git a/Timeline/ClientApp/src/app/views/user/index.tsx b/Timeline/ClientApp/src/app/views/user/index.tsx deleted file mode 100644 index 7c0b1563..00000000 --- a/Timeline/ClientApp/src/app/views/user/index.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React, { useState } from "react"; -import { useParams } from "react-router"; - -import { UiLogicError } from "@/common"; -import { useUser, userInfoService } from "@/services/user"; - -import TimelinePageTemplate from "../timeline-common/TimelinePageTemplate"; - -import UserPageUI from "./UserPageUI"; -import { PersonalTimelineManageItem } from "./UserInfoCard"; -import ChangeNicknameDialog from "./ChangeNicknameDialog"; -import ChangeAvatarDialog from "./ChangeAvatarDialog"; - -const UserPage: React.FC = (_) => { - const { username } = useParams<{ username: string }>(); - - const user = useUser(); - - const [dialog, setDialog] = useState(null); - - let dialogElement: React.ReactElement | undefined; - - const closeDialogHandler = (): void => { - setDialog(null); - }; - - if (dialog === "nickname") { - if (user == null) { - throw new UiLogicError("Change nickname without login."); - } - - dialogElement = ( - - userInfoService.setNickname(username, newNickname) - } - /> - ); - } else if (dialog === "avatar") { - if (user == null) { - throw new UiLogicError("Change avatar without login."); - } - - dialogElement = ( - userInfoService.setAvatar(username, file)} - /> - ); - } - - const onManage = React.useCallback((item: PersonalTimelineManageItem) => { - setDialog(item); - }, []); - - return ( - <> - - {dialogElement} - - ); -}; - -export default UserPage; diff --git a/Timeline/ClientApp/src/app/views/user/user.sass b/Timeline/ClientApp/src/app/views/user/user.sass deleted file mode 100644 index 5b7fcae7..00000000 --- a/Timeline/ClientApp/src/app/views/user/user.sass +++ /dev/null @@ -1,7 +0,0 @@ -.change-avatar-cropper-row - max-height: 400px - -.change-avatar-img - min-width: 50% - max-width: 100% - max-height: 400px diff --git a/Timeline/ClientApp/src/sw/sw.ts b/Timeline/ClientApp/src/sw/sw.ts deleted file mode 100644 index d6202f36..00000000 --- a/Timeline/ClientApp/src/sw/sw.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { precacheAndRoute, matchPrecache } from "workbox-precaching"; -import { setDefaultHandler } from "workbox-routing"; -import { NetworkOnly } from "workbox-strategies"; - -declare let self: ServiceWorkerGlobalScope; - -self.addEventListener("message", (event) => { - if (event.data && (event.data as { type: string }).type === "SKIP_WAITING") { - void self.skipWaiting(); - } -}); - -precacheAndRoute(self.__WB_MANIFEST); - -const networkOnly = new NetworkOnly(); - -setDefaultHandler((options) => { - const { request, url } = options; - if (url && url.pathname.startsWith("/api/")) { - return networkOnly.handle(options); - } - - if (request instanceof Request && request.destination === "document") - return matchPrecache("/index.html").then((r) => - r == null ? Response.error() : r - ); - else return networkOnly.handle(options); -}); diff --git a/Timeline/ClientApp/src/sw/tsconfig.json b/Timeline/ClientApp/src/sw/tsconfig.json deleted file mode 100644 index aac99e59..00000000 --- a/Timeline/ClientApp/src/sw/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "lib": [ - "esnext", - "webworker" - ] - }, - "include": [ - "." - ] -} diff --git a/Timeline/ClientApp/src/tsconfig.json b/Timeline/ClientApp/src/tsconfig.json deleted file mode 100644 index 6937be63..00000000 --- a/Timeline/ClientApp/src/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "compilerOptions": { - "target": "esnext", - "allowJs": true, - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "sourceMap": true, - "baseUrl": "./", - "paths": { - "@/*": [ - "app/*" - ] - } - } -} diff --git a/Timeline/ClientApp/webpack.common.js b/Timeline/ClientApp/webpack.common.js deleted file mode 100644 index 3779003e..00000000 --- a/Timeline/ClientApp/webpack.common.js +++ /dev/null @@ -1,86 +0,0 @@ -const path = require("path"); -const HtmlWebpackPlugin = require("html-webpack-plugin"); -const postcssPresetEnv = require("postcss-preset-env"); -const Config = require("webpack-chain"); - -const config = new Config(); - -config.entry("index").add(path.resolve(__dirname, "src/app/index.tsx")); - -config.module - .rule("ts") - .test(/\.ts(x?)$/) - .exclude.add(/node_modules/) - .end() - .use("babel") - .loader("babel-loader") - .end() - .use("ts") - .loader("ts-loader") - .end(); - -config.module - .rule("js") - .test(/\.js(x?)$/) - .exclude.add(/node_modules/) - .end() - .use("babel") - .loader("babel-loader") - .end(); - -config.module - .rule("css") - .test(/\.css$/) - .use("css") - .loader("css-loader") - .end() - .use("postcss") - .loader("postcss-loader") - .end(); - -config.module - .rule("sass") - .test(/\.(scss|sass)$/) - .use("css") - .loader("css-loader") - .end() - .use("postcss") - .loader("postcss-loader") - .end() - .use("sass") - .loader("sass-loader") - .end(); - -config.module - .rule("file") - .test(/\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot)$/i) - .use("url") - .loader("url-loader") - .options({ - limit: 8192, - }); - -config.resolve.extensions - .add("*") - .add(".js") - .add(".jsx") - .add(".ts") - .add(".tsx") - .end(); - -config.resolve.alias.set("@", path.resolve(__dirname, "src/app")); - -config.output - .path(path.resolve(__dirname, "dist/")) - .filename("[name].[contenthash].js") - .chunkFilename("[name].[contenthash].js") - .publicPath("/"); - -config.plugin("html").use(HtmlWebpackPlugin, [ - { - template: "src/app/index.ejs", - title: "Timeline", - }, -]); - -module.exports = config; diff --git a/Timeline/ClientApp/webpack.config.dev.js b/Timeline/ClientApp/webpack.config.dev.js deleted file mode 100644 index c88e1aaf..00000000 --- a/Timeline/ClientApp/webpack.config.dev.js +++ /dev/null @@ -1,52 +0,0 @@ -const path = require("path"); -const webpack = require("webpack"); - -const config = require("./webpack.common"); - -config.mode("development"); - -config.entry("index").add("react-hot-loader/patch"); - -config.module - .rule("ts") - .use("babel") - .options({ - plugins: ["react-hot-loader/babel"], - }); - -config.module - .rule("js") - .use("babel") - .options({ - plugins: ["react-hot-loader/babel"], - }); - -config.module - .rule("css") - .use("style") - .before("css") - .loader("style-loader") - .end(); - -config.module - .rule("sass") - .use("style") - .before("css") - .loader("style-loader") - .end(); - -config.devtool("eval-cheap-module-source-map"); - -config.resolve.alias.set("react-dom", "@hot-loader/react-dom"); - -config.devServer - .contentBase(path.resolve(__dirname, "public/")) - .host("0.0.0.0") - .port(3000) - .historyApiFallback(true) - .hotOnly(true) - .allowedHosts.add(".myide.io"); - -config.plugin("hot").use(webpack.HotModuleReplacementPlugin); - -module.exports = config.toConfig(); diff --git a/Timeline/ClientApp/webpack.config.prod.js b/Timeline/ClientApp/webpack.config.prod.js deleted file mode 100644 index 188cb940..00000000 --- a/Timeline/ClientApp/webpack.config.prod.js +++ /dev/null @@ -1,53 +0,0 @@ -const path = require("path"); -const { CleanWebpackPlugin } = require("clean-webpack-plugin"); -const CopyPlugin = require("copy-webpack-plugin"); -const WorkboxPlugin = require("workbox-webpack-plugin"); -const MiniCssExtractPlugin = require("mini-css-extract-plugin"); - -const config = require("./webpack.common"); - -config.mode("production"); - -config - .entry("index") - .add(path.resolve(__dirname, "src/app/service-worker.tsx")); - -config.module - .rule("css") - .use("mini-css-extract") - .before("css") - .loader(MiniCssExtractPlugin.loader) - .end(); - -config.module - .rule("sass") - .use("mini-css-extract") - .before("css") - .loader(MiniCssExtractPlugin.loader) - .end(); - -config.devtool("source-map"); - -config.plugin("mini-css-extract").use(MiniCssExtractPlugin); - -config.plugin("clean").use(CleanWebpackPlugin); - -config.plugin("copy").use(CopyPlugin, [ - { - patterns: [ - { - from: path.resolve(__dirname, "public/"), - to: path.resolve(__dirname, "dist/"), - }, - ], - }, -]); - -config.plugin("workbox").use(WorkboxPlugin.InjectManifest, [ - { - swSrc: path.resolve(__dirname, "src/sw/sw.ts"), - maximumFileSizeToCacheInBytes: 15000000, - }, -]); - -module.exports = config.toConfig(); diff --git a/Timeline/Configs/ApplicationConfiguration.cs b/Timeline/Configs/ApplicationConfiguration.cs deleted file mode 100644 index df281adb..00000000 --- a/Timeline/Configs/ApplicationConfiguration.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Timeline.Configs -{ - public static class ApplicationConfiguration - { - public const string WorkDirKey = "WorkDir"; - public const string DefaultWorkDir = "/timeline"; - public const string DatabaseFileName = "timeline.db"; - public const string DatabaseBackupDirectoryName = "backup"; - public const string DisableFrontEndKey = "DisableFrontEnd"; - public const string UseMockFrontEndKey = "UseMockFrontEnd"; - public const string UseProxyFrontEndKey = "UseProxyFrontEnd"; - } -} diff --git a/Timeline/Configs/JwtConfiguration.cs b/Timeline/Configs/JwtConfiguration.cs deleted file mode 100644 index af8052de..00000000 --- a/Timeline/Configs/JwtConfiguration.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Timeline.Configs -{ - public class JwtConfiguration - { - public string Issuer { get; set; } = default!; - public string Audience { get; set; } = default!; - - /// - /// Set the default value of expire offset of jwt token. - /// Unit is second. Default is 3600 * 24 seconds, aka 1 day. - /// - public long DefaultExpireOffset { get; set; } = 3600 * 24; - } -} diff --git a/Timeline/Controllers/ControllerAuthExtensions.cs b/Timeline/Controllers/ControllerAuthExtensions.cs deleted file mode 100644 index 00a65454..00000000 --- a/Timeline/Controllers/ControllerAuthExtensions.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using System; -using System.Security.Claims; -using Timeline.Auth; -using static Timeline.Resources.Controllers.ControllerAuthExtensions; - -namespace Timeline.Controllers -{ - public static class ControllerAuthExtensions - { - public static bool IsAdministrator(this ControllerBase controller) - { - return controller.User != null && controller.User.IsAdministrator(); - } - - public static long GetUserId(this ControllerBase controller) - { - var claim = controller.User.FindFirst(ClaimTypes.NameIdentifier); - if (claim == null) - throw new InvalidOperationException(ExceptionNoUserIdentifierClaim); - - if (long.TryParse(claim.Value, out var value)) - return value; - - throw new InvalidOperationException(ExceptionUserIdentifierClaimBadFormat); - } - - public static long? GetOptionalUserId(this ControllerBase controller) - { - var claim = controller.User.FindFirst(ClaimTypes.NameIdentifier); - if (claim == null) - return null; - - if (long.TryParse(claim.Value, out var value)) - return value; - - throw new InvalidOperationException(ExceptionUserIdentifierClaimBadFormat); - } - } -} diff --git a/Timeline/Controllers/Testing/TestingAuthController.cs b/Timeline/Controllers/Testing/TestingAuthController.cs deleted file mode 100644 index 4d3b3ec7..00000000 --- a/Timeline/Controllers/Testing/TestingAuthController.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Timeline.Auth; - -namespace Timeline.Controllers.Testing -{ - [Route("testing/auth")] - [ApiController] - public class TestingAuthController : Controller - { - [HttpGet("[action]")] - [Authorize] - public ActionResult Authorize() - { - return Ok(); - } - - [HttpGet("[action]")] - [UserAuthorize] - public new ActionResult User() - { - return Ok(); - } - - [HttpGet("[action]")] - [AdminAuthorize] - public ActionResult Admin() - { - return Ok(); - } - } -} diff --git a/Timeline/Controllers/TimelineController.cs b/Timeline/Controllers/TimelineController.cs deleted file mode 100644 index 9a3147ea..00000000 --- a/Timeline/Controllers/TimelineController.cs +++ /dev/null @@ -1,491 +0,0 @@ -using AutoMapper; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Threading.Tasks; -using Timeline.Filters; -using Timeline.Helpers; -using Timeline.Models; -using Timeline.Models.Http; -using Timeline.Models.Validation; -using Timeline.Services; -using Timeline.Services.Exceptions; - -namespace Timeline.Controllers -{ - /// - /// Operations about timeline. - /// - [ApiController] - [CatchTimelineNotExistException] - [ProducesErrorResponseType(typeof(CommonResponse))] - public class TimelineController : Controller - { - private readonly ILogger _logger; - - private readonly IUserService _userService; - private readonly ITimelineService _service; - - private readonly IMapper _mapper; - - /// - /// - /// - public TimelineController(ILogger logger, IUserService userService, ITimelineService service, IMapper mapper) - { - _logger = logger; - _userService = userService; - _service = service; - _mapper = mapper; - } - - /// - /// List all timelines. - /// - /// A username. If set, only timelines related to the user will return. - /// Specify the relation type, may be 'own' or 'join'. If not set, both type will return. - /// "Private" or "Register" or "Public". If set, only timelines whose visibility is specified one will return. - /// The timeline list. - [HttpGet("timelines")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task>> TimelineList([FromQuery][Username] string? relate, [FromQuery][RegularExpression("(own)|(join)")] string? relateType, [FromQuery] string? visibility) - { - List? visibilityFilter = null; - if (visibility != null) - { - visibilityFilter = new List(); - var items = visibility.Split('|'); - foreach (var item in items) - { - if (item.Equals(nameof(TimelineVisibility.Private), StringComparison.OrdinalIgnoreCase)) - { - if (!visibilityFilter.Contains(TimelineVisibility.Private)) - visibilityFilter.Add(TimelineVisibility.Private); - } - else if (item.Equals(nameof(TimelineVisibility.Register), StringComparison.OrdinalIgnoreCase)) - { - if (!visibilityFilter.Contains(TimelineVisibility.Register)) - visibilityFilter.Add(TimelineVisibility.Register); - } - else if (item.Equals(nameof(TimelineVisibility.Public), StringComparison.OrdinalIgnoreCase)) - { - if (!visibilityFilter.Contains(TimelineVisibility.Public)) - visibilityFilter.Add(TimelineVisibility.Public); - } - else - { - return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_QueryVisibilityUnknown, item)); - } - } - } - - TimelineUserRelationship? relationship = null; - if (relate != null) - { - try - { - var relatedUserId = await _userService.GetUserIdByUsername(relate); - - relationship = new TimelineUserRelationship(relateType switch - { - "own" => TimelineUserRelationshipType.Own, - "join" => TimelineUserRelationshipType.Join, - _ => TimelineUserRelationshipType.Default - }, relatedUserId); - } - catch (UserNotExistException) - { - return BadRequest(ErrorResponse.TimelineController.QueryRelateNotExist()); - } - } - - var timelines = await _service.GetTimelines(relationship, visibilityFilter); - var result = _mapper.Map>(timelines); - return result; - } - - /// - /// Get info of a timeline. - /// - /// The timeline name. - /// A unique id. If specified and if-modified-since is also specified, the timeline info will return when unique id is not the specified one even if it is not modified. - /// Same effect as If-Modified-Since header and take precedence than it. - /// If specified, will return 304 if not modified. - /// The timeline info. - [HttpGet("timelines/{name}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status304NotModified)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> TimelineGet([FromRoute][GeneralTimelineName] string name, [FromQuery] string? checkUniqueId, [FromQuery(Name = "ifModifiedSince")] DateTime? queryIfModifiedSince, [FromHeader(Name = "If-Modified-Since")] DateTime? headerIfModifiedSince) - { - DateTime? ifModifiedSince = null; - if (queryIfModifiedSince.HasValue) - { - ifModifiedSince = queryIfModifiedSince.Value; - } - else if (headerIfModifiedSince != null) - { - ifModifiedSince = headerIfModifiedSince.Value; - } - - bool returnNotModified = false; - - if (ifModifiedSince.HasValue) - { - var lastModified = await _service.GetTimelineLastModifiedTime(name); - if (lastModified < ifModifiedSince.Value) - { - if (checkUniqueId != null) - { - var uniqueId = await _service.GetTimelineUniqueId(name); - if (uniqueId == checkUniqueId) - { - returnNotModified = true; - } - } - else - { - returnNotModified = true; - } - } - } - - if (returnNotModified) - { - return StatusCode(StatusCodes.Status304NotModified); - } - else - { - var timeline = await _service.GetTimeline(name); - var result = _mapper.Map(timeline); - return result; - } - } - - /// - /// Get posts of a timeline. - /// - /// The name of the timeline. - /// If set, only posts modified since the time will return. - /// If set to true, deleted post will also return. - /// The post list. - [HttpGet("timelines/{name}/posts")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> PostListGet([FromRoute][GeneralTimelineName] string name, [FromQuery] DateTime? modifiedSince, [FromQuery] bool? includeDeleted) - { - if (!this.IsAdministrator() && !await _service.HasReadPermission(name, this.GetOptionalUserId())) - { - return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); - } - - List posts = await _service.GetPosts(name, modifiedSince, includeDeleted ?? false); - - var result = _mapper.Map>(posts); - return result; - } - - /// - /// Get the data of a post. Usually a image post. - /// - /// Timeline name. - /// The id of the post. - /// If-None-Match header. - /// The data. - [HttpGet("timelines/{name}/posts/{id}/data")] - [Produces("image/png", "image/jpeg", "image/gif", "image/webp", "application/json", "text/json")] - [ProducesResponseType(typeof(byte[]), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(void), StatusCodes.Status304NotModified)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task PostDataGet([FromRoute][GeneralTimelineName] string name, [FromRoute] long id, [FromHeader(Name = "If-None-Match")] string? ifNoneMatch) - { - _ = ifNoneMatch; - if (!this.IsAdministrator() && !await _service.HasReadPermission(name, this.GetOptionalUserId())) - { - return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); - } - - try - { - return await DataCacheHelper.GenerateActionResult(this, () => _service.GetPostDataETag(name, id), async () => - { - var data = await _service.GetPostData(name, id); - return data; - }); - } - catch (TimelinePostNotExistException) - { - return NotFound(ErrorResponse.TimelineController.PostNotExist()); - } - catch (TimelinePostNoDataException) - { - return BadRequest(ErrorResponse.TimelineController.PostNoData()); - } - } - - /// - /// Create a new post. - /// - /// Timeline name. - /// - /// Info of new post. - [HttpPost("timelines/{name}/posts")] - [Authorize] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task> PostPost([FromRoute][GeneralTimelineName] string name, [FromBody] TimelinePostCreateRequest body) - { - var id = this.GetUserId(); - if (!this.IsAdministrator() && !await _service.IsMemberOf(name, id)) - { - return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); - } - - var content = body.Content; - - TimelinePost post; - - if (content.Type == TimelinePostContentTypes.Text) - { - var text = content.Text; - if (text == null) - { - return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_TextContentTextRequired)); - } - post = await _service.CreateTextPost(name, id, text, body.Time); - } - else if (content.Type == TimelinePostContentTypes.Image) - { - var base64Data = content.Data; - if (base64Data == null) - { - return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataRequired)); - } - byte[] data; - try - { - data = Convert.FromBase64String(base64Data); - } - catch (FormatException) - { - return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataNotBase64)); - } - - try - { - post = await _service.CreateImagePost(name, id, data, body.Time); - } - catch (ImageException) - { - return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataNotImage)); - } - } - else - { - return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ContentUnknownType)); - } - - var result = _mapper.Map(post); - return result; - } - - /// - /// Delete a post. - /// - /// Timeline name. - /// Post id. - /// Info of deletion. - [HttpDelete("timelines/{name}/posts/{id}")] - [Authorize] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task> PostDelete([FromRoute][GeneralTimelineName] string name, [FromRoute] long id) - { - if (!this.IsAdministrator() && !await _service.HasPostModifyPermission(name, id, this.GetUserId())) - { - return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); - } - try - { - await _service.DeletePost(name, id); - return CommonDeleteResponse.Delete(); - } - catch (TimelinePostNotExistException) - { - return CommonDeleteResponse.NotExist(); - } - } - - /// - /// Change properties of a timeline. - /// - /// Timeline name. - /// - /// The new info. - [HttpPatch("timelines/{name}")] - [Authorize] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task> TimelinePatch([FromRoute][GeneralTimelineName] string name, [FromBody] TimelinePatchRequest body) - { - if (!this.IsAdministrator() && !(await _service.HasManagePermission(name, this.GetUserId()))) - { - return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); - } - await _service.ChangeProperty(name, _mapper.Map(body)); - var timeline = await _service.GetTimeline(name); - var result = _mapper.Map(timeline); - return result; - } - - /// - /// Add a member to timeline. - /// - /// Timeline name. - /// The new member's username. - [HttpPut("timelines/{name}/members/{member}")] - [Authorize] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task TimelineMemberPut([FromRoute][GeneralTimelineName] string name, [FromRoute][Username] string member) - { - if (!this.IsAdministrator() && !(await _service.HasManagePermission(name, this.GetUserId()))) - { - return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); - } - - try - { - await _service.ChangeMember(name, new List { member }, null); - return Ok(); - } - catch (UserNotExistException) - { - return BadRequest(ErrorResponse.TimelineController.MemberPut_NotExist()); - } - } - - /// - /// Remove a member from timeline. - /// - /// Timeline name. - /// The member's username. - [HttpDelete("timelines/{name}/members/{member}")] - [Authorize] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task TimelineMemberDelete([FromRoute][GeneralTimelineName] string name, [FromRoute][Username] string member) - { - if (!this.IsAdministrator() && !(await _service.HasManagePermission(name, this.GetUserId()))) - { - return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); - } - - try - { - await _service.ChangeMember(name, null, new List { member }); - return Ok(CommonDeleteResponse.Delete()); - } - catch (UserNotExistException) - { - return Ok(CommonDeleteResponse.NotExist()); - } - } - - /// - /// Create a timeline. - /// - /// - /// Info of new timeline. - [HttpPost("timelines")] - [Authorize] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public async Task> TimelineCreate([FromBody] TimelineCreateRequest body) - { - var userId = this.GetUserId(); - - try - { - var timeline = await _service.CreateTimeline(body.Name, userId); - var result = _mapper.Map(timeline); - return result; - } - catch (EntityAlreadyExistException e) when (e.EntityName == EntityNames.Timeline) - { - return BadRequest(ErrorResponse.TimelineController.NameConflict()); - } - } - - /// - /// Delete a timeline. - /// - /// Timeline name. - /// Info of deletion. - [HttpDelete("timelines/{name}")] - [Authorize] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task> TimelineDelete([FromRoute][TimelineName] string name) - { - if (!this.IsAdministrator() && !(await _service.HasManagePermission(name, this.GetUserId()))) - { - return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); - } - - try - { - await _service.DeleteTimeline(name); - return CommonDeleteResponse.Delete(); - } - catch (TimelineNotExistException) - { - return CommonDeleteResponse.NotExist(); - } - } - - [HttpPost("timelineop/changename")] - [Authorize] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task> TimelineOpChangeName([FromBody] TimelineChangeNameRequest body) - { - if (!this.IsAdministrator() && !(await _service.HasManagePermission(body.OldName, this.GetUserId()))) - { - return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); - } - - try - { - var timeline = await _service.ChangeTimelineName(body.OldName, body.NewName); - return Ok(_mapper.Map(timeline)); - } - catch (EntityAlreadyExistException) - { - return BadRequest(ErrorResponse.TimelineController.NameConflict()); - } - } - } -} diff --git a/Timeline/Controllers/TokenController.cs b/Timeline/Controllers/TokenController.cs deleted file mode 100644 index 8f2ca600..00000000 --- a/Timeline/Controllers/TokenController.cs +++ /dev/null @@ -1,142 +0,0 @@ -using AutoMapper; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using System; -using System.Globalization; -using System.Threading.Tasks; -using Timeline.Helpers; -using Timeline.Models.Http; -using Timeline.Services; -using Timeline.Services.Exceptions; -using static Timeline.Resources.Controllers.TokenController; - -namespace Timeline.Controllers -{ - /// - /// Operation about tokens. - /// - [Route("token")] - [ApiController] - [ProducesErrorResponseType(typeof(CommonResponse))] - public class TokenController : Controller - { - private readonly IUserTokenManager _userTokenManager; - private readonly ILogger _logger; - private readonly IClock _clock; - - private readonly IMapper _mapper; - - /// - public TokenController(IUserTokenManager userTokenManager, ILogger logger, IClock clock, IMapper mapper) - { - _userTokenManager = userTokenManager; - _logger = logger; - _clock = clock; - _mapper = mapper; - } - - /// - /// Create a new token for a user. - /// - /// Result of token creation. - [HttpPost("create")] - [AllowAnonymous] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task> Create([FromBody] CreateTokenRequest request) - { - void LogFailure(string reason, Exception? e = null) - { - _logger.LogInformation(e, Log.Format(LogCreateFailure, - ("Reason", reason), - ("Username", request.Username), - ("Password", request.Password), - ("Expire (in days)", request.Expire) - )); - } - - try - { - DateTime? expireTime = null; - if (request.Expire != null) - expireTime = _clock.GetCurrentTime().AddDays(request.Expire.Value); - - var result = await _userTokenManager.CreateToken(request.Username, request.Password, expireTime); - - _logger.LogInformation(Log.Format(LogCreateSuccess, - ("Username", request.Username), - ("Expire At", expireTime?.ToString(CultureInfo.CurrentCulture.DateTimeFormat) ?? "default") - )); - return Ok(new CreateTokenResponse - { - Token = result.Token, - User = _mapper.Map(result.User) - }); - } - catch (UserNotExistException e) - { - LogFailure(LogUserNotExist, e); - return BadRequest(ErrorResponse.TokenController.Create_BadCredential()); - } - catch (BadPasswordException e) - { - LogFailure(LogBadPassword, e); - return BadRequest(ErrorResponse.TokenController.Create_BadCredential()); - } - } - - /// - /// Verify a token. - /// - /// Result of token verification. - [HttpPost("verify")] - [AllowAnonymous] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task> Verify([FromBody] VerifyTokenRequest request) - { - void LogFailure(string reason, Exception? e = null, params (string, object?)[] otherProperties) - { - var properties = new (string, object?)[2 + otherProperties.Length]; - properties[0] = ("Reason", reason); - properties[1] = ("Token", request.Token); - otherProperties.CopyTo(properties, 2); - _logger.LogInformation(e, Log.Format(LogVerifyFailure, properties)); - } - - try - { - var result = await _userTokenManager.VerifyToken(request.Token); - _logger.LogInformation(Log.Format(LogVerifySuccess, - ("Username", result.Username), ("Token", request.Token))); - return Ok(new VerifyTokenResponse - { - User = _mapper.Map(result) - }); - } - catch (UserTokenTimeExpireException e) - { - LogFailure(LogVerifyExpire, e, ("Expire Time", e.ExpireTime), ("Verify Time", e.VerifyTime)); - return BadRequest(ErrorResponse.TokenController.Verify_TimeExpired()); - } - catch (UserTokenBadVersionException e) - { - LogFailure(LogVerifyOldVersion, e, ("Token Version", e.TokenVersion), ("Required Version", e.RequiredVersion)); - return BadRequest(ErrorResponse.TokenController.Verify_OldVersion()); - - } - catch (UserTokenBadFormatException e) - { - LogFailure(LogVerifyBadFormat, e); - return BadRequest(ErrorResponse.TokenController.Verify_BadFormat()); - } - catch (UserNotExistException e) - { - LogFailure(LogVerifyUserNotExist, e); - return BadRequest(ErrorResponse.TokenController.Verify_UserNotExist()); - } - } - } -} diff --git a/Timeline/Controllers/UserAvatarController.cs b/Timeline/Controllers/UserAvatarController.cs deleted file mode 100644 index bc4afa30..00000000 --- a/Timeline/Controllers/UserAvatarController.cs +++ /dev/null @@ -1,174 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; -using System; -using System.Threading.Tasks; -using Timeline.Auth; -using Timeline.Filters; -using Timeline.Helpers; -using Timeline.Models; -using Timeline.Models.Http; -using Timeline.Models.Validation; -using Timeline.Services; -using Timeline.Services.Exceptions; -using static Timeline.Resources.Controllers.UserAvatarController; - -namespace Timeline.Controllers -{ - /// - /// Operations about user avatar. - /// - [ApiController] - [ProducesErrorResponseType(typeof(CommonResponse))] - public class UserAvatarController : Controller - { - private readonly ILogger _logger; - - private readonly IUserService _userService; - private readonly IUserAvatarService _service; - - /// - /// - /// - public UserAvatarController(ILogger logger, IUserService userService, IUserAvatarService service) - { - _logger = logger; - _userService = userService; - _service = service; - } - - /// - /// Get avatar of a user. - /// - /// Username of the user to get avatar of. - /// If-None-Match header. - /// Avatar data. - [HttpGet("users/{username}/avatar")] - [Produces("image/png", "image/jpeg", "image/gif", "image/webp", "application/json", "text/json")] - [ProducesResponseType(typeof(byte[]), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(void), StatusCodes.Status304NotModified)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task Get([FromRoute][Username] string username, [FromHeader(Name = "If-None-Match")] string? ifNoneMatch) - { - _ = ifNoneMatch; - long id; - try - { - id = await _userService.GetUserIdByUsername(username); - } - catch (UserNotExistException e) - { - _logger.LogInformation(e, Log.Format(LogGetUserNotExist, ("Username", username))); - return NotFound(ErrorResponse.UserCommon.NotExist()); - } - - return await DataCacheHelper.GenerateActionResult(this, () => _service.GetAvatarETag(id), async () => - { - var avatar = await _service.GetAvatar(id); - return avatar.ToCacheableData(); - }); - } - - /// - /// Set avatar of a user. You have to be administrator to change other's. - /// - /// Username of the user to set avatar of. - /// The avatar data. - [HttpPut("users/{username}/avatar")] - [Authorize] - [Consumes("image/png", "image/jpeg", "image/gif", "image/webp")] - [MaxContentLength(1000 * 1000 * 10)] - [ProducesResponseType(typeof(void), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task Put([FromRoute][Username] string username, [FromBody] ByteData body) - { - if (!User.IsAdministrator() && User.Identity.Name != username) - { - _logger.LogInformation(Log.Format(LogPutForbid, - ("Operator Username", User.Identity.Name), ("Username To Put Avatar", username))); - return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); - } - - long id; - try - { - id = await _userService.GetUserIdByUsername(username); - } - catch (UserNotExistException e) - { - _logger.LogInformation(e, Log.Format(LogPutUserNotExist, ("Username", username))); - return BadRequest(ErrorResponse.UserCommon.NotExist()); - } - - try - { - var etag = await _service.SetAvatar(id, new Avatar - { - Data = body.Data, - Type = body.ContentType - }); - - _logger.LogInformation(Log.Format(LogPutSuccess, - ("Username", username), ("Mime Type", Request.ContentType))); - - Response.Headers.Append("ETag", new EntityTagHeaderValue($"\"{etag}\"").ToString()); - - return Ok(); - } - catch (ImageException e) - { - _logger.LogInformation(e, Log.Format(LogPutUserBadFormat, ("Username", username))); - return BadRequest(e.Error switch - { - ImageException.ErrorReason.CantDecode => ErrorResponse.UserAvatar.BadFormat_CantDecode(), - ImageException.ErrorReason.UnmatchedFormat => ErrorResponse.UserAvatar.BadFormat_UnmatchedFormat(), - ImageException.ErrorReason.NotSquare => ErrorResponse.UserAvatar.BadFormat_BadSize(), - _ => - throw new Exception(ExceptionUnknownAvatarFormatError) - }); - } - } - - /// - /// Reset the avatar to the default one. You have to be administrator to reset other's. - /// - /// Username of the user. - /// Succeeded to reset. - /// Error code is 10010001 if user does not exist. - /// You have not logged in. - /// You are not administrator. - [HttpDelete("users/{username}/avatar")] - [ProducesResponseType(typeof(void), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [Authorize] - public async Task Delete([FromRoute][Username] string username) - { - if (!User.IsAdministrator() && User.Identity.Name != username) - { - _logger.LogInformation(Log.Format(LogDeleteForbid, - ("Operator Username", User.Identity.Name), ("Username To Delete Avatar", username))); - return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); - } - - long id; - try - { - id = await _userService.GetUserIdByUsername(username); - } - catch (UserNotExistException e) - { - _logger.LogInformation(e, Log.Format(LogDeleteNotExist, ("Username", username))); - return BadRequest(ErrorResponse.UserCommon.NotExist()); - } - - await _service.SetAvatar(id, null); - return Ok(); - } - } -} diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs deleted file mode 100644 index 02c09aab..00000000 --- a/Timeline/Controllers/UserController.cs +++ /dev/null @@ -1,195 +0,0 @@ -using AutoMapper; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using System.Linq; -using System.Threading.Tasks; -using Timeline.Auth; -using Timeline.Helpers; -using Timeline.Models; -using Timeline.Models.Http; -using Timeline.Models.Validation; -using Timeline.Services; -using Timeline.Services.Exceptions; -using static Timeline.Resources.Controllers.UserController; -using static Timeline.Resources.Messages; - -namespace Timeline.Controllers -{ - /// - /// Operations about users. - /// - [ApiController] - [ProducesErrorResponseType(typeof(CommonResponse))] - public class UserController : Controller - { - private readonly ILogger _logger; - private readonly IUserService _userService; - private readonly IUserDeleteService _userDeleteService; - private readonly IMapper _mapper; - - /// - public UserController(ILogger logger, IUserService userService, IUserDeleteService userDeleteService, IMapper mapper) - { - _logger = logger; - _userService = userService; - _userDeleteService = userDeleteService; - _mapper = mapper; - } - - private UserInfo ConvertToUserInfo(User user) => _mapper.Map(user); - - /// - /// Get all users. - /// - /// All user list. - [HttpGet("users")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> List() - { - var users = await _userService.GetUsers(); - var result = users.Select(u => ConvertToUserInfo(u)).ToArray(); - return Ok(result); - } - - /// - /// Get a user's info. - /// - /// Username of the user. - /// User info. - [HttpGet("users/{username}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> Get([FromRoute][Username] string username) - { - try - { - var user = await _userService.GetUserByUsername(username); - return Ok(ConvertToUserInfo(user)); - } - catch (UserNotExistException e) - { - _logger.LogInformation(e, Log.Format(LogGetUserNotExist, ("Username", username))); - return NotFound(ErrorResponse.UserCommon.NotExist()); - } - } - - /// - /// Change a user's property. - /// - /// - /// Username of the user to change. - /// The new user info. - [HttpPatch("users/{username}"), Authorize] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> Patch([FromBody] UserPatchRequest body, [FromRoute][Username] string username) - { - if (this.IsAdministrator()) - { - try - { - var user = await _userService.ModifyUser(username, _mapper.Map(body)); - return Ok(ConvertToUserInfo(user)); - } - catch (UserNotExistException e) - { - _logger.LogInformation(e, Log.Format(LogPatchUserNotExist, ("Username", username))); - return NotFound(ErrorResponse.UserCommon.NotExist()); - } - catch (EntityAlreadyExistException e) when (e.EntityName == EntityNames.User) - { - return BadRequest(ErrorResponse.UserController.UsernameConflict()); - } - } - else - { - if (User.Identity.Name != username) - return StatusCode(StatusCodes.Status403Forbidden, - ErrorResponse.Common.CustomMessage_Forbid(Common_Forbid_NotSelf)); - - if (body.Username != null) - return StatusCode(StatusCodes.Status403Forbidden, - ErrorResponse.Common.CustomMessage_Forbid(UserController_Patch_Forbid_Username)); - - if (body.Password != null) - return StatusCode(StatusCodes.Status403Forbidden, - ErrorResponse.Common.CustomMessage_Forbid(UserController_Patch_Forbid_Password)); - - if (body.Administrator != null) - return StatusCode(StatusCodes.Status403Forbidden, - ErrorResponse.Common.CustomMessage_Forbid(UserController_Patch_Forbid_Administrator)); - - var user = await _userService.ModifyUser(this.GetUserId(), _mapper.Map(body)); - return Ok(ConvertToUserInfo(user)); - } - } - - /// - /// Delete a user and all his related data. You have to be administrator. - /// - /// Username of the user to delete. - /// Info of deletion. - [HttpDelete("users/{username}"), AdminAuthorize] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task> Delete([FromRoute][Username] string username) - { - var delete = await _userDeleteService.DeleteUser(username); - if (delete) - return Ok(CommonDeleteResponse.Delete()); - else - return Ok(CommonDeleteResponse.NotExist()); - } - - /// - /// Create a new user. You have to be administrator. - /// - /// The new user's info. - [HttpPost("userop/createuser"), AdminAuthorize] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task> CreateUser([FromBody] CreateUserRequest body) - { - try - { - var user = await _userService.CreateUser(_mapper.Map(body)); - return Ok(ConvertToUserInfo(user)); - } - catch (EntityAlreadyExistException e) when (e.EntityName == EntityNames.User) - { - return BadRequest(ErrorResponse.UserController.UsernameConflict()); - } - } - - /// - /// Change password with old password. - /// - [HttpPost("userop/changepassword"), Authorize] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public async Task ChangePassword([FromBody] ChangePasswordRequest request) - { - try - { - await _userService.ChangePassword(this.GetUserId(), request.OldPassword, request.NewPassword); - return Ok(); - } - catch (BadPasswordException e) - { - _logger.LogInformation(e, Log.Format(LogChangePasswordBadPassword, - ("Username", User.Identity.Name), ("Old Password", request.OldPassword))); - return BadRequest(ErrorResponse.UserController.ChangePassword_BadOldPassword()); - } - // User can't be non-existent or the token is bad. - } - } -} diff --git a/Timeline/Entities/DataEntity.cs b/Timeline/Entities/DataEntity.cs deleted file mode 100644 index b21e2dbf..00000000 --- a/Timeline/Entities/DataEntity.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Timeline.Entities -{ - [Table("data")] - public class DataEntity - { - [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public long Id { get; set; } - - [Column("tag"), Required] - public string Tag { get; set; } = default!; - - [Column("data"), Required] -#pragma warning disable CA1819 // Properties should not return arrays - public byte[] Data { get; set; } = default!; -#pragma warning restore CA1819 // Properties should not return arrays - - [Column("ref"), Required] - public int Ref { get; set; } - } -} diff --git a/Timeline/Entities/DatabaseContext.cs b/Timeline/Entities/DatabaseContext.cs deleted file mode 100644 index ecadd703..00000000 --- a/Timeline/Entities/DatabaseContext.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace Timeline.Entities -{ - public class DatabaseContext : DbContext - { - public DatabaseContext(DbContextOptions options) - : base(options) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.Entity().Property(e => e.Version).HasDefaultValue(0); - modelBuilder.Entity().HasIndex(e => e.Username).IsUnique(); - modelBuilder.Entity().Property(e => e.UniqueId).HasDefaultValueSql("lower(hex(randomblob(16)))"); - modelBuilder.Entity().Property(e => e.UsernameChangeTime).HasDefaultValueSql("datetime('now', 'utc')"); - modelBuilder.Entity().Property(e => e.CreateTime).HasDefaultValueSql("datetime('now', 'utc')"); - modelBuilder.Entity().Property(e => e.LastModified).HasDefaultValueSql("datetime('now', 'utc')"); - modelBuilder.Entity().HasIndex(e => e.Tag).IsUnique(); - modelBuilder.Entity().Property(e => e.UniqueId).HasDefaultValueSql("lower(hex(randomblob(16)))"); - - modelBuilder.ApplyUtcDateTimeConverter(); - } - - public DbSet Users { get; set; } = default!; - public DbSet UserAvatars { get; set; } = default!; - public DbSet Timelines { get; set; } = default!; - public DbSet TimelinePosts { get; set; } = default!; - public DbSet TimelineMembers { get; set; } = default!; - public DbSet JwtToken { get; set; } = default!; - public DbSet Data { get; set; } = default!; - } -} diff --git a/Timeline/Entities/JwtTokenEntity.cs b/Timeline/Entities/JwtTokenEntity.cs deleted file mode 100644 index 40cb230a..00000000 --- a/Timeline/Entities/JwtTokenEntity.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Timeline.Entities -{ - [Table("jwt_token")] - public class JwtTokenEntity - { - [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public long Id { get; set; } - - [Required, Column("key")] -#pragma warning disable CA1819 // Properties should not return arrays - public byte[] Key { get; set; } = default!; -#pragma warning restore CA1819 // Properties should not return arrays - } -} diff --git a/Timeline/Entities/TimelineEntity.cs b/Timeline/Entities/TimelineEntity.cs deleted file mode 100644 index 3e592673..00000000 --- a/Timeline/Entities/TimelineEntity.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using Timeline.Models; - -namespace Timeline.Entities -{ -#pragma warning disable CA2227 // Collection properties should be read only - // TODO: Create index for this table. - [Table("timelines")] - public class TimelineEntity - { - [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public long Id { get; set; } - - [Column("unique_id"), Required] - public string UniqueId { get; set; } = default!; - - /// - /// If null, then this timeline is a personal timeline. - /// - [Column("name")] - public string? Name { get; set; } - - [Column("title")] - public string? Title { get; set; } - - [Column("name_last_modified")] - public DateTime NameLastModified { get; set; } - - [Column("description")] - public string? Description { get; set; } - - [Column("owner")] - public long OwnerId { get; set; } - - [ForeignKey(nameof(OwnerId))] - public UserEntity Owner { get; set; } = default!; - - [Column("visibility")] - public TimelineVisibility Visibility { get; set; } - - [Column("create_time")] - public DateTime CreateTime { get; set; } - - [Column("last_modified")] - public DateTime LastModified { get; set; } - - [Column("current_post_local_id")] - public long CurrentPostLocalId { get; set; } - - public List Members { get; set; } = default!; - - public List Posts { get; set; } = default!; - } -#pragma warning restore CA2227 // Collection properties should be read only -} diff --git a/Timeline/Entities/TimelineMemberEntity.cs b/Timeline/Entities/TimelineMemberEntity.cs deleted file mode 100644 index e76f2099..00000000 --- a/Timeline/Entities/TimelineMemberEntity.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Timeline.Entities -{ - [Table("timeline_members")] - public class TimelineMemberEntity - { - [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public long Id { get; set; } - - [Column("user")] - public long UserId { get; set; } - - [ForeignKey(nameof(UserId))] - public UserEntity User { get; set; } = default!; - - [Column("timeline")] - public long TimelineId { get; set; } - - [ForeignKey(nameof(TimelineId))] - public TimelineEntity Timeline { get; set; } = default!; - } -} diff --git a/Timeline/Entities/TimelinePostEntity.cs b/Timeline/Entities/TimelinePostEntity.cs deleted file mode 100644 index 07367fba..00000000 --- a/Timeline/Entities/TimelinePostEntity.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Timeline.Entities -{ - [Table("timeline_posts")] - public class TimelinePostEntity - { - [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public long Id { get; set; } - - [Column("local_id")] - public long LocalId { get; set; } - - [Column("timeline")] - public long TimelineId { get; set; } - - [ForeignKey(nameof(TimelineId))] - public TimelineEntity Timeline { get; set; } = default!; - - [Column("author")] - public long? AuthorId { get; set; } - - [ForeignKey(nameof(AuthorId))] - public UserEntity? Author { get; set; } = default!; - - [Column("content_type"), Required] - public string ContentType { get; set; } = default!; - - [Column("content")] - public string? Content { get; set; } - - [Column("extra_content")] - public string? ExtraContent { get; set; } - - [Column("time")] - public DateTime Time { get; set; } - - [Column("last_updated")] - public DateTime LastUpdated { get; set; } - } -} diff --git a/Timeline/Entities/UserAvatarEntity.cs b/Timeline/Entities/UserAvatarEntity.cs deleted file mode 100644 index 3c2720f7..00000000 --- a/Timeline/Entities/UserAvatarEntity.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Timeline.Entities -{ - [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "This is data base entity.")] - [Table("user_avatars")] - public class UserAvatarEntity - { - [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public long Id { get; set; } - - [Column("data_tag")] - public string? DataTag { get; set; } - - [Column("type")] - public string? Type { get; set; } - - [Column("last_modified"), Required] - public DateTime LastModified { get; set; } - - [Column("user"), Required] - public long UserId { get; set; } - - [ForeignKey(nameof(UserId))] - public UserEntity User { get; set; } = default!; - } -} diff --git a/Timeline/Entities/UserEntity.cs b/Timeline/Entities/UserEntity.cs deleted file mode 100644 index 0cfaa335..00000000 --- a/Timeline/Entities/UserEntity.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Timeline.Entities -{ - public static class UserRoles - { - public const string Admin = "admin"; - public const string User = "user"; - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "This is an entity class.")] - [Table("users")] - public class UserEntity - { - [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public long Id { get; set; } - - [Column("unique_id"), Required] - public string UniqueId { get; set; } = default!; - - [Column("username"), Required] - public string Username { get; set; } = default!; - - [Column("username_change_time")] - public DateTime UsernameChangeTime { get; set; } - - [Column("password"), Required] - public string Password { get; set; } = default!; - - [Column("roles"), Required] - public string Roles { get; set; } = default!; - - [Column("version"), Required] - public long Version { get; set; } - - [Column("nickname")] - public string? Nickname { get; set; } - - [Column("create_time")] - public DateTime CreateTime { get; set; } - - [Column("last_modified")] - public DateTime LastModified { get; set; } - - public UserAvatarEntity? Avatar { get; set; } - - public List Timelines { get; set; } = default!; - - public List TimelinePosts { get; set; } = default!; - - public List TimelinesJoined { get; set; } = default!; - } -} diff --git a/Timeline/Entities/UtcDateAnnotation.cs b/Timeline/Entities/UtcDateAnnotation.cs deleted file mode 100644 index 6600e701..00000000 --- a/Timeline/Entities/UtcDateAnnotation.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using System; - -namespace Timeline.Entities -{ - // Copied from https://github.com/dotnet/efcore/issues/4711#issuecomment-589842988 - public static class UtcDateAnnotation - { - private const string IsUtcAnnotation = "IsUtc"; - private static readonly ValueConverter UtcConverter = - new ValueConverter(v => v, v => DateTime.SpecifyKind(v, DateTimeKind.Utc)); - - public static PropertyBuilder IsUtc(this PropertyBuilder builder, bool isUtc = true) => - builder.HasAnnotation(IsUtcAnnotation, isUtc); - - public static bool IsUtc(this IMutableProperty property) => - ((bool?)property.FindAnnotation(IsUtcAnnotation)?.Value) ?? true; - - /// - /// Make sure this is called after configuring all your entities. - /// - public static void ApplyUtcDateTimeConverter(this ModelBuilder builder) - { - foreach (var entityType in builder.Model.GetEntityTypes()) - { - foreach (var property in entityType.GetProperties()) - { - if (!property.IsUtc()) - { - continue; - } - - if (property.ClrType == typeof(DateTime)) - { - property.SetValueConverter(UtcConverter); - } - } - } - } - } -} diff --git a/Timeline/Filters/Header.cs b/Timeline/Filters/Header.cs deleted file mode 100644 index cc5ddd9f..00000000 --- a/Timeline/Filters/Header.cs +++ /dev/null @@ -1,63 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Timeline.Models.Http; - -namespace Timeline.Filters -{ - /// - /// Restrict max content length. - /// - public class MaxContentLengthFilter : IResourceFilter - { - /// - /// - /// - /// Max length. - public MaxContentLengthFilter(long maxByteLength) - { - MaxByteLength = maxByteLength; - } - - /// - /// Max length. - /// - public long MaxByteLength { get; set; } - - /// - public void OnResourceExecuted(ResourceExecutedContext context) - { - } - - /// - public void OnResourceExecuting(ResourceExecutingContext context) - { - var contentLength = context.HttpContext.Request.ContentLength; - if (contentLength != null && contentLength > MaxByteLength) - { - context.Result = new BadRequestObjectResult(ErrorResponse.Common.Content.TooBig(MaxByteLength + "B")); - } - } - } - - /// - /// Restrict max content length. - /// - public class MaxContentLengthAttribute : TypeFilterAttribute - { - /// - /// - /// - /// Max length. - public MaxContentLengthAttribute(long maxByteLength) - : base(typeof(MaxContentLengthFilter)) - { - MaxByteLength = maxByteLength; - Arguments = new object[] { maxByteLength }; - } - - /// - /// Max length. - /// - public long MaxByteLength { get; } - } -} diff --git a/Timeline/Filters/Timeline.cs b/Timeline/Filters/Timeline.cs deleted file mode 100644 index 6a730ee7..00000000 --- a/Timeline/Filters/Timeline.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Timeline.Models.Http; -using Timeline.Services.Exceptions; - -namespace Timeline.Filters -{ - public class CatchTimelineNotExistExceptionAttribute : ExceptionFilterAttribute - { - public override void OnException(ExceptionContext context) - { - if (context.Exception is TimelineNotExistException e) - { - if (e.InnerException is UserNotExistException) - { - if (HttpMethods.IsGet(context.HttpContext.Request.Method)) - context.Result = new NotFoundObjectResult(ErrorResponse.UserCommon.NotExist()); - else - context.Result = new BadRequestObjectResult(ErrorResponse.UserCommon.NotExist()); - } - else - { - if (HttpMethods.IsGet(context.HttpContext.Request.Method)) - context.Result = new NotFoundObjectResult(ErrorResponse.TimelineController.NotExist()); - else - context.Result = new BadRequestObjectResult(ErrorResponse.TimelineController.NotExist()); - } - } - } - } -} diff --git a/Timeline/Formatters/BytesInputFormatter.cs b/Timeline/Formatters/BytesInputFormatter.cs deleted file mode 100644 index ac6537c9..00000000 --- a/Timeline/Formatters/BytesInputFormatter.cs +++ /dev/null @@ -1,79 +0,0 @@ -using Microsoft.AspNetCore.Mvc.Formatters; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; -using System; -using System.Threading.Tasks; -using Timeline.Models; - -namespace Timeline.Formatters -{ - /// - /// Formatter that reads body as bytes. - /// - public class BytesInputFormatter : InputFormatter - { - /// - /// - /// - public BytesInputFormatter() - { - SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/png")); - SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/jpeg")); - SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/gif")); - SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/webp")); - } - - /// - public override bool CanRead(InputFormatterContext context) - { - if (context == null) throw new ArgumentNullException(nameof(context)); - - if (context.ModelType == typeof(ByteData)) - return true; - - return false; - } - - /// - public override async Task ReadRequestBodyAsync(InputFormatterContext context) - { - var request = context.HttpContext.Request; - var contentLength = request.ContentLength; - - var logger = context.HttpContext.RequestServices.GetRequiredService>(); - - if (contentLength == null) - { - logger.LogInformation("Failed to read body as bytes. Content-Length is not set."); - return await InputFormatterResult.FailureAsync(); - } - - if (contentLength == 0) - { - logger.LogInformation("Failed to read body as bytes. Content-Length is 0."); - return await InputFormatterResult.FailureAsync(); - } - - var bodyStream = request.Body; - - var data = new byte[contentLength.Value]; - var bytesRead = await bodyStream.ReadAsync(data); - - if (bytesRead != contentLength) - { - logger.LogInformation("Failed to read body as bytes. Actual length of body is smaller than Content-Length."); - return await InputFormatterResult.FailureAsync(); - } - - var extraByte = new byte[1]; - if (await bodyStream.ReadAsync(extraByte) != 0) - { - logger.LogInformation("Failed to read body as bytes. Actual length of body is greater than Content-Length."); - return await InputFormatterResult.FailureAsync(); - } - - return await InputFormatterResult.SuccessAsync(new ByteData(data, request.ContentType)); - } - } -} diff --git a/Timeline/Formatters/StringInputFormatter.cs b/Timeline/Formatters/StringInputFormatter.cs deleted file mode 100644 index b1924268..00000000 --- a/Timeline/Formatters/StringInputFormatter.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Microsoft.AspNetCore.Mvc.Formatters; -using Microsoft.Net.Http.Headers; -using System.IO; -using System.Net.Mime; -using System.Text; -using System.Threading.Tasks; - -namespace Timeline.Formatters -{ - public class StringInputFormatter : TextInputFormatter - { - public StringInputFormatter() - { - SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(MediaTypeNames.Text.Plain)); - SupportedEncodings.Add(Encoding.UTF8); - } - - public override async Task ReadRequestBodyAsync(InputFormatterContext context, Encoding effectiveEncoding) - { - var request = context.HttpContext.Request; - using var reader = new StreamReader(request.Body, effectiveEncoding); - var stringContent = await reader.ReadToEndAsync(); - return await InputFormatterResult.SuccessAsync(stringContent); - } - } -} diff --git a/Timeline/GlobalSuppressions.cs b/Timeline/GlobalSuppressions.cs deleted file mode 100644 index 2b0da576..00000000 --- a/Timeline/GlobalSuppressions.cs +++ /dev/null @@ -1,14 +0,0 @@ -// This file is used by Code Analysis to maintain SuppressMessage -// attributes that are applied to this project. -// Project-level suppressions either have no target or are given -// a specific target and scoped to a namespace, type, member, etc. - -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "This is not a UI application.")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "This is not bad.")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "No need to check the null because it's ASP.Net's duty.", Scope = "namespaceanddescendants", Target = "Timeline.Controllers")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Migrations code are auto generated.", Scope = "namespaceanddescendants", Target = "Timeline.Migrations")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Generated error response identifiers.", Scope = "type", Target = "Timeline.Models.Http.ErrorResponse")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1724:Type names should not match namespaces", Justification = "Generated error response identifiers.", Scope = "type", Target = "Timeline.Models.Http.ErrorResponse")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "Generated error response.", Scope = "type", Target = "Timeline.Models.Http.ErrorResponse")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1056:Uri properties should not be strings", Justification = "That's unnecessary.")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Adundant")] diff --git a/Timeline/Helpers/DataCacheHelper.cs b/Timeline/Helpers/DataCacheHelper.cs deleted file mode 100644 index 1ad69708..00000000 --- a/Timeline/Helpers/DataCacheHelper.cs +++ /dev/null @@ -1,125 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; -using System; -using System.Linq; -using System.Threading.Tasks; -using Timeline.Models.Http; -using static Timeline.Resources.Helper.DataCacheHelper; - -namespace Timeline.Helpers -{ - public interface ICacheableData - { - string Type { get; } -#pragma warning disable CA1819 // Properties should not return arrays - byte[] Data { get; } -#pragma warning restore CA1819 // Properties should not return arrays - DateTime? LastModified { get; } - } - - public class CacheableData : ICacheableData - { - public CacheableData(string type, byte[] data, DateTime? lastModified) - { - Type = type; - Data = data; - LastModified = lastModified; - } - - public string Type { get; set; } -#pragma warning disable CA1819 // Properties should not return arrays - public byte[] Data { get; set; } -#pragma warning restore CA1819 // Properties should not return arrays - public DateTime? LastModified { get; set; } - } - - public interface ICacheableDataProvider - { - Task GetDataETag(); - Task GetData(); - } - - public class DelegateCacheableDataProvider : ICacheableDataProvider - { - private readonly Func> _getDataETagDelegate; - private readonly Func> _getDataDelegate; - - public DelegateCacheableDataProvider(Func> getDataETagDelegate, Func> getDataDelegate) - { - _getDataETagDelegate = getDataETagDelegate; - _getDataDelegate = getDataDelegate; - } - - public Task GetData() - { - return _getDataDelegate(); - } - - public Task GetDataETag() - { - return _getDataETagDelegate(); - } - } - - public static class DataCacheHelper - { - public static async Task GenerateActionResult(Controller controller, ICacheableDataProvider provider, TimeSpan? maxAge = null) - { - const string CacheControlHeaderKey = "Cache-Control"; - const string IfNonMatchHeaderKey = "If-None-Match"; - const string ETagHeaderKey = "ETag"; - - string GenerateCacheControlHeaderValue() - { - var cacheControlHeader = new CacheControlHeaderValue() - { - NoCache = true, - NoStore = false, - MaxAge = maxAge ?? TimeSpan.FromDays(14), - Private = true, - MustRevalidate = true - }; - return cacheControlHeader.ToString(); - } - - var loggerFactory = controller.HttpContext.RequestServices.GetRequiredService(); - var logger = loggerFactory.CreateLogger(typeof(DataCacheHelper)); - - var eTagValue = await provider.GetDataETag(); - eTagValue = '"' + eTagValue + '"'; - var eTag = new EntityTagHeaderValue(eTagValue); - - - if (controller.Request.Headers.TryGetValue(IfNonMatchHeaderKey, out var value)) - { - if (!EntityTagHeaderValue.TryParseStrictList(value, out var eTagList)) - { - logger.LogInformation(Log.Format(LogBadIfNoneMatch, ("Header Value", value))); - return controller.BadRequest(ErrorResponse.Common.Header.IfNonMatch_BadFormat()); - } - - if (eTagList.FirstOrDefault(e => e.Equals(eTag)) != null) - { - logger.LogInformation(LogResultNotModified); - controller.Response.Headers.Add(ETagHeaderKey, eTagValue); - controller.Response.Headers.Add(CacheControlHeaderKey, GenerateCacheControlHeaderValue()); - - return controller.StatusCode(StatusCodes.Status304NotModified, null); - } - } - - var data = await provider.GetData(); - logger.LogInformation(LogResultData); - controller.Response.Headers.Add(CacheControlHeaderKey, GenerateCacheControlHeaderValue()); - return controller.File(data.Data, data.Type, data.LastModified, eTag); - } - - public static Task GenerateActionResult(Controller controller, Func> getDataETagDelegate, Func> getDataDelegate, TimeSpan? maxAge = null) - { - return GenerateActionResult(controller, new DelegateCacheableDataProvider(getDataETagDelegate, getDataDelegate), maxAge); - } - } -} diff --git a/Timeline/Helpers/DateTimeExtensions.cs b/Timeline/Helpers/DateTimeExtensions.cs deleted file mode 100644 index 374f3bc9..00000000 --- a/Timeline/Helpers/DateTimeExtensions.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace Timeline.Helpers -{ - public static class DateTimeExtensions - { - public static DateTime MyToUtc(this DateTime dateTime) - { - if (dateTime.Kind == DateTimeKind.Utc) return dateTime; - if (dateTime.Kind == DateTimeKind.Local) return dateTime.ToUniversalTime(); - return DateTime.SpecifyKind(dateTime, DateTimeKind.Utc); - } - } -} diff --git a/Timeline/Helpers/InvalidModelResponseFactory.cs b/Timeline/Helpers/InvalidModelResponseFactory.cs deleted file mode 100644 index 9b253e7d..00000000 --- a/Timeline/Helpers/InvalidModelResponseFactory.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using System.Text; -using Timeline.Models.Http; - -namespace Timeline.Helpers -{ - public static class InvalidModelResponseFactory - { - public static IActionResult Factory(ActionContext context) - { - var modelState = context.ModelState; - - var messageBuilder = new StringBuilder(); - foreach (var model in modelState) - foreach (var error in model.Value.Errors) - { - messageBuilder.Append(model.Key); - messageBuilder.Append(" : "); - messageBuilder.AppendLine(error.ErrorMessage); - } - - return new BadRequestObjectResult(ErrorResponse.Common.CustomMessage_InvalidModel(messageBuilder.ToString())); - } - } -} diff --git a/Timeline/Helpers/LanguageHelper.cs b/Timeline/Helpers/LanguageHelper.cs deleted file mode 100644 index b0156b8b..00000000 --- a/Timeline/Helpers/LanguageHelper.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Linq; - -namespace Timeline.Helpers -{ - public static class LanguageHelper - { - public static bool AreSame(this bool firstBool, params bool[] otherBools) - { - return otherBools.All(b => b == firstBool); - } - } -} diff --git a/Timeline/Helpers/Log.cs b/Timeline/Helpers/Log.cs deleted file mode 100644 index af0b7e13..00000000 --- a/Timeline/Helpers/Log.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Text; - -namespace Timeline.Helpers -{ - public static class Log - { - public static string Format(string summary, params (string, object?)[] properties) - { - var builder = new StringBuilder(); - builder.Append(summary); - foreach (var property in properties) - { - var (key, value) = property; - builder.AppendLine(); - builder.Append(key); - builder.Append(" : "); - builder.Append(value); - } - return builder.ToString(); - } - } -} diff --git a/Timeline/Migrations/20200105150407_Initialize.Designer.cs b/Timeline/Migrations/20200105150407_Initialize.Designer.cs deleted file mode 100644 index 99e4eaac..00000000 --- a/Timeline/Migrations/20200105150407_Initialize.Designer.cs +++ /dev/null @@ -1,266 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Timeline.Entities; - -namespace Timeline.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20200105150407_Initialize")] - partial class Initialize - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "3.1.0"); - - modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("CreateTime") - .HasColumnName("create_time") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnName("description") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnName("name") - .HasColumnType("TEXT"); - - b.Property("OwnerId") - .HasColumnName("owner") - .HasColumnType("INTEGER"); - - b.Property("Visibility") - .HasColumnName("visibility") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("OwnerId"); - - b.ToTable("timelines"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("TimelineId") - .HasColumnName("timeline") - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnName("user") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("TimelineId"); - - b.HasIndex("UserId"); - - b.ToTable("timeline_members"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("AuthorId") - .HasColumnName("author") - .HasColumnType("INTEGER"); - - b.Property("Content") - .HasColumnName("content") - .HasColumnType("TEXT"); - - b.Property("LastUpdated") - .HasColumnName("last_updated") - .HasColumnType("TEXT"); - - b.Property("Time") - .HasColumnName("time") - .HasColumnType("TEXT"); - - b.Property("TimelineId") - .HasColumnName("timeline") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AuthorId"); - - b.HasIndex("TimelineId"); - - b.ToTable("timeline_posts"); - }); - - modelBuilder.Entity("Timeline.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("EncryptedPassword") - .IsRequired() - .HasColumnName("password") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasColumnName("name") - .HasColumnType("TEXT"); - - b.Property("RoleString") - .IsRequired() - .HasColumnName("roles") - .HasColumnType("TEXT"); - - b.Property("Version") - .ValueGeneratedOnAdd() - .HasColumnName("version") - .HasColumnType("INTEGER") - .HasDefaultValue(0L); - - b.HasKey("Id"); - - b.HasIndex("Name") - .IsUnique(); - - b.ToTable("users"); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatar", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Data") - .HasColumnName("data") - .HasColumnType("BLOB"); - - b.Property("ETag") - .HasColumnName("etag") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnName("last_modified") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnName("type") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId") - .IsUnique(); - - b.ToTable("user_avatars"); - }); - - modelBuilder.Entity("Timeline.Entities.UserDetail", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Nickname") - .HasColumnName("nickname") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId") - .IsUnique(); - - b.ToTable("user_details"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => - { - b.HasOne("Timeline.Entities.User", "Owner") - .WithMany("Timelines") - .HasForeignKey("OwnerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => - { - b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") - .WithMany("Members") - .HasForeignKey("TimelineId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Timeline.Entities.User", "User") - .WithMany("TimelinesJoined") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => - { - b.HasOne("Timeline.Entities.User", "Author") - .WithMany("TimelinePosts") - .HasForeignKey("AuthorId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") - .WithMany("Posts") - .HasForeignKey("TimelineId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatar", b => - { - b.HasOne("Timeline.Entities.User", null) - .WithOne("Avatar") - .HasForeignKey("Timeline.Entities.UserAvatar", "UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.UserDetail", b => - { - b.HasOne("Timeline.Entities.User", null) - .WithOne("Detail") - .HasForeignKey("Timeline.Entities.UserDetail", "UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Timeline/Migrations/20200105150407_Initialize.cs b/Timeline/Migrations/20200105150407_Initialize.cs deleted file mode 100644 index 4e12ef83..00000000 --- a/Timeline/Migrations/20200105150407_Initialize.cs +++ /dev/null @@ -1,217 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Timeline.Migrations -{ - public partial class Initialize : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "users", - columns: table => new - { - id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true), - name = table.Column(nullable: false), - password = table.Column(nullable: false), - roles = table.Column(nullable: false), - version = table.Column(nullable: false, defaultValue: 0L) - .Annotation("Sqlite:Autoincrement", true) - }, - constraints: table => - { - table.PrimaryKey("PK_users", x => x.id); - }); - - migrationBuilder.CreateTable( - name: "timelines", - columns: table => new - { - id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true), - name = table.Column(nullable: true), - description = table.Column(nullable: true), - owner = table.Column(nullable: false), - visibility = table.Column(nullable: false), - create_time = table.Column(nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_timelines", x => x.id); - table.ForeignKey( - name: "FK_timelines_users_owner", - column: x => x.owner, - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "user_avatars", - columns: table => new - { - id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true), - data = table.Column(nullable: true), - type = table.Column(nullable: true), - etag = table.Column(nullable: true), - last_modified = table.Column(nullable: false), - UserId = table.Column(nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_user_avatars", x => x.id); - table.ForeignKey( - name: "FK_user_avatars_users_UserId", - column: x => x.UserId, - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "user_details", - columns: table => new - { - id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true), - nickname = table.Column(nullable: true), - UserId = table.Column(nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_user_details", x => x.id); - table.ForeignKey( - name: "FK_user_details_users_UserId", - column: x => x.UserId, - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "timeline_members", - columns: table => new - { - id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true), - user = table.Column(nullable: false), - timeline = table.Column(nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_timeline_members", x => x.id); - table.ForeignKey( - name: "FK_timeline_members_timelines_timeline", - column: x => x.timeline, - principalTable: "timelines", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_timeline_members_users_user", - column: x => x.user, - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "timeline_posts", - columns: table => new - { - id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true), - timeline = table.Column(nullable: false), - author = table.Column(nullable: false), - content = table.Column(nullable: true), - time = table.Column(nullable: false), - last_updated = table.Column(nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_timeline_posts", x => x.id); - table.ForeignKey( - name: "FK_timeline_posts_users_author", - column: x => x.author, - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_timeline_posts_timelines_timeline", - column: x => x.timeline, - principalTable: "timelines", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_timeline_members_timeline", - table: "timeline_members", - column: "timeline"); - - migrationBuilder.CreateIndex( - name: "IX_timeline_members_user", - table: "timeline_members", - column: "user"); - - migrationBuilder.CreateIndex( - name: "IX_timeline_posts_author", - table: "timeline_posts", - column: "author"); - - migrationBuilder.CreateIndex( - name: "IX_timeline_posts_timeline", - table: "timeline_posts", - column: "timeline"); - - migrationBuilder.CreateIndex( - name: "IX_timelines_owner", - table: "timelines", - column: "owner"); - - migrationBuilder.CreateIndex( - name: "IX_user_avatars_UserId", - table: "user_avatars", - column: "UserId", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_user_details_UserId", - table: "user_details", - column: "UserId", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_users_name", - table: "users", - column: "name", - unique: true); - - // Add a init user. Username is "administrator". Password is "crupest". - migrationBuilder.InsertData("users", new string[] { "name", "password", "roles" }, - new object[] { "administrator", "AQAAAAEAACcQAAAAENsspZrk8Wo+UuMyg6QuWJsNvRg6gVu4K/TumVod3h9GVLX9zDVuQQds3o7V8QWJ2w==", "user,admin" }); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "timeline_members"); - - migrationBuilder.DropTable( - name: "timeline_posts"); - - migrationBuilder.DropTable( - name: "user_avatars"); - - migrationBuilder.DropTable( - name: "user_details"); - - migrationBuilder.DropTable( - name: "timelines"); - - migrationBuilder.DropTable( - name: "users"); - } - } -} diff --git a/Timeline/Migrations/20200131100517_RefactorUser.Designer.cs b/Timeline/Migrations/20200131100517_RefactorUser.Designer.cs deleted file mode 100644 index 9b78eb15..00000000 --- a/Timeline/Migrations/20200131100517_RefactorUser.Designer.cs +++ /dev/null @@ -1,240 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Timeline.Entities; - -namespace Timeline.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20200131100517_RefactorUser")] - partial class RefactorUser - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "3.1.1"); - - modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("CreateTime") - .HasColumnName("create_time") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnName("description") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnName("name") - .HasColumnType("TEXT"); - - b.Property("OwnerId") - .HasColumnName("owner") - .HasColumnType("INTEGER"); - - b.Property("Visibility") - .HasColumnName("visibility") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("OwnerId"); - - b.ToTable("timelines"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("TimelineId") - .HasColumnName("timeline") - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnName("user") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("TimelineId"); - - b.HasIndex("UserId"); - - b.ToTable("timeline_members"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("AuthorId") - .HasColumnName("author") - .HasColumnType("INTEGER"); - - b.Property("Content") - .HasColumnName("content") - .HasColumnType("TEXT"); - - b.Property("LastUpdated") - .HasColumnName("last_updated") - .HasColumnType("TEXT"); - - b.Property("Time") - .HasColumnName("time") - .HasColumnType("TEXT"); - - b.Property("TimelineId") - .HasColumnName("timeline") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AuthorId"); - - b.HasIndex("TimelineId"); - - b.ToTable("timeline_posts"); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Data") - .HasColumnName("data") - .HasColumnType("BLOB"); - - b.Property("ETag") - .HasColumnName("etag") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnName("last_modified") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnName("type") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnName("user") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId") - .IsUnique(); - - b.ToTable("user_avatars"); - }); - - modelBuilder.Entity("Timeline.Entities.UserEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Nickname") - .HasColumnName("nickname") - .HasColumnType("TEXT"); - - b.Property("Password") - .IsRequired() - .HasColumnName("password") - .HasColumnType("TEXT"); - - b.Property("Roles") - .IsRequired() - .HasColumnName("roles") - .HasColumnType("TEXT"); - - b.Property("Username") - .IsRequired() - .HasColumnName("username") - .HasColumnType("TEXT"); - - b.Property("Version") - .ValueGeneratedOnAdd() - .HasColumnName("version") - .HasColumnType("INTEGER") - .HasDefaultValue(0L); - - b.HasKey("Id"); - - b.HasIndex("Username") - .IsUnique(); - - b.ToTable("users"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "Owner") - .WithMany("Timelines") - .HasForeignKey("OwnerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => - { - b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") - .WithMany("Members") - .HasForeignKey("TimelineId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Timeline.Entities.UserEntity", "User") - .WithMany("TimelinesJoined") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "Author") - .WithMany("TimelinePosts") - .HasForeignKey("AuthorId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") - .WithMany("Posts") - .HasForeignKey("TimelineId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "User") - .WithOne("Avatar") - .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Timeline/Migrations/20200131100517_RefactorUser.cs b/Timeline/Migrations/20200131100517_RefactorUser.cs deleted file mode 100644 index 8597ed50..00000000 --- a/Timeline/Migrations/20200131100517_RefactorUser.cs +++ /dev/null @@ -1,128 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Timeline.Migrations -{ - public partial class RefactorUser : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.RenameColumn(name: "name", table: "users", newName: "username"); - migrationBuilder.RenameIndex(name: "IX_users_name", table: "users", newName: "IX_users_username"); - - migrationBuilder.AddColumn( - name: "nickname", - table: "users", - nullable: true); - - migrationBuilder.Sql(@" -UPDATE users - SET nickname = ( - SELECT nickname - FROM user_details - WHERE user_details.UserId = users.id - ); - "); - - /* - migrationBuilder.RenameColumn(name: "UserId", table: "user_avatars", newName: "user"); - - migrationBuilder.DropForeignKey( - name: "FK_user_avatars_users_UserId", - table: "user_avatars"); - - migrationBuilder.AddForeignKey( - name: "FK_user_avatars_users_user", - table: "user_avatars", - column: "user", - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - - migrationBuilder.RenameIndex( - name: "IX_user_avatars_UserId", - table: "user_avatars", - newName: "IX_user_avatars_user"); - */ - - migrationBuilder.Sql(@" -CREATE TABLE user_avatars_backup ( - id INTEGER NOT NULL - CONSTRAINT PK_user_avatars PRIMARY KEY AUTOINCREMENT, - data BLOB, - type TEXT, - etag TEXT, - last_modified TEXT NOT NULL, - user INTEGER NOT NULL, - CONSTRAINT FK_user_avatars_users_user FOREIGN KEY ( - user - ) - REFERENCES users (id) ON DELETE CASCADE -); - -INSERT INTO user_avatars_backup (id, data, type, etag, last_modified, user) - SELECT id, data, type, etag, last_modified, UserId FROM user_avatars; - -DROP TABLE user_avatars; - -ALTER TABLE user_avatars_backup - RENAME TO user_avatars; - -CREATE UNIQUE INDEX IX_user_avatars_user ON user_avatars (user); - "); - - // migrationBuilder.DropTable(name: "user_details"); - - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.Sql(@" -CREATE TABLE user_avatars_backup ( - id INTEGER NOT NULL - CONSTRAINT PK_user_avatars PRIMARY KEY AUTOINCREMENT, - data BLOB, - type TEXT, - etag TEXT, - last_modified TEXT NOT NULL, - UserId INTEGER NOT NULL, - CONSTRAINT FK_user_avatars_users_UserId FOREIGN KEY ( - user - ) - REFERENCES users (id) ON DELETE CASCADE -); - -INSERT INTO user_avatars_backup (id, data, type, etag, last_modified, UserId) - SELECT id, data, type, etag, last_modified, user FROM user_avatars; - -DROP TABLE user_avatars; - -ALTER TABLE user_avatars_backup - RENAME TO user_avatars; - -CREATE UNIQUE INDEX IX_user_avatars_UserId ON user_avatars (UserId); - "); - - migrationBuilder.Sql(@" -CREATE TABLE users_backup ( - id INTEGER NOT NULL - CONSTRAINT PK_users PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - password TEXT NOT NULL, - roles TEXT NOT NULL, - version INTEGER NOT NULL - DEFAULT 0 -); - -INSERT INTO users_backup (id, name, password, roles, version) - SELECT id, username, password, roles, version FROM users; - -DROP TABLE users; - -ALTER TABLE users_backup - RENAME TO users; - -CREATE UNIQUE INDEX IX_users_name ON users (name); - "); - } - } -} diff --git a/Timeline/Migrations/20200221064341_AddJwtToken.Designer.cs b/Timeline/Migrations/20200221064341_AddJwtToken.Designer.cs deleted file mode 100644 index eb328b52..00000000 --- a/Timeline/Migrations/20200221064341_AddJwtToken.Designer.cs +++ /dev/null @@ -1,257 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Timeline.Entities; - -namespace Timeline.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20200221064341_AddJwtToken")] - partial class AddJwtToken - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "3.1.2"); - - modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Key") - .IsRequired() - .HasColumnName("key") - .HasColumnType("BLOB"); - - b.HasKey("Id"); - - b.ToTable("jwt_token"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("CreateTime") - .HasColumnName("create_time") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnName("description") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnName("name") - .HasColumnType("TEXT"); - - b.Property("OwnerId") - .HasColumnName("owner") - .HasColumnType("INTEGER"); - - b.Property("Visibility") - .HasColumnName("visibility") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("OwnerId"); - - b.ToTable("timelines"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("TimelineId") - .HasColumnName("timeline") - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnName("user") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("TimelineId"); - - b.HasIndex("UserId"); - - b.ToTable("timeline_members"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("AuthorId") - .HasColumnName("author") - .HasColumnType("INTEGER"); - - b.Property("Content") - .HasColumnName("content") - .HasColumnType("TEXT"); - - b.Property("LastUpdated") - .HasColumnName("last_updated") - .HasColumnType("TEXT"); - - b.Property("Time") - .HasColumnName("time") - .HasColumnType("TEXT"); - - b.Property("TimelineId") - .HasColumnName("timeline") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AuthorId"); - - b.HasIndex("TimelineId"); - - b.ToTable("timeline_posts"); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Data") - .HasColumnName("data") - .HasColumnType("BLOB"); - - b.Property("ETag") - .HasColumnName("etag") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnName("last_modified") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnName("type") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnName("user") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId") - .IsUnique(); - - b.ToTable("user_avatars"); - }); - - modelBuilder.Entity("Timeline.Entities.UserEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Nickname") - .HasColumnName("nickname") - .HasColumnType("TEXT"); - - b.Property("Password") - .IsRequired() - .HasColumnName("password") - .HasColumnType("TEXT"); - - b.Property("Roles") - .IsRequired() - .HasColumnName("roles") - .HasColumnType("TEXT"); - - b.Property("Username") - .IsRequired() - .HasColumnName("username") - .HasColumnType("TEXT"); - - b.Property("Version") - .ValueGeneratedOnAdd() - .HasColumnName("version") - .HasColumnType("INTEGER") - .HasDefaultValue(0L); - - b.HasKey("Id"); - - b.HasIndex("Username") - .IsUnique(); - - b.ToTable("users"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "Owner") - .WithMany("Timelines") - .HasForeignKey("OwnerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => - { - b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") - .WithMany("Members") - .HasForeignKey("TimelineId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Timeline.Entities.UserEntity", "User") - .WithMany("TimelinesJoined") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "Author") - .WithMany("TimelinePosts") - .HasForeignKey("AuthorId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") - .WithMany("Posts") - .HasForeignKey("TimelineId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "User") - .WithOne("Avatar") - .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Timeline/Migrations/20200221064341_AddJwtToken.cs b/Timeline/Migrations/20200221064341_AddJwtToken.cs deleted file mode 100644 index 628970c6..00000000 --- a/Timeline/Migrations/20200221064341_AddJwtToken.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Security.Cryptography; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Timeline.Migrations -{ - public static class JwtTokenGenerateHelper - { - public static byte[] GenerateKey() - { - using var random = RandomNumberGenerator.Create(); - var key = new byte[16]; - random.GetBytes(key); - return key; - } - } - - public partial class AddJwtToken : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "jwt_token", - columns: table => new - { - id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true), - key = table.Column(nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_jwt_token", x => x.id); - }); - - - migrationBuilder.InsertData("jwt_token", "key", JwtTokenGenerateHelper.GenerateKey()); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "jwt_token"); - } - } -} diff --git a/Timeline/Migrations/20200229103848_AddPostLocalId.Designer.cs b/Timeline/Migrations/20200229103848_AddPostLocalId.Designer.cs deleted file mode 100644 index cf6ae8a3..00000000 --- a/Timeline/Migrations/20200229103848_AddPostLocalId.Designer.cs +++ /dev/null @@ -1,265 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Timeline.Entities; - -namespace Timeline.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20200229103848_AddPostLocalId")] - partial class AddPostLocalId - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "3.1.2"); - - modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Key") - .IsRequired() - .HasColumnName("key") - .HasColumnType("BLOB"); - - b.HasKey("Id"); - - b.ToTable("jwt_token"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("CreateTime") - .HasColumnName("create_time") - .HasColumnType("TEXT"); - - b.Property("CurrentPostLocalId") - .HasColumnName("current_post_local_id") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnName("description") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnName("name") - .HasColumnType("TEXT"); - - b.Property("OwnerId") - .HasColumnName("owner") - .HasColumnType("INTEGER"); - - b.Property("Visibility") - .HasColumnName("visibility") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("OwnerId"); - - b.ToTable("timelines"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("TimelineId") - .HasColumnName("timeline") - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnName("user") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("TimelineId"); - - b.HasIndex("UserId"); - - b.ToTable("timeline_members"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("AuthorId") - .HasColumnName("author") - .HasColumnType("INTEGER"); - - b.Property("Content") - .HasColumnName("content") - .HasColumnType("TEXT"); - - b.Property("LastUpdated") - .HasColumnName("last_updated") - .HasColumnType("TEXT"); - - b.Property("LocalId") - .HasColumnName("local_id") - .HasColumnType("INTEGER"); - - b.Property("Time") - .HasColumnName("time") - .HasColumnType("TEXT"); - - b.Property("TimelineId") - .HasColumnName("timeline") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AuthorId"); - - b.HasIndex("TimelineId"); - - b.ToTable("timeline_posts"); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Data") - .HasColumnName("data") - .HasColumnType("BLOB"); - - b.Property("ETag") - .HasColumnName("etag") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnName("last_modified") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnName("type") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnName("user") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId") - .IsUnique(); - - b.ToTable("user_avatars"); - }); - - modelBuilder.Entity("Timeline.Entities.UserEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Nickname") - .HasColumnName("nickname") - .HasColumnType("TEXT"); - - b.Property("Password") - .IsRequired() - .HasColumnName("password") - .HasColumnType("TEXT"); - - b.Property("Roles") - .IsRequired() - .HasColumnName("roles") - .HasColumnType("TEXT"); - - b.Property("Username") - .IsRequired() - .HasColumnName("username") - .HasColumnType("TEXT"); - - b.Property("Version") - .ValueGeneratedOnAdd() - .HasColumnName("version") - .HasColumnType("INTEGER") - .HasDefaultValue(0L); - - b.HasKey("Id"); - - b.HasIndex("Username") - .IsUnique(); - - b.ToTable("users"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "Owner") - .WithMany("Timelines") - .HasForeignKey("OwnerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => - { - b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") - .WithMany("Members") - .HasForeignKey("TimelineId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Timeline.Entities.UserEntity", "User") - .WithMany("TimelinesJoined") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "Author") - .WithMany("TimelinePosts") - .HasForeignKey("AuthorId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") - .WithMany("Posts") - .HasForeignKey("TimelineId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "User") - .WithOne("Avatar") - .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Timeline/Migrations/20200229103848_AddPostLocalId.cs b/Timeline/Migrations/20200229103848_AddPostLocalId.cs deleted file mode 100644 index 497b38a1..00000000 --- a/Timeline/Migrations/20200229103848_AddPostLocalId.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Timeline.Migrations -{ - public partial class AddPostLocalId : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - - migrationBuilder.AddColumn( - name: "current_post_local_id", - table: "timelines", - nullable: false, - defaultValue: 0L); - - migrationBuilder.AddColumn( - name: "local_id", - table: "timeline_posts", - nullable: false, - defaultValue: 0L); - - migrationBuilder.Sql(@" -UPDATE timeline_posts -SET local_id = (SELECT COUNT (*) - FROM timeline_posts AS p - WHERE p.timeline = timeline_posts.timeline - AND p.id <= timeline_posts.id); - -UPDATE timelines -SET current_post_local_id = (SELECT COUNT (*) - FROM timeline_posts AS p - WHERE p.timeline = timelines.id); - "); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - - } - } -} diff --git a/Timeline/Migrations/20200306110049_AddDataTable.Designer.cs b/Timeline/Migrations/20200306110049_AddDataTable.Designer.cs deleted file mode 100644 index 336ffc18..00000000 --- a/Timeline/Migrations/20200306110049_AddDataTable.Designer.cs +++ /dev/null @@ -1,290 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Timeline.Entities; - -namespace Timeline.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20200306110049_AddDataTable")] - partial class AddDataTable - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "3.1.2"); - - modelBuilder.Entity("Timeline.Entities.DataEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Data") - .IsRequired() - .HasColumnName("data") - .HasColumnType("BLOB"); - - b.Property("Ref") - .HasColumnName("ref") - .HasColumnType("INTEGER"); - - b.Property("Tag") - .IsRequired() - .HasColumnName("tag") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Tag") - .IsUnique(); - - b.ToTable("data"); - }); - - modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Key") - .IsRequired() - .HasColumnName("key") - .HasColumnType("BLOB"); - - b.HasKey("Id"); - - b.ToTable("jwt_token"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("CreateTime") - .HasColumnName("create_time") - .HasColumnType("TEXT"); - - b.Property("CurrentPostLocalId") - .HasColumnName("current_post_local_id") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnName("description") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnName("name") - .HasColumnType("TEXT"); - - b.Property("OwnerId") - .HasColumnName("owner") - .HasColumnType("INTEGER"); - - b.Property("Visibility") - .HasColumnName("visibility") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("OwnerId"); - - b.ToTable("timelines"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("TimelineId") - .HasColumnName("timeline") - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnName("user") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("TimelineId"); - - b.HasIndex("UserId"); - - b.ToTable("timeline_members"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("AuthorId") - .HasColumnName("author") - .HasColumnType("INTEGER"); - - b.Property("Content") - .HasColumnName("content") - .HasColumnType("TEXT"); - - b.Property("LastUpdated") - .HasColumnName("last_updated") - .HasColumnType("TEXT"); - - b.Property("LocalId") - .HasColumnName("local_id") - .HasColumnType("INTEGER"); - - b.Property("Time") - .HasColumnName("time") - .HasColumnType("TEXT"); - - b.Property("TimelineId") - .HasColumnName("timeline") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AuthorId"); - - b.HasIndex("TimelineId"); - - b.ToTable("timeline_posts"); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("DataTag") - .HasColumnName("data_tag") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnName("last_modified") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnName("type") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnName("user") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId") - .IsUnique(); - - b.ToTable("user_avatars"); - }); - - modelBuilder.Entity("Timeline.Entities.UserEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Nickname") - .HasColumnName("nickname") - .HasColumnType("TEXT"); - - b.Property("Password") - .IsRequired() - .HasColumnName("password") - .HasColumnType("TEXT"); - - b.Property("Roles") - .IsRequired() - .HasColumnName("roles") - .HasColumnType("TEXT"); - - b.Property("Username") - .IsRequired() - .HasColumnName("username") - .HasColumnType("TEXT"); - - b.Property("Version") - .ValueGeneratedOnAdd() - .HasColumnName("version") - .HasColumnType("INTEGER") - .HasDefaultValue(0L); - - b.HasKey("Id"); - - b.HasIndex("Username") - .IsUnique(); - - b.ToTable("users"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "Owner") - .WithMany("Timelines") - .HasForeignKey("OwnerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => - { - b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") - .WithMany("Members") - .HasForeignKey("TimelineId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Timeline.Entities.UserEntity", "User") - .WithMany("TimelinesJoined") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "Author") - .WithMany("TimelinePosts") - .HasForeignKey("AuthorId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") - .WithMany("Posts") - .HasForeignKey("TimelineId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "User") - .WithOne("Avatar") - .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Timeline/Migrations/20200306110049_AddDataTable.cs b/Timeline/Migrations/20200306110049_AddDataTable.cs deleted file mode 100644 index e33bf4c9..00000000 --- a/Timeline/Migrations/20200306110049_AddDataTable.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Timeline.Migrations -{ - public partial class AddDataTable : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "data", - columns: table => new - { - id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true), - tag = table.Column(nullable: false), - data = table.Column(nullable: false), - @ref = table.Column(name: "ref", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_data", x => x.id); - }); - - migrationBuilder.CreateIndex( - name: "IX_data_tag", - table: "data", - column: "tag", - unique: true); - - migrationBuilder.Sql(@" -ALTER TABLE user_avatars - RENAME TO user_avatars_backup; - -CREATE TABLE user_avatars ( - id INTEGER NOT NULL - CONSTRAINT PK_user_avatars PRIMARY KEY AUTOINCREMENT, - data_tag TEXT, - type TEXT, - last_modified TEXT NOT NULL, - user INTEGER NOT NULL, - CONSTRAINT FK_user_avatars_users_user FOREIGN KEY ( - user - ) - REFERENCES users (id) ON DELETE CASCADE -); - -INSERT INTO user_avatars (id, data_tag, type, last_modified, user) - SELECT id, etag, type, last_modified, user FROM user_avatars_backup; - -INSERT OR IGNORE INTO data (tag, data, ref) - SELECT etag, data, 0 FROM user_avatars_backup; - -UPDATE data -SET ref = (SELECT COUNT (*) - FROM user_avatars_backup AS a - WHERE a.etag == data.tag); - -DROP TABLE user_avatars_backup; - -CREATE UNIQUE INDEX IX_user_avatars_user ON user_avatars (user); - "); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "data"); - - migrationBuilder.DropColumn( - name: "data_tag", - table: "user_avatars"); - - migrationBuilder.AddColumn( - name: "data", - table: "user_avatars", - type: "BLOB", - nullable: true); - - migrationBuilder.AddColumn( - name: "etag", - table: "user_avatars", - type: "TEXT", - nullable: true); - } - } -} diff --git a/Timeline/Migrations/20200306111553_DropUserDetails.Designer.cs b/Timeline/Migrations/20200306111553_DropUserDetails.Designer.cs deleted file mode 100644 index f0c4dc08..00000000 --- a/Timeline/Migrations/20200306111553_DropUserDetails.Designer.cs +++ /dev/null @@ -1,290 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Timeline.Entities; - -namespace Timeline.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20200306111553_DropUserDetails")] - partial class DropUserDetails - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "3.1.2"); - - modelBuilder.Entity("Timeline.Entities.DataEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Data") - .IsRequired() - .HasColumnName("data") - .HasColumnType("BLOB"); - - b.Property("Ref") - .HasColumnName("ref") - .HasColumnType("INTEGER"); - - b.Property("Tag") - .IsRequired() - .HasColumnName("tag") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Tag") - .IsUnique(); - - b.ToTable("data"); - }); - - modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Key") - .IsRequired() - .HasColumnName("key") - .HasColumnType("BLOB"); - - b.HasKey("Id"); - - b.ToTable("jwt_token"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("CreateTime") - .HasColumnName("create_time") - .HasColumnType("TEXT"); - - b.Property("CurrentPostLocalId") - .HasColumnName("current_post_local_id") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnName("description") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnName("name") - .HasColumnType("TEXT"); - - b.Property("OwnerId") - .HasColumnName("owner") - .HasColumnType("INTEGER"); - - b.Property("Visibility") - .HasColumnName("visibility") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("OwnerId"); - - b.ToTable("timelines"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("TimelineId") - .HasColumnName("timeline") - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnName("user") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("TimelineId"); - - b.HasIndex("UserId"); - - b.ToTable("timeline_members"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("AuthorId") - .HasColumnName("author") - .HasColumnType("INTEGER"); - - b.Property("Content") - .HasColumnName("content") - .HasColumnType("TEXT"); - - b.Property("LastUpdated") - .HasColumnName("last_updated") - .HasColumnType("TEXT"); - - b.Property("LocalId") - .HasColumnName("local_id") - .HasColumnType("INTEGER"); - - b.Property("Time") - .HasColumnName("time") - .HasColumnType("TEXT"); - - b.Property("TimelineId") - .HasColumnName("timeline") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AuthorId"); - - b.HasIndex("TimelineId"); - - b.ToTable("timeline_posts"); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("DataTag") - .HasColumnName("data_tag") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnName("last_modified") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnName("type") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnName("user") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId") - .IsUnique(); - - b.ToTable("user_avatars"); - }); - - modelBuilder.Entity("Timeline.Entities.UserEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Nickname") - .HasColumnName("nickname") - .HasColumnType("TEXT"); - - b.Property("Password") - .IsRequired() - .HasColumnName("password") - .HasColumnType("TEXT"); - - b.Property("Roles") - .IsRequired() - .HasColumnName("roles") - .HasColumnType("TEXT"); - - b.Property("Username") - .IsRequired() - .HasColumnName("username") - .HasColumnType("TEXT"); - - b.Property("Version") - .ValueGeneratedOnAdd() - .HasColumnName("version") - .HasColumnType("INTEGER") - .HasDefaultValue(0L); - - b.HasKey("Id"); - - b.HasIndex("Username") - .IsUnique(); - - b.ToTable("users"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "Owner") - .WithMany("Timelines") - .HasForeignKey("OwnerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => - { - b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") - .WithMany("Members") - .HasForeignKey("TimelineId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Timeline.Entities.UserEntity", "User") - .WithMany("TimelinesJoined") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "Author") - .WithMany("TimelinePosts") - .HasForeignKey("AuthorId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") - .WithMany("Posts") - .HasForeignKey("TimelineId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "User") - .WithOne("Avatar") - .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Timeline/Migrations/20200306111553_DropUserDetails.cs b/Timeline/Migrations/20200306111553_DropUserDetails.cs deleted file mode 100644 index 0a176461..00000000 --- a/Timeline/Migrations/20200306111553_DropUserDetails.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Timeline.Migrations -{ - public partial class DropUserDetails : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable(name: "user_details"); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - - } - } -} diff --git a/Timeline/Migrations/20200312112552_AddImagePost.Designer.cs b/Timeline/Migrations/20200312112552_AddImagePost.Designer.cs deleted file mode 100644 index bd75a916..00000000 --- a/Timeline/Migrations/20200312112552_AddImagePost.Designer.cs +++ /dev/null @@ -1,299 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Timeline.Entities; - -namespace Timeline.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20200312112552_AddImagePost")] - partial class AddImagePost - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "3.1.2"); - - modelBuilder.Entity("Timeline.Entities.DataEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Data") - .IsRequired() - .HasColumnName("data") - .HasColumnType("BLOB"); - - b.Property("Ref") - .HasColumnName("ref") - .HasColumnType("INTEGER"); - - b.Property("Tag") - .IsRequired() - .HasColumnName("tag") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Tag") - .IsUnique(); - - b.ToTable("data"); - }); - - modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Key") - .IsRequired() - .HasColumnName("key") - .HasColumnType("BLOB"); - - b.HasKey("Id"); - - b.ToTable("jwt_token"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("CreateTime") - .HasColumnName("create_time") - .HasColumnType("TEXT"); - - b.Property("CurrentPostLocalId") - .HasColumnName("current_post_local_id") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnName("description") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnName("name") - .HasColumnType("TEXT"); - - b.Property("OwnerId") - .HasColumnName("owner") - .HasColumnType("INTEGER"); - - b.Property("Visibility") - .HasColumnName("visibility") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("OwnerId"); - - b.ToTable("timelines"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("TimelineId") - .HasColumnName("timeline") - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnName("user") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("TimelineId"); - - b.HasIndex("UserId"); - - b.ToTable("timeline_members"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("AuthorId") - .HasColumnName("author") - .HasColumnType("INTEGER"); - - b.Property("Content") - .HasColumnName("content") - .HasColumnType("TEXT"); - - b.Property("ContentType") - .IsRequired() - .HasColumnName("content_type") - .HasColumnType("TEXT"); - - b.Property("ExtraContent") - .HasColumnName("extra_content") - .HasColumnType("TEXT"); - - b.Property("LastUpdated") - .HasColumnName("last_updated") - .HasColumnType("TEXT"); - - b.Property("LocalId") - .HasColumnName("local_id") - .HasColumnType("INTEGER"); - - b.Property("Time") - .HasColumnName("time") - .HasColumnType("TEXT"); - - b.Property("TimelineId") - .HasColumnName("timeline") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AuthorId"); - - b.HasIndex("TimelineId"); - - b.ToTable("timeline_posts"); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("DataTag") - .HasColumnName("data_tag") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnName("last_modified") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnName("type") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnName("user") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId") - .IsUnique(); - - b.ToTable("user_avatars"); - }); - - modelBuilder.Entity("Timeline.Entities.UserEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Nickname") - .HasColumnName("nickname") - .HasColumnType("TEXT"); - - b.Property("Password") - .IsRequired() - .HasColumnName("password") - .HasColumnType("TEXT"); - - b.Property("Roles") - .IsRequired() - .HasColumnName("roles") - .HasColumnType("TEXT"); - - b.Property("Username") - .IsRequired() - .HasColumnName("username") - .HasColumnType("TEXT"); - - b.Property("Version") - .ValueGeneratedOnAdd() - .HasColumnName("version") - .HasColumnType("INTEGER") - .HasDefaultValue(0L); - - b.HasKey("Id"); - - b.HasIndex("Username") - .IsUnique(); - - b.ToTable("users"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "Owner") - .WithMany("Timelines") - .HasForeignKey("OwnerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => - { - b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") - .WithMany("Members") - .HasForeignKey("TimelineId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Timeline.Entities.UserEntity", "User") - .WithMany("TimelinesJoined") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "Author") - .WithMany("TimelinePosts") - .HasForeignKey("AuthorId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") - .WithMany("Posts") - .HasForeignKey("TimelineId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "User") - .WithOne("Avatar") - .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Timeline/Migrations/20200312112552_AddImagePost.cs b/Timeline/Migrations/20200312112552_AddImagePost.cs deleted file mode 100644 index d5098ce0..00000000 --- a/Timeline/Migrations/20200312112552_AddImagePost.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; -using Timeline.Models; - -namespace Timeline.Migrations -{ - public partial class AddImagePost : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "content_type", - table: "timeline_posts", - nullable: false, - defaultValue: ""); - - migrationBuilder.AddColumn( - name: "extra_content", - table: "timeline_posts", - nullable: true); - - migrationBuilder.Sql($@" -UPDATE timeline_posts -SET content_type = '{TimelinePostContentTypes.Text}'; - "); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "content_type", - table: "timeline_posts"); - - migrationBuilder.DropColumn( - name: "extra_content", - table: "timeline_posts"); - } - } -} diff --git a/Timeline/Migrations/20200614061237_AddTimelineUniqueId.Designer.cs b/Timeline/Migrations/20200614061237_AddTimelineUniqueId.Designer.cs deleted file mode 100644 index adcc6308..00000000 --- a/Timeline/Migrations/20200614061237_AddTimelineUniqueId.Designer.cs +++ /dev/null @@ -1,306 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Timeline.Entities; - -namespace Timeline.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20200614061237_AddTimelineUniqueId")] - partial class AddTimelineUniqueId - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "3.1.4"); - - modelBuilder.Entity("Timeline.Entities.DataEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Data") - .IsRequired() - .HasColumnName("data") - .HasColumnType("BLOB"); - - b.Property("Ref") - .HasColumnName("ref") - .HasColumnType("INTEGER"); - - b.Property("Tag") - .IsRequired() - .HasColumnName("tag") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Tag") - .IsUnique(); - - b.ToTable("data"); - }); - - modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Key") - .IsRequired() - .HasColumnName("key") - .HasColumnType("BLOB"); - - b.HasKey("Id"); - - b.ToTable("jwt_token"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("CreateTime") - .HasColumnName("create_time") - .HasColumnType("TEXT"); - - b.Property("CurrentPostLocalId") - .HasColumnName("current_post_local_id") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnName("description") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnName("name") - .HasColumnType("TEXT"); - - b.Property("OwnerId") - .HasColumnName("owner") - .HasColumnType("INTEGER"); - - b.Property("UniqueId") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnName("unique_id") - .HasColumnType("TEXT") - .HasDefaultValueSql("lower(hex(randomblob(16)))"); - - b.Property("Visibility") - .HasColumnName("visibility") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("OwnerId"); - - b.ToTable("timelines"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("TimelineId") - .HasColumnName("timeline") - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnName("user") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("TimelineId"); - - b.HasIndex("UserId"); - - b.ToTable("timeline_members"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("AuthorId") - .HasColumnName("author") - .HasColumnType("INTEGER"); - - b.Property("Content") - .HasColumnName("content") - .HasColumnType("TEXT"); - - b.Property("ContentType") - .IsRequired() - .HasColumnName("content_type") - .HasColumnType("TEXT"); - - b.Property("ExtraContent") - .HasColumnName("extra_content") - .HasColumnType("TEXT"); - - b.Property("LastUpdated") - .HasColumnName("last_updated") - .HasColumnType("TEXT"); - - b.Property("LocalId") - .HasColumnName("local_id") - .HasColumnType("INTEGER"); - - b.Property("Time") - .HasColumnName("time") - .HasColumnType("TEXT"); - - b.Property("TimelineId") - .HasColumnName("timeline") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AuthorId"); - - b.HasIndex("TimelineId"); - - b.ToTable("timeline_posts"); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("DataTag") - .HasColumnName("data_tag") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnName("last_modified") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnName("type") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnName("user") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId") - .IsUnique(); - - b.ToTable("user_avatars"); - }); - - modelBuilder.Entity("Timeline.Entities.UserEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Nickname") - .HasColumnName("nickname") - .HasColumnType("TEXT"); - - b.Property("Password") - .IsRequired() - .HasColumnName("password") - .HasColumnType("TEXT"); - - b.Property("Roles") - .IsRequired() - .HasColumnName("roles") - .HasColumnType("TEXT"); - - b.Property("Username") - .IsRequired() - .HasColumnName("username") - .HasColumnType("TEXT"); - - b.Property("Version") - .ValueGeneratedOnAdd() - .HasColumnName("version") - .HasColumnType("INTEGER") - .HasDefaultValue(0L); - - b.HasKey("Id"); - - b.HasIndex("Username") - .IsUnique(); - - b.ToTable("users"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "Owner") - .WithMany("Timelines") - .HasForeignKey("OwnerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => - { - b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") - .WithMany("Members") - .HasForeignKey("TimelineId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Timeline.Entities.UserEntity", "User") - .WithMany("TimelinesJoined") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "Author") - .WithMany("TimelinePosts") - .HasForeignKey("AuthorId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") - .WithMany("Posts") - .HasForeignKey("TimelineId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "User") - .WithOne("Avatar") - .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Timeline/Migrations/20200614061237_AddTimelineUniqueId.cs b/Timeline/Migrations/20200614061237_AddTimelineUniqueId.cs deleted file mode 100644 index 7abbed79..00000000 --- a/Timeline/Migrations/20200614061237_AddTimelineUniqueId.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Timeline.Migrations -{ - public partial class AddTimelineUniqueId : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.Sql( -@" -PRAGMA foreign_keys=OFF; - -BEGIN TRANSACTION; - -CREATE TABLE new_timelines ( - id INTEGER NOT NULL CONSTRAINT PK_timelines PRIMARY KEY AUTOINCREMENT, - unique_id TEXT NOT NULL DEFAULT (lower(hex(randomblob(16)))), - name TEXT NULL, - description TEXT NULL, - owner INTEGER NOT NULL, - visibility INTEGER NOT NULL, - create_time TEXT NOT NULL, - current_post_local_id INTEGER NOT NULL DEFAULT 0, - CONSTRAINT FK_timelines_users_owner FOREIGN KEY (owner) REFERENCES users (id) ON DELETE CASCADE -); - -INSERT INTO new_timelines (id, name, description, owner, visibility, create_time, current_post_local_id) - SELECT id, name, description, owner, visibility, create_time, current_post_local_id FROM timelines; - -DROP TABLE timelines; - -ALTER TABLE new_timelines - RENAME TO timelines; - -CREATE INDEX IX_timelines_owner ON timelines (owner); - -PRAGMA foreign_key_check; - -COMMIT TRANSACTION; - -PRAGMA foreign_keys=ON; -" - , true); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - } - } -} diff --git a/Timeline/Migrations/20200618064936_TimelineAddModifiedTime.Designer.cs b/Timeline/Migrations/20200618064936_TimelineAddModifiedTime.Designer.cs deleted file mode 100644 index fd10dfa9..00000000 --- a/Timeline/Migrations/20200618064936_TimelineAddModifiedTime.Designer.cs +++ /dev/null @@ -1,314 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Timeline.Entities; - -namespace Timeline.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20200618064936_TimelineAddModifiedTime")] - partial class TimelineAddModifiedTime - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "3.1.5"); - - modelBuilder.Entity("Timeline.Entities.DataEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Data") - .IsRequired() - .HasColumnName("data") - .HasColumnType("BLOB"); - - b.Property("Ref") - .HasColumnName("ref") - .HasColumnType("INTEGER"); - - b.Property("Tag") - .IsRequired() - .HasColumnName("tag") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Tag") - .IsUnique(); - - b.ToTable("data"); - }); - - modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Key") - .IsRequired() - .HasColumnName("key") - .HasColumnType("BLOB"); - - b.HasKey("Id"); - - b.ToTable("jwt_token"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("CreateTime") - .HasColumnName("create_time") - .HasColumnType("TEXT"); - - b.Property("CurrentPostLocalId") - .HasColumnName("current_post_local_id") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnName("description") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnName("last_modified") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnName("name") - .HasColumnType("TEXT"); - - b.Property("NameLastModified") - .HasColumnName("name_last_modified") - .HasColumnType("TEXT"); - - b.Property("OwnerId") - .HasColumnName("owner") - .HasColumnType("INTEGER"); - - b.Property("UniqueId") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnName("unique_id") - .HasColumnType("TEXT") - .HasDefaultValueSql("lower(hex(randomblob(16)))"); - - b.Property("Visibility") - .HasColumnName("visibility") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("OwnerId"); - - b.ToTable("timelines"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("TimelineId") - .HasColumnName("timeline") - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnName("user") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("TimelineId"); - - b.HasIndex("UserId"); - - b.ToTable("timeline_members"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("AuthorId") - .HasColumnName("author") - .HasColumnType("INTEGER"); - - b.Property("Content") - .HasColumnName("content") - .HasColumnType("TEXT"); - - b.Property("ContentType") - .IsRequired() - .HasColumnName("content_type") - .HasColumnType("TEXT"); - - b.Property("ExtraContent") - .HasColumnName("extra_content") - .HasColumnType("TEXT"); - - b.Property("LastUpdated") - .HasColumnName("last_updated") - .HasColumnType("TEXT"); - - b.Property("LocalId") - .HasColumnName("local_id") - .HasColumnType("INTEGER"); - - b.Property("Time") - .HasColumnName("time") - .HasColumnType("TEXT"); - - b.Property("TimelineId") - .HasColumnName("timeline") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AuthorId"); - - b.HasIndex("TimelineId"); - - b.ToTable("timeline_posts"); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("DataTag") - .HasColumnName("data_tag") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnName("last_modified") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnName("type") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnName("user") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId") - .IsUnique(); - - b.ToTable("user_avatars"); - }); - - modelBuilder.Entity("Timeline.Entities.UserEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Nickname") - .HasColumnName("nickname") - .HasColumnType("TEXT"); - - b.Property("Password") - .IsRequired() - .HasColumnName("password") - .HasColumnType("TEXT"); - - b.Property("Roles") - .IsRequired() - .HasColumnName("roles") - .HasColumnType("TEXT"); - - b.Property("Username") - .IsRequired() - .HasColumnName("username") - .HasColumnType("TEXT"); - - b.Property("Version") - .ValueGeneratedOnAdd() - .HasColumnName("version") - .HasColumnType("INTEGER") - .HasDefaultValue(0L); - - b.HasKey("Id"); - - b.HasIndex("Username") - .IsUnique(); - - b.ToTable("users"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "Owner") - .WithMany("Timelines") - .HasForeignKey("OwnerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => - { - b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") - .WithMany("Members") - .HasForeignKey("TimelineId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Timeline.Entities.UserEntity", "User") - .WithMany("TimelinesJoined") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "Author") - .WithMany("TimelinePosts") - .HasForeignKey("AuthorId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") - .WithMany("Posts") - .HasForeignKey("TimelineId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "User") - .WithOne("Avatar") - .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Timeline/Migrations/20200618064936_TimelineAddModifiedTime.cs b/Timeline/Migrations/20200618064936_TimelineAddModifiedTime.cs deleted file mode 100644 index c277fe39..00000000 --- a/Timeline/Migrations/20200618064936_TimelineAddModifiedTime.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace Timeline.Migrations -{ - public partial class TimelineAddModifiedTime : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - var currentTime = new DateTimeToStringConverter().ConvertToProvider(DateTime.Now); - - migrationBuilder.Sql( -@$" -PRAGMA foreign_keys=OFF; - -BEGIN TRANSACTION; - -CREATE TABLE new_timelines ( - id INTEGER NOT NULL CONSTRAINT PK_timelines PRIMARY KEY AUTOINCREMENT, - unique_id TEXT NOT NULL DEFAULT (lower(hex(randomblob(16)))), - name TEXT NULL, - name_last_modified TEXT NOT NULL, - description TEXT NULL, - owner INTEGER NOT NULL, - visibility INTEGER NOT NULL, - create_time TEXT NOT NULL, - last_modified TEXT NOT NULL, - current_post_local_id INTEGER NOT NULL DEFAULT 0, - CONSTRAINT FK_timelines_users_owner FOREIGN KEY (owner) REFERENCES users (id) ON DELETE CASCADE -); - -INSERT INTO new_timelines (id, unique_id, name, name_last_modified, description, owner, visibility, create_time, last_modified, current_post_local_id) - SELECT id, unique_id, name, '{currentTime}', description, owner, visibility, create_time, '{currentTime}', current_post_local_id FROM timelines; - -DROP TABLE timelines; - -ALTER TABLE new_timelines - RENAME TO timelines; - -CREATE INDEX IX_timelines_owner ON timelines (owner); - -PRAGMA foreign_key_check; - -COMMIT TRANSACTION; - -PRAGMA foreign_keys=ON; -" - , true); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - - } - } -} diff --git a/Timeline/Migrations/20200808071611_UserAddUniqueId.Designer.cs b/Timeline/Migrations/20200808071611_UserAddUniqueId.Designer.cs deleted file mode 100644 index fe2329e4..00000000 --- a/Timeline/Migrations/20200808071611_UserAddUniqueId.Designer.cs +++ /dev/null @@ -1,321 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Timeline.Entities; - -namespace Timeline.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20200808071611_UserAddUniqueId")] - partial class UserAddUniqueId - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "3.1.5"); - - modelBuilder.Entity("Timeline.Entities.DataEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Data") - .IsRequired() - .HasColumnName("data") - .HasColumnType("BLOB"); - - b.Property("Ref") - .HasColumnName("ref") - .HasColumnType("INTEGER"); - - b.Property("Tag") - .IsRequired() - .HasColumnName("tag") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Tag") - .IsUnique(); - - b.ToTable("data"); - }); - - modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Key") - .IsRequired() - .HasColumnName("key") - .HasColumnType("BLOB"); - - b.HasKey("Id"); - - b.ToTable("jwt_token"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("CreateTime") - .HasColumnName("create_time") - .HasColumnType("TEXT"); - - b.Property("CurrentPostLocalId") - .HasColumnName("current_post_local_id") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnName("description") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnName("last_modified") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnName("name") - .HasColumnType("TEXT"); - - b.Property("NameLastModified") - .HasColumnName("name_last_modified") - .HasColumnType("TEXT"); - - b.Property("OwnerId") - .HasColumnName("owner") - .HasColumnType("INTEGER"); - - b.Property("UniqueId") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnName("unique_id") - .HasColumnType("TEXT") - .HasDefaultValueSql("lower(hex(randomblob(16)))"); - - b.Property("Visibility") - .HasColumnName("visibility") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("OwnerId"); - - b.ToTable("timelines"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("TimelineId") - .HasColumnName("timeline") - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnName("user") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("TimelineId"); - - b.HasIndex("UserId"); - - b.ToTable("timeline_members"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("AuthorId") - .HasColumnName("author") - .HasColumnType("INTEGER"); - - b.Property("Content") - .HasColumnName("content") - .HasColumnType("TEXT"); - - b.Property("ContentType") - .IsRequired() - .HasColumnName("content_type") - .HasColumnType("TEXT"); - - b.Property("ExtraContent") - .HasColumnName("extra_content") - .HasColumnType("TEXT"); - - b.Property("LastUpdated") - .HasColumnName("last_updated") - .HasColumnType("TEXT"); - - b.Property("LocalId") - .HasColumnName("local_id") - .HasColumnType("INTEGER"); - - b.Property("Time") - .HasColumnName("time") - .HasColumnType("TEXT"); - - b.Property("TimelineId") - .HasColumnName("timeline") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AuthorId"); - - b.HasIndex("TimelineId"); - - b.ToTable("timeline_posts"); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("DataTag") - .HasColumnName("data_tag") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnName("last_modified") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnName("type") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnName("user") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId") - .IsUnique(); - - b.ToTable("user_avatars"); - }); - - modelBuilder.Entity("Timeline.Entities.UserEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Nickname") - .HasColumnName("nickname") - .HasColumnType("TEXT"); - - b.Property("Password") - .IsRequired() - .HasColumnName("password") - .HasColumnType("TEXT"); - - b.Property("Roles") - .IsRequired() - .HasColumnName("roles") - .HasColumnType("TEXT"); - - b.Property("UniqueId") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnName("unique_id") - .HasColumnType("TEXT") - .HasDefaultValueSql("lower(hex(randomblob(16)))"); - - b.Property("Username") - .IsRequired() - .HasColumnName("username") - .HasColumnType("TEXT"); - - b.Property("Version") - .ValueGeneratedOnAdd() - .HasColumnName("version") - .HasColumnType("INTEGER") - .HasDefaultValue(0L); - - b.HasKey("Id"); - - b.HasIndex("Username") - .IsUnique(); - - b.ToTable("users"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "Owner") - .WithMany("Timelines") - .HasForeignKey("OwnerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => - { - b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") - .WithMany("Members") - .HasForeignKey("TimelineId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Timeline.Entities.UserEntity", "User") - .WithMany("TimelinesJoined") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "Author") - .WithMany("TimelinePosts") - .HasForeignKey("AuthorId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") - .WithMany("Posts") - .HasForeignKey("TimelineId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "User") - .WithOne("Avatar") - .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Timeline/Migrations/20200808071611_UserAddUniqueId.cs b/Timeline/Migrations/20200808071611_UserAddUniqueId.cs deleted file mode 100644 index 651a2b05..00000000 --- a/Timeline/Migrations/20200808071611_UserAddUniqueId.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Timeline.Migrations -{ - public partial class UserAddUniqueId : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.Sql( -@" -PRAGMA foreign_keys=OFF; - -BEGIN TRANSACTION; - -CREATE TABLE new_users ( - id INTEGER NOT NULL - CONSTRAINT PK_users PRIMARY KEY AUTOINCREMENT, - unique_id TEXT NOT NULL DEFAULT (lower(hex(randomblob(16)))), - username TEXT NOT NULL, - password TEXT NOT NULL, - roles TEXT NOT NULL, - version INTEGER NOT NULL - DEFAULT 0, - nickname TEXT -); - -INSERT INTO new_users (id, username, password, roles, version, nickname) - SELECT id, username, password, roles, version, nickname FROM users; - -DROP TABLE users; - -ALTER TABLE new_users - RENAME TO users; - -CREATE UNIQUE INDEX IX_users_username ON users ( - username -); - -PRAGMA foreign_key_check; - -COMMIT TRANSACTION; - -PRAGMA foreign_keys=ON; -" - , true); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "unique_id", - table: "users"); - } - } -} diff --git a/Timeline/Migrations/20200810155908_AddTimesToUser.Designer.cs b/Timeline/Migrations/20200810155908_AddTimesToUser.Designer.cs deleted file mode 100644 index 71cc54dc..00000000 --- a/Timeline/Migrations/20200810155908_AddTimesToUser.Designer.cs +++ /dev/null @@ -1,339 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Timeline.Entities; - -namespace Timeline.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20200810155908_AddTimesToUser")] - partial class AddTimesToUser - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "3.1.5"); - - modelBuilder.Entity("Timeline.Entities.DataEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Data") - .IsRequired() - .HasColumnName("data") - .HasColumnType("BLOB"); - - b.Property("Ref") - .HasColumnName("ref") - .HasColumnType("INTEGER"); - - b.Property("Tag") - .IsRequired() - .HasColumnName("tag") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Tag") - .IsUnique(); - - b.ToTable("data"); - }); - - modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Key") - .IsRequired() - .HasColumnName("key") - .HasColumnType("BLOB"); - - b.HasKey("Id"); - - b.ToTable("jwt_token"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("CreateTime") - .HasColumnName("create_time") - .HasColumnType("TEXT"); - - b.Property("CurrentPostLocalId") - .HasColumnName("current_post_local_id") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnName("description") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnName("last_modified") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnName("name") - .HasColumnType("TEXT"); - - b.Property("NameLastModified") - .HasColumnName("name_last_modified") - .HasColumnType("TEXT"); - - b.Property("OwnerId") - .HasColumnName("owner") - .HasColumnType("INTEGER"); - - b.Property("UniqueId") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnName("unique_id") - .HasColumnType("TEXT") - .HasDefaultValueSql("lower(hex(randomblob(16)))"); - - b.Property("Visibility") - .HasColumnName("visibility") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("OwnerId"); - - b.ToTable("timelines"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("TimelineId") - .HasColumnName("timeline") - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnName("user") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("TimelineId"); - - b.HasIndex("UserId"); - - b.ToTable("timeline_members"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("AuthorId") - .HasColumnName("author") - .HasColumnType("INTEGER"); - - b.Property("Content") - .HasColumnName("content") - .HasColumnType("TEXT"); - - b.Property("ContentType") - .IsRequired() - .HasColumnName("content_type") - .HasColumnType("TEXT"); - - b.Property("ExtraContent") - .HasColumnName("extra_content") - .HasColumnType("TEXT"); - - b.Property("LastUpdated") - .HasColumnName("last_updated") - .HasColumnType("TEXT"); - - b.Property("LocalId") - .HasColumnName("local_id") - .HasColumnType("INTEGER"); - - b.Property("Time") - .HasColumnName("time") - .HasColumnType("TEXT"); - - b.Property("TimelineId") - .HasColumnName("timeline") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AuthorId"); - - b.HasIndex("TimelineId"); - - b.ToTable("timeline_posts"); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("DataTag") - .HasColumnName("data_tag") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnName("last_modified") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnName("type") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnName("user") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId") - .IsUnique(); - - b.ToTable("user_avatars"); - }); - - modelBuilder.Entity("Timeline.Entities.UserEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("CreateTime") - .ValueGeneratedOnAdd() - .HasColumnName("create_time") - .HasColumnType("TEXT") - .HasDefaultValueSql("datetime('now', 'utc')"); - - b.Property("LastModified") - .ValueGeneratedOnAdd() - .HasColumnName("last_modified") - .HasColumnType("TEXT") - .HasDefaultValueSql("datetime('now', 'utc')"); - - b.Property("Nickname") - .HasColumnName("nickname") - .HasColumnType("TEXT"); - - b.Property("Password") - .IsRequired() - .HasColumnName("password") - .HasColumnType("TEXT"); - - b.Property("Roles") - .IsRequired() - .HasColumnName("roles") - .HasColumnType("TEXT"); - - b.Property("UniqueId") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnName("unique_id") - .HasColumnType("TEXT") - .HasDefaultValueSql("lower(hex(randomblob(16)))"); - - b.Property("Username") - .IsRequired() - .HasColumnName("username") - .HasColumnType("TEXT"); - - b.Property("UsernameChangeTime") - .ValueGeneratedOnAdd() - .HasColumnName("username_change_time") - .HasColumnType("TEXT") - .HasDefaultValueSql("datetime('now', 'utc')"); - - b.Property("Version") - .ValueGeneratedOnAdd() - .HasColumnName("version") - .HasColumnType("INTEGER") - .HasDefaultValue(0L); - - b.HasKey("Id"); - - b.HasIndex("Username") - .IsUnique(); - - b.ToTable("users"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "Owner") - .WithMany("Timelines") - .HasForeignKey("OwnerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => - { - b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") - .WithMany("Members") - .HasForeignKey("TimelineId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Timeline.Entities.UserEntity", "User") - .WithMany("TimelinesJoined") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "Author") - .WithMany("TimelinePosts") - .HasForeignKey("AuthorId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") - .WithMany("Posts") - .HasForeignKey("TimelineId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "User") - .WithOne("Avatar") - .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Timeline/Migrations/20200810155908_AddTimesToUser.cs b/Timeline/Migrations/20200810155908_AddTimesToUser.cs deleted file mode 100644 index 369f85e6..00000000 --- a/Timeline/Migrations/20200810155908_AddTimesToUser.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Timeline.Migrations -{ - public partial class AddTimesToUser : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.Sql( -@" -PRAGMA foreign_keys=OFF; - -BEGIN TRANSACTION; - -CREATE TABLE new_users ( - id INTEGER NOT NULL - CONSTRAINT PK_users PRIMARY KEY AUTOINCREMENT, - unique_id TEXT NOT NULL DEFAULT (lower(hex(randomblob(16)))), - username TEXT NOT NULL, - password TEXT NOT NULL, - roles TEXT NOT NULL, - version INTEGER NOT NULL - DEFAULT 0, - nickname TEXT, - create_time TEXT NOT NULL DEFAULT (datetime('now', 'utc')), - last_modified TEXT NOT NULL DEFAULT (datetime('now', 'utc')), - username_change_time TEXT NOT NULL DEFAULT (datetime('now', 'utc')) -); - -INSERT INTO new_users (id, unique_id, username, password, roles, version, nickname) - SELECT id, unique_id, username, password, roles, version, nickname FROM users; - -DROP TABLE users; - -ALTER TABLE new_users - RENAME TO users; - -CREATE UNIQUE INDEX IX_users_username ON users ( - username -); - -PRAGMA foreign_key_check; - -COMMIT TRANSACTION; - -PRAGMA foreign_keys=ON; -" -, true); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "create_time", - table: "users"); - - migrationBuilder.DropColumn( - name: "last_modified", - table: "users"); - - migrationBuilder.DropColumn( - name: "username_change_time", - table: "users"); - } - } -} diff --git a/Timeline/Migrations/20200810170533_MakePostAuthorOptional.Designer.cs b/Timeline/Migrations/20200810170533_MakePostAuthorOptional.Designer.cs deleted file mode 100644 index 80598fdf..00000000 --- a/Timeline/Migrations/20200810170533_MakePostAuthorOptional.Designer.cs +++ /dev/null @@ -1,337 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Timeline.Entities; - -namespace Timeline.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20200810170533_MakePostAuthorOptional")] - partial class MakePostAuthorOptional - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "3.1.5"); - - modelBuilder.Entity("Timeline.Entities.DataEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Data") - .IsRequired() - .HasColumnName("data") - .HasColumnType("BLOB"); - - b.Property("Ref") - .HasColumnName("ref") - .HasColumnType("INTEGER"); - - b.Property("Tag") - .IsRequired() - .HasColumnName("tag") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Tag") - .IsUnique(); - - b.ToTable("data"); - }); - - modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Key") - .IsRequired() - .HasColumnName("key") - .HasColumnType("BLOB"); - - b.HasKey("Id"); - - b.ToTable("jwt_token"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("CreateTime") - .HasColumnName("create_time") - .HasColumnType("TEXT"); - - b.Property("CurrentPostLocalId") - .HasColumnName("current_post_local_id") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnName("description") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnName("last_modified") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnName("name") - .HasColumnType("TEXT"); - - b.Property("NameLastModified") - .HasColumnName("name_last_modified") - .HasColumnType("TEXT"); - - b.Property("OwnerId") - .HasColumnName("owner") - .HasColumnType("INTEGER"); - - b.Property("UniqueId") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnName("unique_id") - .HasColumnType("TEXT") - .HasDefaultValueSql("lower(hex(randomblob(16)))"); - - b.Property("Visibility") - .HasColumnName("visibility") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("OwnerId"); - - b.ToTable("timelines"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("TimelineId") - .HasColumnName("timeline") - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnName("user") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("TimelineId"); - - b.HasIndex("UserId"); - - b.ToTable("timeline_members"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("AuthorId") - .HasColumnName("author") - .HasColumnType("INTEGER"); - - b.Property("Content") - .HasColumnName("content") - .HasColumnType("TEXT"); - - b.Property("ContentType") - .IsRequired() - .HasColumnName("content_type") - .HasColumnType("TEXT"); - - b.Property("ExtraContent") - .HasColumnName("extra_content") - .HasColumnType("TEXT"); - - b.Property("LastUpdated") - .HasColumnName("last_updated") - .HasColumnType("TEXT"); - - b.Property("LocalId") - .HasColumnName("local_id") - .HasColumnType("INTEGER"); - - b.Property("Time") - .HasColumnName("time") - .HasColumnType("TEXT"); - - b.Property("TimelineId") - .HasColumnName("timeline") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AuthorId"); - - b.HasIndex("TimelineId"); - - b.ToTable("timeline_posts"); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("DataTag") - .HasColumnName("data_tag") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnName("last_modified") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnName("type") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnName("user") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId") - .IsUnique(); - - b.ToTable("user_avatars"); - }); - - modelBuilder.Entity("Timeline.Entities.UserEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("CreateTime") - .ValueGeneratedOnAdd() - .HasColumnName("create_time") - .HasColumnType("TEXT") - .HasDefaultValueSql("datetime('now', 'utc')"); - - b.Property("LastModified") - .ValueGeneratedOnAdd() - .HasColumnName("last_modified") - .HasColumnType("TEXT") - .HasDefaultValueSql("datetime('now', 'utc')"); - - b.Property("Nickname") - .HasColumnName("nickname") - .HasColumnType("TEXT"); - - b.Property("Password") - .IsRequired() - .HasColumnName("password") - .HasColumnType("TEXT"); - - b.Property("Roles") - .IsRequired() - .HasColumnName("roles") - .HasColumnType("TEXT"); - - b.Property("UniqueId") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnName("unique_id") - .HasColumnType("TEXT") - .HasDefaultValueSql("lower(hex(randomblob(16)))"); - - b.Property("Username") - .IsRequired() - .HasColumnName("username") - .HasColumnType("TEXT"); - - b.Property("UsernameChangeTime") - .ValueGeneratedOnAdd() - .HasColumnName("username_change_time") - .HasColumnType("TEXT") - .HasDefaultValueSql("datetime('now', 'utc')"); - - b.Property("Version") - .ValueGeneratedOnAdd() - .HasColumnName("version") - .HasColumnType("INTEGER") - .HasDefaultValue(0L); - - b.HasKey("Id"); - - b.HasIndex("Username") - .IsUnique(); - - b.ToTable("users"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "Owner") - .WithMany("Timelines") - .HasForeignKey("OwnerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => - { - b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") - .WithMany("Members") - .HasForeignKey("TimelineId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Timeline.Entities.UserEntity", "User") - .WithMany("TimelinesJoined") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "Author") - .WithMany("TimelinePosts") - .HasForeignKey("AuthorId"); - - b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") - .WithMany("Posts") - .HasForeignKey("TimelineId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "User") - .WithOne("Avatar") - .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Timeline/Migrations/20200810170533_MakePostAuthorOptional.cs b/Timeline/Migrations/20200810170533_MakePostAuthorOptional.cs deleted file mode 100644 index b0f0bca7..00000000 --- a/Timeline/Migrations/20200810170533_MakePostAuthorOptional.cs +++ /dev/null @@ -1,78 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Timeline.Migrations -{ - public partial class MakePostAuthorOptional : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.Sql(@" -PRAGMA foreign_keys = 0; - -BEGIN TRANSACTION; - -CREATE TABLE new_timeline_posts ( - id INTEGER NOT NULL - CONSTRAINT PK_timeline_posts PRIMARY KEY AUTOINCREMENT, - timeline INTEGER NOT NULL, - author INTEGER, - content TEXT, - time TEXT NOT NULL, - last_updated TEXT NOT NULL, - local_id INTEGER NOT NULL - DEFAULT 0, - content_type TEXT NOT NULL - DEFAULT '', - extra_content TEXT, - CONSTRAINT FK_timeline_posts_users_author FOREIGN KEY ( - author - ) - REFERENCES users (id), - CONSTRAINT FK_timeline_posts_timelines_timeline FOREIGN KEY ( - timeline - ) - REFERENCES timelines (id) ON DELETE CASCADE -); - -INSERT INTO new_timeline_posts SELECT * FROM timeline_posts; - -DROP TABLE timeline_posts; - -ALTER TABLE new_timeline_posts RENAME TO timeline_posts; - -CREATE INDEX IX_timeline_posts_author ON timeline_posts (author); - -CREATE INDEX IX_timeline_posts_timeline ON timeline_posts(timeline); - -PRAGMA foreign_key_check; - -COMMIT TRANSACTION; - -PRAGMA foreign_keys = 1; - ", true); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_timeline_posts_users_author", - table: "timeline_posts"); - - migrationBuilder.AlterColumn( - name: "author", - table: "timeline_posts", - type: "INTEGER", - nullable: false, - oldClrType: typeof(long), - oldNullable: true); - - migrationBuilder.AddForeignKey( - name: "FK_timeline_posts_users_author", - table: "timeline_posts", - column: "author", - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - } - } -} diff --git a/Timeline/Migrations/20200811080808_ChangeDateTimeOffsetToDateTime.Designer.cs b/Timeline/Migrations/20200811080808_ChangeDateTimeOffsetToDateTime.Designer.cs deleted file mode 100644 index 58238557..00000000 --- a/Timeline/Migrations/20200811080808_ChangeDateTimeOffsetToDateTime.Designer.cs +++ /dev/null @@ -1,337 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Timeline.Entities; - -namespace Timeline.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20200811080808_ChangeDateTimeOffsetToDateTime")] - partial class ChangeDateTimeOffsetToDateTime - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "3.1.5"); - - modelBuilder.Entity("Timeline.Entities.DataEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Data") - .IsRequired() - .HasColumnName("data") - .HasColumnType("BLOB"); - - b.Property("Ref") - .HasColumnName("ref") - .HasColumnType("INTEGER"); - - b.Property("Tag") - .IsRequired() - .HasColumnName("tag") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Tag") - .IsUnique(); - - b.ToTable("data"); - }); - - modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Key") - .IsRequired() - .HasColumnName("key") - .HasColumnType("BLOB"); - - b.HasKey("Id"); - - b.ToTable("jwt_token"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("CreateTime") - .HasColumnName("create_time") - .HasColumnType("TEXT"); - - b.Property("CurrentPostLocalId") - .HasColumnName("current_post_local_id") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnName("description") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnName("last_modified") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnName("name") - .HasColumnType("TEXT"); - - b.Property("NameLastModified") - .HasColumnName("name_last_modified") - .HasColumnType("TEXT"); - - b.Property("OwnerId") - .HasColumnName("owner") - .HasColumnType("INTEGER"); - - b.Property("UniqueId") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnName("unique_id") - .HasColumnType("TEXT") - .HasDefaultValueSql("lower(hex(randomblob(16)))"); - - b.Property("Visibility") - .HasColumnName("visibility") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("OwnerId"); - - b.ToTable("timelines"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("TimelineId") - .HasColumnName("timeline") - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnName("user") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("TimelineId"); - - b.HasIndex("UserId"); - - b.ToTable("timeline_members"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("AuthorId") - .HasColumnName("author") - .HasColumnType("INTEGER"); - - b.Property("Content") - .HasColumnName("content") - .HasColumnType("TEXT"); - - b.Property("ContentType") - .IsRequired() - .HasColumnName("content_type") - .HasColumnType("TEXT"); - - b.Property("ExtraContent") - .HasColumnName("extra_content") - .HasColumnType("TEXT"); - - b.Property("LastUpdated") - .HasColumnName("last_updated") - .HasColumnType("TEXT"); - - b.Property("LocalId") - .HasColumnName("local_id") - .HasColumnType("INTEGER"); - - b.Property("Time") - .HasColumnName("time") - .HasColumnType("TEXT"); - - b.Property("TimelineId") - .HasColumnName("timeline") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AuthorId"); - - b.HasIndex("TimelineId"); - - b.ToTable("timeline_posts"); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("DataTag") - .HasColumnName("data_tag") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnName("last_modified") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnName("type") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnName("user") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId") - .IsUnique(); - - b.ToTable("user_avatars"); - }); - - modelBuilder.Entity("Timeline.Entities.UserEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("CreateTime") - .ValueGeneratedOnAdd() - .HasColumnName("create_time") - .HasColumnType("TEXT") - .HasDefaultValueSql("datetime('now', 'utc')"); - - b.Property("LastModified") - .ValueGeneratedOnAdd() - .HasColumnName("last_modified") - .HasColumnType("TEXT") - .HasDefaultValueSql("datetime('now', 'utc')"); - - b.Property("Nickname") - .HasColumnName("nickname") - .HasColumnType("TEXT"); - - b.Property("Password") - .IsRequired() - .HasColumnName("password") - .HasColumnType("TEXT"); - - b.Property("Roles") - .IsRequired() - .HasColumnName("roles") - .HasColumnType("TEXT"); - - b.Property("UniqueId") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnName("unique_id") - .HasColumnType("TEXT") - .HasDefaultValueSql("lower(hex(randomblob(16)))"); - - b.Property("Username") - .IsRequired() - .HasColumnName("username") - .HasColumnType("TEXT"); - - b.Property("UsernameChangeTime") - .ValueGeneratedOnAdd() - .HasColumnName("username_change_time") - .HasColumnType("TEXT") - .HasDefaultValueSql("datetime('now', 'utc')"); - - b.Property("Version") - .ValueGeneratedOnAdd() - .HasColumnName("version") - .HasColumnType("INTEGER") - .HasDefaultValue(0L); - - b.HasKey("Id"); - - b.HasIndex("Username") - .IsUnique(); - - b.ToTable("users"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "Owner") - .WithMany("Timelines") - .HasForeignKey("OwnerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => - { - b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") - .WithMany("Members") - .HasForeignKey("TimelineId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Timeline.Entities.UserEntity", "User") - .WithMany("TimelinesJoined") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "Author") - .WithMany("TimelinePosts") - .HasForeignKey("AuthorId"); - - b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") - .WithMany("Posts") - .HasForeignKey("TimelineId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "User") - .WithOne("Avatar") - .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Timeline/Migrations/20200811080808_ChangeDateTimeOffsetToDateTime.cs b/Timeline/Migrations/20200811080808_ChangeDateTimeOffsetToDateTime.cs deleted file mode 100644 index eb6b44f3..00000000 --- a/Timeline/Migrations/20200811080808_ChangeDateTimeOffsetToDateTime.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Timeline.Migrations -{ - public partial class ChangeDateTimeOffsetToDateTime : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - - } - } -} diff --git a/Timeline/Migrations/20200826164553_TimelineAddTitle.Designer.cs b/Timeline/Migrations/20200826164553_TimelineAddTitle.Designer.cs deleted file mode 100644 index f2279f3b..00000000 --- a/Timeline/Migrations/20200826164553_TimelineAddTitle.Designer.cs +++ /dev/null @@ -1,341 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Timeline.Entities; - -namespace Timeline.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20200826164553_TimelineAddTitle")] - partial class TimelineAddTitle - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "3.1.7"); - - modelBuilder.Entity("Timeline.Entities.DataEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Data") - .IsRequired() - .HasColumnName("data") - .HasColumnType("BLOB"); - - b.Property("Ref") - .HasColumnName("ref") - .HasColumnType("INTEGER"); - - b.Property("Tag") - .IsRequired() - .HasColumnName("tag") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Tag") - .IsUnique(); - - b.ToTable("data"); - }); - - modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Key") - .IsRequired() - .HasColumnName("key") - .HasColumnType("BLOB"); - - b.HasKey("Id"); - - b.ToTable("jwt_token"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("CreateTime") - .HasColumnName("create_time") - .HasColumnType("TEXT"); - - b.Property("CurrentPostLocalId") - .HasColumnName("current_post_local_id") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnName("description") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnName("last_modified") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnName("name") - .HasColumnType("TEXT"); - - b.Property("NameLastModified") - .HasColumnName("name_last_modified") - .HasColumnType("TEXT"); - - b.Property("OwnerId") - .HasColumnName("owner") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnName("title") - .HasColumnType("TEXT"); - - b.Property("UniqueId") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnName("unique_id") - .HasColumnType("TEXT") - .HasDefaultValueSql("lower(hex(randomblob(16)))"); - - b.Property("Visibility") - .HasColumnName("visibility") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("OwnerId"); - - b.ToTable("timelines"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("TimelineId") - .HasColumnName("timeline") - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnName("user") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("TimelineId"); - - b.HasIndex("UserId"); - - b.ToTable("timeline_members"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("AuthorId") - .HasColumnName("author") - .HasColumnType("INTEGER"); - - b.Property("Content") - .HasColumnName("content") - .HasColumnType("TEXT"); - - b.Property("ContentType") - .IsRequired() - .HasColumnName("content_type") - .HasColumnType("TEXT"); - - b.Property("ExtraContent") - .HasColumnName("extra_content") - .HasColumnType("TEXT"); - - b.Property("LastUpdated") - .HasColumnName("last_updated") - .HasColumnType("TEXT"); - - b.Property("LocalId") - .HasColumnName("local_id") - .HasColumnType("INTEGER"); - - b.Property("Time") - .HasColumnName("time") - .HasColumnType("TEXT"); - - b.Property("TimelineId") - .HasColumnName("timeline") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AuthorId"); - - b.HasIndex("TimelineId"); - - b.ToTable("timeline_posts"); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("DataTag") - .HasColumnName("data_tag") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnName("last_modified") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnName("type") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnName("user") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId") - .IsUnique(); - - b.ToTable("user_avatars"); - }); - - modelBuilder.Entity("Timeline.Entities.UserEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("CreateTime") - .ValueGeneratedOnAdd() - .HasColumnName("create_time") - .HasColumnType("TEXT") - .HasDefaultValueSql("datetime('now', 'utc')"); - - b.Property("LastModified") - .ValueGeneratedOnAdd() - .HasColumnName("last_modified") - .HasColumnType("TEXT") - .HasDefaultValueSql("datetime('now', 'utc')"); - - b.Property("Nickname") - .HasColumnName("nickname") - .HasColumnType("TEXT"); - - b.Property("Password") - .IsRequired() - .HasColumnName("password") - .HasColumnType("TEXT"); - - b.Property("Roles") - .IsRequired() - .HasColumnName("roles") - .HasColumnType("TEXT"); - - b.Property("UniqueId") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnName("unique_id") - .HasColumnType("TEXT") - .HasDefaultValueSql("lower(hex(randomblob(16)))"); - - b.Property("Username") - .IsRequired() - .HasColumnName("username") - .HasColumnType("TEXT"); - - b.Property("UsernameChangeTime") - .ValueGeneratedOnAdd() - .HasColumnName("username_change_time") - .HasColumnType("TEXT") - .HasDefaultValueSql("datetime('now', 'utc')"); - - b.Property("Version") - .ValueGeneratedOnAdd() - .HasColumnName("version") - .HasColumnType("INTEGER") - .HasDefaultValue(0L); - - b.HasKey("Id"); - - b.HasIndex("Username") - .IsUnique(); - - b.ToTable("users"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "Owner") - .WithMany("Timelines") - .HasForeignKey("OwnerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => - { - b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") - .WithMany("Members") - .HasForeignKey("TimelineId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Timeline.Entities.UserEntity", "User") - .WithMany("TimelinesJoined") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "Author") - .WithMany("TimelinePosts") - .HasForeignKey("AuthorId"); - - b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") - .WithMany("Posts") - .HasForeignKey("TimelineId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "User") - .WithOne("Avatar") - .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Timeline/Migrations/20200826164553_TimelineAddTitle.cs b/Timeline/Migrations/20200826164553_TimelineAddTitle.cs deleted file mode 100644 index 7e8c498b..00000000 --- a/Timeline/Migrations/20200826164553_TimelineAddTitle.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Timeline.Migrations -{ - public partial class TimelineAddTitle : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "title", - table: "timelines", - nullable: true); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "title", - table: "timelines"); - } - } -} diff --git a/Timeline/Migrations/DatabaseContextModelSnapshot.cs b/Timeline/Migrations/DatabaseContextModelSnapshot.cs deleted file mode 100644 index 65ae6c9a..00000000 --- a/Timeline/Migrations/DatabaseContextModelSnapshot.cs +++ /dev/null @@ -1,339 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Timeline.Entities; - -namespace Timeline.Migrations -{ - [DbContext(typeof(DatabaseContext))] - partial class DatabaseContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "3.1.7"); - - modelBuilder.Entity("Timeline.Entities.DataEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Data") - .IsRequired() - .HasColumnName("data") - .HasColumnType("BLOB"); - - b.Property("Ref") - .HasColumnName("ref") - .HasColumnType("INTEGER"); - - b.Property("Tag") - .IsRequired() - .HasColumnName("tag") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Tag") - .IsUnique(); - - b.ToTable("data"); - }); - - modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("Key") - .IsRequired() - .HasColumnName("key") - .HasColumnType("BLOB"); - - b.HasKey("Id"); - - b.ToTable("jwt_token"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("CreateTime") - .HasColumnName("create_time") - .HasColumnType("TEXT"); - - b.Property("CurrentPostLocalId") - .HasColumnName("current_post_local_id") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnName("description") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnName("last_modified") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnName("name") - .HasColumnType("TEXT"); - - b.Property("NameLastModified") - .HasColumnName("name_last_modified") - .HasColumnType("TEXT"); - - b.Property("OwnerId") - .HasColumnName("owner") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnName("title") - .HasColumnType("TEXT"); - - b.Property("UniqueId") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnName("unique_id") - .HasColumnType("TEXT") - .HasDefaultValueSql("lower(hex(randomblob(16)))"); - - b.Property("Visibility") - .HasColumnName("visibility") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("OwnerId"); - - b.ToTable("timelines"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("TimelineId") - .HasColumnName("timeline") - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnName("user") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("TimelineId"); - - b.HasIndex("UserId"); - - b.ToTable("timeline_members"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("AuthorId") - .HasColumnName("author") - .HasColumnType("INTEGER"); - - b.Property("Content") - .HasColumnName("content") - .HasColumnType("TEXT"); - - b.Property("ContentType") - .IsRequired() - .HasColumnName("content_type") - .HasColumnType("TEXT"); - - b.Property("ExtraContent") - .HasColumnName("extra_content") - .HasColumnType("TEXT"); - - b.Property("LastUpdated") - .HasColumnName("last_updated") - .HasColumnType("TEXT"); - - b.Property("LocalId") - .HasColumnName("local_id") - .HasColumnType("INTEGER"); - - b.Property("Time") - .HasColumnName("time") - .HasColumnType("TEXT"); - - b.Property("TimelineId") - .HasColumnName("timeline") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AuthorId"); - - b.HasIndex("TimelineId"); - - b.ToTable("timeline_posts"); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("DataTag") - .HasColumnName("data_tag") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnName("last_modified") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnName("type") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnName("user") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId") - .IsUnique(); - - b.ToTable("user_avatars"); - }); - - modelBuilder.Entity("Timeline.Entities.UserEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("CreateTime") - .ValueGeneratedOnAdd() - .HasColumnName("create_time") - .HasColumnType("TEXT") - .HasDefaultValueSql("datetime('now', 'utc')"); - - b.Property("LastModified") - .ValueGeneratedOnAdd() - .HasColumnName("last_modified") - .HasColumnType("TEXT") - .HasDefaultValueSql("datetime('now', 'utc')"); - - b.Property("Nickname") - .HasColumnName("nickname") - .HasColumnType("TEXT"); - - b.Property("Password") - .IsRequired() - .HasColumnName("password") - .HasColumnType("TEXT"); - - b.Property("Roles") - .IsRequired() - .HasColumnName("roles") - .HasColumnType("TEXT"); - - b.Property("UniqueId") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnName("unique_id") - .HasColumnType("TEXT") - .HasDefaultValueSql("lower(hex(randomblob(16)))"); - - b.Property("Username") - .IsRequired() - .HasColumnName("username") - .HasColumnType("TEXT"); - - b.Property("UsernameChangeTime") - .ValueGeneratedOnAdd() - .HasColumnName("username_change_time") - .HasColumnType("TEXT") - .HasDefaultValueSql("datetime('now', 'utc')"); - - b.Property("Version") - .ValueGeneratedOnAdd() - .HasColumnName("version") - .HasColumnType("INTEGER") - .HasDefaultValue(0L); - - b.HasKey("Id"); - - b.HasIndex("Username") - .IsUnique(); - - b.ToTable("users"); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "Owner") - .WithMany("Timelines") - .HasForeignKey("OwnerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => - { - b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") - .WithMany("Members") - .HasForeignKey("TimelineId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Timeline.Entities.UserEntity", "User") - .WithMany("TimelinesJoined") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "Author") - .WithMany("TimelinePosts") - .HasForeignKey("AuthorId"); - - b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") - .WithMany("Posts") - .HasForeignKey("TimelineId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => - { - b.HasOne("Timeline.Entities.UserEntity", "User") - .WithOne("Avatar") - .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Timeline/MockClientApp/index.html b/Timeline/MockClientApp/index.html deleted file mode 100644 index 03cf371e..00000000 --- a/Timeline/MockClientApp/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - Mock Client App - - - This is a mock client app for testing. - - diff --git a/Timeline/Models/ByteData.cs b/Timeline/Models/ByteData.cs deleted file mode 100644 index 7b832eb5..00000000 --- a/Timeline/Models/ByteData.cs +++ /dev/null @@ -1,33 +0,0 @@ -using NSwag.Annotations; - -namespace Timeline.Models -{ - /// - /// Model for reading http body as bytes. - /// - [OpenApiFile] - public class ByteData - { - /// - /// - /// The data. - /// The content type. - public ByteData(byte[] data, string contentType) - { - Data = data; - ContentType = contentType; - } - - /// - /// Data. - /// -#pragma warning disable CA1819 // Properties should not return arrays - public byte[] Data { get; } -#pragma warning restore CA1819 // Properties should not return arrays - - /// - /// Content type. - /// - public string ContentType { get; } - } -} diff --git a/Timeline/Models/Converters/JsonDateTimeConverter.cs b/Timeline/Models/Converters/JsonDateTimeConverter.cs deleted file mode 100644 index 94b5cab0..00000000 --- a/Timeline/Models/Converters/JsonDateTimeConverter.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Diagnostics; -using System.Globalization; -using System.Text.Json; -using System.Text.Json.Serialization; -using Timeline.Helpers; - -namespace Timeline.Models.Converters -{ - public class JsonDateTimeConverter : JsonConverter - { - public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - Debug.Assert(typeToConvert == typeof(DateTime)); - return DateTime.Parse(reader.GetString(), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); - } - - public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.MyToUtc().ToString("s", CultureInfo.InvariantCulture) + "Z"); - } - } -} diff --git a/Timeline/Models/Converters/MyDateTimeConverter.cs b/Timeline/Models/Converters/MyDateTimeConverter.cs deleted file mode 100644 index f125cd5c..00000000 --- a/Timeline/Models/Converters/MyDateTimeConverter.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.ComponentModel; -using System.Globalization; - -namespace Timeline.Models.Converters -{ - public class MyDateTimeConverter : TypeConverter - { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); - } - - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - return base.CanConvertTo(context, destinationType); - } - - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) - { - if (value is string text) - { - text = text.Trim(); - if (text.Length == 0) - { - return DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc); - } - - return DateTime.Parse(text, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal); - } - - return base.ConvertFrom(context, culture, value); - } - - public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) - { - if (destinationType == typeof(string) && value is DateTime) - { - DateTime dt = (DateTime)value; - if (dt == DateTime.MinValue) - { - return string.Empty; - } - - return dt.ToString("s", CultureInfo.InvariantCulture) + "Z"; - } - - return base.ConvertTo(context, culture, value, destinationType); - } - } -} diff --git a/Timeline/Models/Http/ActionContextAccessorExtensions.cs b/Timeline/Models/Http/ActionContextAccessorExtensions.cs deleted file mode 100644 index bcc55c5a..00000000 --- a/Timeline/Models/Http/ActionContextAccessorExtensions.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using System; - -namespace Timeline.Models.Http -{ - public static class ActionContextAccessorExtensions - { - public static ActionContext AssertActionContextForUrlFill(this IActionContextAccessor accessor) - { - return accessor.ActionContext ?? throw new InvalidOperationException(Resources.Models.Http.Exception.ActionContextNull); - } - } -} diff --git a/Timeline/Models/Http/Common.cs b/Timeline/Models/Http/Common.cs deleted file mode 100644 index 5fa22c9e..00000000 --- a/Timeline/Models/Http/Common.cs +++ /dev/null @@ -1,120 +0,0 @@ -using static Timeline.Resources.Models.Http.Common; - -namespace Timeline.Models.Http -{ - public class CommonResponse - { - public CommonResponse() - { - - } - - public CommonResponse(int code, string message) - { - Code = code; - Message = message; - } - - public int Code { get; set; } - public string? Message { get; set; } - } - - public class CommonDataResponse : CommonResponse - { - public CommonDataResponse() - { - - } - - public CommonDataResponse(int code, string message, T data) - : base(code, message) - { - Data = data; - } - - public T Data { get; set; } = default!; - } - - public class CommonPutResponse : CommonDataResponse - { - public class ResponseData - { - public ResponseData() { } - - public ResponseData(bool create) - { - Create = create; - } - - public bool Create { get; set; } - } - - public CommonPutResponse() - { - - } - - public CommonPutResponse(int code, string message, bool create) - : base(code, message, new ResponseData(create)) - { - - } - - internal static CommonPutResponse Create() - { - return new CommonPutResponse(0, MessagePutCreate, true); - } - - internal static CommonPutResponse Modify() - { - return new CommonPutResponse(0, MessagePutModify, false); - } - } - - /// - /// Common response for delete method. - /// - public class CommonDeleteResponse : CommonDataResponse - { - /// - public class ResponseData - { - /// - public ResponseData() { } - - /// - public ResponseData(bool delete) - { - Delete = delete; - } - - /// - /// True if the entry is deleted. False if the entry does not exist. - /// - public bool Delete { get; set; } - } - - /// - public CommonDeleteResponse() - { - - } - - /// - public CommonDeleteResponse(int code, string message, bool delete) - : base(code, message, new ResponseData(delete)) - { - - } - - internal static CommonDeleteResponse Delete() - { - return new CommonDeleteResponse(0, MessageDeleteDelete, true); - } - - internal static CommonDeleteResponse NotExist() - { - return new CommonDeleteResponse(0, MessageDeleteNotExist, false); - } - } -} diff --git a/Timeline/Models/Http/ErrorResponse.cs b/Timeline/Models/Http/ErrorResponse.cs deleted file mode 100644 index ac86481f..00000000 --- a/Timeline/Models/Http/ErrorResponse.cs +++ /dev/null @@ -1,261 +0,0 @@ -using static Timeline.Resources.Messages; - -namespace Timeline.Models.Http -{ - public static class ErrorResponse - { - public static class Common - { - public static CommonResponse InvalidModel(params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.Common.InvalidModel, string.Format(Common_InvalidModel, formatArgs)); - } - - public static CommonResponse CustomMessage_InvalidModel(string message, params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.Common.InvalidModel, string.Format(message, formatArgs)); - } - - public static CommonResponse Forbid(params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.Common.Forbid, string.Format(Common_Forbid, formatArgs)); - } - - public static CommonResponse CustomMessage_Forbid(string message, params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.Common.Forbid, string.Format(message, formatArgs)); - } - - public static CommonResponse UnknownEndpoint(params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.Common.UnknownEndpoint, string.Format(Common_UnknownEndpoint, formatArgs)); - } - - public static CommonResponse CustomMessage_UnknownEndpoint(string message, params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.Common.UnknownEndpoint, string.Format(message, formatArgs)); - } - - public static class Header - { - public static CommonResponse IfNonMatch_BadFormat(params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.Common.Header.IfNonMatch_BadFormat, string.Format(Common_Header_IfNonMatch_BadFormat, formatArgs)); - } - - public static CommonResponse CustomMessage_IfNonMatch_BadFormat(string message, params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.Common.Header.IfNonMatch_BadFormat, string.Format(message, formatArgs)); - } - - } - - public static class Content - { - public static CommonResponse TooBig(params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.Common.Content.TooBig, string.Format(Common_Content_TooBig, formatArgs)); - } - - public static CommonResponse CustomMessage_TooBig(string message, params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.Common.Content.TooBig, string.Format(message, formatArgs)); - } - - } - - } - - public static class UserCommon - { - public static CommonResponse NotExist(params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.UserCommon.NotExist, string.Format(UserCommon_NotExist, formatArgs)); - } - - public static CommonResponse CustomMessage_NotExist(string message, params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.UserCommon.NotExist, string.Format(message, formatArgs)); - } - - } - - public static class TokenController - { - public static CommonResponse Create_BadCredential(params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.TokenController.Create_BadCredential, string.Format(TokenController_Create_BadCredential, formatArgs)); - } - - public static CommonResponse CustomMessage_Create_BadCredential(string message, params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.TokenController.Create_BadCredential, string.Format(message, formatArgs)); - } - - public static CommonResponse Verify_BadFormat(params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.TokenController.Verify_BadFormat, string.Format(TokenController_Verify_BadFormat, formatArgs)); - } - - public static CommonResponse CustomMessage_Verify_BadFormat(string message, params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.TokenController.Verify_BadFormat, string.Format(message, formatArgs)); - } - - public static CommonResponse Verify_UserNotExist(params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.TokenController.Verify_UserNotExist, string.Format(TokenController_Verify_UserNotExist, formatArgs)); - } - - public static CommonResponse CustomMessage_Verify_UserNotExist(string message, params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.TokenController.Verify_UserNotExist, string.Format(message, formatArgs)); - } - - public static CommonResponse Verify_OldVersion(params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.TokenController.Verify_OldVersion, string.Format(TokenController_Verify_OldVersion, formatArgs)); - } - - public static CommonResponse CustomMessage_Verify_OldVersion(string message, params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.TokenController.Verify_OldVersion, string.Format(message, formatArgs)); - } - - public static CommonResponse Verify_TimeExpired(params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.TokenController.Verify_TimeExpired, string.Format(TokenController_Verify_TimeExpired, formatArgs)); - } - - public static CommonResponse CustomMessage_Verify_TimeExpired(string message, params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.TokenController.Verify_TimeExpired, string.Format(message, formatArgs)); - } - - } - - public static class UserController - { - public static CommonResponse UsernameConflict(params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.UserController.UsernameConflict, string.Format(UserController_UsernameConflict, formatArgs)); - } - - public static CommonResponse CustomMessage_UsernameConflict(string message, params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.UserController.UsernameConflict, string.Format(message, formatArgs)); - } - - public static CommonResponse ChangePassword_BadOldPassword(params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.UserController.ChangePassword_BadOldPassword, string.Format(UserController_ChangePassword_BadOldPassword, formatArgs)); - } - - public static CommonResponse CustomMessage_ChangePassword_BadOldPassword(string message, params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.UserController.ChangePassword_BadOldPassword, string.Format(message, formatArgs)); - } - - } - - public static class UserAvatar - { - public static CommonResponse BadFormat_CantDecode(params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.UserAvatar.BadFormat_CantDecode, string.Format(UserAvatar_BadFormat_CantDecode, formatArgs)); - } - - public static CommonResponse CustomMessage_BadFormat_CantDecode(string message, params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.UserAvatar.BadFormat_CantDecode, string.Format(message, formatArgs)); - } - - public static CommonResponse BadFormat_UnmatchedFormat(params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.UserAvatar.BadFormat_UnmatchedFormat, string.Format(UserAvatar_BadFormat_UnmatchedFormat, formatArgs)); - } - - public static CommonResponse CustomMessage_BadFormat_UnmatchedFormat(string message, params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.UserAvatar.BadFormat_UnmatchedFormat, string.Format(message, formatArgs)); - } - - public static CommonResponse BadFormat_BadSize(params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.UserAvatar.BadFormat_BadSize, string.Format(UserAvatar_BadFormat_BadSize, formatArgs)); - } - - public static CommonResponse CustomMessage_BadFormat_BadSize(string message, params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.UserAvatar.BadFormat_BadSize, string.Format(message, formatArgs)); - } - - } - - public static class TimelineController - { - public static CommonResponse NameConflict(params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.TimelineController.NameConflict, string.Format(TimelineController_NameConflict, formatArgs)); - } - - public static CommonResponse CustomMessage_NameConflict(string message, params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.TimelineController.NameConflict, string.Format(message, formatArgs)); - } - - public static CommonResponse NotExist(params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.TimelineController.NotExist, string.Format(TimelineController_NotExist, formatArgs)); - } - - public static CommonResponse CustomMessage_NotExist(string message, params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.TimelineController.NotExist, string.Format(message, formatArgs)); - } - - public static CommonResponse MemberPut_NotExist(params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.TimelineController.MemberPut_NotExist, string.Format(TimelineController_MemberPut_NotExist, formatArgs)); - } - - public static CommonResponse CustomMessage_MemberPut_NotExist(string message, params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.TimelineController.MemberPut_NotExist, string.Format(message, formatArgs)); - } - - public static CommonResponse QueryRelateNotExist(params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.TimelineController.QueryRelateNotExist, string.Format(TimelineController_QueryRelateNotExist, formatArgs)); - } - - public static CommonResponse CustomMessage_QueryRelateNotExist(string message, params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.TimelineController.QueryRelateNotExist, string.Format(message, formatArgs)); - } - - public static CommonResponse PostNotExist(params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.TimelineController.PostNotExist, string.Format(TimelineController_PostNotExist, formatArgs)); - } - - public static CommonResponse CustomMessage_PostNotExist(string message, params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.TimelineController.PostNotExist, string.Format(message, formatArgs)); - } - - public static CommonResponse PostNoData(params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.TimelineController.PostNoData, string.Format(TimelineController_PostNoData, formatArgs)); - } - - public static CommonResponse CustomMessage_PostNoData(string message, params object?[] formatArgs) - { - return new CommonResponse(ErrorCodes.TimelineController.PostNoData, string.Format(message, formatArgs)); - } - - } - - } - -} diff --git a/Timeline/Models/Http/Timeline.cs b/Timeline/Models/Http/Timeline.cs deleted file mode 100644 index a81b33f5..00000000 --- a/Timeline/Models/Http/Timeline.cs +++ /dev/null @@ -1,219 +0,0 @@ -using AutoMapper; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Mvc.Routing; -using System; -using System.Collections.Generic; -using Timeline.Controllers; - -namespace Timeline.Models.Http -{ - /// - /// Info of post content. - /// - public class TimelinePostContentInfo - { - /// - /// Type of the post content. - /// - public string Type { get; set; } = default!; - /// - /// If post is of text type. This is the text. - /// - public string? Text { get; set; } - /// - /// If post is of image type. This is the image url. - /// - public string? Url { get; set; } - /// - /// If post has data (currently it means it's a image post), this is the data etag. - /// - public string? ETag { get; set; } - } - - /// - /// Info of a post. - /// - public class TimelinePostInfo - { - /// - /// Post id. - /// - public long Id { get; set; } - /// - /// Content of the post. May be null if post is deleted. - /// - public TimelinePostContentInfo? Content { get; set; } - /// - /// True if post is deleted. - /// - public bool Deleted { get; set; } - /// - /// Post time. - /// - public DateTime Time { get; set; } - /// - /// The author. May be null if the user has been deleted. - /// - public UserInfo? Author { get; set; } = default!; - /// - /// Last updated time. - /// - public DateTime LastUpdated { get; set; } = default!; - } - - /// - /// Info of a timeline. - /// - public class TimelineInfo - { - /// - /// Unique id. - /// - public string UniqueId { get; set; } = default!; - /// - /// Title. - /// - public string Title { get; set; } = default!; - /// - /// Name of timeline. - /// - public string Name { get; set; } = default!; - /// - /// Last modified time of timeline name. - /// - public DateTime NameLastModifed { get; set; } = default!; - /// - /// Timeline description. - /// - public string Description { get; set; } = default!; - /// - /// Owner of the timeline. - /// - public UserInfo Owner { get; set; } = default!; - /// - /// Visibility of the timeline. - /// - public TimelineVisibility Visibility { get; set; } -#pragma warning disable CA2227 // Collection properties should be read only - /// - /// Members of timeline. - /// - public List Members { get; set; } = default!; -#pragma warning restore CA2227 // Collection properties should be read only - /// - /// Create time of timeline. - /// - public DateTime CreateTime { get; set; } = default!; - /// - /// Last modified time of timeline. - /// - public DateTime LastModified { get; set; } = default!; - -#pragma warning disable CA1707 // Identifiers should not contain underscores - /// - /// Related links. - /// - public TimelineInfoLinks _links { get; set; } = default!; -#pragma warning restore CA1707 // Identifiers should not contain underscores - } - - /// - /// Related links for timeline. - /// - public class TimelineInfoLinks - { - /// - /// Self. - /// - public string Self { get; set; } = default!; - /// - /// Posts url. - /// - public string Posts { get; set; } = default!; - } - - public class TimelineInfoLinksValueResolver : IValueResolver - { - private readonly IActionContextAccessor _actionContextAccessor; - private readonly IUrlHelperFactory _urlHelperFactory; - - public TimelineInfoLinksValueResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) - { - _actionContextAccessor = actionContextAccessor; - _urlHelperFactory = urlHelperFactory; - } - - public TimelineInfoLinks Resolve(Timeline source, TimelineInfo destination, TimelineInfoLinks destMember, ResolutionContext context) - { - var actionContext = _actionContextAccessor.AssertActionContextForUrlFill(); - var urlHelper = _urlHelperFactory.GetUrlHelper(actionContext); - - return new TimelineInfoLinks - { - Self = urlHelper.ActionLink(nameof(TimelineController.TimelineGet), nameof(TimelineController)[0..^nameof(Controller).Length], new { source.Name }), - Posts = urlHelper.ActionLink(nameof(TimelineController.PostListGet), nameof(TimelineController)[0..^nameof(Controller).Length], new { source.Name }) - }; - } - } - - public class TimelinePostContentResolver : IValueResolver - { - private readonly IActionContextAccessor _actionContextAccessor; - private readonly IUrlHelperFactory _urlHelperFactory; - - public TimelinePostContentResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) - { - _actionContextAccessor = actionContextAccessor; - _urlHelperFactory = urlHelperFactory; - } - - public TimelinePostContentInfo? Resolve(TimelinePost source, TimelinePostInfo destination, TimelinePostContentInfo? destMember, ResolutionContext context) - { - var actionContext = _actionContextAccessor.AssertActionContextForUrlFill(); - var urlHelper = _urlHelperFactory.GetUrlHelper(actionContext); - - var sourceContent = source.Content; - - if (sourceContent == null) - { - return null; - } - - if (sourceContent is TextTimelinePostContent textContent) - { - return new TimelinePostContentInfo - { - Type = TimelinePostContentTypes.Text, - Text = textContent.Text - }; - } - else if (sourceContent is ImageTimelinePostContent imageContent) - { - return new TimelinePostContentInfo - { - Type = TimelinePostContentTypes.Image, - Url = urlHelper.ActionLink( - action: nameof(TimelineController.PostDataGet), - controller: nameof(TimelineController)[0..^nameof(Controller).Length], - values: new { Name = source.TimelineName, Id = source.Id }), - ETag = $"\"{imageContent.DataTag}\"" - }; - } - else - { - throw new InvalidOperationException(Resources.Models.Http.Exception.UnknownPostContentType); - } - } - } - - public class TimelineInfoAutoMapperProfile : Profile - { - public TimelineInfoAutoMapperProfile() - { - CreateMap().ForMember(u => u._links, opt => opt.MapFrom()); - CreateMap().ForMember(p => p.Content, opt => opt.MapFrom()); - CreateMap(); - } - } -} diff --git a/Timeline/Models/Http/TimelineController.cs b/Timeline/Models/Http/TimelineController.cs deleted file mode 100644 index 7bd141ed..00000000 --- a/Timeline/Models/Http/TimelineController.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using Timeline.Models.Validation; - -namespace Timeline.Models.Http -{ - /// - /// Content of post create request. - /// - public class TimelinePostCreateRequestContent - { - /// - /// Type of post content. - /// - [Required] - public string Type { get; set; } = default!; - /// - /// If post is of text type, this is the text. - /// - public string? Text { get; set; } - /// - /// If post is of image type, this is base64 of image data. - /// - public string? Data { get; set; } - } - - public class TimelinePostCreateRequest - { - /// - /// Content of the new post. - /// - [Required] - public TimelinePostCreateRequestContent Content { get; set; } = default!; - - /// - /// Time of the post. If not set, current time will be used. - /// - public DateTime? Time { get; set; } - } - - /// - /// Create timeline request model. - /// - public class TimelineCreateRequest - { - /// - /// Name of the new timeline. Must be a valid name. - /// - [Required] - [TimelineName] - public string Name { get; set; } = default!; - } - - /// - /// Patch timeline request model. - /// - public class TimelinePatchRequest - { - /// - /// New title. Null for not change. - /// - public string? Title { get; set; } - - /// - /// New description. Null for not change. - /// - public string? Description { get; set; } - - /// - /// New visibility. Null for not change. - /// - public TimelineVisibility? Visibility { get; set; } - } - - /// - /// Change timeline name request model. - /// - public class TimelineChangeNameRequest - { - /// - /// Old name of timeline. - /// - [Required] - [TimelineName] - public string OldName { get; set; } = default!; - /// - /// New name of timeline. - /// - [Required] - [TimelineName] - public string NewName { get; set; } = default!; - } -} diff --git a/Timeline/Models/Http/TokenController.cs b/Timeline/Models/Http/TokenController.cs deleted file mode 100644 index a42c44e5..00000000 --- a/Timeline/Models/Http/TokenController.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Timeline.Controllers; - -namespace Timeline.Models.Http -{ - /// - /// Request model for . - /// - public class CreateTokenRequest - { - /// - /// The username. - /// - public string Username { get; set; } = default!; - /// - /// The password. - /// - public string Password { get; set; } = default!; - /// - /// Optional token validation period. In days. If not specified, server will use a default one. - /// - [Range(1, 365)] - public int? Expire { get; set; } - } - - /// - /// Response model for . - /// - public class CreateTokenResponse - { - /// - /// The token created. - /// - public string Token { get; set; } = default!; - /// - /// The user owning the token. - /// - public UserInfo User { get; set; } = default!; - } - - /// - /// Request model for . - /// - public class VerifyTokenRequest - { - /// - /// The token to verify. - /// - public string Token { get; set; } = default!; - } - - /// - /// Response model for . - /// - public class VerifyTokenResponse - { - /// - /// The user owning the token. - /// - public UserInfo User { get; set; } = default!; - } -} diff --git a/Timeline/Models/Http/UserController.cs b/Timeline/Models/Http/UserController.cs deleted file mode 100644 index 6bc5a66e..00000000 --- a/Timeline/Models/Http/UserController.cs +++ /dev/null @@ -1,93 +0,0 @@ -using AutoMapper; -using System.ComponentModel.DataAnnotations; -using Timeline.Controllers; -using Timeline.Models.Validation; - -namespace Timeline.Models.Http -{ - /// - /// Request model for . - /// - public class UserPatchRequest - { - /// - /// New username. Null if not change. Need to be administrator. - /// - [Username] - public string? Username { get; set; } - - /// - /// New password. Null if not change. Need to be administrator. - /// - [MinLength(1)] - public string? Password { get; set; } - - /// - /// New nickname. Null if not change. Need to be administrator to change other's. - /// - [Nickname] - public string? Nickname { get; set; } - - /// - /// Whether to be administrator. Null if not change. Need to be administrator. - /// - public bool? Administrator { get; set; } - } - - /// - /// Request model for . - /// - public class CreateUserRequest - { - /// - /// Username of the new user. - /// - [Required, Username] - public string Username { get; set; } = default!; - - /// - /// Password of the new user. - /// - [Required, MinLength(1)] - public string Password { get; set; } = default!; - - /// - /// Whether the new user is administrator. - /// - [Required] - public bool? Administrator { get; set; } - - /// - /// Nickname of the new user. - /// - [Nickname] - public string? Nickname { get; set; } - } - - /// - /// Request model for . - /// - public class ChangePasswordRequest - { - /// - /// Old password. - /// - [Required(AllowEmptyStrings = false)] - public string OldPassword { get; set; } = default!; - - /// - /// New password. - /// - [Required(AllowEmptyStrings = false)] - public string NewPassword { get; set; } = default!; - } - - public class UserControllerAutoMapperProfile : Profile - { - public UserControllerAutoMapperProfile() - { - CreateMap(MemberList.Source); - CreateMap(MemberList.Source); - } - } -} diff --git a/Timeline/Models/Http/UserInfo.cs b/Timeline/Models/Http/UserInfo.cs deleted file mode 100644 index d92a12c4..00000000 --- a/Timeline/Models/Http/UserInfo.cs +++ /dev/null @@ -1,90 +0,0 @@ -using AutoMapper; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Mvc.Routing; -using Timeline.Controllers; - -namespace Timeline.Models.Http -{ - /// - /// Info of a user. - /// - public class UserInfo - { - /// - /// Unique id. - /// - public string UniqueId { get; set; } = default!; - /// - /// Username. - /// - public string Username { get; set; } = default!; - /// - /// Nickname. - /// - public string Nickname { get; set; } = default!; - /// - /// True if the user is a administrator. - /// - public bool? Administrator { get; set; } = default!; -#pragma warning disable CA1707 // Identifiers should not contain underscores - /// - /// Related links. - /// - public UserInfoLinks _links { get; set; } = default!; -#pragma warning restore CA1707 // Identifiers should not contain underscores - } - - /// - /// Related links for user. - /// - public class UserInfoLinks - { - /// - /// Self. - /// - public string Self { get; set; } = default!; - /// - /// Avatar url. - /// - public string Avatar { get; set; } = default!; - /// - /// Personal timeline url. - /// - public string Timeline { get; set; } = default!; - } - - public class UserInfoLinksValueResolver : IValueResolver - { - private readonly IActionContextAccessor _actionContextAccessor; - private readonly IUrlHelperFactory _urlHelperFactory; - - public UserInfoLinksValueResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) - { - _actionContextAccessor = actionContextAccessor; - _urlHelperFactory = urlHelperFactory; - } - - public UserInfoLinks Resolve(User source, UserInfo destination, UserInfoLinks destMember, ResolutionContext context) - { - var actionContext = _actionContextAccessor.AssertActionContextForUrlFill(); - var urlHelper = _urlHelperFactory.GetUrlHelper(actionContext); - - var result = new UserInfoLinks - { - Self = urlHelper.ActionLink(nameof(UserController.Get), nameof(UserController)[0..^nameof(Controller).Length], new { destination.Username }), - Avatar = urlHelper.ActionLink(nameof(UserAvatarController.Get), nameof(UserAvatarController)[0..^nameof(Controller).Length], new { destination.Username }), - Timeline = urlHelper.ActionLink(nameof(TimelineController.TimelineGet), nameof(TimelineController)[0..^nameof(Controller).Length], new { Name = "@" + destination.Username }) - }; - return result; - } - } - - public class UserInfoAutoMapperProfile : Profile - { - public UserInfoAutoMapperProfile() - { - CreateMap().ForMember(u => u._links, opt => opt.MapFrom()); - } - } -} diff --git a/Timeline/Models/Timeline.cs b/Timeline/Models/Timeline.cs deleted file mode 100644 index a5987577..00000000 --- a/Timeline/Models/Timeline.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Timeline.Models -{ - public enum TimelineVisibility - { - /// - /// All people including those without accounts. - /// - Public, - /// - /// Only people signed in. - /// - Register, - /// - /// Only member. - /// - Private - } - - public static class TimelinePostContentTypes - { - public const string Text = "text"; - public const string Image = "image"; - } - - public interface ITimelinePostContent - { - public string Type { get; } - } - - public class TextTimelinePostContent : ITimelinePostContent - { - public TextTimelinePostContent(string text) { Text = text; } - - public string Type { get; } = TimelinePostContentTypes.Text; - public string Text { get; set; } - } - - public class ImageTimelinePostContent : ITimelinePostContent - { - public ImageTimelinePostContent(string dataTag) { DataTag = dataTag; } - - public string Type { get; } = TimelinePostContentTypes.Image; - - /// - /// The tag of the data. The tag of the entry in DataManager. Also the etag (not quoted). - /// - public string DataTag { get; set; } - } - - public class TimelinePost - { - public TimelinePost(long id, ITimelinePostContent? content, DateTime time, User? author, DateTime lastUpdated, string timelineName) - { - Id = id; - Content = content; - Time = time; - Author = author; - LastUpdated = lastUpdated; - TimelineName = timelineName; - } - - public long Id { get; set; } - public ITimelinePostContent? Content { get; set; } - public bool Deleted => Content == null; - public DateTime Time { get; set; } - public User? Author { get; set; } - public DateTime LastUpdated { get; set; } - public string TimelineName { get; set; } - } - -#pragma warning disable CA1724 // Type names should not match namespaces - public class Timeline -#pragma warning restore CA1724 // Type names should not match namespaces - { - public string UniqueID { get; set; } = default!; - public string Name { get; set; } = default!; - public DateTime NameLastModified { get; set; } = default!; - public string Title { get; set; } = default!; - public string Description { get; set; } = default!; - public User Owner { get; set; } = default!; - public TimelineVisibility Visibility { get; set; } -#pragma warning disable CA2227 // Collection properties should be read only - public List Members { get; set; } = default!; -#pragma warning restore CA2227 // Collection properties should be read only - public DateTime CreateTime { get; set; } = default!; - public DateTime LastModified { get; set; } = default!; - } - - public class TimelineChangePropertyRequest - { - public string? Title { get; set; } - public string? Description { get; set; } - public TimelineVisibility? Visibility { get; set; } - } -} diff --git a/Timeline/Models/User.cs b/Timeline/Models/User.cs deleted file mode 100644 index f08a62db..00000000 --- a/Timeline/Models/User.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; - -namespace Timeline.Models -{ - public class User - { - public string? UniqueId { get; set; } - public string? Username { get; set; } - public string? Nickname { get; set; } - public bool? Administrator { get; set; } - - #region secret - public long? Id { get; set; } - public string? Password { get; set; } - public long? Version { get; set; } - public DateTime? UsernameChangeTime { get; set; } - public DateTime? CreateTime { get; set; } - public DateTime? LastModified { get; set; } - #endregion secret - } -} diff --git a/Timeline/Models/Validation/GeneralTimelineNameValidator.cs b/Timeline/Models/Validation/GeneralTimelineNameValidator.cs deleted file mode 100644 index e1c96fbd..00000000 --- a/Timeline/Models/Validation/GeneralTimelineNameValidator.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; - -namespace Timeline.Models.Validation -{ - public class GeneralTimelineNameValidator : Validator - { - private readonly UsernameValidator _usernameValidator = new UsernameValidator(); - private readonly TimelineNameValidator _timelineNameValidator = new TimelineNameValidator(); - - protected override (bool, string) DoValidate(string value) - { - if (value.StartsWith('@')) - { - return _usernameValidator.Validate(value.Substring(1)); - } - else - { - return _timelineNameValidator.Validate(value); - } - } - } - - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, - AllowMultiple = false)] - public class GeneralTimelineNameAttribute : ValidateWithAttribute - { - public GeneralTimelineNameAttribute() - : base(typeof(GeneralTimelineNameValidator)) - { - - } - } -} diff --git a/Timeline/Models/Validation/NameValidator.cs b/Timeline/Models/Validation/NameValidator.cs deleted file mode 100644 index b74c40b7..00000000 --- a/Timeline/Models/Validation/NameValidator.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Linq; -using System.Text.RegularExpressions; -using static Timeline.Resources.Models.Validation.NameValidator; - -namespace Timeline.Models.Validation -{ - public class NameValidator : Validator - { - private static Regex UniqueIdRegex { get; } = new Regex(@"^[a-zA-Z0-9]{32}$"); - - public const int MaxLength = 26; - - protected override (bool, string) DoValidate(string value) - { - if (value.Length == 0) - { - return (false, MessageEmptyString); - } - - if (value.Length > MaxLength) - { - return (false, MessageTooLong); - } - - foreach ((char c, int i) in value.Select((c, i) => (c, i))) - { - if (!(char.IsLetterOrDigit(c) || c == '-' || c == '_')) - { - return (false, MessageInvalidChar); - } - } - - // Currently name can't be longer than 26. So this is not needed. But reserve it for future use. - if (UniqueIdRegex.IsMatch(value)) - { - return (false, MessageUnqiueId); - } - - return (true, GetSuccessMessage()); - } - } -} diff --git a/Timeline/Models/Validation/NicknameValidator.cs b/Timeline/Models/Validation/NicknameValidator.cs deleted file mode 100644 index 1d6ab163..00000000 --- a/Timeline/Models/Validation/NicknameValidator.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using static Timeline.Resources.Models.Validation.NicknameValidator; - -namespace Timeline.Models.Validation -{ - public class NicknameValidator : Validator - { - protected override (bool, string) DoValidate(string value) - { - if (value.Length > 25) - return (false, MessageTooLong); - - return (true, GetSuccessMessage()); - } - } - - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] - public class NicknameAttribute : ValidateWithAttribute - { - public NicknameAttribute() : base(typeof(NicknameValidator)) - { - - } - } -} diff --git a/Timeline/Models/Validation/TimelineNameValidator.cs b/Timeline/Models/Validation/TimelineNameValidator.cs deleted file mode 100644 index f1ab54e8..00000000 --- a/Timeline/Models/Validation/TimelineNameValidator.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; - -namespace Timeline.Models.Validation -{ - public class TimelineNameValidator : NameValidator - { - } - - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, - AllowMultiple = false)] - public class TimelineNameAttribute : ValidateWithAttribute - { - public TimelineNameAttribute() - : base(typeof(TimelineNameValidator)) - { - - } - } -} diff --git a/Timeline/Models/Validation/UsernameValidator.cs b/Timeline/Models/Validation/UsernameValidator.cs deleted file mode 100644 index 87bbf85f..00000000 --- a/Timeline/Models/Validation/UsernameValidator.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; - -namespace Timeline.Models.Validation -{ - public class UsernameValidator : NameValidator - { - } - - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, - AllowMultiple = false)] - public class UsernameAttribute : ValidateWithAttribute - { - public UsernameAttribute() - : base(typeof(UsernameValidator)) - { - - } - } -} diff --git a/Timeline/Models/Validation/Validator.cs b/Timeline/Models/Validation/Validator.cs deleted file mode 100644 index aef7891c..00000000 --- a/Timeline/Models/Validation/Validator.cs +++ /dev/null @@ -1,127 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using static Timeline.Resources.Models.Validation.Validator; - -namespace Timeline.Models.Validation -{ - /// - /// A validator to validate value. - /// - public interface IValidator - { - /// - /// Validate given value. - /// - /// The value to validate. - /// Validation success or not and message. - (bool, string) Validate(object? value); - } - - public static class ValidatorExtensions - { - public static bool Validate(this IValidator validator, object? value, out string message) - { - if (validator == null) - throw new ArgumentNullException(nameof(validator)); - - var (r, m) = validator.Validate(value); - message = m; - return r; - } - } - - /// - /// Convenient base class for validator. - /// - /// The type of accepted value. - /// - /// Subclass should override to do the real validation. - /// This class will check the nullity and type of value. - /// If value is null, it will pass or fail depending on . - /// If value is not null and not of type - /// it will fail and not call . - /// - /// is true by default. - /// - /// If you want some other behaviours, write the validator from scratch. - /// - public abstract class Validator : IValidator - { - protected bool PermitNull { get; set; } = true; - - public (bool, string) Validate(object? value) - { - if (value == null) - { - if (PermitNull) - return (true, GetSuccessMessage()); - else - return (false, ValidatorMessageNull); - } - - if (value is T v) - { - return DoValidate(v); - } - else - { - return (false, ValidatorMessageBadType); - } - } - - protected static string GetSuccessMessage() => ValidatorMessageSuccess; - - protected abstract (bool, string) DoValidate(T value); - } - - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, - AllowMultiple = false)] - public class ValidateWithAttribute : ValidationAttribute - { - private readonly IValidator _validator; - - /// - /// Create with a given validator. - /// - /// The validator used to validate. - public ValidateWithAttribute(IValidator validator) - { - _validator = validator ?? throw new ArgumentNullException(nameof(validator)); - } - - /// - /// Create the validator with default constructor. - /// - /// The type of the validator. - public ValidateWithAttribute(Type validatorType) - { - if (validatorType == null) - throw new ArgumentNullException(nameof(validatorType)); - - if (!typeof(IValidator).IsAssignableFrom(validatorType)) - throw new ArgumentException(ValidateWithAttributeExceptionNotValidator, nameof(validatorType)); - - try - { - _validator = (Activator.CreateInstance(validatorType) as IValidator)!; - } - catch (Exception e) - { - throw new ArgumentException(ValidateWithAttributeExceptionCreateFail, e); - } - } - - protected override ValidationResult IsValid(object value, ValidationContext validationContext) - { - var (result, message) = _validator.Validate(value); - if (result) - { - return ValidationResult.Success; - } - else - { - return new ValidationResult(message); - } - } - } -} diff --git a/Timeline/Program.cs b/Timeline/Program.cs deleted file mode 100644 index 87e330a2..00000000 --- a/Timeline/Program.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using System.Resources; -using Timeline.Entities; -using Timeline.Services; - -[assembly: NeutralResourcesLanguage("en")] - -namespace Timeline -{ - public static class Program - { - public static void Main(string[] args) - { - var host = CreateWebHostBuilder(args).Build(); - - var env = host.Services.GetRequiredService(); - - var databaseBackupService = host.Services.GetRequiredService(); - databaseBackupService.BackupNow(); - - if (env.IsProduction()) - { - using (var scope = host.Services.CreateScope()) - { - var databaseContext = scope.ServiceProvider.GetRequiredService(); - databaseContext.Database.Migrate(); - } - } - - host.Run(); - } - - public static IHostBuilder CreateWebHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); - } -} diff --git a/Timeline/Properties/launchSettings.json b/Timeline/Properties/launchSettings.json deleted file mode 100644 index de8186db..00000000 --- a/Timeline/Properties/launchSettings.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "profiles": { - "Development": { - "commandName": "Project", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "ASPNETCORE_USEPROXYFRONTEND": "true", - "ASPNETCORE_WORKDIR": "D:\\timeline-development" - } - }, - "Development-Mock": { - "commandName": "Project", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "ASPNETCORE_USEMOCKFRONTEND": "true", - "ASPNETCORE_WORKDIR": "D:\\timeline-development" - } - }, - "Staging": { - "commandName": "Project", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Staging", - "ASPNETCORE_WORKDIR": "D:\\timeline-development" - } - } - } -} diff --git a/Timeline/Resources/Authentication/AuthHandler.Designer.cs b/Timeline/Resources/Authentication/AuthHandler.Designer.cs deleted file mode 100644 index fd4540ea..00000000 --- a/Timeline/Resources/Authentication/AuthHandler.Designer.cs +++ /dev/null @@ -1,99 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Resources.Authentication { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class AuthHandler { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal AuthHandler() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Authentication.AuthHandler", typeof(AuthHandler).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Token is found in authorization header. Token is {0} .. - /// - internal static string LogTokenFoundInHeader { - get { - return ResourceManager.GetString("LogTokenFoundInHeader", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Token is found in query param with key "{0}". Token is {1} .. - /// - internal static string LogTokenFoundInQuery { - get { - return ResourceManager.GetString("LogTokenFoundInQuery", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to No jwt token is found.. - /// - internal static string LogTokenNotFound { - get { - return ResourceManager.GetString("LogTokenNotFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A jwt token validation failed.. - /// - internal static string LogTokenValidationFail { - get { - return ResourceManager.GetString("LogTokenValidationFail", resourceCulture); - } - } - } -} diff --git a/Timeline/Resources/Authentication/AuthHandler.resx b/Timeline/Resources/Authentication/AuthHandler.resx deleted file mode 100644 index 4cddc8ce..00000000 --- a/Timeline/Resources/Authentication/AuthHandler.resx +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Token is found in authorization header. Token is {0} . - - - Token is found in query param with key "{0}". Token is {1} . - - - No jwt token is found. - - - A jwt token validation failed. - - \ No newline at end of file diff --git a/Timeline/Resources/Controllers/ControllerAuthExtensions.Designer.cs b/Timeline/Resources/Controllers/ControllerAuthExtensions.Designer.cs deleted file mode 100644 index 70a1d605..00000000 --- a/Timeline/Resources/Controllers/ControllerAuthExtensions.Designer.cs +++ /dev/null @@ -1,81 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Resources.Controllers { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class ControllerAuthExtensions { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal ControllerAuthExtensions() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Controllers.ControllerAuthExtensions", typeof(ControllerAuthExtensions).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Failed to get user id because User has no NameIdentifier claim.. - /// - internal static string ExceptionNoUserIdentifierClaim { - get { - return ResourceManager.GetString("ExceptionNoUserIdentifierClaim", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Failed to get user id because NameIdentifier claim is not a number.. - /// - internal static string ExceptionUserIdentifierClaimBadFormat { - get { - return ResourceManager.GetString("ExceptionUserIdentifierClaimBadFormat", resourceCulture); - } - } - } -} diff --git a/Timeline/Resources/Controllers/ControllerAuthExtensions.resx b/Timeline/Resources/Controllers/ControllerAuthExtensions.resx deleted file mode 100644 index 03e6d95a..00000000 --- a/Timeline/Resources/Controllers/ControllerAuthExtensions.resx +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Failed to get user id because User has no NameIdentifier claim. - - - Failed to get user id because NameIdentifier claim is not a number. - - \ No newline at end of file diff --git a/Timeline/Resources/Controllers/TimelineController.Designer.cs b/Timeline/Resources/Controllers/TimelineController.Designer.cs deleted file mode 100644 index ae6414e6..00000000 --- a/Timeline/Resources/Controllers/TimelineController.Designer.cs +++ /dev/null @@ -1,81 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Resources.Controllers { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class TimelineController { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal TimelineController() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Controllers.TimelineController", typeof(TimelineController).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to An unknown timeline visibility value. Can't convert it.. - /// - internal static string ExceptionStringToVisibility { - get { - return ResourceManager.GetString("ExceptionStringToVisibility", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to An unknown TimelineMemberOperationUserException is thrown. Can't recognize its inner exception. It is rethrown.. - /// - internal static string LogUnknownTimelineMemberOperationUserException { - get { - return ResourceManager.GetString("LogUnknownTimelineMemberOperationUserException", resourceCulture); - } - } - } -} diff --git a/Timeline/Resources/Controllers/TimelineController.resx b/Timeline/Resources/Controllers/TimelineController.resx deleted file mode 100644 index 4cf3d6fb..00000000 --- a/Timeline/Resources/Controllers/TimelineController.resx +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - An unknown timeline visibility value. Can't convert it. - - - An unknown TimelineMemberOperationUserException is thrown. Can't recognize its inner exception. It is rethrown. - - \ No newline at end of file diff --git a/Timeline/Resources/Controllers/TokenController.Designer.cs b/Timeline/Resources/Controllers/TokenController.Designer.cs deleted file mode 100644 index a7c2864b..00000000 --- a/Timeline/Resources/Controllers/TokenController.Designer.cs +++ /dev/null @@ -1,153 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Resources.Controllers { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class TokenController { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal TokenController() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Controllers.TokenController", typeof(TokenController).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to The password is wrong.. - /// - internal static string LogBadPassword { - get { - return ResourceManager.GetString("LogBadPassword", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A user failed to create a token.. - /// - internal static string LogCreateFailure { - get { - return ResourceManager.GetString("LogCreateFailure", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A user succeeded to create a token.. - /// - internal static string LogCreateSuccess { - get { - return ResourceManager.GetString("LogCreateSuccess", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The user does not exist.. - /// - internal static string LogUserNotExist { - get { - return ResourceManager.GetString("LogUserNotExist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The token is of bad format. It might not be created by the server.. - /// - internal static string LogVerifyBadFormat { - get { - return ResourceManager.GetString("LogVerifyBadFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The token is expired.. - /// - internal static string LogVerifyExpire { - get { - return ResourceManager.GetString("LogVerifyExpire", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A token failed to be verified.. - /// - internal static string LogVerifyFailure { - get { - return ResourceManager.GetString("LogVerifyFailure", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Token has an old version. User might have update some info.. - /// - internal static string LogVerifyOldVersion { - get { - return ResourceManager.GetString("LogVerifyOldVersion", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A token succeeded to be verified.. - /// - internal static string LogVerifySuccess { - get { - return ResourceManager.GetString("LogVerifySuccess", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to User does not exist. Administrator might have deleted this user.. - /// - internal static string LogVerifyUserNotExist { - get { - return ResourceManager.GetString("LogVerifyUserNotExist", resourceCulture); - } - } - } -} diff --git a/Timeline/Resources/Controllers/TokenController.resx b/Timeline/Resources/Controllers/TokenController.resx deleted file mode 100644 index 683d6cc9..00000000 --- a/Timeline/Resources/Controllers/TokenController.resx +++ /dev/null @@ -1,150 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - The password is wrong. - - - A user failed to create a token. - - - A user succeeded to create a token. - - - The user does not exist. - - - The token is of bad format. It might not be created by the server. - - - The token is expired. - - - A token failed to be verified. - - - Token has an old version. User might have update some info. - - - A token succeeded to be verified. - - - User does not exist. Administrator might have deleted this user. - - \ No newline at end of file diff --git a/Timeline/Resources/Controllers/UserAvatarController.Designer.cs b/Timeline/Resources/Controllers/UserAvatarController.Designer.cs deleted file mode 100644 index b0c35ff9..00000000 --- a/Timeline/Resources/Controllers/UserAvatarController.Designer.cs +++ /dev/null @@ -1,144 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Resources.Controllers { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class UserAvatarController { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal UserAvatarController() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Controllers.UserAvatarController", typeof(UserAvatarController).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Unknown AvatarDataException.ErrorReason value.. - /// - internal static string ExceptionUnknownAvatarFormatError { - get { - return ResourceManager.GetString("ExceptionUnknownAvatarFormatError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Attempt to delete a avatar of other user as a non-admin failed.. - /// - internal static string LogDeleteForbid { - get { - return ResourceManager.GetString("LogDeleteForbid", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Attempt to delete a avatar of a non-existent user failed.. - /// - internal static string LogDeleteNotExist { - get { - return ResourceManager.GetString("LogDeleteNotExist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Succeed to delete a avatar of a user.. - /// - internal static string LogDeleteSuccess { - get { - return ResourceManager.GetString("LogDeleteSuccess", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Attempt to get a avatar of a non-existent user failed.. - /// - internal static string LogGetUserNotExist { - get { - return ResourceManager.GetString("LogGetUserNotExist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Attempt to put a avatar of other user as a non-admin failed.. - /// - internal static string LogPutForbid { - get { - return ResourceManager.GetString("LogPutForbid", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Succeed to put a avatar of a user.. - /// - internal static string LogPutSuccess { - get { - return ResourceManager.GetString("LogPutSuccess", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Attempt to put a avatar of a bad format failed.. - /// - internal static string LogPutUserBadFormat { - get { - return ResourceManager.GetString("LogPutUserBadFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Attempt to put a avatar of a non-existent user failed.. - /// - internal static string LogPutUserNotExist { - get { - return ResourceManager.GetString("LogPutUserNotExist", resourceCulture); - } - } - } -} diff --git a/Timeline/Resources/Controllers/UserAvatarController.resx b/Timeline/Resources/Controllers/UserAvatarController.resx deleted file mode 100644 index 864d96c0..00000000 --- a/Timeline/Resources/Controllers/UserAvatarController.resx +++ /dev/null @@ -1,147 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Unknown AvatarDataException.ErrorReason value. - - - Attempt to delete a avatar of other user as a non-admin failed. - - - Attempt to delete a avatar of a non-existent user failed. - - - Succeed to delete a avatar of a user. - - - Attempt to get a avatar of a non-existent user failed. - - - Attempt to put a avatar of other user as a non-admin failed. - - - Succeed to put a avatar of a user. - - - Attempt to put a avatar of a bad format failed. - - - Attempt to put a avatar of a non-existent user failed. - - \ No newline at end of file diff --git a/Timeline/Resources/Controllers/UserController.Designer.cs b/Timeline/Resources/Controllers/UserController.Designer.cs deleted file mode 100644 index c8067614..00000000 --- a/Timeline/Resources/Controllers/UserController.Designer.cs +++ /dev/null @@ -1,117 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Resources.Controllers { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class UserController { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal UserController() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Controllers.UserController", typeof(UserController).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Unknown PutResult.. - /// - internal static string ExceptionUnknownPutResult { - get { - return ResourceManager.GetString("ExceptionUnknownPutResult", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Attempt to change password with wrong old password failed.. - /// - internal static string LogChangePasswordBadPassword { - get { - return ResourceManager.GetString("LogChangePasswordBadPassword", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Attempt to change a user's username to a existent one failed.. - /// - internal static string LogChangeUsernameConflict { - get { - return ResourceManager.GetString("LogChangeUsernameConflict", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Attempt to change a username of a user that does not exist failed.. - /// - internal static string LogChangeUsernameNotExist { - get { - return ResourceManager.GetString("LogChangeUsernameNotExist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Attempt to retrieve info of a user that does not exist failed.. - /// - internal static string LogGetUserNotExist { - get { - return ResourceManager.GetString("LogGetUserNotExist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Attempt to patch a user that does not exist failed.. - /// - internal static string LogPatchUserNotExist { - get { - return ResourceManager.GetString("LogPatchUserNotExist", resourceCulture); - } - } - } -} diff --git a/Timeline/Resources/Controllers/UserController.resx b/Timeline/Resources/Controllers/UserController.resx deleted file mode 100644 index 0bdf4845..00000000 --- a/Timeline/Resources/Controllers/UserController.resx +++ /dev/null @@ -1,138 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Unknown PutResult. - - - Attempt to change password with wrong old password failed. - - - Attempt to change a user's username to a existent one failed. - - - Attempt to change a username of a user that does not exist failed. - - - Attempt to retrieve info of a user that does not exist failed. - - - Attempt to patch a user that does not exist failed. - - \ No newline at end of file diff --git a/Timeline/Resources/Entities.Designer.cs b/Timeline/Resources/Entities.Designer.cs deleted file mode 100644 index 5f286f23..00000000 --- a/Timeline/Resources/Entities.Designer.cs +++ /dev/null @@ -1,72 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Resources { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Entities { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Entities() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Entities", typeof(Entities).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Only sqlite is supported.. - /// - internal static string ExceptionOnlySqliteSupported { - get { - return ResourceManager.GetString("ExceptionOnlySqliteSupported", resourceCulture); - } - } - } -} diff --git a/Timeline/Resources/Entities.resx b/Timeline/Resources/Entities.resx deleted file mode 100644 index 1538b533..00000000 --- a/Timeline/Resources/Entities.resx +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Only sqlite is supported. - - \ No newline at end of file diff --git a/Timeline/Resources/Filters.Designer.cs b/Timeline/Resources/Filters.Designer.cs deleted file mode 100644 index dedfe498..00000000 --- a/Timeline/Resources/Filters.Designer.cs +++ /dev/null @@ -1,90 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Resources { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Filters { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Filters() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Filters", typeof(Filters).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to You apply a SelfOrAdminAttribute on an action, but there is no user. Try add AuthorizeAttribute.. - /// - internal static string LogSelfOrAdminNoUser { - get { - return ResourceManager.GetString("LogSelfOrAdminNoUser", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You apply a SelfOrAdminAttribute on an action, but it does not have a model named username.. - /// - internal static string LogSelfOrAdminNoUsername { - get { - return ResourceManager.GetString("LogSelfOrAdminNoUsername", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You apply a SelfOrAdminAttribute on an action, found a model named username, but it is not string.. - /// - internal static string LogSelfOrAdminUsernameNotString { - get { - return ResourceManager.GetString("LogSelfOrAdminUsernameNotString", resourceCulture); - } - } - } -} diff --git a/Timeline/Resources/Filters.resx b/Timeline/Resources/Filters.resx deleted file mode 100644 index 22620889..00000000 --- a/Timeline/Resources/Filters.resx +++ /dev/null @@ -1,129 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - You apply a SelfOrAdminAttribute on an action, but there is no user. Try add AuthorizeAttribute. - - - You apply a SelfOrAdminAttribute on an action, but it does not have a model named username. - - - You apply a SelfOrAdminAttribute on an action, found a model named username, but it is not string. - - \ No newline at end of file diff --git a/Timeline/Resources/Helper/DataCacheHelper.Designer.cs b/Timeline/Resources/Helper/DataCacheHelper.Designer.cs deleted file mode 100644 index acf56d13..00000000 --- a/Timeline/Resources/Helper/DataCacheHelper.Designer.cs +++ /dev/null @@ -1,90 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Resources.Helper { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class DataCacheHelper { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal DataCacheHelper() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Helper.DataCacheHelper", typeof(DataCacheHelper).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Header If-None-Match is of bad format.. - /// - internal static string LogBadIfNoneMatch { - get { - return ResourceManager.GetString("LogBadIfNoneMatch", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cache is invalid and data is returned.. - /// - internal static string LogResultData { - get { - return ResourceManager.GetString("LogResultData", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cache is valid and 304 Not Modified is returned.. - /// - internal static string LogResultNotModified { - get { - return ResourceManager.GetString("LogResultNotModified", resourceCulture); - } - } - } -} diff --git a/Timeline/Resources/Helper/DataCacheHelper.resx b/Timeline/Resources/Helper/DataCacheHelper.resx deleted file mode 100644 index 515cfa9b..00000000 --- a/Timeline/Resources/Helper/DataCacheHelper.resx +++ /dev/null @@ -1,129 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Header If-None-Match is of bad format. - - - Cache is invalid and data is returned. - - - Cache is valid and 304 Not Modified is returned. - - \ No newline at end of file diff --git a/Timeline/Resources/Messages.Designer.cs b/Timeline/Resources/Messages.Designer.cs deleted file mode 100644 index bb654ce6..00000000 --- a/Timeline/Resources/Messages.Designer.cs +++ /dev/null @@ -1,396 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Resources { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Messages { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Messages() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Messages", typeof(Messages).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Body is too big. It can't be bigger than {0}.. - /// - internal static string Common_Content_TooBig { - get { - return ResourceManager.GetString("Common_Content_TooBig", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Actual body length is bigger than it in header.. - /// - internal static string Common_Content_UnmatchedLength_Bigger { - get { - return ResourceManager.GetString("Common_Content_UnmatchedLength_Bigger", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Actual body length is smaller than it in header.. - /// - internal static string Common_Content_UnmatchedLength_Smaller { - get { - return ResourceManager.GetString("Common_Content_UnmatchedLength_Smaller", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You have no permission to do the operation.. - /// - internal static string Common_Forbid { - get { - return ResourceManager.GetString("Common_Forbid", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You are not the resource owner.. - /// - internal static string Common_Forbid_NotSelf { - get { - return ResourceManager.GetString("Common_Forbid_NotSelf", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Header Content-Length is missing or of bad format.. - /// - internal static string Common_Header_ContentLength_Missing { - get { - return ResourceManager.GetString("Common_Header_ContentLength_Missing", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Header Content-Length must not be 0.. - /// - internal static string Common_Header_ContentLength_Zero { - get { - return ResourceManager.GetString("Common_Header_ContentLength_Zero", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Header Content-Type is missing.. - /// - internal static string Common_Header_ContentType_Missing { - get { - return ResourceManager.GetString("Common_Header_ContentType_Missing", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Header If-Non-Match is of bad format.. - /// - internal static string Common_Header_IfNonMatch_BadFormat { - get { - return ResourceManager.GetString("Common_Header_IfNonMatch_BadFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Model is of bad format.. - /// - internal static string Common_InvalidModel { - get { - return ResourceManager.GetString("Common_InvalidModel", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The api endpoint you request is unknown. You might get the wrong api entry.. - /// - internal static string Common_UnknownEndpoint { - get { - return ResourceManager.GetString("Common_UnknownEndpoint", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unknown type of post content.. - /// - internal static string TimelineController_ContentUnknownType { - get { - return ResourceManager.GetString("TimelineController_ContentUnknownType", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Data field is not a valid base64 string in image content.. - /// - internal static string TimelineController_ImageContentDataNotBase64 { - get { - return ResourceManager.GetString("TimelineController_ImageContentDataNotBase64", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Data field is not a valid image after base64 decoding in image content.. - /// - internal static string TimelineController_ImageContentDataNotImage { - get { - return ResourceManager.GetString("TimelineController_ImageContentDataNotImage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Data field is required for image content.. - /// - internal static string TimelineController_ImageContentDataRequired { - get { - return ResourceManager.GetString("TimelineController_ImageContentDataRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The user to set as member does not exist.. - /// - internal static string TimelineController_MemberPut_NotExist { - get { - return ResourceManager.GetString("TimelineController_MemberPut_NotExist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A timeline with given name already exists.. - /// - internal static string TimelineController_NameConflict { - get { - return ResourceManager.GetString("TimelineController_NameConflict", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The timeline with given name does not exist.. - /// - internal static string TimelineController_NotExist { - get { - return ResourceManager.GetString("TimelineController_NotExist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The post of that type has no data.. - /// - internal static string TimelineController_PostNoData { - get { - return ResourceManager.GetString("TimelineController_PostNoData", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The post to operate on does not exist.. - /// - internal static string TimelineController_PostNotExist { - get { - return ResourceManager.GetString("TimelineController_PostNotExist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The user specified by query param "relate" does not exist.. - /// - internal static string TimelineController_QueryRelateNotExist { - get { - return ResourceManager.GetString("TimelineController_QueryRelateNotExist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to '{0}' is an unkown visibility in the query parameter 'visibility'. . - /// - internal static string TimelineController_QueryVisibilityUnknown { - get { - return ResourceManager.GetString("TimelineController_QueryVisibilityUnknown", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Text field is required for text content.. - /// - internal static string TimelineController_TextContentTextRequired { - get { - return ResourceManager.GetString("TimelineController_TextContentTextRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Username or password is invalid.. - /// - internal static string TokenController_Create_BadCredential { - get { - return ResourceManager.GetString("TokenController_Create_BadCredential", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The token is of bad format. It might not be created by the server.. - /// - internal static string TokenController_Verify_BadFormat { - get { - return ResourceManager.GetString("TokenController_Verify_BadFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Token has an old version. User might have update some info.. - /// - internal static string TokenController_Verify_OldVersion { - get { - return ResourceManager.GetString("TokenController_Verify_OldVersion", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The token is expired.. - /// - internal static string TokenController_Verify_TimeExpired { - get { - return ResourceManager.GetString("TokenController_Verify_TimeExpired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to User does not exist. Administrator might have deleted this user.. - /// - internal static string TokenController_Verify_UserNotExist { - get { - return ResourceManager.GetString("TokenController_Verify_UserNotExist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Image is not a square.. - /// - internal static string UserAvatar_BadFormat_BadSize { - get { - return ResourceManager.GetString("UserAvatar_BadFormat_BadSize", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Image decode failed.. - /// - internal static string UserAvatar_BadFormat_CantDecode { - get { - return ResourceManager.GetString("UserAvatar_BadFormat_CantDecode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Image format does not match the one in header.. - /// - internal static string UserAvatar_BadFormat_UnmatchedFormat { - get { - return ResourceManager.GetString("UserAvatar_BadFormat_UnmatchedFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The user to operate on does not exist.. - /// - internal static string UserCommon_NotExist { - get { - return ResourceManager.GetString("UserCommon_NotExist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Old password is wrong.. - /// - internal static string UserController_ChangePassword_BadOldPassword { - get { - return ResourceManager.GetString("UserController_ChangePassword_BadOldPassword", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You can't set permission unless you are administrator.. - /// - internal static string UserController_Patch_Forbid_Administrator { - get { - return ResourceManager.GetString("UserController_Patch_Forbid_Administrator", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You can't set password unless you are administrator. If you want to change password, use /userop/changepassword .. - /// - internal static string UserController_Patch_Forbid_Password { - get { - return ResourceManager.GetString("UserController_Patch_Forbid_Password", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You can't set username unless you are administrator.. - /// - internal static string UserController_Patch_Forbid_Username { - get { - return ResourceManager.GetString("UserController_Patch_Forbid_Username", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A user with given username already exists.. - /// - internal static string UserController_UsernameConflict { - get { - return ResourceManager.GetString("UserController_UsernameConflict", resourceCulture); - } - } - } -} diff --git a/Timeline/Resources/Messages.resx b/Timeline/Resources/Messages.resx deleted file mode 100644 index 2bbf494e..00000000 --- a/Timeline/Resources/Messages.resx +++ /dev/null @@ -1,231 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Body is too big. It can't be bigger than {0}. - - - Actual body length is bigger than it in header. - - - Actual body length is smaller than it in header. - - - You have no permission to do the operation. - - - You are not the resource owner. - - - Header Content-Length is missing or of bad format. - - - Header Content-Length must not be 0. - - - Header Content-Type is missing. - - - Header If-Non-Match is of bad format. - - - Model is of bad format. - - - The api endpoint you request is unknown. You might get the wrong api entry. - - - Unknown type of post content. - - - Data field is not a valid base64 string in image content. - - - Data field is not a valid image after base64 decoding in image content. - - - Data field is required for image content. - - - The user to set as member does not exist. - - - A timeline with given name already exists. - - - The timeline with given name does not exist. - - - The post of that type has no data. - - - The post to operate on does not exist. - - - The user specified by query param "relate" does not exist. - - - '{0}' is an unkown visibility in the query parameter 'visibility'. - - - Text field is required for text content. - - - Username or password is invalid. - - - The token is of bad format. It might not be created by the server. - - - Token has an old version. User might have update some info. - - - The token is expired. - - - User does not exist. Administrator might have deleted this user. - - - Image is not a square. - - - Image decode failed. - - - Image format does not match the one in header. - - - The user to operate on does not exist. - - - Old password is wrong. - - - You can't set permission unless you are administrator. - - - You can't set password unless you are administrator. If you want to change password, use /userop/changepassword . - - - You can't set username unless you are administrator. - - - A user with given username already exists. - - \ No newline at end of file diff --git a/Timeline/Resources/Models/Http/Common.Designer.cs b/Timeline/Resources/Models/Http/Common.Designer.cs deleted file mode 100644 index 5165463e..00000000 --- a/Timeline/Resources/Models/Http/Common.Designer.cs +++ /dev/null @@ -1,99 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Resources.Models.Http { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Common { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Common() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Models.Http.Common", typeof(Common).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to An existent item is deleted.. - /// - internal static string MessageDeleteDelete { - get { - return ResourceManager.GetString("MessageDeleteDelete", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The item does not exist, so nothing is changed.. - /// - internal static string MessageDeleteNotExist { - get { - return ResourceManager.GetString("MessageDeleteNotExist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A new item is created.. - /// - internal static string MessagePutCreate { - get { - return ResourceManager.GetString("MessagePutCreate", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to An existent item is modified.. - /// - internal static string MessagePutModify { - get { - return ResourceManager.GetString("MessagePutModify", resourceCulture); - } - } - } -} diff --git a/Timeline/Resources/Models/Http/Common.resx b/Timeline/Resources/Models/Http/Common.resx deleted file mode 100644 index 85ec4d32..00000000 --- a/Timeline/Resources/Models/Http/Common.resx +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - An existent item is deleted. - - - The item does not exist, so nothing is changed. - - - A new item is created. - - - An existent item is modified. - - \ No newline at end of file diff --git a/Timeline/Resources/Models/Http/Exception.Designer.cs b/Timeline/Resources/Models/Http/Exception.Designer.cs deleted file mode 100644 index 19f42793..00000000 --- a/Timeline/Resources/Models/Http/Exception.Designer.cs +++ /dev/null @@ -1,81 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Resources.Models.Http { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Exception { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Exception() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Models.Http.Exception", typeof(Exception).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to No action context currently, can't fill urls in value resolver.. - /// - internal static string ActionContextNull { - get { - return ResourceManager.GetString("ActionContextNull", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unknown post content type.. - /// - internal static string UnknownPostContentType { - get { - return ResourceManager.GetString("UnknownPostContentType", resourceCulture); - } - } - } -} diff --git a/Timeline/Resources/Models/Http/Exception.resx b/Timeline/Resources/Models/Http/Exception.resx deleted file mode 100644 index 3f7bddb6..00000000 --- a/Timeline/Resources/Models/Http/Exception.resx +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - No action context currently, can't fill urls in value resolver. - - - Unknown post content type. - - \ No newline at end of file diff --git a/Timeline/Resources/Models/Validation/NameValidator.Designer.cs b/Timeline/Resources/Models/Validation/NameValidator.Designer.cs deleted file mode 100644 index 3050049e..00000000 --- a/Timeline/Resources/Models/Validation/NameValidator.Designer.cs +++ /dev/null @@ -1,99 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Resources.Models.Validation { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class NameValidator { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal NameValidator() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Models.Validation.NameValidator", typeof(NameValidator).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to An empty string is not allowed.. - /// - internal static string MessageEmptyString { - get { - return ResourceManager.GetString("MessageEmptyString", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid character, only alphabet, digit, underscore and hyphen are allowed.. - /// - internal static string MessageInvalidChar { - get { - return ResourceManager.GetString("MessageInvalidChar", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Too long, more than 26 characters is not premitted.. - /// - internal static string MessageTooLong { - get { - return ResourceManager.GetString("MessageTooLong", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Name can't be of the same format of unique id.. - /// - internal static string MessageUnqiueId { - get { - return ResourceManager.GetString("MessageUnqiueId", resourceCulture); - } - } - } -} diff --git a/Timeline/Resources/Models/Validation/NameValidator.resx b/Timeline/Resources/Models/Validation/NameValidator.resx deleted file mode 100644 index 5e7e1745..00000000 --- a/Timeline/Resources/Models/Validation/NameValidator.resx +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - An empty string is not allowed. - - - Invalid character, only alphabet, digit, underscore and hyphen are allowed. - - - Too long, more than 26 characters is not premitted. - - - Name can't be of the same format of unique id. - - \ No newline at end of file diff --git a/Timeline/Resources/Models/Validation/NicknameValidator.Designer.cs b/Timeline/Resources/Models/Validation/NicknameValidator.Designer.cs deleted file mode 100644 index 522f305a..00000000 --- a/Timeline/Resources/Models/Validation/NicknameValidator.Designer.cs +++ /dev/null @@ -1,72 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Resources.Models.Validation { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class NicknameValidator { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal NicknameValidator() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Models.Validation.NicknameValidator", typeof(NicknameValidator).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Nickname is too long.. - /// - internal static string MessageTooLong { - get { - return ResourceManager.GetString("MessageTooLong", resourceCulture); - } - } - } -} diff --git a/Timeline/Resources/Models/Validation/NicknameValidator.resx b/Timeline/Resources/Models/Validation/NicknameValidator.resx deleted file mode 100644 index b191b505..00000000 --- a/Timeline/Resources/Models/Validation/NicknameValidator.resx +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Nickname is too long. - - \ No newline at end of file diff --git a/Timeline/Resources/Models/Validation/Validator.Designer.cs b/Timeline/Resources/Models/Validation/Validator.Designer.cs deleted file mode 100644 index 74d4c169..00000000 --- a/Timeline/Resources/Models/Validation/Validator.Designer.cs +++ /dev/null @@ -1,108 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Resources.Models.Validation { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Validator { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Validator() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Models.Validation.Validator", typeof(Validator).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Failed to create a validator instance from default constructor. See inner exception.. - /// - internal static string ValidateWithAttributeExceptionCreateFail { - get { - return ResourceManager.GetString("ValidateWithAttributeExceptionCreateFail", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Given type is not assignable to IValidator.. - /// - internal static string ValidateWithAttributeExceptionNotValidator { - get { - return ResourceManager.GetString("ValidateWithAttributeExceptionNotValidator", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Value is not of type {0}.. - /// - internal static string ValidatorMessageBadType { - get { - return ResourceManager.GetString("ValidatorMessageBadType", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Value can't be null.. - /// - internal static string ValidatorMessageNull { - get { - return ResourceManager.GetString("ValidatorMessageNull", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Validation succeeded.. - /// - internal static string ValidatorMessageSuccess { - get { - return ResourceManager.GetString("ValidatorMessageSuccess", resourceCulture); - } - } - } -} diff --git a/Timeline/Resources/Models/Validation/Validator.resx b/Timeline/Resources/Models/Validation/Validator.resx deleted file mode 100644 index 8317e3eb..00000000 --- a/Timeline/Resources/Models/Validation/Validator.resx +++ /dev/null @@ -1,135 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Failed to create a validator instance from default constructor. See inner exception. - - - Given type is not assignable to IValidator. - - - Value is not of type {0}. - - - Value can't be null. - - - Validation succeeded. - - \ No newline at end of file diff --git a/Timeline/Resources/Services/DataManager.Designer.cs b/Timeline/Resources/Services/DataManager.Designer.cs deleted file mode 100644 index 0872059a..00000000 --- a/Timeline/Resources/Services/DataManager.Designer.cs +++ /dev/null @@ -1,72 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Resources.Services { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class DataManager { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal DataManager() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.DataManager", typeof(DataManager).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Entry with given tag does not exist.. - /// - internal static string ExceptionEntryNotExist { - get { - return ResourceManager.GetString("ExceptionEntryNotExist", resourceCulture); - } - } - } -} diff --git a/Timeline/Resources/Services/DataManager.resx b/Timeline/Resources/Services/DataManager.resx deleted file mode 100644 index 688e0e96..00000000 --- a/Timeline/Resources/Services/DataManager.resx +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Entry with given tag does not exist. - - \ No newline at end of file diff --git a/Timeline/Resources/Services/Exception.Designer.cs b/Timeline/Resources/Services/Exception.Designer.cs deleted file mode 100644 index 21ca7b86..00000000 --- a/Timeline/Resources/Services/Exception.Designer.cs +++ /dev/null @@ -1,234 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Resources.Services { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Exception { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Exception() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.Exception", typeof(Exception).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to The password is wrong.. - /// - internal static string BadPasswordException { - get { - return ResourceManager.GetString("BadPasswordException", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The hashes password is of bad format. It might not be created by server.. - /// - internal static string HashedPasswordBadFromatException { - get { - return ResourceManager.GetString("HashedPasswordBadFromatException", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Not of valid base64 format. See inner exception.. - /// - internal static string HashedPasswordBadFromatExceptionNotBase64 { - get { - return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotBase64", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Decoded hashed password is of length 0.. - /// - internal static string HashedPasswordBadFromatExceptionNotLength0 { - get { - return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotLength0", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to See inner exception.. - /// - internal static string HashedPasswordBadFromatExceptionNotOthers { - get { - return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotOthers", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Salt length < 128 bits.. - /// - internal static string HashedPasswordBadFromatExceptionNotSaltTooShort { - get { - return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotSaltTooShort", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Subkey length < 128 bits.. - /// - internal static string HashedPasswordBadFromatExceptionNotSubkeyTooShort { - get { - return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotSubkeyTooShort", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unknown format marker.. - /// - internal static string HashedPasswordBadFromatExceptionNotUnknownMarker { - get { - return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotUnknownMarker", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The token didn't pass verification because {0}.. - /// - internal static string JwtUserTokenBadFormatException { - get { - return ResourceManager.GetString("JwtUserTokenBadFormatException", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to id claim is not a number. - /// - internal static string JwtUserTokenBadFormatExceptionIdBadFormat { - get { - return ResourceManager.GetString("JwtUserTokenBadFormatExceptionIdBadFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to id claim does not exist. - /// - internal static string JwtUserTokenBadFormatExceptionIdMissing { - get { - return ResourceManager.GetString("JwtUserTokenBadFormatExceptionIdMissing", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to other error, see inner exception for information. - /// - internal static string JwtUserTokenBadFormatExceptionOthers { - get { - return ResourceManager.GetString("JwtUserTokenBadFormatExceptionOthers", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to unknown error. - /// - internal static string JwtUserTokenBadFormatExceptionUnknown { - get { - return ResourceManager.GetString("JwtUserTokenBadFormatExceptionUnknown", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to version claim is not a number.. - /// - internal static string JwtUserTokenBadFormatExceptionVersionBadFormat { - get { - return ResourceManager.GetString("JwtUserTokenBadFormatExceptionVersionBadFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to version claim does not exist.. - /// - internal static string JwtUserTokenBadFormatExceptionVersionMissing { - get { - return ResourceManager.GetString("JwtUserTokenBadFormatExceptionVersionMissing", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Password is of bad format.. - /// - internal static string PasswordBadFormatException { - get { - return ResourceManager.GetString("PasswordBadFormatException", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The token is of bad format, which means it may not be created by the server.. - /// - internal static string UserTokenBadFormatException { - get { - return ResourceManager.GetString("UserTokenBadFormatException", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The token is of bad version.. - /// - internal static string UserTokenBadVersionException { - get { - return ResourceManager.GetString("UserTokenBadVersionException", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The token is expired because its expiration time has passed.. - /// - internal static string UserTokenTimeExpireException { - get { - return ResourceManager.GetString("UserTokenTimeExpireException", resourceCulture); - } - } - } -} diff --git a/Timeline/Resources/Services/Exception.resx b/Timeline/Resources/Services/Exception.resx deleted file mode 100644 index c31ed7c7..00000000 --- a/Timeline/Resources/Services/Exception.resx +++ /dev/null @@ -1,177 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - The password is wrong. - - - The hashes password is of bad format. It might not be created by server. - - - Not of valid base64 format. See inner exception. - - - Decoded hashed password is of length 0. - - - See inner exception. - - - Salt length < 128 bits. - - - Subkey length < 128 bits. - - - Unknown format marker. - - - The token didn't pass verification because {0}. - - - id claim is not a number - - - id claim does not exist - - - other error, see inner exception for information - - - unknown error - - - version claim is not a number. - - - version claim does not exist. - - - Password is of bad format. - - - The token is of bad format, which means it may not be created by the server. - - - The token is of bad version. - - - The token is expired because its expiration time has passed. - - \ No newline at end of file diff --git a/Timeline/Resources/Services/Exceptions.Designer.cs b/Timeline/Resources/Services/Exceptions.Designer.cs deleted file mode 100644 index 1dbe11c9..00000000 --- a/Timeline/Resources/Services/Exceptions.Designer.cs +++ /dev/null @@ -1,189 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Resources.Services { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Exceptions { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Exceptions() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.Exceptions", typeof(Exceptions).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to A entity of type "{0}" already exists.. - /// - internal static string EntityAlreadyExistError { - get { - return ResourceManager.GetString("EntityAlreadyExistError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The entity already exists.. - /// - internal static string EntityAlreadyExistErrorDefault { - get { - return ResourceManager.GetString("EntityAlreadyExistErrorDefault", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The required entity of type "{0}" does not exist.. - /// - internal static string EntityNotExistError { - get { - return ResourceManager.GetString("EntityNotExistError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The required entity does not exist.. - /// - internal static string EntityNotExistErrorDefault { - get { - return ResourceManager.GetString("EntityNotExistErrorDefault", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Image is in valid because {0}.. - /// - internal static string ImageException { - get { - return ResourceManager.GetString("ImageException", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to image is not of required size. - /// - internal static string ImageExceptionBadSize { - get { - return ResourceManager.GetString("ImageExceptionBadSize", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to failed to decode image, see inner exception. - /// - internal static string ImageExceptionCantDecode { - get { - return ResourceManager.GetString("ImageExceptionCantDecode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to unknown error. - /// - internal static string ImageExceptionUnknownError { - get { - return ResourceManager.GetString("ImageExceptionUnknownError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to image's actual mime type is not the specified one. - /// - internal static string ImageExceptionUnmatchedFormat { - get { - return ResourceManager.GetString("ImageExceptionUnmatchedFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The timeline has no data.. - /// - internal static string TimelineNoDataException { - get { - return ResourceManager.GetString("TimelineNoDataException", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Request timeline name is "{0}". If this is a personal timeline whose name starts with '@', it means the user does not exist and inner exception should be a UserNotExistException.. - /// - internal static string TimelineNotExistException { - get { - return ResourceManager.GetString("TimelineNotExistException", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Request timeline name is "{0}". Request timeline post id is "{1}".. - /// - internal static string TimelinePostNotExistException { - get { - return ResourceManager.GetString("TimelinePostNotExistException", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Request timeline name is "{0}". Request timeline post id is "{1}". The post does not exist because it is deleted.. - /// - internal static string TimelinePostNotExistExceptionDeleted { - get { - return ResourceManager.GetString("TimelinePostNotExistExceptionDeleted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Request username is "{0}". Request id is "{1}".. - /// - internal static string UserNotExistException { - get { - return ResourceManager.GetString("UserNotExistException", resourceCulture); - } - } - } -} diff --git a/Timeline/Resources/Services/Exceptions.resx b/Timeline/Resources/Services/Exceptions.resx deleted file mode 100644 index e9595caa..00000000 --- a/Timeline/Resources/Services/Exceptions.resx +++ /dev/null @@ -1,142 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 1.3 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - The required entity of type "{0}" does not exist. - - - The entity already exists. - - - A entity of type "{0}" already exists. - - - The required entity does not exist. - - - Request timeline name is "{0}". If this is a personal timeline whose name starts with '@', it means the user does not exist and inner exception should be a UserNotExistException. - - - Request timeline name is "{0}". Request timeline post id is "{1}". - - - Request username is "{0}". Request id is "{1}". - - - The timeline has no data. - - - Image is in valid because {0}. - - - image is not of required size - - - failed to decode image, see inner exception - - - unknown error - - - image's actual mime type is not the specified one - - - Request timeline name is "{0}". Request timeline post id is "{1}". The post does not exist because it is deleted. - - \ No newline at end of file diff --git a/Timeline/Resources/Services/TimelineService.Designer.cs b/Timeline/Resources/Services/TimelineService.Designer.cs deleted file mode 100644 index e16c1337..00000000 --- a/Timeline/Resources/Services/TimelineService.Designer.cs +++ /dev/null @@ -1,144 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Resources.Services { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class TimelineService { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal TimelineService() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.TimelineService", typeof(TimelineService).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to The number {0} username is invalid.. - /// - internal static string ExceptionChangeMemberUsernameBadFormat { - get { - return ResourceManager.GetString("ExceptionChangeMemberUsernameBadFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unknown post content type "{0}" is saved in database.. - /// - internal static string ExceptionDatabaseUnknownContentType { - get { - return ResourceManager.GetString("ExceptionDatabaseUnknownContentType", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The owner username of personal timeline is of bad format.. - /// - internal static string ExceptionFindTimelineUsernameBadFormat { - get { - return ResourceManager.GetString("ExceptionFindTimelineUsernameBadFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The data entry of the tag of the image post does not exist.. - /// - internal static string ExceptionGetDataDataEntryNotExist { - get { - return ResourceManager.GetString("ExceptionGetDataDataEntryNotExist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Can't get data of a non-image post.. - /// - internal static string ExceptionGetDataNonImagePost { - get { - return ResourceManager.GetString("ExceptionGetDataNonImagePost", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The post has been deleted because content of entity is null.. - /// - internal static string ExceptionPostDeleted { - get { - return ResourceManager.GetString("ExceptionPostDeleted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The timeline name is of bad format.. - /// - internal static string ExceptionTimelineNameBadFormat { - get { - return ResourceManager.GetString("ExceptionTimelineNameBadFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The timeline with given name already exists.. - /// - internal static string ExceptionTimelineNameConflict { - get { - return ResourceManager.GetString("ExceptionTimelineNameConflict", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Image format type of the post does not exist in column "extra_content". Normally this couldn't be possible because it should be saved when post was created. However, we now re-detect the format and save it.. - /// - internal static string LogGetDataNoFormat { - get { - return ResourceManager.GetString("LogGetDataNoFormat", resourceCulture); - } - } - } -} diff --git a/Timeline/Resources/Services/TimelineService.resx b/Timeline/Resources/Services/TimelineService.resx deleted file mode 100644 index 9314f51b..00000000 --- a/Timeline/Resources/Services/TimelineService.resx +++ /dev/null @@ -1,147 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - The number {0} username is invalid. - - - Unknown post content type "{0}" is saved in database. - - - The owner username of personal timeline is of bad format. - - - The data entry of the tag of the image post does not exist. - - - Can't get data of a non-image post. - - - The timeline name is of bad format. - - - The timeline with given name already exists. - - - Image format type of the post does not exist in column "extra_content". Normally this couldn't be possible because it should be saved when post was created. However, we now re-detect the format and save it. - - - The post has been deleted because content of entity is null. - - \ No newline at end of file diff --git a/Timeline/Resources/Services/UserAvatarService.Designer.cs b/Timeline/Resources/Services/UserAvatarService.Designer.cs deleted file mode 100644 index c72d4215..00000000 --- a/Timeline/Resources/Services/UserAvatarService.Designer.cs +++ /dev/null @@ -1,108 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Resources.Services { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class UserAvatarService { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal UserAvatarService() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.UserAvatarService", typeof(UserAvatarService).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Data of avatar is null.. - /// - internal static string ExceptionAvatarDataNull { - get { - return ResourceManager.GetString("ExceptionAvatarDataNull", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Type of avatar is null or empty.. - /// - internal static string ExceptionAvatarTypeNullOrEmpty { - get { - return ResourceManager.GetString("ExceptionAvatarTypeNullOrEmpty", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Database corupted! One of type and data of a avatar is null but the other is not.. - /// - internal static string ExceptionDatabaseCorruptedDataAndTypeNotSame { - get { - return ResourceManager.GetString("ExceptionDatabaseCorruptedDataAndTypeNotSame", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Created an entry in user_avatars.. - /// - internal static string LogCreateEntity { - get { - return ResourceManager.GetString("LogCreateEntity", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Updated an entry in user_avatars.. - /// - internal static string LogUpdateEntity { - get { - return ResourceManager.GetString("LogUpdateEntity", resourceCulture); - } - } - } -} diff --git a/Timeline/Resources/Services/UserAvatarService.resx b/Timeline/Resources/Services/UserAvatarService.resx deleted file mode 100644 index da9d7203..00000000 --- a/Timeline/Resources/Services/UserAvatarService.resx +++ /dev/null @@ -1,135 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Data of avatar is null. - - - Type of avatar is null or empty. - - - Database corupted! One of type and data of a avatar is null but the other is not. - - - Created an entry in user_avatars. - - - Updated an entry in user_avatars. - - \ No newline at end of file diff --git a/Timeline/Resources/Services/UserService.Designer.cs b/Timeline/Resources/Services/UserService.Designer.cs deleted file mode 100644 index cdf7f390..00000000 --- a/Timeline/Resources/Services/UserService.Designer.cs +++ /dev/null @@ -1,162 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Resources.Services { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class UserService { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal UserService() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.UserService", typeof(UserService).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to New username is of bad format.. - /// - internal static string ExceptionNewUsernameBadFormat { - get { - return ResourceManager.GetString("ExceptionNewUsernameBadFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Nickname is of bad format, because {}.. - /// - internal static string ExceptionNicknameBadFormat { - get { - return ResourceManager.GetString("ExceptionNicknameBadFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Old username is of bad format.. - /// - internal static string ExceptionOldUsernameBadFormat { - get { - return ResourceManager.GetString("ExceptionOldUsernameBadFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Password can't be empty.. - /// - internal static string ExceptionPasswordEmpty { - get { - return ResourceManager.GetString("ExceptionPasswordEmpty", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Password can't be null.. - /// - internal static string ExceptionPasswordNull { - get { - return ResourceManager.GetString("ExceptionPasswordNull", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Username is of bad format, because {}.. - /// - internal static string ExceptionUsernameBadFormat { - get { - return ResourceManager.GetString("ExceptionUsernameBadFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A user with given username already exists.. - /// - internal static string ExceptionUsernameConflict { - get { - return ResourceManager.GetString("ExceptionUsernameConflict", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Username can't be null.. - /// - internal static string ExceptionUsernameNull { - get { - return ResourceManager.GetString("ExceptionUsernameNull", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A new user entry is added to the database.. - /// - internal static string LogDatabaseCreate { - get { - return ResourceManager.GetString("LogDatabaseCreate", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A user entry is removed from the database.. - /// - internal static string LogDatabaseRemove { - get { - return ResourceManager.GetString("LogDatabaseRemove", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A user entry is updated to the database.. - /// - internal static string LogDatabaseUpdate { - get { - return ResourceManager.GetString("LogDatabaseUpdate", resourceCulture); - } - } - } -} diff --git a/Timeline/Resources/Services/UserService.resx b/Timeline/Resources/Services/UserService.resx deleted file mode 100644 index 09bd4abb..00000000 --- a/Timeline/Resources/Services/UserService.resx +++ /dev/null @@ -1,153 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - New username is of bad format. - - - Nickname is of bad format, because {}. - - - Old username is of bad format. - - - Password can't be empty. - - - Password can't be null. - - - Username is of bad format, because {}. - - - A user with given username already exists. - - - Username can't be null. - - - A new user entry is added to the database. - - - A user entry is removed from the database. - - - A user entry is updated to the database. - - \ No newline at end of file diff --git a/Timeline/Resources/Services/UserTokenService.Designer.cs b/Timeline/Resources/Services/UserTokenService.Designer.cs deleted file mode 100644 index 3c3c7e41..00000000 --- a/Timeline/Resources/Services/UserTokenService.Designer.cs +++ /dev/null @@ -1,72 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Resources.Services { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class UserTokenService { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal UserTokenService() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.UserTokenService", typeof(UserTokenService).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Jwt token key is not set in database. Maybe you forget to migrate the database.. - /// - internal static string JwtKeyNotExist { - get { - return ResourceManager.GetString("JwtKeyNotExist", resourceCulture); - } - } - } -} diff --git a/Timeline/Resources/Services/UserTokenService.resx b/Timeline/Resources/Services/UserTokenService.resx deleted file mode 100644 index 1ce78427..00000000 --- a/Timeline/Resources/Services/UserTokenService.resx +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Jwt token key is not set in database. Maybe you forget to migrate the database. - - \ No newline at end of file diff --git a/Timeline/Routes/ApiRoutePrefixConvention.cs b/Timeline/Routes/ApiRoutePrefixConvention.cs deleted file mode 100644 index ca38a0d9..00000000 --- a/Timeline/Routes/ApiRoutePrefixConvention.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ApplicationModels; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Mvc.Routing; -using System.Linq; - -namespace Timeline.Routes -{ - public static class MvcOptionsExtensions - { - public static void UseApiRoutePrefix(this MvcOptions opts, IRouteTemplateProvider routeAttribute) - { - opts.Conventions.Add(new ApiRoutePrefixConvention(routeAttribute)); - } - - public static void UseApiRoutePrefix(this MvcOptions opts, string prefix) - { - opts.UseApiRoutePrefix(new RouteAttribute(prefix)); - } - } - - public class ApiRoutePrefixConvention : IApplicationModelConvention - { - private readonly AttributeRouteModel _routePrefix; - - public ApiRoutePrefixConvention(IRouteTemplateProvider route) - { - _routePrefix = new AttributeRouteModel(route); - } - - public void Apply(ApplicationModel application) - { - foreach (var selector in application.Controllers.Where(c => c.Filters.Any(f => f is IApiBehaviorMetadata)).SelectMany(c => c.Selectors)) - { - if (selector.AttributeRouteModel != null) - { - selector.AttributeRouteModel = AttributeRouteModel.CombineAttributeRouteModel(_routePrefix, selector.AttributeRouteModel); - } - else - { - selector.AttributeRouteModel = _routePrefix; - } - } - } - } -} diff --git a/Timeline/Routes/UnknownEndpointMiddleware.cs b/Timeline/Routes/UnknownEndpointMiddleware.cs deleted file mode 100644 index 25ec563c..00000000 --- a/Timeline/Routes/UnknownEndpointMiddleware.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using System; -using System.Net.Mime; -using System.Text.Json; -using Timeline.Models.Http; - -namespace Timeline.Routes -{ - public static class UnknownEndpointMiddleware - { - public static void Attach(IApplicationBuilder app) - { - app.Use(async (context, next) => - { - if (context.GetEndpoint() != null) - { - await next(); - return; - } - - if (context.Request.Path.StartsWithSegments("/api", StringComparison.OrdinalIgnoreCase)) - { - context.Response.StatusCode = StatusCodes.Status400BadRequest; - context.Response.ContentType = MediaTypeNames.Application.Json; - - var body = JsonSerializer.SerializeToUtf8Bytes(ErrorResponse.Common.UnknownEndpoint(), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); - - context.Response.ContentLength = body.Length; - await context.Response.Body.WriteAsync(body); - await context.Response.CompleteAsync(); - return; - } - - await next(); - }); - } - } -} diff --git a/Timeline/Services/BadPasswordException.cs b/Timeline/Services/BadPasswordException.cs deleted file mode 100644 index f609371d..00000000 --- a/Timeline/Services/BadPasswordException.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using Timeline.Helpers; - -namespace Timeline.Services -{ - [Serializable] - public class BadPasswordException : Exception - { - public BadPasswordException() : base(Resources.Services.Exception.BadPasswordException) { } - public BadPasswordException(string message, Exception inner) : base(message, inner) { } - - public BadPasswordException(string badPassword) - : base(Log.Format(Resources.Services.Exception.BadPasswordException, ("Bad Password", badPassword))) - { - Password = badPassword; - } - - protected BadPasswordException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - /// - /// The wrong password. - /// - public string? Password { get; set; } - } -} diff --git a/Timeline/Services/Clock.cs b/Timeline/Services/Clock.cs deleted file mode 100644 index 4395edcd..00000000 --- a/Timeline/Services/Clock.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; - -namespace Timeline.Services -{ - /// - /// Convenient for unit test. - /// - public interface IClock - { - /// - /// Get current time. - /// - /// Current time. - DateTime GetCurrentTime(); - } - - public class Clock : IClock - { - public Clock() - { - - } - - public DateTime GetCurrentTime() - { - return DateTime.UtcNow; - } - } -} diff --git a/Timeline/Services/DataManager.cs b/Timeline/Services/DataManager.cs deleted file mode 100644 index d447b0d5..00000000 --- a/Timeline/Services/DataManager.cs +++ /dev/null @@ -1,122 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using System; -using System.Linq; -using System.Threading.Tasks; -using Timeline.Entities; - -namespace Timeline.Services -{ - /// - /// A data manager controlling data. - /// - /// - /// Identical data will be saved as one copy and return the same tag. - /// Every data has a ref count. When data is retained, ref count increase. - /// When data is freed, ref count decease. If ref count is decreased - /// to 0, the data entry will be destroyed and no longer occupy space. - /// - public interface IDataManager - { - /// - /// Saves the data to a new entry if it does not exist, - /// increases its ref count and returns a tag to the entry. - /// - /// The data. Can't be null. - /// The tag of the created entry. - /// Thrown when is null. - public Task RetainEntry(byte[] data); - - /// - /// Decrease the the ref count of the entry. - /// Remove it if ref count is zero. - /// - /// The tag of the entry. - /// Thrown when is null. - /// - /// It's no-op if entry with tag does not exist. - /// - public Task FreeEntry(string tag); - - /// - /// Retrieve the entry with given tag. - /// - /// The tag of the entry. - /// The data of the entry. - /// Thrown when is null. - /// Thrown when entry with given tag does not exist. - public Task GetEntry(string tag); - } - - public class DataManager : IDataManager - { - private readonly DatabaseContext _database; - private readonly IETagGenerator _eTagGenerator; - - public DataManager(DatabaseContext database, IETagGenerator eTagGenerator) - { - _database = database; - _eTagGenerator = eTagGenerator; - } - - public async Task RetainEntry(byte[] data) - { - if (data == null) - throw new ArgumentNullException(nameof(data)); - - var tag = await _eTagGenerator.Generate(data); - - var entity = await _database.Data.Where(d => d.Tag == tag).SingleOrDefaultAsync(); - - if (entity == null) - { - entity = new DataEntity - { - Tag = tag, - Data = data, - Ref = 1 - }; - _database.Data.Add(entity); - } - else - { - entity.Ref += 1; - } - await _database.SaveChangesAsync(); - return tag; - } - - public async Task FreeEntry(string tag) - { - if (tag == null) - throw new ArgumentNullException(nameof(tag)); - - var entity = await _database.Data.Where(d => d.Tag == tag).SingleOrDefaultAsync(); - - if (entity != null) - { - if (entity.Ref == 1) - { - _database.Data.Remove(entity); - } - else - { - entity.Ref -= 1; - } - await _database.SaveChangesAsync(); - } - } - - public async Task GetEntry(string tag) - { - if (tag == null) - throw new ArgumentNullException(nameof(tag)); - - var entity = await _database.Data.Where(d => d.Tag == tag).Select(d => new { d.Data }).SingleOrDefaultAsync(); - - if (entity == null) - throw new InvalidOperationException(Resources.Services.DataManager.ExceptionEntryNotExist); - - return entity.Data; - } - } -} diff --git a/Timeline/Services/DatabaseBackupService.cs b/Timeline/Services/DatabaseBackupService.cs deleted file mode 100644 index a76b2a0d..00000000 --- a/Timeline/Services/DatabaseBackupService.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Globalization; -using System.IO; - -namespace Timeline.Services -{ - public interface IDatabaseBackupService - { - void BackupNow(); - } - - public class DatabaseBackupService : IDatabaseBackupService - { - private readonly IPathProvider _pathProvider; - private readonly IClock _clock; - - public DatabaseBackupService(IPathProvider pathProvider, IClock clock) - { - _pathProvider = pathProvider; - _clock = clock; - } - - public void BackupNow() - { - var databasePath = _pathProvider.GetDatabaseFilePath(); - if (File.Exists(databasePath)) - { - var backupDirPath = _pathProvider.GetDatabaseBackupDirectory(); - Directory.CreateDirectory(backupDirPath); - var fileName = _clock.GetCurrentTime().ToString("yyyy-MM-ddTHH-mm-ss", CultureInfo.InvariantCulture); - var path = Path.Combine(backupDirPath, fileName); - File.Copy(databasePath, path); - } - } - } -} diff --git a/Timeline/Services/DatabaseCorruptedException.cs b/Timeline/Services/DatabaseCorruptedException.cs deleted file mode 100644 index 9988e0ad..00000000 --- a/Timeline/Services/DatabaseCorruptedException.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Timeline.Services -{ - [Serializable] - public class DatabaseCorruptedException : Exception - { - public DatabaseCorruptedException() { } - public DatabaseCorruptedException(string message) : base(message) { } - public DatabaseCorruptedException(string message, Exception inner) : base(message, inner) { } - protected DatabaseCorruptedException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - } -} diff --git a/Timeline/Services/ETagGenerator.cs b/Timeline/Services/ETagGenerator.cs deleted file mode 100644 index 4493e903..00000000 --- a/Timeline/Services/ETagGenerator.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Security.Cryptography; -using System.Threading.Tasks; - -namespace Timeline.Services -{ - public interface IETagGenerator - { - /// - /// Generate a etag for given source. - /// - /// The source data. - /// The generated etag. - /// Thrown if is null. - Task Generate(byte[] source); - } - - public sealed class ETagGenerator : IETagGenerator, IDisposable - { - private readonly SHA1 _sha1; - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Security", "CA5350:Do Not Use Weak Cryptographic Algorithms", Justification = "Sha1 is enough ??? I don't know.")] - public ETagGenerator() - { - _sha1 = SHA1.Create(); - } - - public Task Generate(byte[] source) - { - if (source == null) - throw new ArgumentNullException(nameof(source)); - - return Task.Run(() => Convert.ToBase64String(_sha1.ComputeHash(source))); - } - - private bool _disposed; // To detect redundant calls - - public void Dispose() - { - if (_disposed) return; - _sha1.Dispose(); - _disposed = true; - } - } -} diff --git a/Timeline/Services/EntityNames.cs b/Timeline/Services/EntityNames.cs deleted file mode 100644 index 0ce1de3b..00000000 --- a/Timeline/Services/EntityNames.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Timeline.Services -{ - public static class EntityNames - { - public const string User = "User"; - public const string Timeline = "Timeline"; - public const string TimelinePost = "TimelinePost"; - } -} diff --git a/Timeline/Services/Exceptions/EntityAlreadyExistError.cs b/Timeline/Services/Exceptions/EntityAlreadyExistError.cs deleted file mode 100644 index 7db2e860..00000000 --- a/Timeline/Services/Exceptions/EntityAlreadyExistError.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Globalization; -using System.Text; - -namespace Timeline.Services.Exceptions -{ - /// - /// Thrown when an entity is already exists. - /// - /// - /// For example, want to create a timeline but a timeline with the same name already exists. - /// - [Serializable] - public class EntityAlreadyExistException : Exception - { - private readonly string? _entityName; - - public EntityAlreadyExistException() : this(null, null, null, null) { } - - public EntityAlreadyExistException(string? entityName) : this(entityName, null) { } - - public EntityAlreadyExistException(string? entityName, Exception? inner) : this(entityName, null, null, null, inner) { } - - public EntityAlreadyExistException(string? entityName, object? entity = null) : this(entityName, null, entity, null, null) { } - public EntityAlreadyExistException(Type? entityType, object? entity = null) : this(null, entityType, entity, null, null) { } - public EntityAlreadyExistException(string? entityName, Type? entityType, object? entity = null, string? message = null, Exception? inner = null) : base(MakeMessage(entityName, entityType, message), inner) - { - _entityName = entityName; - EntityType = entityType; - Entity = entity; - } - - private static string MakeMessage(string? entityName, Type? entityType, string? message) - { - string? name = entityName ?? (entityType?.Name); - - var result = new StringBuilder(); - - if (name == null) - result.Append(Resources.Services.Exceptions.EntityAlreadyExistErrorDefault); - else - result.AppendFormat(CultureInfo.InvariantCulture, Resources.Services.Exceptions.EntityAlreadyExistError, name); - - if (message != null) - { - result.Append(' '); - result.Append(message); - } - - return result.ToString(); - } - - protected EntityAlreadyExistException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - public string? EntityName => _entityName ?? (EntityType?.Name); - - public Type? EntityType { get; } - - public object? Entity { get; } - } -} diff --git a/Timeline/Services/Exceptions/EntityNotExistError.cs b/Timeline/Services/Exceptions/EntityNotExistError.cs deleted file mode 100644 index e79496d3..00000000 --- a/Timeline/Services/Exceptions/EntityNotExistError.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Globalization; -using System.Text; - -namespace Timeline.Services.Exceptions -{ - /// - /// Thrown when you want to get an entity that does not exist. - /// - /// - /// For example, you want to get a timeline with given name but it does not exist. - /// - [Serializable] - public class EntityNotExistException : Exception - { - public EntityNotExistException() : this(null, null, null, null) { } - public EntityNotExistException(string? entityName) : this(entityName, null, null, null) { } - public EntityNotExistException(Type? entityType) : this(null, entityType, null, null) { } - public EntityNotExistException(string? entityName, Exception? inner) : this(entityName, null, null, inner) { } - public EntityNotExistException(Type? entityType, Exception? inner) : this(null, entityType, null, inner) { } - public EntityNotExistException(string? entityName, Type? entityType, string? message = null, Exception? inner = null) : base(MakeMessage(entityName, entityType, message), inner) - { - EntityName = entityName; - EntityType = entityType; - } - - private static string MakeMessage(string? entityName, Type? entityType, string? message) - { - string? name = entityName ?? (entityType?.Name); - - var result = new StringBuilder(); - - if (name == null) - result.Append(Resources.Services.Exceptions.EntityNotExistErrorDefault); - else - result.AppendFormat(CultureInfo.InvariantCulture, Resources.Services.Exceptions.EntityNotExistError, name); - - if (message != null) - { - result.Append(' '); - result.Append(message); - } - - return result.ToString(); - } - - protected EntityNotExistException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - public string? EntityName { get; } - - public Type? EntityType { get; } - } -} diff --git a/Timeline/Services/Exceptions/ExceptionMessageHelper.cs b/Timeline/Services/Exceptions/ExceptionMessageHelper.cs deleted file mode 100644 index be3c42a4..00000000 --- a/Timeline/Services/Exceptions/ExceptionMessageHelper.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Timeline.Services.Exceptions -{ - public static class ExceptionMessageHelper - { - public static string AppendAdditionalMessage(this string origin, string? message) - { - if (message == null) - return origin; - else - return origin + " " + message; - } - } -} diff --git a/Timeline/Services/Exceptions/ImageException.cs b/Timeline/Services/Exceptions/ImageException.cs deleted file mode 100644 index 20dd48ae..00000000 --- a/Timeline/Services/Exceptions/ImageException.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Globalization; - -namespace Timeline.Services.Exceptions -{ - [Serializable] - public class ImageException : Exception - { - public enum ErrorReason - { - /// - /// Decoding image failed. - /// - CantDecode, - /// - /// Decoding succeeded but the real type is not the specified type. - /// - UnmatchedFormat, - /// - /// Image is not of required size. - /// - NotSquare, - /// - /// Other unknown errer. - /// - Unknown - } - - public ImageException() : this(null) { } - public ImageException(string? message) : this(message, null) { } - public ImageException(string? message, Exception? inner) : this(ErrorReason.Unknown, null, null, null, message, inner) { } - - public ImageException(ErrorReason error, byte[]? data, string? requestType = null, string? realType = null, string? message = null, Exception? inner = null) : base(MakeMessage(error).AppendAdditionalMessage(message), inner) { Error = error; ImageData = data; RequestType = requestType; RealType = realType; } - - protected ImageException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - private static string MakeMessage(ErrorReason? reason) => - string.Format(CultureInfo.InvariantCulture, Resources.Services.Exceptions.ImageException, reason switch - { - ErrorReason.CantDecode => Resources.Services.Exceptions.ImageExceptionCantDecode, - ErrorReason.UnmatchedFormat => Resources.Services.Exceptions.ImageExceptionUnmatchedFormat, - ErrorReason.NotSquare => Resources.Services.Exceptions.ImageExceptionBadSize, - _ => Resources.Services.Exceptions.ImageExceptionUnknownError - }); - - public ErrorReason Error { get; } -#pragma warning disable CA1819 // Properties should not return arrays - public byte[]? ImageData { get; } -#pragma warning restore CA1819 // Properties should not return arrays - public string? RequestType { get; } - - // This field will be null if decoding failed. - public string? RealType { get; } - } -} diff --git a/Timeline/Services/Exceptions/TimelineNotExistException.cs b/Timeline/Services/Exceptions/TimelineNotExistException.cs deleted file mode 100644 index 70970b24..00000000 --- a/Timeline/Services/Exceptions/TimelineNotExistException.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Globalization; - -namespace Timeline.Services.Exceptions -{ - [Serializable] - public class TimelineNotExistException : EntityNotExistException - { - public TimelineNotExistException() : this(null, null) { } - public TimelineNotExistException(string? timelineName) : this(timelineName, null) { } - public TimelineNotExistException(string? timelineName, Exception? inner) : this(timelineName, null, inner) { } - public TimelineNotExistException(string? timelineName, string? message, Exception? inner = null) - : base(EntityNames.Timeline, null, string.Format(CultureInfo.InvariantCulture, Resources.Services.Exceptions.TimelineNotExistException, timelineName ?? "").AppendAdditionalMessage(message), inner) { TimelineName = timelineName; } - - protected TimelineNotExistException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - public string? TimelineName { get; set; } - } -} diff --git a/Timeline/Services/Exceptions/TimelinePostNoDataException.cs b/Timeline/Services/Exceptions/TimelinePostNoDataException.cs deleted file mode 100644 index c4b6bf62..00000000 --- a/Timeline/Services/Exceptions/TimelinePostNoDataException.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Timeline.Services.Exceptions -{ - [Serializable] - public class TimelinePostNoDataException : Exception - { - public TimelinePostNoDataException() : this(null, null) { } - public TimelinePostNoDataException(string? message) : this(message, null) { } - public TimelinePostNoDataException(string? message, Exception? inner) : base(Resources.Services.Exceptions.TimelineNoDataException.AppendAdditionalMessage(message), inner) { } - protected TimelinePostNoDataException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - } -} diff --git a/Timeline/Services/Exceptions/TimelinePostNotExistException.cs b/Timeline/Services/Exceptions/TimelinePostNotExistException.cs deleted file mode 100644 index f95dd410..00000000 --- a/Timeline/Services/Exceptions/TimelinePostNotExistException.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Globalization; - -namespace Timeline.Services.Exceptions -{ - [Serializable] - public class TimelinePostNotExistException : EntityNotExistException - { - public TimelinePostNotExistException() : this(null, null, false, null, null) { } - [Obsolete("This has no meaning.")] - public TimelinePostNotExistException(string? message) : this(message, null) { } - [Obsolete("This has no meaning.")] - public TimelinePostNotExistException(string? message, Exception? inner) : this(null, null, false, message, inner) { } - protected TimelinePostNotExistException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - public TimelinePostNotExistException(string? timelineName, long? id, bool isDelete, string? message = null, Exception? inner = null) : base(EntityNames.TimelinePost, null, MakeMessage(timelineName, id, isDelete).AppendAdditionalMessage(message), inner) { TimelineName = timelineName; Id = id; IsDelete = isDelete; } - - private static string MakeMessage(string? timelineName, long? id, bool isDelete) - { - return string.Format(CultureInfo.InvariantCulture, isDelete ? Resources.Services.Exceptions.TimelinePostNotExistExceptionDeleted : Resources.Services.Exceptions.TimelinePostNotExistException, timelineName ?? "", id); - } - - public string? TimelineName { get; set; } - public long? Id { get; set; } - - /// - /// True if the post is deleted. False if the post does not exist at all. - /// - public bool IsDelete { get; set; } - } -} diff --git a/Timeline/Services/Exceptions/UserNotExistException.cs b/Timeline/Services/Exceptions/UserNotExistException.cs deleted file mode 100644 index 7ef714df..00000000 --- a/Timeline/Services/Exceptions/UserNotExistException.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Globalization; - -namespace Timeline.Services.Exceptions -{ - /// - /// The user requested does not exist. - /// - [Serializable] - public class UserNotExistException : EntityNotExistException - { - public UserNotExistException() : this(null, null, null, null) { } - public UserNotExistException(string? username, Exception? inner) : this(username, null, null, inner) { } - - public UserNotExistException(string? username) : this(username, null, null, null) { } - - public UserNotExistException(long id) : this(null, id, null, null) { } - - public UserNotExistException(string? username, long? id, string? message, Exception? inner) : base(EntityNames.User, null, - string.Format(CultureInfo.InvariantCulture, Resources.Services.Exceptions.UserNotExistException, username ?? "", id).AppendAdditionalMessage(message), inner) - { - Username = username; - Id = id; - } - - protected UserNotExistException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - /// - /// The username of the user that does not exist. - /// - public string? Username { get; set; } - - /// - /// The id of the user that does not exist. - /// - public long? Id { get; set; } - } -} diff --git a/Timeline/Services/ImageValidator.cs b/Timeline/Services/ImageValidator.cs deleted file mode 100644 index 59424a7c..00000000 --- a/Timeline/Services/ImageValidator.cs +++ /dev/null @@ -1,54 +0,0 @@ -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats; -using System; -using System.Linq; -using System.Threading.Tasks; -using Timeline.Services.Exceptions; - -namespace Timeline.Services -{ - public interface IImageValidator - { - /// - /// Validate a image data. - /// - /// The data of the image. Can't be null. - /// If not null, the real image format will be check against the requested format and throw if not match. If null, then do not check. - /// If true, image must be square. - /// The format. - /// Thrown when is null. - /// Thrown when image data can't be decoded or real type does not match request type or image is not square when required. - Task Validate(byte[] data, string? requestType = null, bool square = false); - } - - public class ImageValidator : IImageValidator - { - public ImageValidator() - { - } - - public async Task Validate(byte[] data, string? requestType = null, bool square = false) - { - if (data == null) - throw new ArgumentNullException(nameof(data)); - - var format = await Task.Run(() => - { - try - { - using var image = Image.Load(data, out IImageFormat format); - if (requestType != null && !format.MimeTypes.Contains(requestType)) - throw new ImageException(ImageException.ErrorReason.UnmatchedFormat, data, requestType, format.DefaultMimeType); - if (square && image.Width != image.Height) - throw new ImageException(ImageException.ErrorReason.NotSquare, data, requestType, format.DefaultMimeType); - return format; - } - catch (UnknownImageFormatException e) - { - throw new ImageException(ImageException.ErrorReason.CantDecode, data, requestType, null, null, e); - } - }); - return format; - } - } -} diff --git a/Timeline/Services/JwtUserTokenBadFormatException.cs b/Timeline/Services/JwtUserTokenBadFormatException.cs deleted file mode 100644 index c528c3e3..00000000 --- a/Timeline/Services/JwtUserTokenBadFormatException.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Globalization; -using static Timeline.Resources.Services.Exception; - -namespace Timeline.Services -{ - [Serializable] - public class JwtUserTokenBadFormatException : UserTokenBadFormatException - { - public enum ErrorKind - { - NoIdClaim, - IdClaimBadFormat, - NoVersionClaim, - VersionClaimBadFormat, - Other - } - - public JwtUserTokenBadFormatException() : this("", ErrorKind.Other) { } - public JwtUserTokenBadFormatException(string message) : base(message) { } - public JwtUserTokenBadFormatException(string message, Exception inner) : base(message, inner) { } - - public JwtUserTokenBadFormatException(string token, ErrorKind type) : base(token, GetErrorMessage(type)) { ErrorType = type; } - public JwtUserTokenBadFormatException(string token, ErrorKind type, Exception inner) : base(token, GetErrorMessage(type), inner) { ErrorType = type; } - public JwtUserTokenBadFormatException(string token, ErrorKind type, string message, Exception inner) : base(token, message, inner) { ErrorType = type; } - protected JwtUserTokenBadFormatException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - public ErrorKind ErrorType { get; set; } - - private static string GetErrorMessage(ErrorKind type) - { - var reason = type switch - { - ErrorKind.NoIdClaim => JwtUserTokenBadFormatExceptionIdMissing, - ErrorKind.IdClaimBadFormat => JwtUserTokenBadFormatExceptionIdBadFormat, - ErrorKind.NoVersionClaim => JwtUserTokenBadFormatExceptionVersionMissing, - ErrorKind.VersionClaimBadFormat => JwtUserTokenBadFormatExceptionVersionBadFormat, - ErrorKind.Other => JwtUserTokenBadFormatExceptionOthers, - _ => JwtUserTokenBadFormatExceptionUnknown - }; - - return string.Format(CultureInfo.CurrentCulture, - Resources.Services.Exception.JwtUserTokenBadFormatException, reason); - } - } -} diff --git a/Timeline/Services/PasswordBadFormatException.cs b/Timeline/Services/PasswordBadFormatException.cs deleted file mode 100644 index 2029ebb4..00000000 --- a/Timeline/Services/PasswordBadFormatException.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; - -namespace Timeline.Services -{ - - [Serializable] - public class PasswordBadFormatException : Exception - { - public PasswordBadFormatException() : base(Resources.Services.Exception.PasswordBadFormatException) { } - public PasswordBadFormatException(string message) : base(message) { } - public PasswordBadFormatException(string message, Exception inner) : base(message, inner) { } - - public PasswordBadFormatException(string password, string validationMessage) : this() - { - Password = password; - ValidationMessage = validationMessage; - } - - protected PasswordBadFormatException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - public string Password { get; set; } = ""; - - public string ValidationMessage { get; set; } = ""; - } -} diff --git a/Timeline/Services/PasswordService.cs b/Timeline/Services/PasswordService.cs deleted file mode 100644 index 8114a520..00000000 --- a/Timeline/Services/PasswordService.cs +++ /dev/null @@ -1,224 +0,0 @@ -using Microsoft.AspNetCore.Cryptography.KeyDerivation; -using System; -using System.Runtime.CompilerServices; -using System.Security.Cryptography; - -namespace Timeline.Services -{ - /// - /// Hashed password is of bad format. - /// - /// - [Serializable] - public class HashedPasswordBadFromatException : Exception - { - private static string MakeMessage(string reason) - { - return Resources.Services.Exception.HashedPasswordBadFromatException + " Reason: " + reason; - } - - public HashedPasswordBadFromatException() : base(Resources.Services.Exception.HashedPasswordBadFromatException) { } - - public HashedPasswordBadFromatException(string message) : base(message) { } - public HashedPasswordBadFromatException(string message, Exception inner) : base(message, inner) { } - - public HashedPasswordBadFromatException(string hashedPassword, string reason) : base(MakeMessage(reason)) { HashedPassword = hashedPassword; } - public HashedPasswordBadFromatException(string hashedPassword, string reason, Exception inner) : base(MakeMessage(reason), inner) { HashedPassword = hashedPassword; } - protected HashedPasswordBadFromatException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - public string? HashedPassword { get; set; } - } - - public interface IPasswordService - { - /// - /// Hash a password. - /// - /// The password to hash. - /// A hashed representation of the supplied . - /// Thrown when is null. - string HashPassword(string password); - - /// - /// Verify whether the password fits into the hashed one. - /// - /// Usually you only need to check the returned bool value. - /// Catching usually is not necessary. - /// Because if your program logic is right and always call - /// and in pair, this exception will never be thrown. - /// A thrown one usually means the data you saved is corupted, which is a critical problem. - /// - /// The hashed password. - /// The password supplied for comparison. - /// True indicating password is right. Otherwise false. - /// Thrown when or is null. - /// Thrown when the hashed password is of bad format. - bool VerifyPassword(string hashedPassword, string providedPassword); - } - - /// - /// Copied from https://github.com/aspnet/AspNetCore/blob/master/src/Identity/Extensions.Core/src/PasswordHasher.cs - /// Remove V2 format and unnecessary format version check. - /// Remove configuration options. - /// Remove user related parts. - /// Change the exceptions. - /// - public class PasswordService : IPasswordService - { - /* ======================= - * HASHED PASSWORD FORMATS - * ======================= - * - * Version 3: - * PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations. - * Format: { 0x01, prf (UInt32), iter count (UInt32), salt length (UInt32), salt, subkey } - * (All UInt32s are stored big-endian.) - */ - - private readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create(); - - public PasswordService() - { - } - - // Compares two byte arrays for equality. The method is specifically written so that the loop is not optimized. - [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] - private static bool ByteArraysEqual(byte[] a, byte[] b) - { - if (a == null && b == null) - { - return true; - } - if (a == null || b == null || a.Length != b.Length) - { - return false; - } - var areSame = true; - for (var i = 0; i < a.Length; i++) - { - areSame &= (a[i] == b[i]); - } - return areSame; - } - - public string HashPassword(string password) - { - if (password == null) - throw new ArgumentNullException(nameof(password)); - return Convert.ToBase64String(HashPasswordV3(password, _rng)); - } - - private static byte[] HashPasswordV3(string password, RandomNumberGenerator rng) - { - return HashPasswordV3(password, rng, - prf: KeyDerivationPrf.HMACSHA256, - iterCount: 10000, - saltSize: 128 / 8, - numBytesRequested: 256 / 8); - } - - private static byte[] HashPasswordV3(string password, RandomNumberGenerator rng, KeyDerivationPrf prf, int iterCount, int saltSize, int numBytesRequested) - { - // Produce a version 3 (see comment above) text hash. - byte[] salt = new byte[saltSize]; - rng.GetBytes(salt); - byte[] subkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, numBytesRequested); - - var outputBytes = new byte[13 + salt.Length + subkey.Length]; - outputBytes[0] = 0x01; // format marker - WriteNetworkByteOrder(outputBytes, 1, (uint)prf); - WriteNetworkByteOrder(outputBytes, 5, (uint)iterCount); - WriteNetworkByteOrder(outputBytes, 9, (uint)saltSize); - Buffer.BlockCopy(salt, 0, outputBytes, 13, salt.Length); - Buffer.BlockCopy(subkey, 0, outputBytes, 13 + saltSize, subkey.Length); - return outputBytes; - } - - public bool VerifyPassword(string hashedPassword, string providedPassword) - { - if (hashedPassword == null) - throw new ArgumentNullException(nameof(hashedPassword)); - if (providedPassword == null) - throw new ArgumentNullException(nameof(providedPassword)); - - byte[] decodedHashedPassword; - try - { - decodedHashedPassword = Convert.FromBase64String(hashedPassword); - } - catch (FormatException e) - { - throw new HashedPasswordBadFromatException(hashedPassword, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotBase64, e); - } - - // read the format marker from the hashed password - if (decodedHashedPassword.Length == 0) - { - throw new HashedPasswordBadFromatException(hashedPassword, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotLength0); - } - - return (decodedHashedPassword[0]) switch - { - 0x01 => VerifyHashedPasswordV3(decodedHashedPassword, providedPassword, hashedPassword), - _ => throw new HashedPasswordBadFromatException(hashedPassword, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotUnknownMarker), - }; - } - - private static bool VerifyHashedPasswordV3(byte[] hashedPassword, string password, string hashedPasswordString) - { - try - { - // Read header information - KeyDerivationPrf prf = (KeyDerivationPrf)ReadNetworkByteOrder(hashedPassword, 1); - int iterCount = (int)ReadNetworkByteOrder(hashedPassword, 5); - int saltLength = (int)ReadNetworkByteOrder(hashedPassword, 9); - - // Read the salt: must be >= 128 bits - if (saltLength < 128 / 8) - { - throw new HashedPasswordBadFromatException(hashedPasswordString, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotSaltTooShort); - } - byte[] salt = new byte[saltLength]; - Buffer.BlockCopy(hashedPassword, 13, salt, 0, salt.Length); - - // Read the subkey (the rest of the payload): must be >= 128 bits - int subkeyLength = hashedPassword.Length - 13 - salt.Length; - if (subkeyLength < 128 / 8) - { - throw new HashedPasswordBadFromatException(hashedPasswordString, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotSubkeyTooShort); - } - byte[] expectedSubkey = new byte[subkeyLength]; - Buffer.BlockCopy(hashedPassword, 13 + salt.Length, expectedSubkey, 0, expectedSubkey.Length); - - // Hash the incoming password and verify it - byte[] actualSubkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, subkeyLength); - return ByteArraysEqual(actualSubkey, expectedSubkey); - } - catch (Exception e) - { - // This should never occur except in the case of a malformed payload, where - // we might go off the end of the array. Regardless, a malformed payload - // implies verification failed. - throw new HashedPasswordBadFromatException(hashedPasswordString, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotOthers, e); - } - } - - private static uint ReadNetworkByteOrder(byte[] buffer, int offset) - { - return ((uint)(buffer[offset + 0]) << 24) - | ((uint)(buffer[offset + 1]) << 16) - | ((uint)(buffer[offset + 2]) << 8) - | ((uint)(buffer[offset + 3])); - } - - private static void WriteNetworkByteOrder(byte[] buffer, int offset, uint value) - { - buffer[offset + 0] = (byte)(value >> 24); - buffer[offset + 1] = (byte)(value >> 16); - buffer[offset + 2] = (byte)(value >> 8); - buffer[offset + 3] = (byte)(value >> 0); - } - } -} diff --git a/Timeline/Services/PathProvider.cs b/Timeline/Services/PathProvider.cs deleted file mode 100644 index 1baba5c0..00000000 --- a/Timeline/Services/PathProvider.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Microsoft.Extensions.Configuration; -using System.IO; -using Timeline.Configs; - -namespace Timeline.Services -{ - public interface IPathProvider - { - public string GetWorkingDirectory(); - public string GetDatabaseFilePath(); - public string GetDatabaseBackupDirectory(); - } - - public class PathProvider : IPathProvider - { - private readonly IConfiguration _configuration; - - private readonly string _workingDirectory; - - - public PathProvider(IConfiguration configuration) - { - _configuration = configuration; - _workingDirectory = configuration.GetValue(ApplicationConfiguration.WorkDirKey) ?? ApplicationConfiguration.DefaultWorkDir; - } - - public string GetWorkingDirectory() - { - return _workingDirectory; - } - - public string GetDatabaseFilePath() - { - return Path.Combine(_workingDirectory, ApplicationConfiguration.DatabaseFileName); - } - - public string GetDatabaseBackupDirectory() - { - return Path.Combine(_workingDirectory, ApplicationConfiguration.DatabaseBackupDirectoryName); - } - } -} diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs deleted file mode 100644 index 4bcae596..00000000 --- a/Timeline/Services/TimelineService.cs +++ /dev/null @@ -1,1166 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using SixLabors.ImageSharp; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Timeline.Entities; -using Timeline.Helpers; -using Timeline.Models; -using Timeline.Models.Validation; -using Timeline.Services.Exceptions; -using static Timeline.Resources.Services.TimelineService; - -namespace Timeline.Services -{ - public static class TimelineHelper - { - public static string ExtractTimelineName(string name, out bool isPersonal) - { - if (name.StartsWith("@", StringComparison.OrdinalIgnoreCase)) - { - isPersonal = true; - return name.Substring(1); - } - else - { - isPersonal = false; - return name; - } - } - } - - public enum TimelineUserRelationshipType - { - Own = 0b1, - Join = 0b10, - Default = Own | Join - } - - public class TimelineUserRelationship - { - public TimelineUserRelationship(TimelineUserRelationshipType type, long userId) - { - Type = type; - UserId = userId; - } - - public TimelineUserRelationshipType Type { get; set; } - public long UserId { get; set; } - } - - public class PostData : ICacheableData - { -#pragma warning disable CA1819 // Properties should not return arrays - public byte[] Data { get; set; } = default!; -#pragma warning restore CA1819 // Properties should not return arrays - public string Type { get; set; } = default!; - public string ETag { get; set; } = default!; - public DateTime? LastModified { get; set; } // TODO: Why nullable? - } - - /// - /// This define the interface of both personal timeline and ordinary timeline. - /// - public interface ITimelineService - { - /// - /// Get the timeline last modified time (not include name change). - /// - /// The name of the timeline. - /// The timeline info. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - Task GetTimelineLastModifiedTime(string timelineName); - - /// - /// Get the timeline unique id. - /// - /// The name of the timeline. - /// The timeline info. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - Task GetTimelineUniqueId(string timelineName); - - /// - /// Get the timeline info. - /// - /// The name of the timeline. - /// The timeline info. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - Task GetTimeline(string timelineName); - - /// - /// Set the properties of a timeline. - /// - /// The name of the timeline. - /// The new properties. Null member means not to change. - /// Thrown when or is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - Task ChangeProperty(string timelineName, TimelineChangePropertyRequest newProperties); - - /// - /// Get all the posts in the timeline. - /// - /// The name of the timeline. - /// The time that posts have been modified since. - /// Whether include deleted posts. - /// A list of all posts. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - Task> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false); - - /// - /// Get the etag of data of a post. - /// - /// The name of the timeline of the post. - /// The id of the post. - /// The etag of the data. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - /// Thrown when post of does not exist or has been deleted. - /// Thrown when post has no data. - /// - Task GetPostDataETag(string timelineName, long postId); - - /// - /// Get the data of a post. - /// - /// The name of the timeline of the post. - /// The id of the post. - /// The etag of the data. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - /// Thrown when post of does not exist or has been deleted. - /// Thrown when post has no data. - /// - Task GetPostData(string timelineName, long postId); - - /// - /// Create a new text post in timeline. - /// - /// The name of the timeline to create post against. - /// The author's user id. - /// The content text. - /// The time of the post. If null, then current time is used. - /// The info of the created post. - /// Thrown when or is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - /// Thrown if user of does not exist. - Task CreateTextPost(string timelineName, long authorId, string text, DateTime? time); - - /// - /// Create a new image post in timeline. - /// - /// The name of the timeline to create post against. - /// The author's user id. - /// The image data. - /// The time of the post. If null, then use current time. - /// The info of the created post. - /// Thrown when or is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - /// Thrown if user of does not exist. - /// Thrown if data is not a image. Validated by . - Task CreateImagePost(string timelineName, long authorId, byte[] imageData, DateTime? time); - - /// - /// Delete a post. - /// - /// The name of the timeline to delete post against. - /// The id of the post to delete. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - /// Thrown when the post with given id does not exist or is deleted already. - /// - /// First use to check the permission. - /// - Task DeletePost(string timelineName, long postId); - - /// - /// Delete all posts of the given user. Used when delete a user. - /// - /// The id of the user. - Task DeleteAllPostsOfUser(long userId); - - /// - /// Change member of timeline. - /// - /// The name of the timeline. - /// A list of usernames of members to add. May be null. - /// A list of usernames of members to remove. May be null. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - /// Thrown when names in or is not a valid username. - /// Thrown when one of the user to change does not exist. - /// - /// Operating on a username that is of bad format or does not exist always throws. - /// Add a user that already is a member has no effects. - /// Remove a user that is not a member also has not effects. - /// Add and remove an identical user results in no effects. - /// More than one same usernames are regarded as one. - /// - Task ChangeMember(string timelineName, IList? membersToAdd, IList? membersToRemove); - - /// - /// Check whether a user can manage(change timeline info, member, ...) a timeline. - /// - /// The name of the timeline. - /// The id of the user to check on. - /// True if the user can manage the timeline, otherwise false. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - /// - /// This method does not check whether visitor is administrator. - /// Return false if user with user id does not exist. - /// - Task HasManagePermission(string timelineName, long userId); - - /// - /// Verify whether a visitor has the permission to read a timeline. - /// - /// The name of the timeline. - /// The id of the user to check on. Null means visitor without account. - /// True if can read, false if can't read. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - /// - /// This method does not check whether visitor is administrator. - /// Return false if user with visitor id does not exist. - /// - Task HasReadPermission(string timelineName, long? visitorId); - - /// - /// Verify whether a user has the permission to modify a post. - /// - /// The name of the timeline. - /// The id of the post. - /// The id of the user to check on. - /// True if you want it to throw . Default false. - /// True if can modify, false if can't modify. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - /// Thrown when the post with given id does not exist or is deleted already and is true. - /// - /// Unless is true, this method should return true if the post does not exist. - /// If the post is deleted, its author info still exists, so it is checked as the post is not deleted unless is true. - /// This method does not check whether the user is administrator. - /// It only checks whether he is the author of the post or the owner of the timeline. - /// Return false when user with modifier id does not exist. - /// - Task HasPostModifyPermission(string timelineName, long postId, long modifierId, bool throwOnPostNotExist = false); - - /// - /// Verify whether a user is member of a timeline. - /// - /// The name of the timeline. - /// The id of user to check on. - /// True if it is a member, false if not. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - /// - /// Timeline owner is also considered as a member. - /// Return false when user with user id does not exist. - /// - Task IsMemberOf(string timelineName, long userId); - - /// - /// Get all timelines including personal and ordinary timelines. - /// - /// Filter timelines related (own or is a member) to specific user. - /// Filter timelines with given visibility. If null or empty, all visibilities are returned. Duplicate value are ignored. - /// The list of timelines. - /// - /// If user with related user id does not exist, empty list will be returned. - /// - Task> GetTimelines(TimelineUserRelationship? relate = null, List? visibility = null); - - /// - /// Create a timeline. - /// - /// The name of the timeline. - /// The id of owner of the timeline. - /// The info of the new timeline. - /// Thrown when is null. - /// Thrown when timeline name is invalid. - /// Thrown when the timeline already exists. - /// Thrown when the owner user does not exist. - Task CreateTimeline(string timelineName, long ownerId); - - /// - /// Delete a timeline. - /// - /// The name of the timeline to delete. - /// Thrown when is null. - /// Thrown when timeline name is invalid. - /// Thrown when the timeline does not exist. - Task DeleteTimeline(string timelineName); - - /// - /// Change name of a timeline. - /// - /// The old timeline name. - /// The new timeline name. - /// The new timeline info. - /// Thrown when or is null. - /// Thrown when or is of invalid format. - /// Thrown when timeline does not exist. - /// Thrown when a timeline with new name already exists. - /// - /// You can only change name of general timeline. - /// - Task ChangeTimelineName(string oldTimelineName, string newTimelineName); - } - - public class TimelineService : ITimelineService - { - public TimelineService(ILogger logger, DatabaseContext database, IDataManager dataManager, IUserService userService, IImageValidator imageValidator, IClock clock) - { - _logger = logger; - _database = database; - _dataManager = dataManager; - _userService = userService; - _imageValidator = imageValidator; - _clock = clock; - } - - private readonly ILogger _logger; - - private readonly DatabaseContext _database; - - private readonly IDataManager _dataManager; - - private readonly IUserService _userService; - - private readonly IImageValidator _imageValidator; - - private readonly IClock _clock; - - private readonly UsernameValidator _usernameValidator = new UsernameValidator(); - - private readonly TimelineNameValidator _timelineNameValidator = new TimelineNameValidator(); - - private void ValidateTimelineName(string name, string paramName) - { - if (!_timelineNameValidator.Validate(name, out var message)) - { - throw new ArgumentException(ExceptionTimelineNameBadFormat.AppendAdditionalMessage(message), paramName); - } - } - - /// Remember to include Members when query. - private async Task MapTimelineFromEntity(TimelineEntity entity) - { - var owner = await _userService.GetUserById(entity.OwnerId); - - var members = new List(); - foreach (var memberEntity in entity.Members) - { - members.Add(await _userService.GetUserById(memberEntity.UserId)); - } - - var name = entity.Name ?? ("@" + owner.Username); - - return new Models.Timeline - { - UniqueID = entity.UniqueId, - Name = name, - NameLastModified = entity.NameLastModified, - Title = string.IsNullOrEmpty(entity.Title) ? name : entity.Title, - Description = entity.Description ?? "", - Owner = owner, - Visibility = entity.Visibility, - Members = members, - CreateTime = entity.CreateTime, - LastModified = entity.LastModified - }; - } - - private async Task MapTimelinePostFromEntity(TimelinePostEntity entity, string timelineName) - { - User? author = entity.AuthorId.HasValue ? await _userService.GetUserById(entity.AuthorId.Value) : null; - - ITimelinePostContent? content = null; - - if (entity.Content != null) - { - var type = entity.ContentType; - - content = type switch - { - TimelinePostContentTypes.Text => new TextTimelinePostContent(entity.Content), - TimelinePostContentTypes.Image => new ImageTimelinePostContent(entity.Content), - _ => throw new DatabaseCorruptedException(string.Format(CultureInfo.InvariantCulture, ExceptionDatabaseUnknownContentType, type)) - }; - } - - return new TimelinePost( - id: entity.LocalId, - author: author, - content: content, - time: entity.Time, - lastUpdated: entity.LastUpdated, - timelineName: timelineName - ); - } - - private TimelineEntity CreateNewTimelineEntity(string? name, long ownerId) - { - var currentTime = _clock.GetCurrentTime(); - - return new TimelineEntity - { - Name = name, - NameLastModified = currentTime, - OwnerId = ownerId, - Visibility = TimelineVisibility.Register, - CreateTime = currentTime, - LastModified = currentTime, - CurrentPostLocalId = 0, - Members = new List() - }; - } - - - - // Get timeline id by name. If it is a personal timeline and it does not exist, it will be created. - // - // This method will check the name format and if it is invalid, ArgumentException is thrown. - // - // For personal timeline, if the user does not exist, TimelineNotExistException will be thrown with UserNotExistException as inner exception. - // For ordinary timeline, if the timeline does not exist, TimelineNotExistException will be thrown. - // - // It follows all timeline-related function common interface contracts. - private async Task FindTimelineId(string timelineName) - { - timelineName = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); - - if (isPersonal) - { - long userId; - try - { - userId = await _userService.GetUserIdByUsername(timelineName); - } - catch (ArgumentException e) - { - throw new ArgumentException(ExceptionFindTimelineUsernameBadFormat, nameof(timelineName), e); - } - catch (UserNotExistException e) - { - throw new TimelineNotExistException(timelineName, e); - } - - var timelineEntity = await _database.Timelines.Where(t => t.OwnerId == userId && t.Name == null).Select(t => new { t.Id }).SingleOrDefaultAsync(); - - if (timelineEntity != null) - { - return timelineEntity.Id; - } - else - { - var newTimelineEntity = CreateNewTimelineEntity(null, userId); - _database.Timelines.Add(newTimelineEntity); - await _database.SaveChangesAsync(); - - return newTimelineEntity.Id; - } - } - else - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - ValidateTimelineName(timelineName, nameof(timelineName)); - - var timelineEntity = await _database.Timelines.Where(t => t.Name == timelineName).Select(t => new { t.Id }).SingleOrDefaultAsync(); - - if (timelineEntity == null) - { - throw new TimelineNotExistException(timelineName); - } - else - { - return timelineEntity.Id; - } - } - } - - public async Task GetTimelineLastModifiedTime(string timelineName) - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - var timelineId = await FindTimelineId(timelineName); - - var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.LastModified }).SingleAsync(); - - return timelineEntity.LastModified; - } - - public async Task GetTimelineUniqueId(string timelineName) - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - var timelineId = await FindTimelineId(timelineName); - - var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.UniqueId }).SingleAsync(); - - return timelineEntity.UniqueId; - } - - public async Task GetTimeline(string timelineName) - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - var timelineId = await FindTimelineId(timelineName); - - var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Include(t => t.Members).SingleAsync(); - - return await MapTimelineFromEntity(timelineEntity); - } - - public async Task> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false) - { - modifiedSince = modifiedSince?.MyToUtc(); - - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - var timelineId = await FindTimelineId(timelineName); - IQueryable query = _database.TimelinePosts.Where(p => p.TimelineId == timelineId); - - if (!includeDeleted) - { - query = query.Where(p => p.Content != null); - } - - if (modifiedSince.HasValue) - { - query = query.Include(p => p.Author).Where(p => p.LastUpdated >= modifiedSince || (p.Author != null && p.Author.UsernameChangeTime >= modifiedSince)); - } - - query = query.OrderBy(p => p.Time); - - var postEntities = await query.ToListAsync(); - - var posts = new List(); - foreach (var entity in postEntities) - { - posts.Add(await MapTimelinePostFromEntity(entity, timelineName)); - } - return posts; - } - - public async Task GetPostDataETag(string timelineName, long postId) - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - var timelineId = await FindTimelineId(timelineName); - - var postEntity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync(); - - if (postEntity == null) - throw new TimelinePostNotExistException(timelineName, postId, false); - - if (postEntity.Content == null) - throw new TimelinePostNotExistException(timelineName, postId, true); - - if (postEntity.ContentType != TimelinePostContentTypes.Image) - throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost); - - var tag = postEntity.Content; - - return tag; - } - - public async Task GetPostData(string timelineName, long postId) - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - var timelineId = await FindTimelineId(timelineName); - var postEntity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync(); - - if (postEntity == null) - throw new TimelinePostNotExistException(timelineName, postId, false); - - if (postEntity.Content == null) - throw new TimelinePostNotExistException(timelineName, postId, true); - - if (postEntity.ContentType != TimelinePostContentTypes.Image) - throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost); - - var tag = postEntity.Content; - - byte[] data; - - try - { - data = await _dataManager.GetEntry(tag); - } - catch (InvalidOperationException e) - { - throw new DatabaseCorruptedException(ExceptionGetDataDataEntryNotExist, e); - } - - if (postEntity.ExtraContent == null) - { - _logger.LogWarning(LogGetDataNoFormat); - var format = Image.DetectFormat(data); - postEntity.ExtraContent = format.DefaultMimeType; - await _database.SaveChangesAsync(); - } - - return new PostData - { - Data = data, - Type = postEntity.ExtraContent, - ETag = tag, - LastModified = postEntity.LastUpdated - }; - } - - public async Task CreateTextPost(string timelineName, long authorId, string text, DateTime? time) - { - time = time?.MyToUtc(); - - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - if (text == null) - throw new ArgumentNullException(nameof(text)); - - var timelineId = await FindTimelineId(timelineName); - var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); - - var author = await _userService.GetUserById(authorId); - - var currentTime = _clock.GetCurrentTime(); - var finalTime = time ?? currentTime; - - timelineEntity.CurrentPostLocalId += 1; - - var postEntity = new TimelinePostEntity - { - LocalId = timelineEntity.CurrentPostLocalId, - ContentType = TimelinePostContentTypes.Text, - Content = text, - AuthorId = authorId, - TimelineId = timelineId, - Time = finalTime, - LastUpdated = currentTime - }; - _database.TimelinePosts.Add(postEntity); - await _database.SaveChangesAsync(); - - - return new TimelinePost( - id: postEntity.LocalId, - content: new TextTimelinePostContent(text), - time: finalTime, - author: author, - lastUpdated: currentTime, - timelineName: timelineName - ); - } - - public async Task CreateImagePost(string timelineName, long authorId, byte[] data, DateTime? time) - { - time = time?.MyToUtc(); - - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - if (data == null) - throw new ArgumentNullException(nameof(data)); - - var timelineId = await FindTimelineId(timelineName); - var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); - - var author = await _userService.GetUserById(authorId); - - var imageFormat = await _imageValidator.Validate(data); - - var imageFormatText = imageFormat.DefaultMimeType; - - var tag = await _dataManager.RetainEntry(data); - - var currentTime = _clock.GetCurrentTime(); - var finalTime = time ?? currentTime; - - timelineEntity.CurrentPostLocalId += 1; - - var postEntity = new TimelinePostEntity - { - LocalId = timelineEntity.CurrentPostLocalId, - ContentType = TimelinePostContentTypes.Image, - Content = tag, - ExtraContent = imageFormatText, - AuthorId = authorId, - TimelineId = timelineId, - Time = finalTime, - LastUpdated = currentTime - }; - _database.TimelinePosts.Add(postEntity); - await _database.SaveChangesAsync(); - - return new TimelinePost( - id: postEntity.LocalId, - content: new ImageTimelinePostContent(tag), - time: finalTime, - author: author, - lastUpdated: currentTime, - timelineName: timelineName - ); - } - - public async Task DeletePost(string timelineName, long id) - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - var timelineId = await FindTimelineId(timelineName); - - var post = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == id).SingleOrDefaultAsync(); - - if (post == null) - throw new TimelinePostNotExistException(timelineName, id, false); - - if (post.Content == null) - throw new TimelinePostNotExistException(timelineName, id, true); - - string? dataTag = null; - - if (post.ContentType == TimelinePostContentTypes.Image) - { - dataTag = post.Content; - } - - post.Content = null; - post.LastUpdated = _clock.GetCurrentTime(); - - await _database.SaveChangesAsync(); - - if (dataTag != null) - { - await _dataManager.FreeEntry(dataTag); - } - } - - public async Task DeleteAllPostsOfUser(long userId) - { - var posts = await _database.TimelinePosts.Where(p => p.AuthorId == userId).ToListAsync(); - - var now = _clock.GetCurrentTime(); - - var dataTags = new List(); - - foreach (var post in posts) - { - if (post.Content != null) - { - if (post.ContentType == TimelinePostContentTypes.Image) - { - dataTags.Add(post.Content); - } - post.Content = null; - } - post.LastUpdated = now; - } - - await _database.SaveChangesAsync(); - - foreach (var dataTag in dataTags) - { - await _dataManager.FreeEntry(dataTag); - } - } - - public async Task ChangeProperty(string timelineName, TimelineChangePropertyRequest newProperties) - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - if (newProperties == null) - throw new ArgumentNullException(nameof(newProperties)); - - var timelineId = await FindTimelineId(timelineName); - - var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); - - var changed = false; - - if (newProperties.Title != null) - { - changed = true; - timelineEntity.Title = newProperties.Title; - } - - if (newProperties.Description != null) - { - changed = true; - timelineEntity.Description = newProperties.Description; - } - - if (newProperties.Visibility.HasValue) - { - changed = true; - timelineEntity.Visibility = newProperties.Visibility.Value; - } - - if (changed) - { - var currentTime = _clock.GetCurrentTime(); - timelineEntity.LastModified = currentTime; - } - - await _database.SaveChangesAsync(); - } - - public async Task ChangeMember(string timelineName, IList? add, IList? remove) - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - List? RemoveDuplicateAndCheckFormat(IList? list, string paramName) - { - if (list != null) - { - List result = new List(); - var count = list.Count; - for (var index = 0; index < count; index++) - { - var username = list[index]; - if (result.Contains(username)) - { - continue; - } - var (validationResult, message) = _usernameValidator.Validate(username); - if (!validationResult) - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionChangeMemberUsernameBadFormat, index), nameof(paramName)); - result.Add(username); - } - return result; - } - else - { - return null; - } - } - var simplifiedAdd = RemoveDuplicateAndCheckFormat(add, nameof(add)); - var simplifiedRemove = RemoveDuplicateAndCheckFormat(remove, nameof(remove)); - - // remove those both in add and remove - if (simplifiedAdd != null && simplifiedRemove != null) - { - var usersToClean = simplifiedRemove.Where(u => simplifiedAdd.Contains(u)).ToList(); - foreach (var u in usersToClean) - { - simplifiedAdd.Remove(u); - simplifiedRemove.Remove(u); - } - - if (simplifiedAdd.Count == 0) - simplifiedAdd = null; - - if (simplifiedRemove.Count == 0) - simplifiedRemove = null; - } - - if (simplifiedAdd == null && simplifiedRemove == null) - return; - - var timelineId = await FindTimelineId(timelineName); - - async Task?> CheckExistenceAndGetId(List? list) - { - if (list == null) - return null; - - List result = new List(); - foreach (var username in list) - { - result.Add(await _userService.GetUserIdByUsername(username)); - } - return result; - } - var userIdsAdd = await CheckExistenceAndGetId(simplifiedAdd); - var userIdsRemove = await CheckExistenceAndGetId(simplifiedRemove); - - if (userIdsAdd != null) - { - var membersToAdd = userIdsAdd.Select(id => new TimelineMemberEntity { UserId = id, TimelineId = timelineId }).ToList(); - _database.TimelineMembers.AddRange(membersToAdd); - } - - if (userIdsRemove != null) - { - var membersToRemove = await _database.TimelineMembers.Where(m => m.TimelineId == timelineId && userIdsRemove.Contains(m.UserId)).ToListAsync(); - _database.TimelineMembers.RemoveRange(membersToRemove); - } - - var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); - timelineEntity.LastModified = _clock.GetCurrentTime(); - - await _database.SaveChangesAsync(); - } - - public async Task HasManagePermission(string timelineName, long userId) - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - var timelineId = await FindTimelineId(timelineName); - var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync(); - - return userId == timelineEntity.OwnerId; - } - - public async Task HasReadPermission(string timelineName, long? visitorId) - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - var timelineId = await FindTimelineId(timelineName); - var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.Visibility }).SingleAsync(); - - if (timelineEntity.Visibility == TimelineVisibility.Public) - return true; - - if (timelineEntity.Visibility == TimelineVisibility.Register && visitorId != null) - return true; - - if (visitorId == null) - { - return false; - } - else - { - var memberEntity = await _database.TimelineMembers.Where(m => m.UserId == visitorId && m.TimelineId == timelineId).SingleOrDefaultAsync(); - return memberEntity != null; - } - } - - public async Task HasPostModifyPermission(string timelineName, long postId, long modifierId, bool throwOnPostNotExist = false) - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - var timelineId = await FindTimelineId(timelineName); - - var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync(); - - var postEntity = await _database.TimelinePosts.Where(p => p.Id == postId).Select(p => new { p.Content, p.AuthorId }).SingleOrDefaultAsync(); - - if (postEntity == null) - { - if (throwOnPostNotExist) - throw new TimelinePostNotExistException(timelineName, postId, false); - else - return true; - } - - if (postEntity.Content == null && throwOnPostNotExist) - { - throw new TimelinePostNotExistException(timelineName, postId, true); - } - - return timelineEntity.OwnerId == modifierId || postEntity.AuthorId == modifierId; - } - - public async Task IsMemberOf(string timelineName, long userId) - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - var timelineId = await FindTimelineId(timelineName); - - var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync(); - - if (userId == timelineEntity.OwnerId) - return true; - - return await _database.TimelineMembers.AnyAsync(m => m.TimelineId == timelineId && m.UserId == userId); - } - - public async Task> GetTimelines(TimelineUserRelationship? relate = null, List? visibility = null) - { - List entities; - - IQueryable ApplyTimelineVisibilityFilter(IQueryable query) - { - if (visibility != null && visibility.Count != 0) - { - return query.Where(t => visibility.Contains(t.Visibility)); - } - return query; - } - - bool allVisibilities = visibility == null || visibility.Count == 0; - - if (relate == null) - { - entities = await ApplyTimelineVisibilityFilter(_database.Timelines).Include(t => t.Members).ToListAsync(); - } - else - { - entities = new List(); - - if ((relate.Type & TimelineUserRelationshipType.Own) != 0) - { - entities.AddRange(await ApplyTimelineVisibilityFilter(_database.Timelines.Where(t => t.OwnerId == relate.UserId)).Include(t => t.Members).ToListAsync()); - } - - if ((relate.Type & TimelineUserRelationshipType.Join) != 0) - { - entities.AddRange(await ApplyTimelineVisibilityFilter(_database.TimelineMembers.Where(m => m.UserId == relate.UserId).Include(m => m.Timeline).ThenInclude(t => t.Members).Select(m => m.Timeline)).ToListAsync()); - } - } - - var result = new List(); - - foreach (var entity in entities) - { - result.Add(await MapTimelineFromEntity(entity)); - } - - return result; - } - - public async Task CreateTimeline(string name, long owner) - { - if (name == null) - throw new ArgumentNullException(nameof(name)); - - ValidateTimelineName(name, nameof(name)); - - var user = await _userService.GetUserById(owner); - - var conflict = await _database.Timelines.AnyAsync(t => t.Name == name); - - if (conflict) - throw new EntityAlreadyExistException(EntityNames.Timeline, null, ExceptionTimelineNameConflict); - - var newEntity = CreateNewTimelineEntity(name, user.Id!.Value); - - _database.Timelines.Add(newEntity); - await _database.SaveChangesAsync(); - - return await MapTimelineFromEntity(newEntity); - } - - public async Task DeleteTimeline(string name) - { - if (name == null) - throw new ArgumentNullException(nameof(name)); - - ValidateTimelineName(name, nameof(name)); - - var entity = await _database.Timelines.Where(t => t.Name == name).SingleOrDefaultAsync(); - - if (entity == null) - throw new TimelineNotExistException(name); - - _database.Timelines.Remove(entity); - await _database.SaveChangesAsync(); - } - - public async Task ChangeTimelineName(string oldTimelineName, string newTimelineName) - { - if (oldTimelineName == null) - throw new ArgumentNullException(nameof(oldTimelineName)); - if (newTimelineName == null) - throw new ArgumentNullException(nameof(newTimelineName)); - - ValidateTimelineName(oldTimelineName, nameof(oldTimelineName)); - ValidateTimelineName(newTimelineName, nameof(newTimelineName)); - - var entity = await _database.Timelines.Include(t => t.Members).Where(t => t.Name == oldTimelineName).SingleOrDefaultAsync(); - - if (entity == null) - throw new TimelineNotExistException(oldTimelineName); - - if (oldTimelineName == newTimelineName) - return await MapTimelineFromEntity(entity); - - var conflict = await _database.Timelines.AnyAsync(t => t.Name == newTimelineName); - - if (conflict) - throw new EntityAlreadyExistException(EntityNames.Timeline, null, ExceptionTimelineNameConflict); - - var now = _clock.GetCurrentTime(); - - entity.Name = newTimelineName; - entity.NameLastModified = now; - entity.LastModified = now; - - await _database.SaveChangesAsync(); - - return await MapTimelineFromEntity(entity); - } - } -} diff --git a/Timeline/Services/UserAvatarService.cs b/Timeline/Services/UserAvatarService.cs deleted file mode 100644 index b41c45fd..00000000 --- a/Timeline/Services/UserAvatarService.cs +++ /dev/null @@ -1,265 +0,0 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using System; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Timeline.Entities; -using Timeline.Helpers; -using Timeline.Services.Exceptions; - -namespace Timeline.Services -{ - public class Avatar - { - public string Type { get; set; } = default!; - [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "DTO Object")] - public byte[] Data { get; set; } = default!; - } - - public class AvatarInfo - { - public Avatar Avatar { get; set; } = default!; - public DateTime LastModified { get; set; } - - public CacheableData ToCacheableData() - { - return new CacheableData(Avatar.Type, Avatar.Data, LastModified); - } - } - - /// - /// Provider for default user avatar. - /// - /// - /// Mainly for unit tests. - /// - public interface IDefaultUserAvatarProvider - { - /// - /// Get the etag of default avatar. - /// - /// - Task GetDefaultAvatarETag(); - - /// - /// Get the default avatar. - /// - Task GetDefaultAvatar(); - } - - public interface IUserAvatarService - { - /// - /// Get the etag of a user's avatar. Warning: This method does not check the user existence. - /// - /// The id of the user to get avatar etag of. - /// The etag. - Task GetAvatarETag(long id); - - /// - /// Get avatar of a user. If the user has no avatar set, a default one is returned. Warning: This method does not check the user existence. - /// - /// The id of the user to get avatar of. - /// The avatar info. - Task GetAvatar(long id); - - /// - /// Set avatar for a user. Warning: This method does not check the user existence. - /// - /// The id of the user to set avatar for. - /// The avatar. Can be null to delete the saved avatar. - /// The etag of the avatar. - /// Thrown if any field in is null when is not null. - /// Thrown if avatar is of bad format. - Task SetAvatar(long id, Avatar? avatar); - } - - // TODO! : Make this configurable. - public class DefaultUserAvatarProvider : IDefaultUserAvatarProvider - { - private readonly IETagGenerator _eTagGenerator; - - private readonly string _avatarPath; - - private byte[] _cacheData = default!; - private DateTime _cacheLastModified; - private string _cacheETag = default!; - - public DefaultUserAvatarProvider(IWebHostEnvironment environment, IETagGenerator eTagGenerator) - { - _avatarPath = Path.Combine(environment.ContentRootPath, "default-avatar.png"); - _eTagGenerator = eTagGenerator; - } - - private async Task CheckAndInit() - { - var path = _avatarPath; - if (_cacheData == null || File.GetLastWriteTime(path) > _cacheLastModified) - { - _cacheData = await File.ReadAllBytesAsync(path); - _cacheLastModified = File.GetLastWriteTime(path); - _cacheETag = await _eTagGenerator.Generate(_cacheData); - } - } - - public async Task GetDefaultAvatarETag() - { - await CheckAndInit(); - return _cacheETag; - } - - public async Task GetDefaultAvatar() - { - await CheckAndInit(); - return new AvatarInfo - { - Avatar = new Avatar - { - Type = "image/png", - Data = _cacheData - }, - LastModified = _cacheLastModified - }; - } - } - - public class UserAvatarService : IUserAvatarService - { - - private readonly ILogger _logger; - - private readonly DatabaseContext _database; - - private readonly IDefaultUserAvatarProvider _defaultUserAvatarProvider; - - private readonly IImageValidator _imageValidator; - - private readonly IDataManager _dataManager; - - private readonly IClock _clock; - - public UserAvatarService( - ILogger logger, - DatabaseContext database, - IDefaultUserAvatarProvider defaultUserAvatarProvider, - IImageValidator imageValidator, - IDataManager dataManager, - IClock clock) - { - _logger = logger; - _database = database; - _defaultUserAvatarProvider = defaultUserAvatarProvider; - _imageValidator = imageValidator; - _dataManager = dataManager; - _clock = clock; - } - - public async Task GetAvatarETag(long id) - { - var eTag = (await _database.UserAvatars.Where(a => a.UserId == id).Select(a => new { a.DataTag }).SingleOrDefaultAsync())?.DataTag; - if (eTag == null) - return await _defaultUserAvatarProvider.GetDefaultAvatarETag(); - else - return eTag; - } - - public async Task GetAvatar(long id) - { - var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == id).Select(a => new { a.Type, a.DataTag, a.LastModified }).SingleOrDefaultAsync(); - - if (avatarEntity != null) - { - if (!LanguageHelper.AreSame(avatarEntity.DataTag == null, avatarEntity.Type == null)) - { - var message = Resources.Services.UserAvatarService.ExceptionDatabaseCorruptedDataAndTypeNotSame; - _logger.LogCritical(message); - throw new DatabaseCorruptedException(message); - } - - - if (avatarEntity.DataTag != null) - { - var data = await _dataManager.GetEntry(avatarEntity.DataTag); - return new AvatarInfo - { - Avatar = new Avatar - { - Type = avatarEntity.Type!, - Data = data - }, - LastModified = avatarEntity.LastModified - }; - } - } - var defaultAvatar = await _defaultUserAvatarProvider.GetDefaultAvatar(); - if (avatarEntity != null) - defaultAvatar.LastModified = defaultAvatar.LastModified > avatarEntity.LastModified ? defaultAvatar.LastModified : avatarEntity.LastModified; - return defaultAvatar; - } - - public async Task SetAvatar(long id, Avatar? avatar) - { - if (avatar != null) - { - if (avatar.Data == null) - throw new ArgumentException(Resources.Services.UserAvatarService.ExceptionAvatarDataNull, nameof(avatar)); - if (string.IsNullOrEmpty(avatar.Type)) - throw new ArgumentException(Resources.Services.UserAvatarService.ExceptionAvatarTypeNullOrEmpty, nameof(avatar)); - } - - var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == id).SingleOrDefaultAsync(); - - if (avatar == null) - { - if (avatarEntity != null && avatarEntity.DataTag != null) - { - await _dataManager.FreeEntry(avatarEntity.DataTag); - avatarEntity.DataTag = null; - avatarEntity.Type = null; - avatarEntity.LastModified = _clock.GetCurrentTime(); - await _database.SaveChangesAsync(); - _logger.LogInformation(Resources.Services.UserAvatarService.LogUpdateEntity); - } - return await _defaultUserAvatarProvider.GetDefaultAvatarETag(); - } - else - { - await _imageValidator.Validate(avatar.Data, avatar.Type, true); - var tag = await _dataManager.RetainEntry(avatar.Data); - var oldTag = avatarEntity?.DataTag; - var create = avatarEntity == null; - if (avatarEntity == null) - { - avatarEntity = new UserAvatarEntity(); - _database.UserAvatars.Add(avatarEntity); - } - avatarEntity.DataTag = tag; - avatarEntity.Type = avatar.Type; - avatarEntity.LastModified = _clock.GetCurrentTime(); - avatarEntity.UserId = id; - await _database.SaveChangesAsync(); - _logger.LogInformation(create ? - Resources.Services.UserAvatarService.LogCreateEntity - : Resources.Services.UserAvatarService.LogUpdateEntity); - if (oldTag != null) - { - await _dataManager.FreeEntry(oldTag); - } - - return avatarEntity.DataTag; - } - } - } - - public static class UserAvatarServiceCollectionExtensions - { - public static void AddUserAvatarService(this IServiceCollection services) - { - services.AddScoped(); - services.AddScoped(); - } - } -} diff --git a/Timeline/Services/UserDeleteService.cs b/Timeline/Services/UserDeleteService.cs deleted file mode 100644 index 845de573..00000000 --- a/Timeline/Services/UserDeleteService.cs +++ /dev/null @@ -1,69 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading.Tasks; -using Timeline.Entities; -using Timeline.Helpers; -using Timeline.Models.Validation; -using static Timeline.Resources.Services.UserService; - -namespace Timeline.Services -{ - public interface IUserDeleteService - { - /// - /// Delete a user of given username. - /// - /// Username of the user to delete. Can't be null. - /// True if user is deleted, false if user not exist. - /// Thrown if is null. - /// Thrown when is of bad format. - Task DeleteUser(string username); - } - - public class UserDeleteService : IUserDeleteService - { - private readonly ILogger _logger; - - private readonly DatabaseContext _databaseContext; - - private readonly ITimelineService _timelineService; - - private readonly UsernameValidator _usernameValidator = new UsernameValidator(); - - public UserDeleteService(ILogger logger, DatabaseContext databaseContext, ITimelineService timelineService) - { - _logger = logger; - _databaseContext = databaseContext; - _timelineService = timelineService; - } - - public async Task DeleteUser(string username) - { - if (username == null) - throw new ArgumentNullException(nameof(username)); - - if (!_usernameValidator.Validate(username, out var message)) - { - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionUsernameBadFormat, message), nameof(username)); - } - - var user = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync(); - if (user == null) - return false; - - await _timelineService.DeleteAllPostsOfUser(user.Id); - - _databaseContext.Users.Remove(user); - - await _databaseContext.SaveChangesAsync(); - _logger.LogInformation(Log.Format(LogDatabaseRemove, ("Id", user.Id), ("Username", user.Username))); - - return true; - } - - } -} diff --git a/Timeline/Services/UserRoleConvert.cs b/Timeline/Services/UserRoleConvert.cs deleted file mode 100644 index f27ee1bb..00000000 --- a/Timeline/Services/UserRoleConvert.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Timeline.Entities; - -namespace Timeline.Services -{ - public static class UserRoleConvert - { - public const string UserRole = UserRoles.User; - public const string AdminRole = UserRoles.Admin; - - public static string[] ToArray(bool administrator) - { - return administrator ? new string[] { UserRole, AdminRole } : new string[] { UserRole }; - } - - public static string[] ToArray(string s) - { - return s.Split(',').ToArray(); - } - - public static bool ToBool(IReadOnlyCollection roles) - { - return roles.Contains(AdminRole); - } - - public static string ToString(IReadOnlyCollection roles) - { - return string.Join(',', roles); - } - - public static string ToString(bool administrator) - { - return administrator ? UserRole + "," + AdminRole : UserRole; - } - - public static bool ToBool(string s) - { - return s.Contains("admin", StringComparison.InvariantCulture); - } - } -} diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs deleted file mode 100644 index 821bc33d..00000000 --- a/Timeline/Services/UserService.cs +++ /dev/null @@ -1,437 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using System; -using System.Globalization; -using System.Linq; -using System.Threading.Tasks; -using Timeline.Entities; -using Timeline.Helpers; -using Timeline.Models; -using Timeline.Models.Validation; -using Timeline.Services.Exceptions; -using static Timeline.Resources.Services.UserService; - -namespace Timeline.Services -{ - public interface IUserService - { - /// - /// Try to verify the given username and password. - /// - /// The username of the user to verify. - /// The password of the user to verify. - /// The user info and auth info. - /// Thrown when or is null. - /// Thrown when is of bad format or is empty. - /// Thrown when the user with given username does not exist. - /// Thrown when password is wrong. - Task VerifyCredential(string username, string password); - - /// - /// Try to get a user by id. - /// - /// The id of the user. - /// The user info. - /// Thrown when the user with given id does not exist. - Task GetUserById(long id); - - /// - /// Get the user info of given username. - /// - /// Username of the user. - /// The info of the user. - /// Thrown when is null. - /// Thrown when is of bad format. - /// Thrown when the user with given username does not exist. - Task GetUserByUsername(string username); - - /// - /// Get the user id of given username. - /// - /// Username of the user. - /// The id of the user. - /// Thrown when is null. - /// Thrown when is of bad format. - /// Thrown when the user with given username does not exist. - Task GetUserIdByUsername(string username); - - /// - /// List all users. - /// - /// The user info of users. - Task GetUsers(); - - /// - /// Create a user with given info. - /// - /// The info of new user. - /// The the new user. - /// Thrown when is null. - /// Thrown when some fields in is bad. - /// Thrown when a user with given username already exists. - /// - /// must not be null and must be a valid username. - /// must not be null or empty. - /// is false by default (null). - /// must be a valid nickname if set. It is empty by default. - /// Other fields are ignored. - /// - Task CreateUser(User info); - - /// - /// Modify a user's info. - /// - /// The id of the user. - /// The new info. May be null. - /// The new user info. - /// Thrown when some fields in is bad. - /// Thrown when user with given id does not exist. - /// - /// Only , , and will be used. - /// If null, then not change. - /// Other fields are ignored. - /// Version will increase if password is changed. - /// - /// must be a valid username if set. - /// can't be empty if set. - /// must be a valid nickname if set. - /// - /// - /// - Task ModifyUser(long id, User? info); - - /// - /// Modify a user's info. - /// - /// The username of the user. - /// The new info. May be null. - /// The new user info. - /// Thrown when is null. - /// Thrown when is of bad format or some fields in is bad. - /// Thrown when user with given id does not exist. - /// Thrown when user with the newusername already exist. - /// - /// Only , and will be used. - /// If null, then not change. - /// Other fields are ignored. - /// After modified, even if nothing is changed, version will increase. - /// - /// must be a valid username if set. - /// can't be empty if set. - /// must be a valid nickname if set. - /// - /// Note: Whether is set or not, version will increase and not set to the specified value if there is one. - /// - /// - Task ModifyUser(string username, User? info); - - /// - /// Try to change a user's password with old password. - /// - /// The id of user to change password of. - /// Old password. - /// New password. - /// Thrown if or is null. - /// Thrown if or is empty. - /// Thrown if the user with given username does not exist. - /// Thrown if the old password is wrong. - Task ChangePassword(long id, string oldPassword, string newPassword); - } - - public class UserService : IUserService - { - private readonly ILogger _logger; - private readonly IClock _clock; - - private readonly DatabaseContext _databaseContext; - - private readonly IPasswordService _passwordService; - - private readonly UsernameValidator _usernameValidator = new UsernameValidator(); - private readonly NicknameValidator _nicknameValidator = new NicknameValidator(); - public UserService(ILogger logger, DatabaseContext databaseContext, IPasswordService passwordService, IClock clock) - { - _logger = logger; - _clock = clock; - _databaseContext = databaseContext; - _passwordService = passwordService; - } - - private void CheckUsernameFormat(string username, string? paramName) - { - if (!_usernameValidator.Validate(username, out var message)) - { - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionUsernameBadFormat, message), paramName); - } - } - - private static void CheckPasswordFormat(string password, string? paramName) - { - if (password.Length == 0) - { - throw new ArgumentException(ExceptionPasswordEmpty, paramName); - } - } - - private void CheckNicknameFormat(string nickname, string? paramName) - { - if (!_nicknameValidator.Validate(nickname, out var message)) - { - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionNicknameBadFormat, message), paramName); - } - } - - private static void ThrowUsernameConflict() - { - throw new EntityAlreadyExistException(EntityNames.User, ExceptionUsernameConflict); - } - - private static User CreateUserFromEntity(UserEntity entity) - { - return new User - { - UniqueId = entity.UniqueId, - Username = entity.Username, - Administrator = UserRoleConvert.ToBool(entity.Roles), - Nickname = string.IsNullOrEmpty(entity.Nickname) ? entity.Username : entity.Nickname, - Id = entity.Id, - Version = entity.Version, - CreateTime = entity.CreateTime, - UsernameChangeTime = entity.UsernameChangeTime, - LastModified = entity.LastModified - }; - } - - public async Task VerifyCredential(string username, string password) - { - if (username == null) - throw new ArgumentNullException(nameof(username)); - if (password == null) - throw new ArgumentNullException(nameof(password)); - - CheckUsernameFormat(username, nameof(username)); - CheckPasswordFormat(password, nameof(password)); - - var entity = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync(); - - if (entity == null) - throw new UserNotExistException(username); - - if (!_passwordService.VerifyPassword(entity.Password, password)) - throw new BadPasswordException(password); - - return CreateUserFromEntity(entity); - } - - public async Task GetUserById(long id) - { - var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); - - if (user == null) - throw new UserNotExistException(id); - - return CreateUserFromEntity(user); - } - - public async Task GetUserByUsername(string username) - { - if (username == null) - throw new ArgumentNullException(nameof(username)); - - CheckUsernameFormat(username, nameof(username)); - - var entity = await _databaseContext.Users.Where(user => user.Username == username).SingleOrDefaultAsync(); - - if (entity == null) - throw new UserNotExistException(username); - - return CreateUserFromEntity(entity); - } - - public async Task GetUserIdByUsername(string username) - { - if (username == null) - throw new ArgumentNullException(nameof(username)); - - CheckUsernameFormat(username, nameof(username)); - - var entity = await _databaseContext.Users.Where(user => user.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); - - if (entity == null) - throw new UserNotExistException(username); - - return entity.Id; - } - - public async Task GetUsers() - { - var entities = await _databaseContext.Users.ToArrayAsync(); - return entities.Select(user => CreateUserFromEntity(user)).ToArray(); - } - - public async Task CreateUser(User info) - { - if (info == null) - throw new ArgumentNullException(nameof(info)); - - if (info.Username == null) - throw new ArgumentException(ExceptionUsernameNull, nameof(info)); - CheckUsernameFormat(info.Username, nameof(info)); - - if (info.Password == null) - throw new ArgumentException(ExceptionPasswordNull, nameof(info)); - CheckPasswordFormat(info.Password, nameof(info)); - - if (info.Nickname != null) - CheckNicknameFormat(info.Nickname, nameof(info)); - - var username = info.Username; - - var conflict = await _databaseContext.Users.AnyAsync(u => u.Username == username); - if (conflict) - ThrowUsernameConflict(); - - var administrator = info.Administrator ?? false; - var password = info.Password; - var nickname = info.Nickname; - - var newEntity = new UserEntity - { - Username = username, - Password = _passwordService.HashPassword(password), - Roles = UserRoleConvert.ToString(administrator), - Nickname = nickname, - Version = 1 - }; - _databaseContext.Users.Add(newEntity); - await _databaseContext.SaveChangesAsync(); - - _logger.LogInformation(Log.Format(LogDatabaseCreate, - ("Id", newEntity.Id), ("Username", username), ("Administrator", administrator))); - - return CreateUserFromEntity(newEntity); - } - - private void ValidateModifyUserInfo(User? info) - { - if (info != null) - { - if (info.Username != null) - CheckUsernameFormat(info.Username, nameof(info)); - - if (info.Password != null) - CheckPasswordFormat(info.Password, nameof(info)); - - if (info.Nickname != null) - CheckNicknameFormat(info.Nickname, nameof(info)); - } - } - - private async Task UpdateUserEntity(UserEntity entity, User? info) - { - if (info != null) - { - var now = _clock.GetCurrentTime(); - bool updateLastModified = false; - - var username = info.Username; - if (username != null && username != entity.Username) - { - var conflict = await _databaseContext.Users.AnyAsync(u => u.Username == username); - if (conflict) - ThrowUsernameConflict(); - - entity.Username = username; - entity.UsernameChangeTime = now; - updateLastModified = true; - } - - var password = info.Password; - if (password != null) - { - entity.Password = _passwordService.HashPassword(password); - entity.Version += 1; - } - - var administrator = info.Administrator; - if (administrator.HasValue && UserRoleConvert.ToBool(entity.Roles) != administrator) - { - entity.Roles = UserRoleConvert.ToString(administrator.Value); - updateLastModified = true; - } - - var nickname = info.Nickname; - if (nickname != null && nickname != entity.Nickname) - { - entity.Nickname = nickname; - updateLastModified = true; - } - - if (updateLastModified) - { - entity.LastModified = now; - } - } - } - - - public async Task ModifyUser(long id, User? info) - { - ValidateModifyUserInfo(info); - - var entity = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); - if (entity == null) - throw new UserNotExistException(id); - - await UpdateUserEntity(entity, info); - - await _databaseContext.SaveChangesAsync(); - _logger.LogInformation(LogDatabaseUpdate, ("Id", id)); - - return CreateUserFromEntity(entity); - } - - public async Task ModifyUser(string username, User? info) - { - if (username == null) - throw new ArgumentNullException(nameof(username)); - CheckUsernameFormat(username, nameof(username)); - - ValidateModifyUserInfo(info); - - var entity = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync(); - if (entity == null) - throw new UserNotExistException(username); - - await UpdateUserEntity(entity, info); - - await _databaseContext.SaveChangesAsync(); - _logger.LogInformation(LogDatabaseUpdate, ("Username", username)); - - return CreateUserFromEntity(entity); - } - - public async Task ChangePassword(long id, string oldPassword, string newPassword) - { - if (oldPassword == null) - throw new ArgumentNullException(nameof(oldPassword)); - if (newPassword == null) - throw new ArgumentNullException(nameof(newPassword)); - CheckPasswordFormat(oldPassword, nameof(oldPassword)); - CheckPasswordFormat(newPassword, nameof(newPassword)); - - var entity = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); - - if (entity == null) - throw new UserNotExistException(id); - - if (!_passwordService.VerifyPassword(entity.Password, oldPassword)) - throw new BadPasswordException(oldPassword); - - entity.Password = _passwordService.HashPassword(newPassword); - entity.Version += 1; - await _databaseContext.SaveChangesAsync(); - _logger.LogInformation(Log.Format(LogDatabaseUpdate, ("Id", id), ("Operation", "Change password"))); - } - } -} diff --git a/Timeline/Services/UserTokenException.cs b/Timeline/Services/UserTokenException.cs deleted file mode 100644 index d25fabb3..00000000 --- a/Timeline/Services/UserTokenException.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; - -namespace Timeline.Services -{ - - [Serializable] - public class UserTokenException : Exception - { - public UserTokenException() { } - public UserTokenException(string message) : base(message) { } - public UserTokenException(string message, Exception inner) : base(message, inner) { } - public UserTokenException(string token, string message) : base(message) { Token = token; } - public UserTokenException(string token, string message, Exception inner) : base(message, inner) { Token = token; } - protected UserTokenException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - public string Token { get; private set; } = ""; - } - - - [Serializable] - public class UserTokenTimeExpireException : UserTokenException - { - public UserTokenTimeExpireException() : base(Resources.Services.Exception.UserTokenTimeExpireException) { } - public UserTokenTimeExpireException(string message) : base(message) { } - public UserTokenTimeExpireException(string message, Exception inner) : base(message, inner) { } - public UserTokenTimeExpireException(string token, DateTime expireTime, DateTime verifyTime) : base(token, Resources.Services.Exception.UserTokenTimeExpireException) { ExpireTime = expireTime; VerifyTime = verifyTime; } - public UserTokenTimeExpireException(string token, DateTime expireTime, DateTime verifyTime, Exception inner) : base(token, Resources.Services.Exception.UserTokenTimeExpireException, inner) { ExpireTime = expireTime; VerifyTime = verifyTime; } - protected UserTokenTimeExpireException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - public DateTime ExpireTime { get; private set; } - - public DateTime VerifyTime { get; private set; } - } - - [Serializable] - public class UserTokenBadVersionException : UserTokenException - { - public UserTokenBadVersionException() : base(Resources.Services.Exception.UserTokenBadVersionException) { } - public UserTokenBadVersionException(string message) : base(message) { } - public UserTokenBadVersionException(string message, Exception inner) : base(message, inner) { } - public UserTokenBadVersionException(string token, long tokenVersion, long requiredVersion) : base(token, Resources.Services.Exception.UserTokenBadVersionException) { TokenVersion = tokenVersion; RequiredVersion = requiredVersion; } - public UserTokenBadVersionException(string token, long tokenVersion, long requiredVersion, Exception inner) : base(token, Resources.Services.Exception.UserTokenBadVersionException, inner) { TokenVersion = tokenVersion; RequiredVersion = requiredVersion; } - protected UserTokenBadVersionException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - public long TokenVersion { get; set; } - - public long RequiredVersion { get; set; } - } - - [Serializable] - public class UserTokenBadFormatException : UserTokenException - { - public UserTokenBadFormatException() : base(Resources.Services.Exception.UserTokenBadFormatException) { } - public UserTokenBadFormatException(string token) : base(token, Resources.Services.Exception.UserTokenBadFormatException) { } - public UserTokenBadFormatException(string token, string message) : base(token, message) { } - public UserTokenBadFormatException(string token, Exception inner) : base(token, Resources.Services.Exception.UserTokenBadFormatException, inner) { } - public UserTokenBadFormatException(string token, string message, Exception inner) : base(token, message, inner) { } - protected UserTokenBadFormatException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - } -} diff --git a/Timeline/Services/UserTokenManager.cs b/Timeline/Services/UserTokenManager.cs deleted file mode 100644 index 813dae67..00000000 --- a/Timeline/Services/UserTokenManager.cs +++ /dev/null @@ -1,97 +0,0 @@ -using Microsoft.Extensions.Logging; -using System; -using System.Threading.Tasks; -using Timeline.Helpers; -using Timeline.Models; -using Timeline.Services.Exceptions; - -namespace Timeline.Services -{ - public class UserTokenCreateResult - { - public string Token { get; set; } = default!; - public User User { get; set; } = default!; - } - - public interface IUserTokenManager - { - /// - /// Try to create a token for given username and password. - /// - /// The username. - /// The password. - /// The expire time of the token. - /// The created token and the user info. - /// Thrown when or is null. - /// Thrown when is of bad format. - /// Thrown when the user with does not exist. - /// Thrown when is wrong. - public Task CreateToken(string username, string password, DateTime? expireAt = null); - - /// - /// Verify a token and get the saved user info. This also check the database for existence of the user. - /// - /// The token. - /// The user stored in token. - /// Thrown when is null. - /// Thrown when the token is expired. - /// Thrown when the token is of bad version. - /// Thrown when the token is of bad format. - /// Thrown when the user specified by the token does not exist. Usually the user had been deleted after the token was issued. - public Task VerifyToken(string token); - } - - public class UserTokenManager : IUserTokenManager - { - private readonly ILogger _logger; - private readonly IUserService _userService; - private readonly IUserTokenService _userTokenService; - private readonly IClock _clock; - - public UserTokenManager(ILogger logger, IUserService userService, IUserTokenService userTokenService, IClock clock) - { - _logger = logger; - _userService = userService; - _userTokenService = userTokenService; - _clock = clock; - } - - public async Task CreateToken(string username, string password, DateTime? expireAt = null) - { - expireAt = expireAt?.MyToUtc(); - - if (username == null) - throw new ArgumentNullException(nameof(username)); - if (password == null) - throw new ArgumentNullException(nameof(password)); - - var user = await _userService.VerifyCredential(username, password); - var token = _userTokenService.GenerateToken(new UserTokenInfo { Id = user.Id!.Value, Version = user.Version!.Value, ExpireAt = expireAt }); - - return new UserTokenCreateResult { Token = token, User = user }; - } - - - public async Task VerifyToken(string token) - { - if (token == null) - throw new ArgumentNullException(nameof(token)); - - var tokenInfo = _userTokenService.VerifyToken(token); - - if (tokenInfo.ExpireAt.HasValue) - { - var currentTime = _clock.GetCurrentTime(); - if (tokenInfo.ExpireAt < currentTime) - throw new UserTokenTimeExpireException(token, tokenInfo.ExpireAt.Value, currentTime); - } - - var user = await _userService.GetUserById(tokenInfo.Id); - - if (tokenInfo.Version < user.Version) - throw new UserTokenBadVersionException(token, tokenInfo.Version, user.Version.Value); - - return user; - } - } -} diff --git a/Timeline/Services/UserTokenService.cs b/Timeline/Services/UserTokenService.cs deleted file mode 100644 index 86f3a0f7..00000000 --- a/Timeline/Services/UserTokenService.cs +++ /dev/null @@ -1,149 +0,0 @@ -using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Tokens; -using System; -using System.Globalization; -using System.IdentityModel.Tokens.Jwt; -using System.Linq; -using System.Security.Claims; -using Timeline.Configs; -using Timeline.Entities; - -namespace Timeline.Services -{ - public class UserTokenInfo - { - public long Id { get; set; } - public long Version { get; set; } - public DateTime? ExpireAt { get; set; } - } - - public interface IUserTokenService - { - /// - /// Create a token for a given token info. - /// - /// The info to generate token. - /// Return the generated token. - /// Thrown when is null. - string GenerateToken(UserTokenInfo tokenInfo); - - /// - /// Verify a token and get the saved info. - /// - /// The token to verify. - /// The saved info in token. - /// Thrown when is null. - /// Thrown when the token is of bad format. - /// - /// If this method throw , it usually means the token is not created by this service. - /// - UserTokenInfo VerifyToken(string token); - } - - public class JwtUserTokenService : IUserTokenService - { - private const string VersionClaimType = "timeline_version"; - - private readonly IOptionsMonitor _jwtConfig; - private readonly IClock _clock; - - private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler(); - private SymmetricSecurityKey _tokenSecurityKey; - - public JwtUserTokenService(IOptionsMonitor jwtConfig, IClock clock, DatabaseContext database) - { - _jwtConfig = jwtConfig; - _clock = clock; - - var key = database.JwtToken.Select(t => t.Key).SingleOrDefault(); - - if (key == null) - { - throw new InvalidOperationException(Resources.Services.UserTokenService.JwtKeyNotExist); - } - - _tokenSecurityKey = new SymmetricSecurityKey(key); - } - - public string GenerateToken(UserTokenInfo tokenInfo) - { - if (tokenInfo == null) - throw new ArgumentNullException(nameof(tokenInfo)); - - var config = _jwtConfig.CurrentValue; - - var identity = new ClaimsIdentity(); - identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, tokenInfo.Id.ToString(CultureInfo.InvariantCulture.NumberFormat), ClaimValueTypes.Integer64)); - identity.AddClaim(new Claim(VersionClaimType, tokenInfo.Version.ToString(CultureInfo.InvariantCulture.NumberFormat), ClaimValueTypes.Integer64)); - - var tokenDescriptor = new SecurityTokenDescriptor() - { - Subject = identity, - Issuer = config.Issuer, - Audience = config.Audience, - SigningCredentials = new SigningCredentials(_tokenSecurityKey, SecurityAlgorithms.HmacSha384), - IssuedAt = _clock.GetCurrentTime(), - Expires = tokenInfo.ExpireAt.GetValueOrDefault(_clock.GetCurrentTime().AddSeconds(config.DefaultExpireOffset)), - NotBefore = _clock.GetCurrentTime() // I must explicitly set this or it will use the current time by default and mock is not work in which case test will not pass. - }; - - var token = _tokenHandler.CreateToken(tokenDescriptor); - var tokenString = _tokenHandler.WriteToken(token); - - return tokenString; - } - - - public UserTokenInfo VerifyToken(string token) - { - if (token == null) - throw new ArgumentNullException(nameof(token)); - - var config = _jwtConfig.CurrentValue; - try - { - var principal = _tokenHandler.ValidateToken(token, new TokenValidationParameters - { - ValidateIssuer = true, - ValidateAudience = true, - ValidateIssuerSigningKey = true, - ValidateLifetime = false, - ValidIssuer = config.Issuer, - ValidAudience = config.Audience, - IssuerSigningKey = _tokenSecurityKey - }, out var t); - - var idClaim = principal.FindFirstValue(ClaimTypes.NameIdentifier); - if (idClaim == null) - throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.NoIdClaim); - if (!long.TryParse(idClaim, out var id)) - throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.IdClaimBadFormat); - - var versionClaim = principal.FindFirstValue(VersionClaimType); - if (versionClaim == null) - throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.NoVersionClaim); - if (!long.TryParse(versionClaim, out var version)) - throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.VersionClaimBadFormat); - - var decodedToken = (JwtSecurityToken)t; - var exp = decodedToken.Payload.Exp; - DateTime? expireAt = null; - if (exp.HasValue) - { - expireAt = EpochTime.DateTime(exp.Value); - } - - return new UserTokenInfo - { - Id = id, - Version = version, - ExpireAt = expireAt - }; - } - catch (Exception e) when (e is SecurityTokenException || e is ArgumentException) - { - throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.Other, e); - } - } - } -} diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs deleted file mode 100644 index 82c231cb..00000000 --- a/Timeline/Startup.cs +++ /dev/null @@ -1,187 +0,0 @@ -using AutoMapper; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Hosting; -using NSwag; -using NSwag.Generation.Processors.Security; -using System; -using System.ComponentModel; -using System.Net.Mime; -using System.Text.Json.Serialization; -using Timeline.Auth; -using Timeline.Configs; -using Timeline.Entities; -using Timeline.Formatters; -using Timeline.Helpers; -using Timeline.Models.Converters; -using Timeline.Routes; -using Timeline.Services; -using Timeline.Swagger; - -namespace Timeline -{ - public class Startup - { - private readonly bool disableFrontEnd; - private readonly bool useMockFrontEnd; - - public Startup(IConfiguration configuration, IWebHostEnvironment environment) - { - Environment = environment; - Configuration = configuration; - - disableFrontEnd = Configuration.GetValue(ApplicationConfiguration.DisableFrontEndKey) ?? false; - useMockFrontEnd = Configuration.GetValue(ApplicationConfiguration.UseMockFrontEndKey) ?? false; - } - - public IWebHostEnvironment Environment { get; } - public IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - TypeDescriptor.AddAttributes(typeof(DateTime), new TypeConverterAttribute(typeof(MyDateTimeConverter))); - - services.AddControllers(setup => - { - setup.InputFormatters.Add(new StringInputFormatter()); - setup.InputFormatters.Add(new BytesInputFormatter()); - setup.Filters.Add(new ConsumesAttribute(MediaTypeNames.Application.Json, "text/json")); - setup.Filters.Add(new ProducesAttribute(MediaTypeNames.Application.Json, "text/json")); - setup.UseApiRoutePrefix("api"); - }) - .AddJsonOptions(options => - { - options.JsonSerializerOptions.IgnoreNullValues = true; - options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); - options.JsonSerializerOptions.Converters.Add(new JsonDateTimeConverter()); - }) - .ConfigureApiBehaviorOptions(options => - { - options.InvalidModelStateResponseFactory = InvalidModelResponseFactory.Factory; - }); - - services.Configure(Configuration.GetSection("Jwt")); - services.AddAuthentication(AuthenticationConstants.Scheme) - .AddScheme(AuthenticationConstants.Scheme, AuthenticationConstants.DisplayName, o => { }); - services.AddAuthorization(); - - services.AddSingleton(); - - services.AddSingleton(); - - services.AddAutoMapper(GetType().Assembly); - - services.AddTransient(); - - services.AddTransient(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - - services.AddScoped(); - services.AddScoped(); - - services.AddScoped(); - - services.AddUserAvatarService(); - - services.AddScoped(); - - services.TryAddSingleton(); - - services.AddDbContext((services, options) => - { - var pathProvider = services.GetRequiredService(); - options.UseSqlite($"Data Source={pathProvider.GetDatabaseFilePath()}"); - }); - - services.AddSwaggerDocument(document => - { - document.DocumentName = "Timeline"; - document.Title = "Timeline REST API Reference"; - document.Version = typeof(Startup).Assembly.GetName().Version?.ToString() ?? "unknown version"; - document.DocumentProcessors.Add(new DocumentDescriptionDocumentProcessor()); - document.DocumentProcessors.Add( - new SecurityDefinitionAppender("JWT", - new OpenApiSecurityScheme - { - Type = OpenApiSecuritySchemeType.ApiKey, - Name = "Authorization", - In = OpenApiSecurityApiKeyLocation.Header, - Description = "Create token via `/api/token/create` ." - })); - document.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor("JWT")); - document.OperationProcessors.Add(new DefaultDescriptionOperationProcessor()); - document.OperationProcessors.Add(new ByteDataRequestOperationProcessor()); - }); - - if (!disableFrontEnd) - { - if (useMockFrontEnd) - { - services.AddSpaStaticFiles(config => - { - config.RootPath = "MockClientApp"; - }); - - } - else if (!Environment.IsDevelopment()) // In development, we don't want to serve dist. Or it will take precedence than front end dev server. - { - services.AddSpaStaticFiles(config => - { - config.RootPath = "ClientApp/dist"; - }); - } - } - } - - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app) - { - app.UseRouting(); - - if (!disableFrontEnd && (useMockFrontEnd || !Environment.IsDevelopment())) - { - app.UseSpaStaticFiles(new StaticFileOptions - { - ServeUnknownFileTypes = true - }); - } - - app.UseOpenApi(); - app.UseReDoc(); - - app.UseAuthentication(); - app.UseAuthorization(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - - UnknownEndpointMiddleware.Attach(app); - - if (!disableFrontEnd) - { - app.UseSpa(spa => - { - spa.Options.SourcePath = useMockFrontEnd ? "MockClientApp" : "ClientApp"; - - if (!useMockFrontEnd && (Configuration.GetValue(ApplicationConfiguration.UseProxyFrontEndKey) ?? false)) - { - spa.UseProxyToSpaDevelopmentServer(new UriBuilder("http", "localhost", 3000).Uri); - } - }); - } - } - } -} diff --git a/Timeline/Swagger/ApiConvention.cs b/Timeline/Swagger/ApiConvention.cs deleted file mode 100644 index dbf0b2fe..00000000 --- a/Timeline/Swagger/ApiConvention.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -[assembly: ApiConventionType(typeof(Timeline.Controllers.ApiConvention))] - -namespace Timeline.Controllers -{ - // There is some bug if nullable is enable. So disable it. -#nullable disable - /// - /// My api convention. - /// - public static class ApiConvention - { - } -} diff --git a/Timeline/Swagger/ByteDataRequestOperationProcessor.cs b/Timeline/Swagger/ByteDataRequestOperationProcessor.cs deleted file mode 100644 index 887831ac..00000000 --- a/Timeline/Swagger/ByteDataRequestOperationProcessor.cs +++ /dev/null @@ -1,27 +0,0 @@ -using NJsonSchema; -using NSwag; -using NSwag.Generation.Processors; -using NSwag.Generation.Processors.Contexts; -using System.Linq; -using Timeline.Models; - -namespace Timeline.Swagger -{ - /// - /// Coerce ByteData body type into the right one. - /// - public class ByteDataRequestOperationProcessor : IOperationProcessor - { - /// - public bool Process(OperationProcessorContext context) - { - var hasByteDataBody = context.MethodInfo.GetParameters().Where(p => p.ParameterType == typeof(ByteData)).Any(); - if (hasByteDataBody) - { - var bodyParameter = context.OperationDescription.Operation.Parameters.Where(p => p.Kind == OpenApiParameterKind.Body).Single(); - bodyParameter.Schema = JsonSchema.FromType(); - } - return true; - } - } -} diff --git a/Timeline/Swagger/DefaultDescriptionOperationProcessor.cs b/Timeline/Swagger/DefaultDescriptionOperationProcessor.cs deleted file mode 100644 index 4967cc6a..00000000 --- a/Timeline/Swagger/DefaultDescriptionOperationProcessor.cs +++ /dev/null @@ -1,39 +0,0 @@ -using NSwag.Generation.Processors; -using NSwag.Generation.Processors.Contexts; -using System.Collections.Generic; - -namespace Timeline.Swagger -{ - /// - /// Swagger operation processor that adds default description to response. - /// - public class DefaultDescriptionOperationProcessor : IOperationProcessor - { - private readonly Dictionary defaultDescriptionMap = new Dictionary - { - ["200"] = "Succeeded to perform the operation.", - ["304"] = "Item does not change.", - ["400"] = "See code and message for error info.", - ["401"] = "You need to log in to perform this operation.", - ["403"] = "You have no permission to perform the operation.", - ["404"] = "Item does not exist. See code and message for error info." - }; - - /// - public bool Process(OperationProcessorContext context) - { - var responses = context.OperationDescription.Operation.Responses; - - foreach (var (httpStatusCode, res) in responses) - { - if (!string.IsNullOrEmpty(res.Description)) continue; - if (defaultDescriptionMap.ContainsKey(httpStatusCode)) - { - res.Description = defaultDescriptionMap[httpStatusCode]; - } - } - - return true; - } - } -} diff --git a/Timeline/Swagger/DocumentDescriptionDocumentProcessor.cs b/Timeline/Swagger/DocumentDescriptionDocumentProcessor.cs deleted file mode 100644 index dc5ddd96..00000000 --- a/Timeline/Swagger/DocumentDescriptionDocumentProcessor.cs +++ /dev/null @@ -1,55 +0,0 @@ -using NSwag.Generation.Processors; -using NSwag.Generation.Processors.Contexts; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; -using Timeline.Models.Http; - -namespace Timeline.Swagger -{ - public class DocumentDescriptionDocumentProcessor : IDocumentProcessor - { - private static Dictionary GetAllErrorCodes() - { - var errorCodes = new Dictionary(); - - void RecursiveCheckErrorCode(Type type) - { - foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy) - .Where(fi => fi.IsLiteral && !fi.IsInitOnly && fi.FieldType == typeof(int))) - { - var name = (type.FullName + "." + field.Name).Remove(0, typeof(ErrorCodes).FullName!.Length + 1).Replace("+", ".", StringComparison.OrdinalIgnoreCase); - int value = (int)field.GetRawConstantValue()!; - errorCodes.Add(name, value); - } - - foreach (var nestedType in type.GetNestedTypes()) - { - RecursiveCheckErrorCode(nestedType); - } - } - - RecursiveCheckErrorCode(typeof(ErrorCodes)); - - return errorCodes; - } - - public void Process(DocumentProcessorContext context) - { - StringBuilder description = new StringBuilder(); - description.AppendLine("# Error Codes"); - description.AppendLine("name | value"); - description.AppendLine("---- | -----"); - foreach (var (name, value) in GetAllErrorCodes()) - { - description.AppendLine($"`{name}` | `{value}`"); - } - - context.Document.Info.Description = description.ToString(); - } - } -} diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj deleted file mode 100644 index 39412a65..00000000 --- a/Timeline/Timeline.csproj +++ /dev/null @@ -1,289 +0,0 @@ - - - netcoreapp3.1 - 1f6fb74d-4277-4bc0-aeea-b1fc5ffb0b43 - crupest - - false - - 8.0 - enable - - true - Latest - ClientApp\ - - $(DefaultItemExcludes);$(SpaRoot)node_modules\** - - 0.3.0 - true - true - - 1701;1702;1591 - - - - - PreserveNewest - - - PreserveNewest - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - - - - - - - - %(DistFiles.Identity) - PreserveNewest - true - - - - - - - - - - - True - True - AuthHandler.resx - - - True - True - ControllerAuthExtensions.resx - - - True - True - TimelineController.resx - - - True - True - TokenController.resx - - - True - True - UserAvatarController.resx - - - True - True - UserController.resx - - - True - True - Entities.resx - - - True - True - Filters.resx - - - True - True - DataCacheHelper.resx - - - True - True - Messages.resx - - - True - True - Common.resx - - - True - True - Exception.resx - - - True - True - NicknameValidator.resx - - - True - True - NameValidator.resx - - - True - True - Validator.resx - - - True - True - DataManager.resx - - - True - True - Exception.resx - - - True - True - Exceptions.resx - - - True - True - TimelineService.resx - - - True - True - UserAvatarService.resx - - - True - True - UserService.resx - - - True - True - UserTokenService.resx - - - - - - ResXFileCodeGenerator - AuthHandler.Designer.cs - - - ResXFileCodeGenerator - ControllerAuthExtensions.Designer.cs - - - ResXFileCodeGenerator - TimelineController.Designer.cs - - - Designer - ResXFileCodeGenerator - TokenController.Designer.cs - - - ResXFileCodeGenerator - UserAvatarController.Designer.cs - - - ResXFileCodeGenerator - UserController.Designer.cs - - - ResXFileCodeGenerator - Entities.Designer.cs - - - ResXFileCodeGenerator - Filters.Designer.cs - - - ResXFileCodeGenerator - DataCacheHelper.Designer.cs - - - ResXFileCodeGenerator - Messages.Designer.cs - - - ResXFileCodeGenerator - Common.Designer.cs - - - ResXFileCodeGenerator - Exception.Designer.cs - - - ResXFileCodeGenerator - NicknameValidator.Designer.cs - - - ResXFileCodeGenerator - NameValidator.Designer.cs - - - ResXFileCodeGenerator - Validator.Designer.cs - - - ResXFileCodeGenerator - DataManager.Designer.cs - - - ResXFileCodeGenerator - Exception.Designer.cs - - - ResXFileCodeGenerator - Exceptions.Designer.cs - - - ResXFileCodeGenerator - TimelineService.Designer.cs - - - ResXFileCodeGenerator - UserAvatarService.Designer.cs - - - ResXFileCodeGenerator - UserService.Designer.cs - - - ResXFileCodeGenerator - UserTokenService.Designer.cs - - - diff --git a/Timeline/appsettings.Development.json b/Timeline/appsettings.Development.json deleted file mode 100644 index a2880cbf..00000000 --- a/Timeline/appsettings.Development.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information" - } - } -} diff --git a/Timeline/appsettings.json b/Timeline/appsettings.json deleted file mode 100644 index 804ca43a..00000000 --- a/Timeline/appsettings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Warning" - } - }, - "Jwt": { - "Issuer": "api.crupest.xyz", - "Audience": "api.crupest.xyz" - } -} diff --git a/Timeline/default-avatar.png b/Timeline/default-avatar.png deleted file mode 100644 index 4086e1d2..00000000 Binary files a/Timeline/default-avatar.png and /dev/null differ diff --git a/Timeline/packages.lock.json b/Timeline/packages.lock.json deleted file mode 100644 index ed92c672..00000000 --- a/Timeline/packages.lock.json +++ /dev/null @@ -1,1563 +0,0 @@ -{ - "version": 1, - "dependencies": { - ".NETCoreApp,Version=v3.1": { - "AutoMapper": { - "type": "Direct", - "requested": "[10.1.1, )", - "resolved": "10.1.1", - "contentHash": "uMgbqOdu9ZG5cIOty0C85hzzayBH2i9BthnS5FlMqKtMSHDv4ts81a2jS1VFaDBVhlBeIqJ/kQKjQY95BZde9w==", - "dependencies": { - "Microsoft.CSharp": "4.7.0", - "System.Reflection.Emit": "4.7.0" - } - }, - "AutoMapper.Extensions.Microsoft.DependencyInjection": { - "type": "Direct", - "requested": "[8.1.0, )", - "resolved": "8.1.0", - "contentHash": "dQyGCAYcHbGuimVvCMu4Ea2S1oYOlgO9XfVdClmY5wgygJMZoS57emPzH0qNfknmtzMm4QbDO9i237W5IDjU1A==", - "dependencies": { - "AutoMapper": "[10.1.0, 11.0.0)", - "Microsoft.Extensions.DependencyInjection.Abstractions": "3.0.0", - "Microsoft.Extensions.Options": "3.0.0" - } - }, - "Microsoft.AspNetCore.SpaServices.Extensions": { - "type": "Direct", - "requested": "[3.1.9, )", - "resolved": "3.1.9", - "contentHash": "ciy2GCvRnh9C22laArLsaItS+72U6Hqf4nDYShdvFgcen2ZV+NNSitb/B3vsmFfIPM8m4mf2x4T+vZ6OlI5XaA==", - "dependencies": { - "Microsoft.AspNetCore.SpaServices": "3.1.9", - "Microsoft.Extensions.FileProviders.Physical": "3.1.9" - } - }, - "Microsoft.CodeAnalysis.FxCopAnalyzers": { - "type": "Direct", - "requested": "[3.3.0, )", - "resolved": "3.3.0", - "contentHash": "k3Icqx8kc+NrHImuiB8Jc/wd32Xeyd2B/7HOR5Qu9pyKzXQ4ikPeBAwzG2FSTuYhyIuNWvwL5k9yYBbbVz6w9w==", - "dependencies": { - "Microsoft.CodeAnalysis.VersionCheckAnalyzer": "[3.3.0]", - "Microsoft.CodeQuality.Analyzers": "[3.3.0]", - "Microsoft.NetCore.Analyzers": "[3.3.0]", - "Microsoft.NetFramework.Analyzers": "[3.3.0]" - } - }, - "Microsoft.EntityFrameworkCore": { - "type": "Direct", - "requested": "[3.1.9, )", - "resolved": "3.1.9", - "contentHash": "u3A2W0BvAuAF2jgW+WX+C+Sh8sMGX5Kl1hdA0gu6A/XSrZQoW/BUP4a/q2n3iitDGndaorqjAKx+Spb9gBto+w==", - "dependencies": { - "Microsoft.Bcl.AsyncInterfaces": "1.1.1", - "Microsoft.Bcl.HashCode": "1.1.0", - "Microsoft.EntityFrameworkCore.Abstractions": "3.1.9", - "Microsoft.EntityFrameworkCore.Analyzers": "3.1.9", - "Microsoft.Extensions.Caching.Memory": "3.1.9", - "Microsoft.Extensions.DependencyInjection": "3.1.9", - "Microsoft.Extensions.Logging": "3.1.9", - "System.Collections.Immutable": "1.7.1", - "System.ComponentModel.Annotations": "4.7.0", - "System.Diagnostics.DiagnosticSource": "4.7.1" - } - }, - "Microsoft.EntityFrameworkCore.Analyzers": { - "type": "Direct", - "requested": "[3.1.9, )", - "resolved": "3.1.9", - "contentHash": "eXGyx/Lb1fiiKtnIStdxGrfBSSQg8oZytE10f1T/2xAx12W9dKB9U9fg05cwNCDC0S2CXILsmZHYaGqCSXVAqQ==" - }, - "Microsoft.EntityFrameworkCore.Sqlite": { - "type": "Direct", - "requested": "[3.1.9, )", - "resolved": "3.1.9", - "contentHash": "sMFCWv/1UcsFQZeGQcbfPbEZKZ1oKZqWZXTbc7PEZVMIXu82nbavstdNQ84x5IBXJkxl8iW3zjChb/FRBr5uLQ==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Sqlite.Core": "3.1.9", - "SQLitePCLRaw.bundle_e_sqlite3": "2.0.2" - } - }, - "Microsoft.EntityFrameworkCore.Tools": { - "type": "Direct", - "requested": "[3.1.9, )", - "resolved": "3.1.9", - "contentHash": "mSgwjp0h5iqW5V49SVijR5O+kNpI1nitcbN12n9FYx/Ga6oCEFwXR/llBDesD6ASHw3Mx16vodJYJ7CEBx5rig==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Design": "3.1.9" - } - }, - "NSwag.AspNetCore": { - "type": "Direct", - "requested": "[13.8.2, )", - "resolved": "13.8.2", - "contentHash": "SNGlVSZoMyywBWueZBxl3B/nfaIM0fAcuNhTD/cfMKUn3Cn/Oi8d45HZY5vAPqczvppTbk4cZXyVwWDOfgiPbA==", - "dependencies": { - "Microsoft.AspNetCore.Mvc.Core": "1.0.4", - "Microsoft.AspNetCore.Mvc.Formatters.Json": "1.0.4", - "Microsoft.AspNetCore.StaticFiles": "1.0.4", - "Microsoft.Extensions.ApiDescription.Server": "3.0.0", - "Microsoft.Extensions.FileProviders.Embedded": "1.0.1", - "NSwag.Annotations": "13.8.2", - "NSwag.Core": "13.8.2", - "NSwag.Generation": "13.8.2", - "NSwag.Generation.AspNetCore": "13.8.2", - "System.IO.FileSystem": "4.3.0", - "System.Xml.XPath.XDocument": "4.0.1" - } - }, - "SixLabors.ImageSharp": { - "type": "Direct", - "requested": "[1.0.1, )", - "resolved": "1.0.1", - "contentHash": "DjLoFNdUfsDP7RhPpr5hcUhl1XiejqBML9uDWuOUwCkc0Y+sG9IJLLbqSOi9XeoWqPviwdcDm1F8nKdF0qTYIQ==" - }, - "System.IdentityModel.Tokens.Jwt": { - "type": "Direct", - "requested": "[6.8.0, )", - "resolved": "6.8.0", - "contentHash": "5tBCjAub2Bhd5qmcd0WhR5s354e4oLYa//kOWrkX+6/7ZbDDJjMTfwLSOiZ/MMpWdE4DWPLOfTLOq/juj9CKzA==", - "dependencies": { - "Microsoft.IdentityModel.JsonWebTokens": "6.8.0", - "Microsoft.IdentityModel.Tokens": "6.8.0" - } - }, - "Microsoft.AspNetCore.Authorization": { - "type": "Transitive", - "resolved": "1.0.3", - "contentHash": "cN2KJkfHcKwh82c9WGx4Tqfd2h5HflU/Mu5vYLMHON8WahHU9hE32ciIXcEIoKLNpu+zs1u1cN/qxcKTdqu89w==", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "1.0.2", - "Microsoft.Extensions.Options": "1.0.2", - "System.Security.Claims": "4.0.1" - } - }, - "Microsoft.AspNetCore.Hosting.Abstractions": { - "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "ybY8FOkdNfBPB5PLv1JO+It/94ftBzGUI1WqU4XySbIWyhw2TPmmKAUuO9uvJoR0qpsFup8FJz6trsBcBITg9w==", - "dependencies": { - "Microsoft.AspNetCore.Hosting.Server.Abstractions": "1.0.4", - "Microsoft.AspNetCore.Http.Abstractions": "1.0.3", - "Microsoft.Extensions.Configuration.Abstractions": "1.0.2", - "Microsoft.Extensions.DependencyInjection.Abstractions": "1.0.2", - "Microsoft.Extensions.FileProviders.Abstractions": "1.0.1", - "Microsoft.Extensions.Logging.Abstractions": "1.0.2" - } - }, - "Microsoft.AspNetCore.Hosting.Server.Abstractions": { - "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "XUiQPe/CflK1i0Voo9S6/G1iQh00gQ6sMqi3LRtKeceBbO6AOostaAUdhjyME92MapI4VFNl+Z+/KXUlMAExJQ==", - "dependencies": { - "Microsoft.AspNetCore.Http.Features": "1.0.3", - "Microsoft.Extensions.Configuration.Abstractions": "1.0.2" - } - }, - "Microsoft.AspNetCore.Http": { - "type": "Transitive", - "resolved": "1.0.3", - "contentHash": "kfNOIGGgVtMzsSWZzXBqz5zsdo8ssBa90YHzZt95N8ARGXoolSaBHy6yBoMm/XcpbXM+m/x1fixTTMIWMgzJdQ==", - "dependencies": { - "Microsoft.AspNetCore.Http.Abstractions": "1.0.3", - "Microsoft.AspNetCore.WebUtilities": "1.0.3", - "Microsoft.Extensions.ObjectPool": "1.0.1", - "Microsoft.Extensions.Options": "1.0.2", - "Microsoft.Net.Http.Headers": "1.0.3", - "System.Buffers": "4.0.0", - "System.Threading": "4.0.11" - } - }, - "Microsoft.AspNetCore.Http.Abstractions": { - "type": "Transitive", - "resolved": "1.0.3", - "contentHash": "nnjvAf7ag6P0DyD/0nhRGjLpv+3DkPU0juF8aQh46X8uF4kzjJdrh65oL+4PVOu3K6BgSg6OVUs0QC0SE0FRtg==", - "dependencies": { - "Microsoft.AspNetCore.Http.Features": "1.0.3", - "System.Globalization.Extensions": "4.0.1", - "System.Linq.Expressions": "4.1.1", - "System.Reflection.TypeExtensions": "4.1.0", - "System.Runtime.InteropServices": "4.1.0", - "System.Text.Encodings.Web": "4.0.1" - } - }, - "Microsoft.AspNetCore.Http.Extensions": { - "type": "Transitive", - "resolved": "1.0.3", - "contentHash": "+7Sd+14nexIJqcB4S1Eur9kzeMZ5CBtrxkei+PNbD78fg8vO3+TcCgrl5SBNTsUB/VJAfD/s0fgs5t+hHRj2Pg==", - "dependencies": { - "Microsoft.AspNetCore.Http.Abstractions": "1.0.3", - "Microsoft.Extensions.FileProviders.Abstractions": "1.0.1", - "Microsoft.Net.Http.Headers": "1.0.3", - "System.Buffers": "4.0.0", - "System.IO.FileSystem": "4.0.1" - } - }, - "Microsoft.AspNetCore.Http.Features": { - "type": "Transitive", - "resolved": "1.0.3", - "contentHash": "Ihq57tseNyPbJTmFXY4jQ4JkxLP0lh45VRwocQci/sFx+qcJGvWB+sJJ2/YPLy/qTWFAEfNAcswuY3OsNH9Gwg==", - "dependencies": { - "Microsoft.Extensions.Primitives": "1.0.1", - "System.Collections": "4.0.11", - "System.ComponentModel": "4.0.1", - "System.Linq": "4.1.0", - "System.Net.Primitives": "4.0.11", - "System.Net.WebSockets": "4.0.0", - "System.Runtime.Extensions": "4.1.0", - "System.Security.Claims": "4.0.1", - "System.Security.Cryptography.X509Certificates": "4.1.0", - "System.Security.Principal": "4.0.1" - } - }, - "Microsoft.AspNetCore.JsonPatch": { - "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "WVaSVS+dDlWCR/qerHnBxU9tIeJ9GMA3M5tg4cxH7/cJYZZLnr2zvaFHGB+cRRNCKKTJ0pFRxT7ES8knhgAAaA==", - "dependencies": { - "Microsoft.CSharp": "4.0.1", - "Newtonsoft.Json": "9.0.1", - "System.Collections.Concurrent": "4.0.12", - "System.ComponentModel.TypeConverter": "4.1.0", - "System.Diagnostics.Debug": "4.0.11", - "System.Globalization": "4.0.11", - "System.Linq": "4.1.0", - "System.Reflection.Extensions": "4.0.1", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime.Extensions": "4.1.0", - "System.Runtime.Serialization.Primitives": "4.1.1", - "System.Text.Encoding.Extensions": "4.0.11" - } - }, - "Microsoft.AspNetCore.Mvc.Abstractions": { - "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "Isqgif1nuB+um86cEkpL8KnoxFCUCXBsbs9PuiuzElvlSiv4Ek3LvtrSUcbivekDDfys8CDbJhxwEI7WKJieAQ==", - "dependencies": { - "Microsoft.AspNetCore.Routing.Abstractions": "1.0.4", - "Microsoft.CSharp": "4.0.1", - "Microsoft.Net.Http.Headers": "1.0.3", - "System.ComponentModel.TypeConverter": "4.1.0", - "System.Reflection.Extensions": "4.0.1", - "System.Text.Encoding.Extensions": "4.0.11" - } - }, - "Microsoft.AspNetCore.Mvc.ApiExplorer": { - "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "ujCFTM42U2WKUBhdaoLoiI+wVHgYhrmDrkl5+hWJ7EJW4fhp42w4cRZ97tjuveWr+M6JZjpS0q+7PVofQzFUiw==", - "dependencies": { - "Microsoft.AspNetCore.Mvc.Core": "1.0.4" - } - }, - "Microsoft.AspNetCore.Mvc.Core": { - "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "1ukcttN1+T82hWXE8WS5kawkruolKI6LPVqVI4rTzN16kFszS/UqTrcwSUEnmTRpmWgFo665V3c2GpdQ9B6znw==", - "dependencies": { - "Microsoft.AspNetCore.Authorization": "1.0.3", - "Microsoft.AspNetCore.Hosting.Abstractions": "1.0.3", - "Microsoft.AspNetCore.Http": "1.0.3", - "Microsoft.AspNetCore.Mvc.Abstractions": "1.0.4", - "Microsoft.AspNetCore.Routing": "1.0.4", - "Microsoft.Extensions.DependencyModel": "1.0.0", - "Microsoft.Extensions.FileProviders.Abstractions": "1.0.1", - "Microsoft.Extensions.Logging.Abstractions": "1.0.2", - "Microsoft.Extensions.PlatformAbstractions": "1.0.0", - "System.Buffers": "4.0.0", - "System.Diagnostics.DiagnosticSource": "4.0.0", - "System.Text.Encoding": "4.0.11" - } - }, - "Microsoft.AspNetCore.Mvc.Formatters.Json": { - "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "i8WWK2GwlBHfOL+d+kknJWPks6DS9tbN6nfJZU4yb+/wfUAYd311B2CIHzdat3IewubnK1TYONwrhQcs2FbLeA==", - "dependencies": { - "Microsoft.AspNetCore.JsonPatch": "1.0.0", - "Microsoft.AspNetCore.Mvc.Core": "1.0.4" - } - }, - "Microsoft.AspNetCore.NodeServices": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "bbd3FlSPWiRQrIcBLa5TaOvo4gjmmiNMkxA8VmZ6u0eIpS0Yj35/eTopaGdtzqwlqj5jXbdRoib1MruXuPaW8A==", - "dependencies": { - "Microsoft.Extensions.Logging.Console": "3.1.9", - "Newtonsoft.Json": "12.0.2" - } - }, - "Microsoft.AspNetCore.Routing": { - "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "mdIF3ckRothHWuCSFkk6YXACj5zxi5qM+cEAHjcpP04/wCHUoV0gGVnW+HI+LyFXE6JUwu2zXn5tfsCpW0U+SA==", - "dependencies": { - "Microsoft.AspNetCore.Http.Extensions": "1.0.3", - "Microsoft.AspNetCore.Routing.Abstractions": "1.0.4", - "Microsoft.Extensions.Logging.Abstractions": "1.0.2", - "Microsoft.Extensions.ObjectPool": "1.0.1", - "Microsoft.Extensions.Options": "1.0.2", - "System.Collections": "4.0.11", - "System.Text.RegularExpressions": "4.1.0" - } - }, - "Microsoft.AspNetCore.Routing.Abstractions": { - "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "GHxVt6LlXHFsCUd2Un+/vY1tBTXxnogfbDO0b8G5EGmkapSK+dOGOLJviscxQkp338Uabs081JEIdkRymI5GXA==", - "dependencies": { - "Microsoft.AspNetCore.Http.Abstractions": "1.0.3", - "System.Collections.Concurrent": "4.0.12", - "System.Reflection.Extensions": "4.0.1", - "System.Threading.Tasks": "4.0.11" - } - }, - "Microsoft.AspNetCore.SpaServices": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "Fb+N2ZyF1wNrGeWggT+Ovv6W8AAVxfi4V/SnuEsBOR+nmkFhty9zyh6IDRRS98GJK6OE3adqqPbWMtJqbxYnNA==", - "dependencies": { - "Microsoft.AspNetCore.NodeServices": "3.1.9" - } - }, - "Microsoft.AspNetCore.StaticFiles": { - "type": "Transitive", - "resolved": "1.0.4", - "contentHash": "2pNvwewAazhaaCdw2CGUvIcDrNQMlqP57JgBDf3v+pRj1rZ29HVnpvkX6a+TrmRYlJNmmxHOKEt468uE/gDcFw==", - "dependencies": { - "Microsoft.AspNetCore.Hosting.Abstractions": "1.0.4", - "Microsoft.AspNetCore.Http.Extensions": "1.0.3", - "Microsoft.Extensions.FileProviders.Abstractions": "1.0.1", - "Microsoft.Extensions.Logging.Abstractions": "1.0.2", - "Microsoft.Extensions.WebEncoders": "1.0.3" - } - }, - "Microsoft.AspNetCore.WebUtilities": { - "type": "Transitive", - "resolved": "1.0.3", - "contentHash": "snSGNs5EEisqivDjDiskFkFyu+DV2Ib9sMPOBQKtoFwI5H1W5YNB/rIVqDZQL16zj/uzdwwxrdE/5xhkVyf6gQ==", - "dependencies": { - "Microsoft.Extensions.Primitives": "1.0.1", - "System.Buffers": "4.0.0", - "System.Collections": "4.0.11", - "System.IO": "4.1.0", - "System.IO.FileSystem": "4.0.1", - "System.Text.Encodings.Web": "4.0.1" - } - }, - "Microsoft.Bcl.AsyncInterfaces": { - "type": "Transitive", - "resolved": "1.1.1", - "contentHash": "yuvf07qFWFqtK3P/MRkEKLhn5r2UbSpVueRziSqj0yJQIKFwG1pq9mOayK3zE5qZCTs0CbrwL9M6R8VwqyGy2w==" - }, - "Microsoft.Bcl.HashCode": { - "type": "Transitive", - "resolved": "1.1.0", - "contentHash": "J2G1k+u5unBV+aYcwxo94ip16Rkp65pgWFb0R6zwJipzWNMgvqlWeuI7/+R+e8bob66LnSG+llLJ+z8wI94cHg==" - }, - "Microsoft.CodeAnalysis.VersionCheckAnalyzer": { - "type": "Transitive", - "resolved": "3.3.0", - "contentHash": "xjLM3DRFZMan3nQyBQEM1mBw6VqQybi4iMJhMFW6Ic1E1GCvqJR3ABOwEL7WtQjDUzxyrGld9bASnAos7G/Xyg==" - }, - "Microsoft.CodeQuality.Analyzers": { - "type": "Transitive", - "resolved": "3.3.0", - "contentHash": "zZ3miq6u22UFQKhfJyLnVEJ+DgeOopLh3eKJnKAcOetPP2hiv3wa7kHZlBDeTvtqJQiAQhAVbttket8XxjN1zw==" - }, - "Microsoft.CSharp": { - "type": "Transitive", - "resolved": "4.7.0", - "contentHash": "pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA==" - }, - "Microsoft.Data.Sqlite.Core": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "+u4PeT1npi2EzhxGc5r1Z2z73zuXw+TlKVZm44WQhNCUw4LtUVDaxGSpUhrjW+X4snBCBfr4kT/uJyKnL4R4og==", - "dependencies": { - "SQLitePCLRaw.core": "2.0.2" - } - }, - "Microsoft.DotNet.PlatformAbstractions": { - "type": "Transitive", - "resolved": "3.1.6", - "contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg==" - }, - "Microsoft.EntityFrameworkCore.Abstractions": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "IR6Y4RJVlw0QXdWXjF3Kx9s1QLiicJus+BFBKr43lBtriV20j3yrWMoaZ9W1AUUgnicZXpXVcNfklqtmwb9Sxw==" - }, - "Microsoft.EntityFrameworkCore.Design": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "2zgP7BWcw5nqGQiT4bEtiI6ras+4pvKg5D+tA3AYvjEifzzaWvmRTb3B9nRHpIYJAhPtmWNBVnVXLbu3fS1OYA==", - "dependencies": { - "Microsoft.CSharp": "4.7.0", - "Microsoft.EntityFrameworkCore.Relational": "3.1.9" - } - }, - "Microsoft.EntityFrameworkCore.Relational": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "7fhWuSfrCYlv/hvOX5OhbFJF/G9f8sifqTrJiYnAYLDOvNizwv7t9tFPD8JwaF3zM2S54O5/Vni2NxvwzSaW2w==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "3.1.9" - } - }, - "Microsoft.EntityFrameworkCore.Sqlite.Core": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "Da6h8LdpJwKc1az9DMWt2Mt6gHXPRZqwiumV1Zx0AuM3EThyokVDzBGy2sti0AcBhcQMLJHPEr5R9xuiWvaYYQ==", - "dependencies": { - "Microsoft.Data.Sqlite.Core": "3.1.9", - "Microsoft.DotNet.PlatformAbstractions": "3.1.6", - "Microsoft.EntityFrameworkCore.Relational": "3.1.9", - "Microsoft.Extensions.DependencyModel": "3.1.6" - } - }, - "Microsoft.Extensions.ApiDescription.Server": { - "type": "Transitive", - "resolved": "3.0.0", - "contentHash": "LH4OE/76F6sOCslif7+Xh3fS/wUUrE5ryeXAMcoCnuwOQGT5Smw0p57IgDh/pHgHaGz/e+AmEQb7pRgb++wt0w==" - }, - "Microsoft.Extensions.Caching.Abstractions": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "/2QsPAsUZD4qvftZkUKHRRRryPDXWh606/iNXPLrulwHLMr9JNsKBJWVqylT3qU92nJok5VoqSblkY9mSyxFyg==", - "dependencies": { - "Microsoft.Extensions.Primitives": "3.1.9" - } - }, - "Microsoft.Extensions.Caching.Memory": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "/JrVMVetX/kpJQUIlJ6NLQ3zbF0yyryXpo4+uFCqYIUZzgmWk8DS/zSKcyj1tQ3410+vhDEAPngxC+hg0IlJeg==", - "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "3.1.9", - "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.9", - "Microsoft.Extensions.Logging.Abstractions": "3.1.9", - "Microsoft.Extensions.Options": "3.1.9" - } - }, - "Microsoft.Extensions.Configuration": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "lqdkOGNeTMKG981Q7yWGlRiFbIlsRwTlMMiybT+WOzUCFBS/wc25tZgh7Wm/uRoBbWefgvokzmnea7ZjmFedmA==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "3.1.9" - } - }, - "Microsoft.Extensions.Configuration.Abstractions": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "vOJxPKczaHpXeZFrxARxYwsEulhEouXc5aZGgMdkhV/iEXX9/pfjqKk76rTG+4CsJjHV+G/4eMhvOIaQMHENNA==", - "dependencies": { - "Microsoft.Extensions.Primitives": "3.1.9" - } - }, - "Microsoft.Extensions.Configuration.Binder": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "BG6HcT7tARYakftqfQu+cLksgIWG1NdxMY+igI12hdZrUK+WjS973NiRyuao/U9yyTeM9NPwRnC61hCmG3G3jg==", - "dependencies": { - "Microsoft.Extensions.Configuration": "3.1.9" - } - }, - "Microsoft.Extensions.DependencyInjection": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "ORqfrAACcvTInie1oGola5uky344/PiNfgayTPuZWV4WnSfIQZJQm/ZLpGshJE3h7TqwYaYElGazK/yaM2bFLA==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.9" - } - }, - "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "8PkcaPwiTPOhqshoY4+rQUbz86X6YpLDLUqXOezh7L2A3pgpBmeBBByYIffofBlvQxDdQ0zB2DkWjbZWyCxRWg==" - }, - "Microsoft.Extensions.DependencyModel": { - "type": "Transitive", - "resolved": "3.1.6", - "contentHash": "/UlDKULIVkLQYn1BaHcy/rc91ApDxJb7T75HcCbGdqwvxhnRQRKM2di1E70iCPMF9zsr6f4EgQTotBGxFIfXmw==", - "dependencies": { - "System.Text.Json": "4.7.2" - } - }, - "Microsoft.Extensions.FileProviders.Abstractions": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "Q4SGwEFZKiZbpzPgdGbQUULxtcH1zXMOwCPKSm6QwVcOCGshf3QLfBh+O/GyFH4B0RfZ16nKyeW1mMONlRyjUw==", - "dependencies": { - "Microsoft.Extensions.Primitives": "3.1.9" - } - }, - "Microsoft.Extensions.FileProviders.Embedded": { - "type": "Transitive", - "resolved": "1.0.1", - "contentHash": "nSEa8bH3fVdTYGqK4twOKLxxgKIW3cz9g9mrzhPh/CmdvGJWKRTIlBIZi7lz+lqNQpxean5vbAo84R/mU+JpGA==", - "dependencies": { - "Microsoft.Extensions.FileProviders.Abstractions": "1.0.1", - "System.Runtime.Extensions": "4.1.0" - } - }, - "Microsoft.Extensions.FileProviders.Physical": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "HWDSsblTCQp7EEJJmnLzttIhFGzDu+DGqBbOvGCdFT0+pkCuBkn3EiWpEEcm5WMTO5njmsbLSK9ZuUUf2zPsFg==", - "dependencies": { - "Microsoft.Extensions.FileProviders.Abstractions": "3.1.9", - "Microsoft.Extensions.FileSystemGlobbing": "3.1.9" - } - }, - "Microsoft.Extensions.FileSystemGlobbing": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "5bnewG1aBiSESPNwcXGIxDDRN95uqdy+fqZZ8Z63Et5rRNlAwAfXHOrg+FTht7UjHobjvtjzquMCbAWhWEPHIw==" - }, - "Microsoft.Extensions.Logging": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "+V3i0jCQCO6IIOf6e+fL0SqrZd2x/Krug9EEL1JHa9R03RsbEpltCtjVY5hxedyuyuQKwvLoR12sCfu/9XEUAw==", - "dependencies": { - "Microsoft.Extensions.Configuration.Binder": "3.1.9", - "Microsoft.Extensions.DependencyInjection": "3.1.9", - "Microsoft.Extensions.Logging.Abstractions": "3.1.9", - "Microsoft.Extensions.Options": "3.1.9" - } - }, - "Microsoft.Extensions.Logging.Abstractions": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "W5fbF8qVR9SMVVJqDQLIR7meWbev6Pu/lbrm7LDNr4Sp7HOotr4k2UULTdFSXOi5aoDdkQZpWnq0ZSpjrR3tjg==" - }, - "Microsoft.Extensions.Logging.Configuration": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "hv6XsGgikrbkolBJdF1usl9R/nrliC5mifMqHMEY9zWcCLwNkXMJiS8p0lbosrnpVAMi4PbNx39DB51Dqscd0w==", - "dependencies": { - "Microsoft.Extensions.Logging": "3.1.9", - "Microsoft.Extensions.Options.ConfigurationExtensions": "3.1.9" - } - }, - "Microsoft.Extensions.Logging.Console": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "8Dusl1rkDivmvLrwj6QAo917xMHPiDBzG3IG3agiyDdtsC/fRp+1VN5iIN+O09PtEaMged2OLA6wCDwfSTSTZw==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "3.1.9", - "Microsoft.Extensions.Logging": "3.1.9", - "Microsoft.Extensions.Logging.Configuration": "3.1.9" - } - }, - "Microsoft.Extensions.ObjectPool": { - "type": "Transitive", - "resolved": "1.0.1", - "contentHash": "pJMOnxuqmG37OjccfvtqVoo3bQGoN+0EJUzzp7+2uxSdioER82caAk6Yi/z5aysapn5XENNIIa7SaYnYKSS69A==", - "dependencies": { - "System.Diagnostics.Debug": "4.0.11", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime.Extensions": "4.1.0", - "System.Threading": "4.0.11" - } - }, - "Microsoft.Extensions.Options": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "EIb3G1DL+Rl9MvJR7LjI1wCy2nfTN4y8MflbOftn1HLYQBj/Rwl8kUbGTrSFE01c99Wm4ETjWVsjqKcpFvhPng==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.9", - "Microsoft.Extensions.Primitives": "3.1.9" - } - }, - "Microsoft.Extensions.Options.ConfigurationExtensions": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "u5jh7RW+Ev81YqK1ZoBG0lftp2MA9xqXiTiRL46XzaPj2ScNUyiVbzcVY0fPbE27UOpT2hj+yPzRSOMIIo55UA==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "3.1.9", - "Microsoft.Extensions.Configuration.Binder": "3.1.9", - "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.9", - "Microsoft.Extensions.Options": "3.1.9" - } - }, - "Microsoft.Extensions.PlatformAbstractions": { - "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "zyjUzrOmuevOAJpIo3Mt5GmpALVYCVdLZ99keMbmCxxgQH7oxzU58kGHzE6hAgYEiWsdfMJLjVR7r+vSmaJmtg==", - "dependencies": { - "System.AppContext": "4.1.0", - "System.Reflection": "4.1.0", - "System.Reflection.Extensions": "4.0.1", - "System.Reflection.TypeExtensions": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime.Extensions": "4.1.0" - } - }, - "Microsoft.Extensions.Primitives": { - "type": "Transitive", - "resolved": "3.1.9", - "contentHash": "IrHecH0eGG7/XoeEtv++oLg/sJHRNyeCqlA9RhAo6ig4GpOTjtDr32sBMYuuLtUq8ALahneWkrOzoBAwJ4L4iA==" - }, - "Microsoft.Extensions.WebEncoders": { - "type": "Transitive", - "resolved": "1.0.3", - "contentHash": "TClNvczWRxF6bVPhn5EK3Y3QNi5jTP68Qur+5Fk+MQLPeBI18WN7X145DDJ6bFeNOwgdCHl73lHs5uZp9ish1A==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "1.0.2", - "Microsoft.Extensions.Options": "1.0.2", - "System.Text.Encodings.Web": "4.0.1" - } - }, - "Microsoft.IdentityModel.JsonWebTokens": { - "type": "Transitive", - "resolved": "6.8.0", - "contentHash": "+7JIww64PkMt7NWFxoe4Y/joeF7TAtA/fQ0b2GFGcagzB59sKkTt/sMZWR6aSZht5YC7SdHi3W6yM1yylRGJCQ==", - "dependencies": { - "Microsoft.IdentityModel.Tokens": "6.8.0" - } - }, - "Microsoft.IdentityModel.Logging": { - "type": "Transitive", - "resolved": "6.8.0", - "contentHash": "Rfh/p4MaN4gkmhPxwbu8IjrmoDncGfHHPh1sTnc0AcM/Oc39/fzC9doKNWvUAjzFb8LqA6lgZyblTrIsX/wDXg==" - }, - "Microsoft.IdentityModel.Tokens": { - "type": "Transitive", - "resolved": "6.8.0", - "contentHash": "gTqzsGcmD13HgtNePPcuVHZ/NXWmyV+InJgalW/FhWpII1D7V1k0obIseGlWMeA4G+tZfeGMfXr0klnWbMR/mQ==", - "dependencies": { - "Microsoft.CSharp": "4.5.0", - "Microsoft.IdentityModel.Logging": "6.8.0", - "System.Security.Cryptography.Cng": "4.5.0" - } - }, - "Microsoft.Net.Http.Headers": { - "type": "Transitive", - "resolved": "1.0.3", - "contentHash": "2F8USh4hR5xppvaxtw2EStX74Ih+HhRj7aQD1uaB9JmTGy478F7t4VU+IdZXauEDrvS7LYAyyhmOExsUFK3PAw==", - "dependencies": { - "System.Buffers": "4.0.0", - "System.Collections": "4.0.11", - "System.Diagnostics.Contracts": "4.0.1", - "System.Globalization": "4.0.11", - "System.Linq": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime.Extensions": "4.1.0", - "System.Text.Encoding": "4.0.11" - } - }, - "Microsoft.NetCore.Analyzers": { - "type": "Transitive", - "resolved": "3.3.0", - "contentHash": "6qptTHUu1Wfszuf83NhU0IoAb4j7YWOpJs6oc6S4G/nI6aGGWKH/Xi5Vs9L/8lrI74ijEEzPcIwafSQW5ASHtA==" - }, - "Microsoft.NETCore.Platforms": { - "type": "Transitive", - "resolved": "1.1.0", - "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" - }, - "Microsoft.NETCore.Targets": { - "type": "Transitive", - "resolved": "1.1.0", - "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==" - }, - "Microsoft.NetFramework.Analyzers": { - "type": "Transitive", - "resolved": "3.3.0", - "contentHash": "JTfMic5fEFWICePbr7GXOGPranqS9Qxu2U/BZEcnnGbK1SFW8TxRyGp6O1L52xsbfOdqmzjc0t5ubhDrjj+Xpg==" - }, - "Microsoft.Win32.Primitives": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "fQnBHO9DgcmkC9dYSJoBqo6sH1VJwJprUHh8F3hbcRlxiQiBUuTntdk8tUwV490OqC2kQUrinGwZyQHTieuXRA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Runtime": "4.1.0" - } - }, - "Namotion.Reflection": { - "type": "Transitive", - "resolved": "1.0.14", - "contentHash": "wuJGiFvGfehH2w7jAhMbCJt0/rvUuHyqSZn0sMhNTviDfBZRyX8LFlR/ndQcofkGWulPDfH5nKYTeGXE8xBHPA==", - "dependencies": { - "Microsoft.CSharp": "4.3.0" - } - }, - "Newtonsoft.Json": { - "type": "Transitive", - "resolved": "12.0.2", - "contentHash": "rTK0s2EKlfHsQsH6Yx2smvcTCeyoDNgCW7FEYyV01drPlh2T243PR2DiDXqtC5N4GDm4Ma/lkxfW5a/4793vbA==" - }, - "NJsonSchema": { - "type": "Transitive", - "resolved": "10.2.1", - "contentHash": "/BtWbYTusyoSgQkCB4eYijMfZotB/rfASDsl1k9evlkm5vlOP4s4Y09TOzBChU77d/qUABVYL1Xf+TB8E0Wfpw==", - "dependencies": { - "Namotion.Reflection": "1.0.14", - "Newtonsoft.Json": "9.0.1" - } - }, - "NSwag.Annotations": { - "type": "Transitive", - "resolved": "13.8.2", - "contentHash": "/GO+35CjPYQTPS5/Q8udM5JAMEWVo8JsrkV2Uw3OW4/AJU9iOS7t6WJid6ZlkpLMjnW7oex9mvJ2EZNE4eOG/Q==" - }, - "NSwag.Core": { - "type": "Transitive", - "resolved": "13.8.2", - "contentHash": "Hm6pU9qFJuXLo3b27+JTXztfeuI/15Ob1sDsfUu4rchN0+bMogtn8Lia8KVbcalw/M+hXc0rWTFp5ueP23e+iA==", - "dependencies": { - "NJsonSchema": "10.2.1", - "Newtonsoft.Json": "9.0.1" - } - }, - "NSwag.Generation": { - "type": "Transitive", - "resolved": "13.8.2", - "contentHash": "LBIrpHFRZeMMbqL1hdyGb7r8v+T52aOCARxwfAmzE+MlOHVpjsIxyNSXht9EzBFMbSH0tj7CK2Ugo7bm+zUssg==", - "dependencies": { - "NJsonSchema": "10.2.1", - "NSwag.Core": "13.8.2", - "Newtonsoft.Json": "9.0.1" - } - }, - "NSwag.Generation.AspNetCore": { - "type": "Transitive", - "resolved": "13.8.2", - "contentHash": "0ydVv6OidspZ/MS6qmU8hswGtXwq5YZPg+2a2PHGD6jNp2Fef4j1wC3xa3hplDAq7cK+BgpyDKtvj9+X01+P5g==", - "dependencies": { - "Microsoft.AspNetCore.Mvc.ApiExplorer": "1.0.4", - "Microsoft.AspNetCore.Mvc.Core": "1.0.4", - "Microsoft.AspNetCore.Mvc.Formatters.Json": "1.0.4", - "NJsonSchema": "10.2.1", - "NSwag.Generation": "13.8.2" - } - }, - "runtime.native.System": { - "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "QfS/nQI7k/BLgmLrw7qm7YBoULEvgWnPI+cYsbfCVFTW8Aj+i8JhccxcFMu1RWms0YZzF+UHguNBK4Qn89e2Sg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1" - } - }, - "runtime.native.System.Net.Http": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "Nh0UPZx2Vifh8r+J+H2jxifZUD3sBrmolgiFWJd2yiNrxO0xTa6bAw3YwRn1VOiSen/tUXMS31ttNItCZ6lKuA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1" - } - }, - "runtime.native.System.Security.Cryptography": { - "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "2CQK0jmO6Eu7ZeMgD+LOFbNJSXHFVQbCJJkEyEwowh1SCgYnrn9W9RykMfpeeVGw7h4IBvYikzpGUlmZTUafJw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1" - } - }, - "SQLitePCLRaw.bundle_e_sqlite3": { - "type": "Transitive", - "resolved": "2.0.2", - "contentHash": "OVPI/nh5AqfLCIKhAYqjCa6AHhc7oKApGcGM3UhMRSerFiBx58nSpGwxVFdMgjOCWZR+fA49nzsnKlWp5hFo8w==", - "dependencies": { - "SQLitePCLRaw.core": "2.0.2", - "SQLitePCLRaw.lib.e_sqlite3": "2.0.2", - "SQLitePCLRaw.provider.dynamic_cdecl": "2.0.2" - } - }, - "SQLitePCLRaw.core": { - "type": "Transitive", - "resolved": "2.0.2", - "contentHash": "TFSBX426OelS1tkaVC254NVVlrJIe9YLhWPkEvuqJj2104QpmDmEYOhfdfDJD1E/2SmqDhoRw1ek5cQHj8olcQ==", - "dependencies": { - "System.Memory": "4.5.3" - } - }, - "SQLitePCLRaw.lib.e_sqlite3": { - "type": "Transitive", - "resolved": "2.0.2", - "contentHash": "S+Tsqe/M7wsc+9HeediI6UHtBKf2X586aRwhi1aBVLGe0WxkAo52O9ZxwEy/v8XMLefcrEMupd2e9CDlIT6QCw==" - }, - "SQLitePCLRaw.provider.dynamic_cdecl": { - "type": "Transitive", - "resolved": "2.0.2", - "contentHash": "ZSwacbKJUsxJEZxwT23uZVrGbaIvXcADZDz5Sr66fikO5eehdcceDncjzwzTzWfW13di8gpTpstx3WJSt/Ci5Q==", - "dependencies": { - "SQLitePCLRaw.core": "2.0.2" - } - }, - "System.AppContext": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "3QjO4jNV7PdKkmQAVp9atA+usVnKRwI3Kx1nMwJ93T0LcQfx7pKAYk0nKz5wn1oP5iqlhZuy6RXOFdhr7rDwow==", - "dependencies": { - "System.Runtime": "4.1.0" - } - }, - "System.Buffers": { - "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "msXumHfjjURSkvxUjYuq4N2ghHoRi2VpXcKMA7gK6ujQfU3vGpl+B6ld0ATRg+FZFpRyA6PgEPA+VlIkTeNf2w==", - "dependencies": { - "System.Diagnostics.Debug": "4.0.11", - "System.Diagnostics.Tracing": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Threading": "4.0.11" - } - }, - "System.Collections": { - "type": "Transitive", - "resolved": "4.0.11", - "contentHash": "YUJGz6eFKqS0V//mLt25vFGrrCvOnsXjlvFQs+KimpwNxug9x0Pzy4PlFMU3Q2IzqAa9G2L4LsK3+9vCBK7oTg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Collections.Concurrent": { - "type": "Transitive", - "resolved": "4.0.12", - "contentHash": "2gBcbb3drMLgxlI0fBfxMA31ec6AEyYCHygGse4vxceJan8mRIWeKJ24BFzN7+bi/NFTgdIgufzb94LWO5EERQ==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Diagnostics.Debug": "4.0.11", - "System.Diagnostics.Tracing": "4.1.0", - "System.Globalization": "4.0.11", - "System.Reflection": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Threading": "4.0.11", - "System.Threading.Tasks": "4.0.11" - } - }, - "System.Collections.Immutable": { - "type": "Transitive", - "resolved": "1.7.1", - "contentHash": "B43Zsz5EfMwyEbnObwRxW5u85fzJma3lrDeGcSAV1qkhSRTNY5uXAByTn9h9ddNdhM+4/YoLc/CI43umjwIl9Q==" - }, - "System.Collections.NonGeneric": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "hMxFT2RhhlffyCdKLDXjx8WEC5JfCvNozAZxCablAuFRH74SCV4AgzE8yJCh/73bFnEoZgJ9MJmkjQ0dJmnKqA==", - "dependencies": { - "System.Diagnostics.Debug": "4.0.11", - "System.Globalization": "4.0.11", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Threading": "4.0.11" - } - }, - "System.Collections.Specialized": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "/HKQyVP0yH1I0YtK7KJL/28snxHNH/bi+0lgk/+MbURF6ULhAE31MDI+NZDerNWu264YbxklXCCygISgm+HMug==", - "dependencies": { - "System.Collections.NonGeneric": "4.0.1", - "System.Globalization": "4.0.11", - "System.Globalization.Extensions": "4.0.1", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Threading": "4.0.11" - } - }, - "System.ComponentModel": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "oBZFnm7seFiVfugsIyOvQCWobNZs7FzqDV/B7tx20Ep/l3UUFCPDkdTnCNaJZTU27zjeODmy2C/cP60u3D4c9w==", - "dependencies": { - "System.Runtime": "4.1.0" - } - }, - "System.ComponentModel.Annotations": { - "type": "Transitive", - "resolved": "4.7.0", - "contentHash": "0YFqjhp/mYkDGpU0Ye1GjE53HMp9UVfGN7seGpAMttAC0C40v5gw598jCgpbBLMmCo0E5YRLBv5Z2doypO49ZQ==" - }, - "System.ComponentModel.Primitives": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "sc/7eVCdxPrp3ljpgTKVaQGUXiW05phNWvtv/m2kocXqrUQvTVWKou1Edas2aDjTThLPZOxPYIGNb/HN0QjURg==", - "dependencies": { - "System.ComponentModel": "4.0.1", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.ComponentModel.TypeConverter": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "MnDAlaeJZy9pdB5ZdOlwdxfpI+LJQ6e0hmH7d2+y2LkiD8DRJynyDYl4Xxf3fWFm7SbEwBZh4elcfzONQLOoQw==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Collections.NonGeneric": "4.0.1", - "System.Collections.Specialized": "4.0.1", - "System.ComponentModel": "4.0.1", - "System.ComponentModel.Primitives": "4.1.0", - "System.Globalization": "4.0.11", - "System.Linq": "4.1.0", - "System.Reflection": "4.1.0", - "System.Reflection.Extensions": "4.0.1", - "System.Reflection.Primitives": "4.0.1", - "System.Reflection.TypeExtensions": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Threading": "4.0.11" - } - }, - "System.Diagnostics.Contracts": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "HvQQjy712vnlpPxaloZYkuE78Gn353L0SJLJVeLcNASeg9c4qla2a1Xq8I7B3jZoDzKPtHTkyVO7AZ5tpeQGuA==", - "dependencies": { - "System.Runtime": "4.1.0" - } - }, - "System.Diagnostics.Debug": { - "type": "Transitive", - "resolved": "4.0.11", - "contentHash": "w5U95fVKHY4G8ASs/K5iK3J5LY+/dLFd4vKejsnI/ZhBsWS9hQakfx3Zr7lRWKg4tAw9r4iktyvsTagWkqYCiw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Diagnostics.DiagnosticSource": { - "type": "Transitive", - "resolved": "4.7.1", - "contentHash": "j81Lovt90PDAq8kLpaJfJKV/rWdWuEk6jfV+MBkee33vzYLEUsy4gXK8laa9V2nZlLM9VM9yA/OOQxxPEJKAMw==" - }, - "System.Diagnostics.Tools": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "xBfJ8pnd4C17dWaC9FM6aShzbJcRNMChUMD42I6772KGGrqaFdumwhn9OdM68erj1ueNo3xdQ1EwiFjK5k8p0g==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Diagnostics.Tracing": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "vDN1PoMZCkkdNjvZLql592oYJZgS7URcJzJ7bxeBgGtx5UtR5leNm49VmfHGqIffX4FKacHbI3H6UyNSHQknBg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Globalization": { - "type": "Transitive", - "resolved": "4.0.11", - "contentHash": "B95h0YLEL2oSnwF/XjqSWKnwKOy/01VWkNlsCeMTFJLLabflpGV26nK164eRs5GiaRSBGpOxQ3pKoSnnyZN5pg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Globalization.Calendars": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "L1c6IqeQ88vuzC1P81JeHmHA8mxq8a18NUBNXnIY/BVb+TCyAaGIFbhpZt60h9FJNmisymoQkHEFSE9Vslja1Q==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Globalization": "4.0.11", - "System.Runtime": "4.1.0" - } - }, - "System.Globalization.Extensions": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "KKo23iKeOaIg61SSXwjANN7QYDr/3op3OWGGzDzz7mypx0Za0fZSeG0l6cco8Ntp8YMYkIQcAqlk8yhm5/Uhcg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "System.Globalization": "4.0.11", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Runtime.InteropServices": "4.1.0" - } - }, - "System.IO": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.IO.FileSystem": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "3wEMARTnuio+ulnvi+hkRNROYwa1kylvYahhcLk4HSoVdl+xxTFVeVlYOfLwrDPImGls0mDqbMhrza8qnWPTdA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.IO": "4.3.0", - "System.IO.FileSystem.Primitives": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.IO.FileSystem.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "6QOb2XFLch7bEc4lIcJH49nJN2HV+OC3fHDgsLVsBVBk3Y4hFAnOBGzJ2lUu7CyDDFo9IBWkSsnbkT6IBwwiMw==", - "dependencies": { - "System.Runtime": "4.3.0" - } - }, - "System.Linq": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "bQ0iYFOQI0nuTnt+NQADns6ucV4DUvMdwN6CbkB1yj8i7arTGiTN5eok1kQwdnnNWSDZfIUySQY+J3d5KjWn0g==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Diagnostics.Debug": "4.0.11", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0" - } - }, - "System.Linq.Expressions": { - "type": "Transitive", - "resolved": "4.1.1", - "contentHash": "bXwi8FrK/XIGPvtk1ZnawffhqLPyacj7dZnbFaV52YGaQigNqGEzNAByAIvL9FlEe3TCzoInorHF91IK//Q3Xg==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Diagnostics.Debug": "4.0.11", - "System.Globalization": "4.0.11", - "System.IO": "4.1.0", - "System.Linq": "4.1.0", - "System.ObjectModel": "4.0.12", - "System.Reflection": "4.1.0", - "System.Reflection.Emit": "4.0.1", - "System.Reflection.Emit.ILGeneration": "4.0.1", - "System.Reflection.Emit.Lightweight": "4.0.1", - "System.Reflection.Extensions": "4.0.1", - "System.Reflection.Primitives": "4.0.1", - "System.Reflection.TypeExtensions": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Threading": "4.0.11" - } - }, - "System.Memory": { - "type": "Transitive", - "resolved": "4.5.3", - "contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA==" - }, - "System.Net.Primitives": { - "type": "Transitive", - "resolved": "4.0.11", - "contentHash": "hVvfl4405DRjA2408luZekbPhplJK03j2Y2lSfMlny7GHXlkByw1iLnc9mgKW0GdQn73vvMcWrWewAhylXA4Nw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Handles": "4.0.1" - } - }, - "System.Net.WebSockets": { - "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "2KJo8hir6Edi9jnMDAMhiJoI691xRBmKcbNpwjrvpIMOCTYOtBpSsSEGBxBDV7PKbasJNaFp1+PZz1D7xS41Hg==", - "dependencies": { - "Microsoft.Win32.Primitives": "4.0.1", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Threading.Tasks": "4.0.11" - } - }, - "System.ObjectModel": { - "type": "Transitive", - "resolved": "4.0.12", - "contentHash": "tAgJM1xt3ytyMoW4qn4wIqgJYm7L7TShRZG4+Q4Qsi2PCcj96pXN7nRywS9KkB3p/xDUjc2HSwP9SROyPYDYKQ==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Diagnostics.Debug": "4.0.11", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Threading": "4.0.11" - } - }, - "System.Reflection": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "JCKANJ0TI7kzoQzuwB/OoJANy1Lg338B6+JVacPl4TpUwi3cReg3nMLplMq2uqYfHFQpKIlHAUVAJlImZz/4ng==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.IO": "4.1.0", - "System.Reflection.Primitives": "4.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Reflection.Emit": { - "type": "Transitive", - "resolved": "4.7.0", - "contentHash": "VR4kk8XLKebQ4MZuKuIni/7oh+QGFmZW3qORd1GvBq/8026OpW501SzT/oypwiQl4TvT8ErnReh/NzY9u+C6wQ==" - }, - "System.Reflection.Emit.ILGeneration": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "Ov6dU8Bu15Bc7zuqttgHF12J5lwSWyTf1S+FJouUXVMSqImLZzYaQ+vRr1rQ0OZ0HqsrwWl4dsKHELckQkVpgA==", - "dependencies": { - "System.Reflection": "4.1.0", - "System.Reflection.Primitives": "4.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Reflection.Emit.Lightweight": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "sSzHHXueZ5Uh0OLpUQprhr+ZYJrLPA2Cmr4gn0wj9+FftNKXx8RIMKvO9qnjk2ebPYUjZ+F2ulGdPOsvj+MEjA==", - "dependencies": { - "System.Reflection": "4.1.0", - "System.Reflection.Emit.ILGeneration": "4.0.1", - "System.Reflection.Primitives": "4.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Reflection.Extensions": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "GYrtRsZcMuHF3sbmRHfMYpvxZoIN2bQGrYGerUiWLEkqdEUQZhH3TRSaC/oI4wO0II1RKBPlpIa1TOMxIcOOzQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Reflection": "4.1.0", - "System.Runtime": "4.1.0" - } - }, - "System.Reflection.Primitives": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "4inTox4wTBaDhB7V3mPvp9XlCbeGYWVEM9/fXALd52vNEAVisc1BoVWQPuUuD0Ga//dNbA/WeMy9u9mzLxGTHQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Reflection.TypeExtensions": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "tsQ/ptQ3H5FYfON8lL4MxRk/8kFyE0A+tGPXmVP967cT/gzLHYxIejIYSxp4JmIeFHVP78g/F2FE1mUUTbDtrg==", - "dependencies": { - "System.Reflection": "4.1.0", - "System.Runtime": "4.1.0" - } - }, - "System.Resources.ResourceManager": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "TxwVeUNoTgUOdQ09gfTjvW411MF+w9MBYL7AtNVc+HtBCFlutPLhUCdZjNkjbhj3bNQWMdHboF0KIWEOjJssbA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Globalization": "4.0.11", - "System.Reflection": "4.1.0", - "System.Runtime": "4.1.0" - } - }, - "System.Runtime": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0" - } - }, - "System.Runtime.Extensions": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "CUOHjTT/vgP0qGW22U4/hDlOqXmcPq5YicBaXdUR2UiUoLwBT+olO6we4DVbq57jeX5uXH2uerVZhf0qGj+sVQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Runtime.Handles": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Runtime.InteropServices": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "16eu3kjHS633yYdkjwShDHZLRNMKVi/s0bY8ODiqJ2RfMhDMAwxZaUaWVnZ2P71kr/or+X9o/xFWtNqz8ivieQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Reflection": "4.1.0", - "System.Reflection.Primitives": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Handles": "4.0.1" - } - }, - "System.Runtime.Numerics": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "+XbKFuzdmLP3d1o9pdHu2nxjNr2OEPqGzKeegPLCUMM71a0t50A/rOcIRmGs9wR7a8KuHX6hYs/7/TymIGLNqg==", - "dependencies": { - "System.Globalization": "4.0.11", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0" - } - }, - "System.Runtime.Serialization.Primitives": { - "type": "Transitive", - "resolved": "4.1.1", - "contentHash": "HZ6Du5QrTG8MNJbf4e4qMO3JRAkIboGT5Fk804uZtg3Gq516S7hAqTm2UZKUHa7/6HUGdVy3AqMQKbns06G/cg==", - "dependencies": { - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Security.Claims": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "4Jlp0OgJLS/Voj1kyFP6MJlIYp3crgfH8kNQk2p7+4JYfc1aAmh9PZyAMMbDhuoolGNtux9HqSOazsioRiDvCw==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Globalization": "4.0.11", - "System.IO": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Security.Principal": "4.0.1" - } - }, - "System.Security.Cryptography.Algorithms": { - "type": "Transitive", - "resolved": "4.2.0", - "contentHash": "8JQFxbLVdrtIOKMDN38Fn0GWnqYZw/oMlwOUG/qz1jqChvyZlnUmu+0s7wLx7JYua/nAXoESpHA3iw11QFWhXg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "System.Collections": "4.0.11", - "System.IO": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Runtime.Handles": "4.0.1", - "System.Runtime.InteropServices": "4.1.0", - "System.Runtime.Numerics": "4.0.1", - "System.Security.Cryptography.Encoding": "4.0.0", - "System.Security.Cryptography.Primitives": "4.0.0", - "System.Text.Encoding": "4.0.11", - "runtime.native.System.Security.Cryptography": "4.0.0" - } - }, - "System.Security.Cryptography.Cng": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "WG3r7EyjUe9CMPFSs6bty5doUqT+q9pbI80hlNzo2SkPkZ4VTuZkGWjpp77JB8+uaL4DFPRdBsAY+DX3dBK92A==" - }, - "System.Security.Cryptography.Csp": { - "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "/i1Usuo4PgAqgbPNC0NjbO3jPW//BoBlTpcWFD1EHVbidH21y4c1ap5bbEMSGAXjAShhMH4abi/K8fILrnu4BQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "System.IO": "4.1.0", - "System.Reflection": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Runtime.Handles": "4.0.1", - "System.Runtime.InteropServices": "4.1.0", - "System.Security.Cryptography.Algorithms": "4.2.0", - "System.Security.Cryptography.Encoding": "4.0.0", - "System.Security.Cryptography.Primitives": "4.0.0", - "System.Text.Encoding": "4.0.11", - "System.Threading": "4.0.11" - } - }, - "System.Security.Cryptography.Encoding": { - "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "FbKgE5MbxSQMPcSVRgwM6bXN3GtyAh04NkV8E5zKCBE26X0vYW0UtTa2FIgkH33WVqBVxRgxljlVYumWtU+HcQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "System.Collections": "4.0.11", - "System.Collections.Concurrent": "4.0.12", - "System.Linq": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Runtime.Handles": "4.0.1", - "System.Runtime.InteropServices": "4.1.0", - "System.Security.Cryptography.Primitives": "4.0.0", - "System.Text.Encoding": "4.0.11", - "runtime.native.System.Security.Cryptography": "4.0.0" - } - }, - "System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "HUG/zNUJwEiLkoURDixzkzZdB5yGA5pQhDP93ArOpDPQMteURIGERRNzzoJlmTreLBWr5lkFSjjMSk8ySEpQMw==", - "dependencies": { - "System.Collections": "4.0.11", - "System.IO": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Runtime.Handles": "4.0.1", - "System.Runtime.InteropServices": "4.1.0", - "System.Runtime.Numerics": "4.0.1", - "System.Security.Cryptography.Algorithms": "4.2.0", - "System.Security.Cryptography.Encoding": "4.0.0", - "System.Security.Cryptography.Primitives": "4.0.0", - "System.Text.Encoding": "4.0.11", - "runtime.native.System.Security.Cryptography": "4.0.0" - } - }, - "System.Security.Cryptography.Primitives": { - "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "Wkd7QryWYjkQclX0bngpntW5HSlMzeJU24UaLJQ7YTfI8ydAVAaU2J+HXLLABOVJlKTVvAeL0Aj39VeTe7L+oA==", - "dependencies": { - "System.Diagnostics.Debug": "4.0.11", - "System.Globalization": "4.0.11", - "System.IO": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Threading": "4.0.11", - "System.Threading.Tasks": "4.0.11" - } - }, - "System.Security.Cryptography.X509Certificates": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "4HEfsQIKAhA1+ApNn729Gi09zh+lYWwyIuViihoMDWp1vQnEkL2ct7mAbhBlLYm+x/L4Rr/pyGge1lIY635e0w==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "System.Collections": "4.0.11", - "System.Diagnostics.Debug": "4.0.11", - "System.Globalization": "4.0.11", - "System.Globalization.Calendars": "4.0.1", - "System.IO": "4.1.0", - "System.IO.FileSystem": "4.0.1", - "System.IO.FileSystem.Primitives": "4.0.1", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Runtime.Handles": "4.0.1", - "System.Runtime.InteropServices": "4.1.0", - "System.Runtime.Numerics": "4.0.1", - "System.Security.Cryptography.Algorithms": "4.2.0", - "System.Security.Cryptography.Cng": "4.2.0", - "System.Security.Cryptography.Csp": "4.0.0", - "System.Security.Cryptography.Encoding": "4.0.0", - "System.Security.Cryptography.OpenSsl": "4.0.0", - "System.Security.Cryptography.Primitives": "4.0.0", - "System.Text.Encoding": "4.0.11", - "System.Threading": "4.0.11", - "runtime.native.System": "4.0.0", - "runtime.native.System.Net.Http": "4.0.1", - "runtime.native.System.Security.Cryptography": "4.0.0" - } - }, - "System.Security.Principal": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "On+SKhXY5rzxh/S8wlH1Rm0ogBlu7zyHNxeNBiXauNrhHRXAe9EuX8Yl5IOzLPGU5Z4kLWHMvORDOCG8iu9hww==", - "dependencies": { - "System.Runtime": "4.1.0" - } - }, - "System.Text.Encoding": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Text.Encoding.Extensions": { - "type": "Transitive", - "resolved": "4.0.11", - "contentHash": "jtbiTDtvfLYgXn8PTfWI+SiBs51rrmO4AAckx4KR6vFK9Wzf6tI8kcRdsYQNwriUeQ1+CtQbM1W4cMbLXnj/OQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Runtime": "4.1.0", - "System.Text.Encoding": "4.0.11" - } - }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "GgJDO6/1bW6kkttxIiPK2jsqllQ3ifaeeBAJJrcoJq0lAclIZsAZZdEqi6JHq+QLZXL2UsjyWb8K8EOH7nOSPw==", - "dependencies": { - "System.Diagnostics.Debug": "4.0.11", - "System.IO": "4.1.0", - "System.Reflection": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Threading": "4.0.11" - } - }, - "System.Text.Json": { - "type": "Transitive", - "resolved": "4.7.2", - "contentHash": "TcMd95wcrubm9nHvJEQs70rC0H/8omiSGGpU4FQ/ZA1URIqD4pjmFJh2Mfv1yH1eHgJDWTi2hMDXwTET+zOOyg==" - }, - "System.Text.RegularExpressions": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "i88YCXpRTjCnoSQZtdlHkAOx4KNNik4hMy83n0+Ftlb7jvV6ZiZWMpnEZHhjBp6hQVh8gWd/iKNPzlPF7iyA2g==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Globalization": "4.0.11", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Threading": "4.0.11" - } - }, - "System.Threading": { - "type": "Transitive", - "resolved": "4.0.11", - "contentHash": "N+3xqIcg3VDKyjwwCGaZ9HawG9aC6cSDI+s7ROma310GQo8vilFZa86hqKppwTHleR/G0sfOzhvgnUxWCR/DrQ==", - "dependencies": { - "System.Runtime": "4.1.0", - "System.Threading.Tasks": "4.0.11" - } - }, - "System.Threading.Tasks": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Threading.Tasks.Extensions": { - "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "pH4FZDsZQ/WmgJtN4LWYmRdJAEeVkyriSwrv2Teoe5FOU0Yxlb6II6GL8dBPOfRmutHGATduj3ooMt7dJ2+i+w==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Runtime": "4.1.0", - "System.Threading.Tasks": "4.0.11" - } - }, - "System.Xml.ReaderWriter": { - "type": "Transitive", - "resolved": "4.0.11", - "contentHash": "ZIiLPsf67YZ9zgr31vzrFaYQqxRPX9cVHjtPSnmx4eN6lbS/yEyYNr2vs1doGDEscF0tjCZFsk9yUg1sC9e8tg==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Diagnostics.Debug": "4.0.11", - "System.Globalization": "4.0.11", - "System.IO": "4.1.0", - "System.IO.FileSystem": "4.0.1", - "System.IO.FileSystem.Primitives": "4.0.1", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Runtime.InteropServices": "4.1.0", - "System.Text.Encoding": "4.0.11", - "System.Text.Encoding.Extensions": "4.0.11", - "System.Text.RegularExpressions": "4.1.0", - "System.Threading.Tasks": "4.0.11", - "System.Threading.Tasks.Extensions": "4.0.0" - } - }, - "System.Xml.XDocument": { - "type": "Transitive", - "resolved": "4.0.11", - "contentHash": "Mk2mKmPi0nWaoiYeotq1dgeNK1fqWh61+EK+w4Wu8SWuTYLzpUnschb59bJtGywaPq7SmTuPf44wrXRwbIrukg==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Diagnostics.Debug": "4.0.11", - "System.Diagnostics.Tools": "4.0.1", - "System.Globalization": "4.0.11", - "System.IO": "4.1.0", - "System.Reflection": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Text.Encoding": "4.0.11", - "System.Threading": "4.0.11", - "System.Xml.ReaderWriter": "4.0.11" - } - }, - "System.Xml.XPath": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "UWd1H+1IJ9Wlq5nognZ/XJdyj8qPE4XufBUkAW59ijsCPjZkZe0MUzKKJFBr+ZWBe5Wq1u1d5f2CYgE93uH7DA==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Diagnostics.Debug": "4.0.11", - "System.Globalization": "4.0.11", - "System.IO": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Threading": "4.0.11", - "System.Xml.ReaderWriter": "4.0.11" - } - }, - "System.Xml.XPath.XDocument": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "FLhdYJx4331oGovQypQ8JIw2kEmNzCsjVOVYY/16kZTUoquZG85oVn7yUhBE2OZt1yGPSXAL0HTEfzjlbNpM7Q==", - "dependencies": { - "System.Diagnostics.Debug": "4.0.11", - "System.Linq": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Threading": "4.0.11", - "System.Xml.ReaderWriter": "4.0.11", - "System.Xml.XDocument": "4.0.11", - "System.Xml.XPath": "4.0.1" - } - }, - "timeline.errorcodes": { - "type": "Project" - } - } - } -} \ No newline at end of file diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index c0aaee67..00000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,75 +0,0 @@ -trigger: - - master - - dev - -stages: -- stage: build - displayName: Routine Build and Test - - jobs: - - job: frontend_build - displayName: Frontend Build - pool: - vmImage: 'ubuntu-18.04' - - steps: - - script: yarn - workingDirectory: Timeline/ClientApp - displayName: Restore Packages - - - script: yarn build - workingDirectory: Timeline/ClientApp - displayName: Webpack Build - - - publish: Timeline/ClientApp/dist - artifact: timeline-frontend - - - job: backend_build - displayName: Backend Build Debug and Test - pool: - vmImage: 'ubuntu-18.04' - variables: - buildConfiguration: 'Debug' - ASPNETCORE_ENVIRONMENT: 'Development' - steps: - - task: UseDotNet@2 - inputs: - packageType: sdk - version: 3.1.x - - - script: dotnet test --configuration $(buildConfiguration) --logger trx --collect:"XPlat Code Coverage" --settings './Timeline.Tests/coverletArgs.runsettings' - displayName: Dotnet Test - - - task: PublishTestResults@2 - condition: succeededOrFailed() - inputs: - testRunner: VSTest - testResultsFiles: '**/*.trx' - - - task: PublishCodeCoverageResults@1 - condition: succeededOrFailed() - inputs: - codeCoverageTool: 'Cobertura' - summaryFileLocation: '**/TestResults/*/coverage.cobertura.xml' - -- stage: deploy - displayName: Build Release and Deploy - dependsOn: build - condition: eq(variables['Build.SourceBranchName'], 'master') - variables: - buildConfiguration: 'Release' - - jobs: - - job: build - displayName: Build Release Artifact - pool: - vmImage: 'ubuntu-18.04' - steps: - - task: Docker@2 - displayName: Build And Push Docker Image - inputs: - command: buildAndPush - repository: crupest/timeline - tags: latest - buildContext: . - containerRegistry: crupest-docker-hub diff --git a/tools/convert-eol.py b/tools/convert-eol.py deleted file mode 100644 index 3ea8ed7c..00000000 --- a/tools/convert-eol.py +++ /dev/null @@ -1,35 +0,0 @@ -# This is a python script that converts all text source codes into -# CRLF (Windows line ending) eol format and UTF-8 with NO BOM encoding. - -import glob -import os.path - -project_root = os.path.relpath(os.path.join(os.path.dirname(__file__), '..')) - - -def convert(file_path): - with open(file_path, 'r', encoding='utf-8') as open_file: - content = open_file.read() - - #if there is BOM, remove BOM - if content[0] == '\ufeff': - content = content[1:] - - with open(file_path, 'w', encoding='utf-8', newline='\r\n') as open_file: - open_file.write(content) - - -glob_list = [ - './nuget.config', - '**/*.sln', - '**/*.cs', - '**/*.csproj', - '**/appsettings*.json' -] - -for glob_pattern in glob_list: - for f in glob.glob(glob_pattern, recursive=True): - print('Converting {}'.format(f)) - convert(f) - -print('Done!!!') -- cgit v1.2.3