add: dprint fmt
This commit is contained in:
		
							parent
							
								
									04ab39a3ec
								
							
						
					
					
						commit
						edc6104a09
					
				
					 84 changed files with 3674 additions and 3373 deletions
				
			
		|  | @ -4,21 +4,25 @@ Content File Management Program. | ||||||
| For study about nodejs, typescript and react. | For study about nodejs, typescript and react. | ||||||
| 
 | 
 | ||||||
| ### deployment | ### deployment | ||||||
|  | 
 | ||||||
| ```bash | ```bash | ||||||
| pnpm run app:build | pnpm run app:build | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ### test | ### test | ||||||
|  | 
 | ||||||
| ```bash | ```bash | ||||||
| $ pnpm run app | $ pnpm run app | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ### server build | ### server build | ||||||
|  | 
 | ||||||
| ```bash | ```bash | ||||||
| $ pnpm run compile | $ pnpm run compile | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ### client build | ### client build | ||||||
|  | 
 | ||||||
| ```bash | ```bash | ||||||
| $ pnpm run build | $ pnpm run build | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
							
								
								
									
										28
									
								
								app.ts
									
										
									
									
									
								
							
							
						
						
									
										28
									
								
								app.ts
									
										
									
									
									
								
							|  | @ -1,13 +1,13 @@ | ||||||
| import { app, BrowserWindow, session, dialog } from "electron"; | import { app, BrowserWindow, dialog, session } from "electron"; | ||||||
| import { get_setting } from "./src/SettingConfig"; | import { ipcMain } from "electron"; | ||||||
| import { create_server } from "./src/server"; |  | ||||||
| import { getAdminAccessTokenValue,getAdminRefreshTokenValue, accessTokenName, refreshTokenName } from "./src/login"; |  | ||||||
| import { join } from "path"; | import { join } from "path"; | ||||||
| import { ipcMain } from 'electron'; | import { accessTokenName, getAdminAccessTokenValue, getAdminRefreshTokenValue, refreshTokenName } from "./src/login"; | ||||||
| import { UserAccessor } from "./src/model/mod"; | import { UserAccessor } from "./src/model/mod"; | ||||||
|  | import { create_server } from "./src/server"; | ||||||
|  | import { get_setting } from "./src/SettingConfig"; | ||||||
| 
 | 
 | ||||||
| function registerChannel(cntr: UserAccessor) { | function registerChannel(cntr: UserAccessor) { | ||||||
|   ipcMain.handle('reset_password', async(event,username:string,password:string)=>{ |     ipcMain.handle("reset_password", async (event, username: string, password: string) => { | ||||||
|         const user = await cntr.findUser(username); |         const user = await cntr.findUser(username); | ||||||
|         if (user === undefined) { |         if (user === undefined) { | ||||||
|             return false; |             return false; | ||||||
|  | @ -27,11 +27,11 @@ if (!setting.cli) { | ||||||
|             center: true, |             center: true, | ||||||
|             useContentSize: true, |             useContentSize: true, | ||||||
|             webPreferences: { |             webPreferences: { | ||||||
|         preload:join(__dirname,'preload.js'), |                 preload: join(__dirname, "preload.js"), | ||||||
|                 contextIsolation: true, |                 contextIsolation: true, | ||||||
|       } |             }, | ||||||
|         }); |         }); | ||||||
|     await wnd.loadURL(`data:text/html;base64,`+Buffer.from(loading_html).toString('base64')); |         await wnd.loadURL(`data:text/html;base64,` + Buffer.from(loading_html).toString("base64")); | ||||||
|         // await wnd.loadURL('../loading.html');
 |         // await wnd.loadURL('../loading.html');
 | ||||||
|         // set admin cookies.
 |         // set admin cookies.
 | ||||||
|         await session.defaultSession.cookies.set({ |         await session.defaultSession.cookies.set({ | ||||||
|  | @ -40,7 +40,7 @@ if (!setting.cli) { | ||||||
|             value: getAdminAccessTokenValue(), |             value: getAdminAccessTokenValue(), | ||||||
|             httpOnly: true, |             httpOnly: true, | ||||||
|             secure: false, |             secure: false, | ||||||
|       sameSite:"strict" |             sameSite: "strict", | ||||||
|         }); |         }); | ||||||
|         await session.defaultSession.cookies.set({ |         await session.defaultSession.cookies.set({ | ||||||
|             url: `http://localhost:${setting.port}`, |             url: `http://localhost:${setting.port}`, | ||||||
|  | @ -48,23 +48,21 @@ if (!setting.cli) { | ||||||
|             value: getAdminRefreshTokenValue(), |             value: getAdminRefreshTokenValue(), | ||||||
|             httpOnly: true, |             httpOnly: true, | ||||||
|             secure: false, |             secure: false, | ||||||
|       sameSite:"strict" |             sameSite: "strict", | ||||||
|         }); |         }); | ||||||
|         try { |         try { | ||||||
|             const server = await create_server(); |             const server = await create_server(); | ||||||
|             const app = server.start_server(); |             const app = server.start_server(); | ||||||
|             registerChannel(server.userController); |             registerChannel(server.userController); | ||||||
|             await wnd.loadURL(`http://localhost:${setting.port}`); |             await wnd.loadURL(`http://localhost:${setting.port}`); | ||||||
|     } |         } catch (e) { | ||||||
|     catch(e){ |  | ||||||
|             if (e instanceof Error) { |             if (e instanceof Error) { | ||||||
|                 await dialog.showMessageBox({ |                 await dialog.showMessageBox({ | ||||||
|                     type: "error", |                     type: "error", | ||||||
|                     title: "error!", |                     title: "error!", | ||||||
|                     message: e.message, |                     message: e.message, | ||||||
|                 }); |                 }); | ||||||
|       } |             } else { | ||||||
|       else{ |  | ||||||
|                 await dialog.showMessageBox({ |                 await dialog.showMessageBox({ | ||||||
|                     type: "error", |                     type: "error", | ||||||
|                     title: "error!", |                     title: "error!", | ||||||
|  |  | ||||||
							
								
								
									
										23
									
								
								dprint.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								dprint.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,23 @@ | ||||||
|  | { | ||||||
|  |   "incremental": true, | ||||||
|  |   "typescript": { | ||||||
|  |     "indentWidth": 2 | ||||||
|  |   }, | ||||||
|  |   "json": { | ||||||
|  |   }, | ||||||
|  |   "markdown": { | ||||||
|  |   }, | ||||||
|  |   "includes": ["**/*.{ts,tsx,js,jsx,cjs,mjs,json,md}"], | ||||||
|  |   "excludes": [ | ||||||
|  |     "**/node_modules", | ||||||
|  |     "**/*-lock.json", | ||||||
|  |     "**/dist", | ||||||
|  |     "build/", | ||||||
|  |     "app/" | ||||||
|  |   ], | ||||||
|  |   "plugins": [ | ||||||
|  |     "https://plugins.dprint.dev/typescript-0.84.4.wasm", | ||||||
|  |     "https://plugins.dprint.dev/json-0.17.2.wasm", | ||||||
|  |     "https://plugins.dprint.dev/markdown-0.15.2.wasm" | ||||||
|  |   ] | ||||||
|  | } | ||||||
|  | @ -1,13 +1,13 @@ | ||||||
| import { promises } from 'fs'; | import { promises } from "fs"; | ||||||
| const { readdir, writeFile } = promises; | const { readdir, writeFile } = promises; | ||||||
| import {createGenerator} from 'ts-json-schema-generator'; | import { dirname, join } from "path"; | ||||||
| import {dirname,join} from 'path';  | import { createGenerator } from "ts-json-schema-generator"; | ||||||
| 
 | 
 | ||||||
| async function genSchema(path: string, typename: string) { | async function genSchema(path: string, typename: string) { | ||||||
|     const gen = createGenerator({ |     const gen = createGenerator({ | ||||||
|         path: path, |         path: path, | ||||||
|         type: typename, |         type: typename, | ||||||
|         tsconfig:"tsconfig.json" |         tsconfig: "tsconfig.json", | ||||||
|     }); |     }); | ||||||
|     const schema = gen.createSchema(typename); |     const schema = gen.createSchema(typename); | ||||||
|     if (schema.definitions != undefined) { |     if (schema.definitions != undefined) { | ||||||
|  | @ -16,8 +16,8 @@ async function genSchema(path:string,typename:string){ | ||||||
|         if (typeof definition == "object") { |         if (typeof definition == "object") { | ||||||
|             let property = definition.properties; |             let property = definition.properties; | ||||||
|             if (property) { |             if (property) { | ||||||
|                 property['$schema'] = { |                 property["$schema"] = { | ||||||
|                     type:"string" |                     type: "string", | ||||||
|                 }; |                 }; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  | @ -29,7 +29,7 @@ function capitalize(s:string){ | ||||||
|     return s.charAt(0).toUpperCase() + s.slice(1); |     return s.charAt(0).toUpperCase() + s.slice(1); | ||||||
| } | } | ||||||
| async function setToALL(path: string) { | async function setToALL(path: string) { | ||||||
|     console.log(`scan ${path}`) |     console.log(`scan ${path}`); | ||||||
|     const direntry = await readdir(path, { withFileTypes: true }); |     const direntry = await readdir(path, { withFileTypes: true }); | ||||||
|     const works = direntry.filter(x => x.isFile() && x.name.endsWith("Config.ts")).map(x => { |     const works = direntry.filter(x => x.isFile() && x.name.endsWith("Config.ts")).map(x => { | ||||||
|         const name = x.name; |         const name = x.name; | ||||||
|  | @ -38,11 +38,11 @@ async function setToALL(path:string) { | ||||||
|             const typename = m[1]; |             const typename = m[1]; | ||||||
|             return genSchema(join(path, typename), capitalize(typename)); |             return genSchema(join(path, typename), capitalize(typename)); | ||||||
|         } |         } | ||||||
|     }) |     }); | ||||||
|     await Promise.all(works); |     await Promise.all(works); | ||||||
|     const subdir = direntry.filter(x => x.isDirectory()).map(x => x.name); |     const subdir = direntry.filter(x => x.isDirectory()).map(x => x.name); | ||||||
|     for (const x of subdir) { |     for (const x of subdir) { | ||||||
|         await setToALL(join(path, x)); |         await setToALL(join(path, x)); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| setToALL("src") | setToALL("src"); | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| require('ts-node').register(); | require("ts-node").register(); | ||||||
| const {Knex} = require('./src/config'); | const { Knex } = require("./src/config"); | ||||||
| // Update with your config settings.
 | // Update with your config settings.
 | ||||||
| 
 | 
 | ||||||
| module.exports = Knex.config; | module.exports = Knex.config; | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import {Knex} from 'knex'; | import { Knex } from "knex"; | ||||||
| 
 | 
 | ||||||
| export async function up(knex: Knex) { | export async function up(knex: Knex) { | ||||||
|     await knex.schema.createTable("schema_migration", (b) => { |     await knex.schema.createTable("schema_migration", (b) => { | ||||||
|  | @ -36,19 +36,19 @@ export async function up(knex:Knex) { | ||||||
|         b.primary(["doc_id", "tag_name"]); |         b.primary(["doc_id", "tag_name"]); | ||||||
|     }); |     }); | ||||||
|     await knex.schema.createTable("permissions", b => { |     await knex.schema.createTable("permissions", b => { | ||||||
|         b.string('username').notNullable(); |         b.string("username").notNullable(); | ||||||
|         b.string("name").notNullable(); |         b.string("name").notNullable(); | ||||||
|         b.primary(["username", "name"]); |         b.primary(["username", "name"]); | ||||||
|         b.foreign('username').references('users.username'); |         b.foreign("username").references("users.username"); | ||||||
|     }); |     }); | ||||||
|     // create admin account.
 |     // create admin account.
 | ||||||
|     await knex.insert({ |     await knex.insert({ | ||||||
|         username: "admin", |         username: "admin", | ||||||
|         password_hash: "unchecked", |         password_hash: "unchecked", | ||||||
|         password_salt:"unchecked" |         password_salt: "unchecked", | ||||||
|     }).into('users'); |     }).into("users"); | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| export async function down(knex: Knex) { | export async function down(knex: Knex) { | ||||||
|     throw new Error('Downward migrations are not supported. Restore from backup.'); |     throw new Error("Downward migrations are not supported. Restore from backup."); | ||||||
| }; | } | ||||||
|  |  | ||||||
|  | @ -8,6 +8,7 @@ | ||||||
|     "compile:watch": "tsc -w", |     "compile:watch": "tsc -w", | ||||||
|     "build": "cd src/client && pnpm run build:prod", |     "build": "cd src/client && pnpm run build:prod", | ||||||
|     "build:watch": "cd src/client && pnpm run build:watch", |     "build:watch": "cd src/client && pnpm run build:watch", | ||||||
|  |     "fmt": "dprint fmt", | ||||||
|     "app": "electron build/app.js", |     "app": "electron build/app.js", | ||||||
|     "app:build": "electron-builder", |     "app:build": "electron-builder", | ||||||
|     "app:pack": "electron-builder --dir", |     "app:pack": "electron-builder --dir", | ||||||
|  | @ -56,6 +57,7 @@ | ||||||
|     "@louislam/sqlite3": "^6.0.1", |     "@louislam/sqlite3": "^6.0.1", | ||||||
|     "@types/koa-compose": "^3.2.5", |     "@types/koa-compose": "^3.2.5", | ||||||
|     "chokidar": "^3.5.3", |     "chokidar": "^3.5.3", | ||||||
|  |     "dprint": "^0.36.1", | ||||||
|     "jsonschema": "^1.4.1", |     "jsonschema": "^1.4.1", | ||||||
|     "jsonwebtoken": "^8.5.1", |     "jsonwebtoken": "^8.5.1", | ||||||
|     "knex": "^0.95.15", |     "knex": "^0.95.15", | ||||||
|  |  | ||||||
							
								
								
									
										2
									
								
								plan.md
									
										
									
									
									
								
							
							
						
						
									
										2
									
								
								plan.md
									
										
									
									
									
								
							|  | @ -3,6 +3,7 @@ | ||||||
| ## Routing | ## Routing | ||||||
| 
 | 
 | ||||||
| ### server routing | ### server routing | ||||||
|  | 
 | ||||||
| - content | - content | ||||||
|   - \d+ |   - \d+ | ||||||
|     - comic |     - comic | ||||||
|  | @ -31,6 +32,7 @@ | ||||||
| - profile | - profile | ||||||
| 
 | 
 | ||||||
| ## TODO | ## TODO | ||||||
|  | 
 | ||||||
| - server push | - server push | ||||||
| - ~~permission~~ | - ~~permission~~ | ||||||
| - diff | - diff | ||||||
|  |  | ||||||
							
								
								
									
										1133
									
								
								pnpm-lock.yaml
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										1133
									
								
								pnpm-lock.yaml
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							|  | @ -1,7 +1,7 @@ | ||||||
| import {ipcRenderer, contextBridge} from 'electron'; | import { contextBridge, ipcRenderer } from "electron"; | ||||||
| 
 | 
 | ||||||
| contextBridge.exposeInMainWorld('electron',{ | contextBridge.exposeInMainWorld("electron", { | ||||||
|     passwordReset: async (username: string, toPw: string) => { |     passwordReset: async (username: string, toPw: string) => { | ||||||
|         return await ipcRenderer.invoke('reset_password',username,toPw); |         return await ipcRenderer.invoke("reset_password", username, toPw); | ||||||
| } |     }, | ||||||
| }); | }); | ||||||
|  | @ -1,38 +1,38 @@ | ||||||
| import { randomBytes } from 'crypto'; | import { randomBytes } from "crypto"; | ||||||
| import { existsSync, readFileSync, writeFileSync } from 'fs'; | import { existsSync, readFileSync, writeFileSync } from "fs"; | ||||||
| import { Permission } from './permission/permission'; | import { Permission } from "./permission/permission"; | ||||||
| 
 | 
 | ||||||
| export interface SettingConfig { | export interface SettingConfig { | ||||||
|     /** |     /** | ||||||
|      * if true, server will bind on '127.0.0.1' rather than '0.0.0.0' |      * if true, server will bind on '127.0.0.1' rather than '0.0.0.0' | ||||||
|      */ |      */ | ||||||
|     localmode: boolean, |     localmode: boolean; | ||||||
|     /** |     /** | ||||||
|      * secure only |      * secure only | ||||||
|      */ |      */ | ||||||
|     secure: boolean, |     secure: boolean; | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * guest permission |      * guest permission | ||||||
|      */ |      */ | ||||||
|     guest: (Permission)[], |     guest: (Permission)[]; | ||||||
|     /** |     /** | ||||||
|      * JWT secret key. if you change its value, all access tokens are invalidated. |      * JWT secret key. if you change its value, all access tokens are invalidated. | ||||||
|      */ |      */ | ||||||
|     jwt_secretkey: string, |     jwt_secretkey: string; | ||||||
|     /** |     /** | ||||||
|      * the port which running server is binding on. |      * the port which running server is binding on. | ||||||
|      */ |      */ | ||||||
|     port:number, |     port: number; | ||||||
| 
 | 
 | ||||||
|     mode:"development"|"production", |     mode: "development" | "production"; | ||||||
|     /** |     /** | ||||||
|      * if true, do not show 'electron' window and show terminal only. |      * if true, do not show 'electron' window and show terminal only. | ||||||
|      */ |      */ | ||||||
|     cli:boolean, |     cli: boolean; | ||||||
|     /** forbid to login admin from remote client. but, it do not invalidate access token. |     /** forbid to login admin from remote client. but, it do not invalidate access token. | ||||||
|      * if you want to invalidate access token, change 'jwt_secretkey'. */ |      * if you want to invalidate access token, change 'jwt_secretkey'. */ | ||||||
|     forbid_remote_admin_login:boolean, |     forbid_remote_admin_login: boolean; | ||||||
| } | } | ||||||
| const default_setting: SettingConfig = { | const default_setting: SettingConfig = { | ||||||
|     localmode: true, |     localmode: true, | ||||||
|  | @ -43,7 +43,7 @@ const default_setting:SettingConfig = { | ||||||
|     mode: "production", |     mode: "production", | ||||||
|     cli: false, |     cli: false, | ||||||
|     forbid_remote_admin_login: true, |     forbid_remote_admin_login: true, | ||||||
| } | }; | ||||||
| let setting: null | SettingConfig = null; | let setting: null | SettingConfig = null; | ||||||
| 
 | 
 | ||||||
| const setEmptyToDefault = (target: any, default_table: SettingConfig) => { | const setEmptyToDefault = (target: any, default_table: SettingConfig) => { | ||||||
|  | @ -56,7 +56,7 @@ const setEmptyToDefault = (target:any,default_table:SettingConfig)=>{ | ||||||
|         diff_occur = true; |         diff_occur = true; | ||||||
|     } |     } | ||||||
|     return diff_occur; |     return diff_occur; | ||||||
| } | }; | ||||||
| 
 | 
 | ||||||
| export const read_setting_from_file = () => { | export const read_setting_from_file = () => { | ||||||
|     let ret = existsSync("settings.json") ? JSON.parse(readFileSync("settings.json", { encoding: "utf8" })) : {}; |     let ret = existsSync("settings.json") ? JSON.parse(readFileSync("settings.json", { encoding: "utf8" })) : {}; | ||||||
|  | @ -65,7 +65,7 @@ export const read_setting_from_file = ()=>{ | ||||||
|         writeFileSync("settings.json", JSON.stringify(ret)); |         writeFileSync("settings.json", JSON.stringify(ret)); | ||||||
|     } |     } | ||||||
|     return ret as SettingConfig; |     return ret as SettingConfig; | ||||||
| } | }; | ||||||
| export function get_setting(): SettingConfig { | export function get_setting(): SettingConfig { | ||||||
|     if (setting === null) { |     if (setting === null) { | ||||||
|         setting = read_setting_from_file(); |         setting = read_setting_from_file(); | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import { Document, DocumentAccessor, DocumentBody, QueryListOption } from "../../model/doc"; | import { Document, DocumentAccessor, DocumentBody, QueryListOption } from "../../model/doc"; | ||||||
| import {toQueryString} from './util'; | import { toQueryString } from "./util"; | ||||||
| const baseurl = "/api/doc"; | const baseurl = "/api/doc"; | ||||||
| 
 | 
 | ||||||
| export * from "../../model/doc"; | export * from "../../model/doc"; | ||||||
|  | @ -11,20 +11,20 @@ export class ClientDocumentAccessor implements DocumentAccessor{ | ||||||
|     addList: (content_list: DocumentBody[]) => Promise<number[]>; |     addList: (content_list: DocumentBody[]) => Promise<number[]>; | ||||||
|     async findByPath(basepath: string, filename?: string): Promise<Document[]> { |     async findByPath(basepath: string, filename?: string): Promise<Document[]> { | ||||||
|         throw new Error("not allowed"); |         throw new Error("not allowed"); | ||||||
|     }; |     } | ||||||
|     async findDeleted(content_type: string): Promise<Document[]> { |     async findDeleted(content_type: string): Promise<Document[]> { | ||||||
|         throw new Error("not allowed"); |         throw new Error("not allowed"); | ||||||
|     }; |     } | ||||||
|     async findList(option?: QueryListOption | undefined): Promise<Document[]> { |     async findList(option?: QueryListOption | undefined): Promise<Document[]> { | ||||||
|         let res = await fetch(`${baseurl}/search?${option !== undefined ? toQueryString(option) : ""}`); |         let res = await fetch(`${baseurl}/search?${option !== undefined ? toQueryString(option) : ""}`); | ||||||
|         if(res.status == 401) throw new FetchFailError("Unauthorized") |         if (res.status == 401) throw new FetchFailError("Unauthorized"); | ||||||
|         if (res.status !== 200) throw new FetchFailError("findList Failed"); |         if (res.status !== 200) throw new FetchFailError("findList Failed"); | ||||||
|         let ret = await res.json(); |         let ret = await res.json(); | ||||||
|         return ret; |         return ret; | ||||||
|     } |     } | ||||||
|     async findById(id: number, tagload?: boolean | undefined): Promise<Document | undefined> { |     async findById(id: number, tagload?: boolean | undefined): Promise<Document | undefined> { | ||||||
|         let res = await fetch(`${baseurl}/${id}`); |         let res = await fetch(`${baseurl}/${id}`); | ||||||
|         if(res.status !== 200) throw new FetchFailError("findById Failed");; |         if (res.status !== 200) throw new FetchFailError("findById Failed"); | ||||||
|         let ret = await res.json(); |         let ret = await res.json(); | ||||||
|         return ret; |         return ret; | ||||||
|     } |     } | ||||||
|  | @ -35,14 +35,14 @@ export class ClientDocumentAccessor implements DocumentAccessor{ | ||||||
|         throw new Error("not implement"); |         throw new Error("not implement"); | ||||||
|         return []; |         return []; | ||||||
|     } |     } | ||||||
|     async update(c: Partial<Document> & { id: number; }): Promise<boolean>{ |     async update(c: Partial<Document> & { id: number }): Promise<boolean> { | ||||||
|         const { id, ...rest } = c; |         const { id, ...rest } = c; | ||||||
|         const res = await fetch(`${baseurl}/${id}`, { |         const res = await fetch(`${baseurl}/${id}`, { | ||||||
|             method: "POST", |             method: "POST", | ||||||
|             body: JSON.stringify(rest), |             body: JSON.stringify(rest), | ||||||
|             headers: { |             headers: { | ||||||
|                 'content-type':"application/json" |                 "content-type": "application/json", | ||||||
|             } |             }, | ||||||
|         }); |         }); | ||||||
|         const ret = await res.json(); |         const ret = await res.json(); | ||||||
|         return ret; |         return ret; | ||||||
|  | @ -53,15 +53,15 @@ export class ClientDocumentAccessor implements DocumentAccessor{ | ||||||
|             method: "POST", |             method: "POST", | ||||||
|             body: JSON.stringify(c), |             body: JSON.stringify(c), | ||||||
|             headers: { |             headers: { | ||||||
|                 'content-type':"application/json" |                 "content-type": "application/json", | ||||||
|             } |             }, | ||||||
|         }); |         }); | ||||||
|         const ret = await res.json(); |         const ret = await res.json(); | ||||||
|         return ret; |         return ret; | ||||||
|     } |     } | ||||||
|     async del(id: number): Promise<boolean> { |     async del(id: number): Promise<boolean> { | ||||||
|         const res = await fetch(`${baseurl}/${id}`, { |         const res = await fetch(`${baseurl}/${id}`, { | ||||||
|             method: "DELETE" |             method: "DELETE", | ||||||
|         }); |         }); | ||||||
|         const ret = await res.json(); |         const ret = await res.json(); | ||||||
|         return ret; |         return ret; | ||||||
|  | @ -72,8 +72,8 @@ export class ClientDocumentAccessor implements DocumentAccessor{ | ||||||
|             method: "POST", |             method: "POST", | ||||||
|             body: JSON.stringify(rest), |             body: JSON.stringify(rest), | ||||||
|             headers: { |             headers: { | ||||||
|                 'content-type':"application/json" |                 "content-type": "application/json", | ||||||
|             } |             }, | ||||||
|         }); |         }); | ||||||
|         const ret = await res.json(); |         const ret = await res.json(); | ||||||
|         return ret; |         return ret; | ||||||
|  | @ -84,16 +84,16 @@ export class ClientDocumentAccessor implements DocumentAccessor{ | ||||||
|             method: "DELETE", |             method: "DELETE", | ||||||
|             body: JSON.stringify(rest), |             body: JSON.stringify(rest), | ||||||
|             headers: { |             headers: { | ||||||
|                 'content-type':"application/json" |                 "content-type": "application/json", | ||||||
|             } |             }, | ||||||
|         }); |         }); | ||||||
|         const ret = await res.json(); |         const ret = await res.json(); | ||||||
|         return ret; |         return ret; | ||||||
|     } |     } | ||||||
| } | } | ||||||
| export const CDocumentAccessor = new ClientDocumentAccessor; | export const CDocumentAccessor = new ClientDocumentAccessor(); | ||||||
| export const makeThumbnailUrl = (x: Document) => { | export const makeThumbnailUrl = (x: Document) => { | ||||||
|     return `${baseurl}/${x.id}/${x.content_type}/thumbnail`; |     return `${baseurl}/${x.id}/${x.content_type}/thumbnail`; | ||||||
| } | }; | ||||||
| 
 | 
 | ||||||
| export default CDocumentAccessor; | export default CDocumentAccessor; | ||||||
|  | @ -1,20 +1,19 @@ | ||||||
| 
 |  | ||||||
| type Representable = string | number | boolean; | type Representable = string | number | boolean; | ||||||
| 
 | 
 | ||||||
| type ToQueryStringA = { | type ToQueryStringA = { | ||||||
|     [name:string]:Representable|Representable[]|undefined |     [name: string]: Representable | Representable[] | undefined; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const toQueryString = (obj: ToQueryStringA) => { | export const toQueryString = (obj: ToQueryStringA) => { | ||||||
|     return Object.entries(obj) |     return Object.entries(obj) | ||||||
|         .filter((e): e is [string,Representable|Representable[]] =>  |         .filter((e): e is [string, Representable | Representable[]] => e[1] !== undefined) | ||||||
|                 e[1] !== undefined) |  | ||||||
|         .map(e => |         .map(e => | ||||||
|             e[1] instanceof Array |             e[1] instanceof Array | ||||||
|             ? e[1].map(f=>`${e[0]}=${(f)}`).join('&')  |                 ? e[1].map(f => `${e[0]}=${(f)}`).join("&") | ||||||
|             : `${e[0]}=${(e[1])}`) |                 : `${e[0]}=${(e[1])}` | ||||||
|         .join('&'); |         ) | ||||||
| } |         .join("&"); | ||||||
|  | }; | ||||||
| export const QueryStringToMap = (query: string) => { | export const QueryStringToMap = (query: string) => { | ||||||
|     const keyValue = query.slice(query.indexOf("?") + 1).split("&"); |     const keyValue = query.slice(query.indexOf("?") + 1).split("&"); | ||||||
|     const param: { [k: string]: string | string[] } = {}; |     const param: { [k: string]: string | string[] } = {}; | ||||||
|  | @ -23,13 +22,11 @@ export const QueryStringToMap = (query:string) =>{ | ||||||
|         const pv = param[k]; |         const pv = param[k]; | ||||||
|         if (pv === undefined) { |         if (pv === undefined) { | ||||||
|             param[k] = v; |             param[k] = v; | ||||||
|         } |         } else if (typeof pv === "string") { | ||||||
|         else if(typeof pv === "string"){ |  | ||||||
|             param[k] = [pv, v]; |             param[k] = [pv, v]; | ||||||
|         } |         } else { | ||||||
|         else{ |  | ||||||
|             pv.push(v); |             pv.push(v); | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
|     return param; |     return param; | ||||||
| } | }; | ||||||
|  |  | ||||||
|  | @ -1,21 +1,21 @@ | ||||||
| import React, { createContext, useEffect, useRef, useState } from 'react'; | import { createTheme, ThemeProvider } from "@mui/material"; | ||||||
| import ReactDom from 'react-dom'; | import React, { createContext, useEffect, useRef, useState } from "react"; | ||||||
| import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; | import ReactDom from "react-dom"; | ||||||
|  | import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; | ||||||
| import { | import { | ||||||
|     Gallery, |     DifferencePage, | ||||||
|     DocumentAbout, |     DocumentAbout, | ||||||
|  |     Gallery, | ||||||
|     LoginPage, |     LoginPage, | ||||||
|     NotFoundPage, |     NotFoundPage, | ||||||
|     ProfilePage, |     ProfilePage, | ||||||
|     DifferencePage, |  | ||||||
|     SettingPage, |  | ||||||
|     ReaderPage, |     ReaderPage, | ||||||
|     TagsPage |     SettingPage, | ||||||
| } from './page/mod'; |     TagsPage, | ||||||
| import { getInitialValue, UserContext } from './state'; | } from "./page/mod"; | ||||||
| import { ThemeProvider, createTheme } from '@mui/material'; | import { getInitialValue, UserContext } from "./state"; | ||||||
| 
 | 
 | ||||||
| import './css/style.css'; | import "./css/style.css"; | ||||||
| 
 | 
 | ||||||
| const theme = createTheme(); | const theme = createTheme(); | ||||||
| 
 | 
 | ||||||
|  | @ -31,16 +31,18 @@ const App = () => { | ||||||
|     })(); |     })(); | ||||||
|     // useEffect(()=>{});
 |     // useEffect(()=>{});
 | ||||||
|     return ( |     return ( | ||||||
|         <UserContext.Provider value={{ |         <UserContext.Provider | ||||||
|  |             value={{ | ||||||
|                 username: user, |                 username: user, | ||||||
|                 setUsername: setUser, |                 setUsername: setUser, | ||||||
|                 permission: userPermission, |                 permission: userPermission, | ||||||
|             setPermission: setUserPermission |                 setPermission: setUserPermission, | ||||||
|         }}> |             }} | ||||||
|  |         > | ||||||
|             <ThemeProvider theme={theme}> |             <ThemeProvider theme={theme}> | ||||||
|                 <BrowserRouter> |                 <BrowserRouter> | ||||||
|                     <Routes> |                     <Routes> | ||||||
|                         <Route path="/" element={<Navigate replace to='/search?' />} /> |                         <Route path="/" element={<Navigate replace to="/search?" />} /> | ||||||
|                         <Route path="/search" element={<Gallery />} /> |                         <Route path="/search" element={<Gallery />} /> | ||||||
|                         <Route path="/doc/:id" element={<DocumentAbout />}></Route> |                         <Route path="/doc/:id" element={<DocumentAbout />}></Route> | ||||||
|                         <Route path="/doc/:id/reader" element={<ReaderPage />}></Route> |                         <Route path="/doc/:id/reader" element={<ReaderPage />}></Route> | ||||||
|  | @ -53,10 +55,11 @@ const App = () => { | ||||||
|                     </Routes> |                     </Routes> | ||||||
|                 </BrowserRouter> |                 </BrowserRouter> | ||||||
|             </ThemeProvider> |             </ThemeProvider> | ||||||
|         </UserContext.Provider>); |         </UserContext.Provider> | ||||||
|  |     ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| ReactDom.render( | ReactDom.render( | ||||||
|     <App />, |     <App />, | ||||||
|     document.getElementById("root") |     document.getElementById("root"), | ||||||
| ); | ); | ||||||
|  | @ -1,25 +1,24 @@ | ||||||
| import esbuild from 'esbuild'; | import esbuild from "esbuild"; | ||||||
| 
 | 
 | ||||||
| async function main() { | async function main() { | ||||||
|     try { |     try { | ||||||
|         const result = await esbuild.build({ |         const result = await esbuild.build({ | ||||||
|             entryPoints: ['app.tsx'], |             entryPoints: ["app.tsx"], | ||||||
|             bundle: true, |             bundle: true, | ||||||
|             outfile: '../../dist/bundle.js', |             outfile: "../../dist/bundle.js", | ||||||
|             platform: 'browser', |             platform: "browser", | ||||||
|             sourcemap: true, |             sourcemap: true, | ||||||
|             minify: true, |             minify: true, | ||||||
|             target: ['chrome100', 'firefox100'], |             target: ["chrome100", "firefox100"], | ||||||
|             watch: { |             watch: { | ||||||
|                 onRebuild: async (err, _result) => { |                 onRebuild: async (err, _result) => { | ||||||
|                     if (err) { |                     if (err) { | ||||||
|                         console.error('watch build failed: ',err); |                         console.error("watch build failed: ", err); | ||||||
|                     } |                     } else { | ||||||
|                     else{ |                         console.log("watch build success"); | ||||||
|                         console.log('watch build success'); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|                     } |                     } | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|         }); |         }); | ||||||
|         console.log("watching..."); |         console.log("watching..."); | ||||||
|         return result; |         return result; | ||||||
|  |  | ||||||
|  | @ -1,27 +1,27 @@ | ||||||
| import React, { } from 'react'; | import React, {} from "react"; | ||||||
| import { Link as RouterLink } from 'react-router-dom'; | import { Link as RouterLink } from "react-router-dom"; | ||||||
| import { Document } from '../accessor/document'; | import { Document } from "../accessor/document"; | ||||||
| 
 | 
 | ||||||
| import { Link, Paper, Theme, Box, useTheme, Typography, Grid, Button } from '@mui/material'; | import { Box, Button, Grid, Link, Paper, Theme, Typography, useTheme } from "@mui/material"; | ||||||
| import { ThumbnailContainer } from '../page/reader/reader'; | import { TagChip } from "../component/tagchip"; | ||||||
| import { TagChip } from '../component/tagchip'; | import { ThumbnailContainer } from "../page/reader/reader"; | ||||||
| 
 | 
 | ||||||
| import DocumentAccessor from '../accessor/document'; | import DocumentAccessor from "../accessor/document"; | ||||||
| 
 | 
 | ||||||
| export const makeContentInfoUrl = (id: number) => `/doc/${id}`; | export const makeContentInfoUrl = (id: number) => `/doc/${id}`; | ||||||
| export const makeContentReaderUrl = (id: number) => `/doc/${id}/reader`; | export const makeContentReaderUrl = (id: number) => `/doc/${id}/reader`; | ||||||
| 
 | 
 | ||||||
| const useStyles = ((theme: Theme) => ({ | const useStyles = (theme: Theme) => ({ | ||||||
|     thumbnail_content: { |     thumbnail_content: { | ||||||
|         maxHeight: '400px', |         maxHeight: "400px", | ||||||
|         maxWidth: 'min(400px, 100vw)', |         maxWidth: "min(400px, 100vw)", | ||||||
|     }, |     }, | ||||||
|     tag_list: { |     tag_list: { | ||||||
|         display: 'flex', |         display: "flex", | ||||||
|         justifyContent: 'flex-start', |         justifyContent: "flex-start", | ||||||
|         flexWrap: 'wrap', |         flexWrap: "wrap", | ||||||
|         overflowY: 'hidden', |         overflowY: "hidden", | ||||||
|         '& > *': { |         "& > *": { | ||||||
|             margin: theme.spacing(0.5), |             margin: theme.spacing(0.5), | ||||||
|         }, |         }, | ||||||
|     }, |     }, | ||||||
|  | @ -32,100 +32,125 @@ const useStyles = ((theme: Theme) => ({ | ||||||
|         padding: theme.spacing(2), |         padding: theme.spacing(2), | ||||||
|     }, |     }, | ||||||
|     subinfoContainer: { |     subinfoContainer: { | ||||||
|         display: 'grid', |         display: "grid", | ||||||
|         gridTemplateColumns: '100px auto', |         gridTemplateColumns: "100px auto", | ||||||
|         overflowY: 'hidden', |         overflowY: "hidden", | ||||||
|         alignItems: 'baseline', |         alignItems: "baseline", | ||||||
|     }, |     }, | ||||||
|     short_subinfoContainer: { |     short_subinfoContainer: { | ||||||
|         [theme.breakpoints.down("md")]: { |         [theme.breakpoints.down("md")]: { | ||||||
|             display: 'none', |             display: "none", | ||||||
|         }, |         }, | ||||||
|     }, |     }, | ||||||
|     short_root: { |     short_root: { | ||||||
|         overflowY: 'hidden', |         overflowY: "hidden", | ||||||
|         display: 'flex', |         display: "flex", | ||||||
|         flexDirection: 'column', |         flexDirection: "column", | ||||||
|         [theme.breakpoints.up("sm")]: { |         [theme.breakpoints.up("sm")]: { | ||||||
|             height: 200, |             height: 200, | ||||||
|             flexDirection: 'row', |             flexDirection: "row", | ||||||
|         }, |         }, | ||||||
|     }, |     }, | ||||||
|     short_thumbnail_anchor: { |     short_thumbnail_anchor: { | ||||||
|         background: '#272733', |         background: "#272733", | ||||||
|         display: 'flex', |         display: "flex", | ||||||
|         alignItems: 'center', |         alignItems: "center", | ||||||
|         justifyContent: 'center', |         justifyContent: "center", | ||||||
|         [theme.breakpoints.up("sm")]: { |         [theme.breakpoints.up("sm")]: { | ||||||
|             width: theme.spacing(25), |             width: theme.spacing(25), | ||||||
|             height: theme.spacing(25), |             height: theme.spacing(25), | ||||||
|             flexShrink: 0, |             flexShrink: 0, | ||||||
|         } |         }, | ||||||
|     }, |     }, | ||||||
|     short_thumbnail_content: { |     short_thumbnail_content: { | ||||||
|         maxWidth: '100%', |         maxWidth: "100%", | ||||||
|         maxHeight: '100%', |         maxHeight: "100%", | ||||||
|     }, |     }, | ||||||
| })) | }); | ||||||
| 
 | 
 | ||||||
| export const ContentInfo = (props: { | export const ContentInfo = (props: { | ||||||
|     document: Document, children?: React.ReactNode, classes?: { |     document: Document; | ||||||
|         root?: string, |     children?: React.ReactNode; | ||||||
|         thumbnail_anchor?: string, |     classes?: { | ||||||
|         thumbnail_content?: string, |         root?: string; | ||||||
|         tag_list?: string, |         thumbnail_anchor?: string; | ||||||
|         title?: string, |         thumbnail_content?: string; | ||||||
|         infoContainer?: string, |         tag_list?: string; | ||||||
|         subinfoContainer?: string |         title?: string; | ||||||
|     }, |         infoContainer?: string; | ||||||
|     gallery?: string, |         subinfoContainer?: string; | ||||||
|     short?: boolean |     }; | ||||||
|  |     gallery?: string; | ||||||
|  |     short?: boolean; | ||||||
| }) => { | }) => { | ||||||
|     const theme = useTheme(); |     const theme = useTheme(); | ||||||
|     const document = props.document; |     const document = props.document; | ||||||
|     const url = props.gallery === undefined ? makeContentReaderUrl(document.id) : makeContentInfoUrl(document.id); |     const url = props.gallery === undefined ? makeContentReaderUrl(document.id) : makeContentInfoUrl(document.id); | ||||||
|     return (<Paper sx={{ |     return ( | ||||||
|  |         <Paper | ||||||
|  |             sx={{ | ||||||
|                 display: "flex", |                 display: "flex", | ||||||
|                 height: "400px", |                 height: "400px", | ||||||
|                 [theme.breakpoints.down("sm")]: { |                 [theme.breakpoints.down("sm")]: { | ||||||
|                     flexDirection: "column", |                     flexDirection: "column", | ||||||
|                     alignItems: "center", |                     alignItems: "center", | ||||||
|                     height: "auto", |                     height: "auto", | ||||||
|         } |                 }, | ||||||
|     }} elevation={4}> |             }} | ||||||
|         <Link component={RouterLink} to={{ |             elevation={4} | ||||||
|             pathname: makeContentReaderUrl(document.id) |         > | ||||||
|         }}> |             <Link | ||||||
|             {document.deleted_at === null ? |                 component={RouterLink} | ||||||
|                 (<ThumbnailContainer content={document}/>) |                 to={{ | ||||||
|                 : (<Typography variant='h4'>Deleted</Typography>)} |                     pathname: makeContentReaderUrl(document.id), | ||||||
|  |                 }} | ||||||
|  |             > | ||||||
|  |                 {document.deleted_at === null | ||||||
|  |                     ? <ThumbnailContainer content={document} /> | ||||||
|  |                     : <Typography variant="h4">Deleted</Typography>} | ||||||
|             </Link> |             </Link> | ||||||
|             <Box> |             <Box> | ||||||
|             <Link variant='h5' color='inherit' component={RouterLink} to={{pathname: url}}> |                 <Link variant="h5" color="inherit" component={RouterLink} to={{ pathname: url }}> | ||||||
|                     {document.title} |                     {document.title} | ||||||
|                 </Link> |                 </Link> | ||||||
|                 <Box> |                 <Box> | ||||||
|                 {props.short ? (<Box>{document.tags.map(x => |                     {props.short | ||||||
|                     (<TagChip key={x} label={x} clickable tagname={x} size="small"></TagChip>) |                         ? ( | ||||||
|                 )}</Box>) : ( |                             <Box> | ||||||
|                     <ComicDetailTag tags={document.tags} path={document.basepath+"/"+document.filename} |                                 {document.tags.map(x => ( | ||||||
|  |                                     <TagChip key={x} label={x} clickable tagname={x} size="small"></TagChip> | ||||||
|  |                                 ))} | ||||||
|  |                             </Box> | ||||||
|  |                         ) | ||||||
|  |                         : ( | ||||||
|  |                             <ComicDetailTag | ||||||
|  |                                 tags={document.tags} | ||||||
|  |                                 path={document.basepath + "/" + document.filename} | ||||||
|                                 createdAt={document.created_at} |                                 createdAt={document.created_at} | ||||||
|                                 deletedAt={document.deleted_at != null ? document.deleted_at : undefined} |                                 deletedAt={document.deleted_at != null ? document.deleted_at : undefined} | ||||||
|                      ></ComicDetailTag>) |                             > | ||||||
|                 } |                             </ComicDetailTag> | ||||||
|  |                         )} | ||||||
|                 </Box> |                 </Box> | ||||||
|             {document.deleted_at != null && |                 {document.deleted_at != null | ||||||
|             <Button onClick={()=>{documentDelete(document.id);}}>Delete</Button> |                     && ( | ||||||
|             } |                         <Button | ||||||
|  |                             onClick={() => { | ||||||
|  |                                 documentDelete(document.id); | ||||||
|  |                             }} | ||||||
|  |                         > | ||||||
|  |                             Delete | ||||||
|  |                         </Button> | ||||||
|  |                     )} | ||||||
|             </Box> |             </Box> | ||||||
|     </Paper>); |         </Paper> | ||||||
| } |     ); | ||||||
|  | }; | ||||||
| async function documentDelete(id: number) { | async function documentDelete(id: number) { | ||||||
|     const t = await DocumentAccessor.del(id); |     const t = await DocumentAccessor.del(id); | ||||||
|     if (t) { |     if (t) { | ||||||
|         alert("document deleted!"); |         alert("document deleted!"); | ||||||
|     } |     } else { | ||||||
|     else{ |  | ||||||
|         alert("document already deleted."); |         alert("document already deleted."); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -146,40 +171,54 @@ function ComicDetailTag(prop: { | ||||||
|         tagTable[kind] = tags; |         tagTable[kind] = tags; | ||||||
|         allTag = allTag.filter(x => !x.startsWith(kind + ":")); |         allTag = allTag.filter(x => !x.startsWith(kind + ":")); | ||||||
|     } |     } | ||||||
|     return (<Grid container> |     return ( | ||||||
|  |         <Grid container> | ||||||
|             {tagKind.map(key => ( |             {tagKind.map(key => ( | ||||||
|                 <React.Fragment key={key}> |                 <React.Fragment key={key}> | ||||||
|                     <Grid item xs={3}> |                     <Grid item xs={3}> | ||||||
|                     <Typography variant='subtitle1'>{key}</Typography> |                         <Typography variant="subtitle1">{key}</Typography> | ||||||
|                     </Grid> |                     </Grid> | ||||||
|                     <Grid item xs={9}> |                     <Grid item xs={9}> | ||||||
|                         <Box>{tagTable[key].length !== 0 ? tagTable[key].join(", ") : "N/A"}</Box> |                         <Box>{tagTable[key].length !== 0 ? tagTable[key].join(", ") : "N/A"}</Box> | ||||||
|                     </Grid> |                     </Grid> | ||||||
|                 </React.Fragment> |                 </React.Fragment> | ||||||
|             ))} |             ))} | ||||||
|         { prop.path != undefined && <><Grid item xs={3}> |             {prop.path != undefined && ( | ||||||
|             <Typography variant='subtitle1'>Path</Typography> |                 <> | ||||||
|         </Grid><Grid item xs={9}> |  | ||||||
|                 <Box>{prop.path}</Box> |  | ||||||
|             </Grid></> |  | ||||||
|         } |  | ||||||
|         { prop.createdAt != undefined && <><Grid item xs={3}> |  | ||||||
|             <Typography variant='subtitle1'>CreatedAt</Typography> |  | ||||||
|         </Grid><Grid item xs={9}> |  | ||||||
|                 <Box>{new Date(prop.createdAt).toUTCString()}</Box> |  | ||||||
|             </Grid></> |  | ||||||
|         } |  | ||||||
|         { prop.deletedAt != undefined && <><Grid item xs={3}> |  | ||||||
|             <Typography variant='subtitle1'>DeletedAt</Typography> |  | ||||||
|         </Grid><Grid item xs={9}> |  | ||||||
|                 <Box>{new Date(prop.deletedAt).toUTCString()}</Box> |  | ||||||
|             </Grid></> |  | ||||||
|         } |  | ||||||
|                     <Grid item xs={3}> |                     <Grid item xs={3}> | ||||||
|         <Typography variant='subtitle1'>Tags</Typography> |                         <Typography variant="subtitle1">Path</Typography> | ||||||
|                     </Grid> |                     </Grid> | ||||||
|                     <Grid item xs={9}> |                     <Grid item xs={9}> | ||||||
|             {allTag.map(x => (<TagChip key={x} label={x} clickable tagname={x} size="small"></TagChip>))} |                         <Box>{prop.path}</Box> | ||||||
|                     </Grid> |                     </Grid> | ||||||
|     </Grid>); |                 </> | ||||||
|  |             )} | ||||||
|  |             {prop.createdAt != undefined && ( | ||||||
|  |                 <> | ||||||
|  |                     <Grid item xs={3}> | ||||||
|  |                         <Typography variant="subtitle1">CreatedAt</Typography> | ||||||
|  |                     </Grid> | ||||||
|  |                     <Grid item xs={9}> | ||||||
|  |                         <Box>{new Date(prop.createdAt).toUTCString()}</Box> | ||||||
|  |                     </Grid> | ||||||
|  |                 </> | ||||||
|  |             )} | ||||||
|  |             {prop.deletedAt != undefined && ( | ||||||
|  |                 <> | ||||||
|  |                     <Grid item xs={3}> | ||||||
|  |                         <Typography variant="subtitle1">DeletedAt</Typography> | ||||||
|  |                     </Grid> | ||||||
|  |                     <Grid item xs={9}> | ||||||
|  |                         <Box>{new Date(prop.deletedAt).toUTCString()}</Box> | ||||||
|  |                     </Grid> | ||||||
|  |                 </> | ||||||
|  |             )} | ||||||
|  |             <Grid item xs={3}> | ||||||
|  |                 <Typography variant="subtitle1">Tags</Typography> | ||||||
|  |             </Grid> | ||||||
|  |             <Grid item xs={9}> | ||||||
|  |                 {allTag.map(x => <TagChip key={x} label={x} clickable tagname={x} size="small"></TagChip>)} | ||||||
|  |             </Grid> | ||||||
|  |         </Grid> | ||||||
|  |     ); | ||||||
| } | } | ||||||
|  | @ -1,21 +1,35 @@ | ||||||
| import React, { useContext, useState } from 'react'; | import { AccountCircle, ChevronLeft, ChevronRight, Menu as MenuIcon, Search as SearchIcon } from "@mui/icons-material"; | ||||||
| import { | import { | ||||||
|     Button, CssBaseline, Divider, IconButton, List, ListItem, Drawer, |     AppBar, | ||||||
|     AppBar, Toolbar, Typography, InputBase, ListItemIcon, ListItemText, Menu, MenuItem, |     Button, | ||||||
|     Hidden, Tooltip, Link, styled |     CssBaseline, | ||||||
| } from '@mui/material'; |     Divider, | ||||||
| import { alpha, Theme, useTheme } from '@mui/material/styles'; |     Drawer, | ||||||
| import { |     Hidden, | ||||||
|     ChevronLeft, ChevronRight, Menu as MenuIcon, Search as SearchIcon, AccountCircle |     IconButton, | ||||||
| } from '@mui/icons-material'; |     InputBase, | ||||||
|  |     Link, | ||||||
|  |     List, | ||||||
|  |     ListItem, | ||||||
|  |     ListItemIcon, | ||||||
|  |     ListItemText, | ||||||
|  |     Menu, | ||||||
|  |     MenuItem, | ||||||
|  |     styled, | ||||||
|  |     Toolbar, | ||||||
|  |     Tooltip, | ||||||
|  |     Typography, | ||||||
|  | } from "@mui/material"; | ||||||
|  | import { alpha, Theme, useTheme } from "@mui/material/styles"; | ||||||
|  | import React, { useContext, useState } from "react"; | ||||||
| 
 | 
 | ||||||
| import { Link as RouterLink, useNavigate } from 'react-router-dom'; | import { Link as RouterLink, useNavigate } from "react-router-dom"; | ||||||
| import { doLogout, UserContext } from '../state'; | import { doLogout, UserContext } from "../state"; | ||||||
| 
 | 
 | ||||||
| const drawerWidth = 270; | const drawerWidth = 270; | ||||||
| 
 | 
 | ||||||
| const DrawerHeader = styled('div')(({ theme }) => ({ | const DrawerHeader = styled("div")(({ theme }) => ({ | ||||||
|     ...theme.mixins.toolbar |     ...theme.mixins.toolbar, | ||||||
| })); | })); | ||||||
| 
 | 
 | ||||||
| const StyledDrawer = styled(Drawer)(({ theme }) => ({ | const StyledDrawer = styled(Drawer)(({ theme }) => ({ | ||||||
|  | @ -24,58 +38,56 @@ const StyledDrawer = styled(Drawer)(({ theme }) => ({ | ||||||
|     [theme.breakpoints.up("sm")]: { |     [theme.breakpoints.up("sm")]: { | ||||||
|         width: drawerWidth, |         width: drawerWidth, | ||||||
|     }, |     }, | ||||||
| } | })); | ||||||
| )); | const StyledSearchBar = styled("div")(({ theme }) => ({ | ||||||
| const StyledSearchBar = styled('div')(({ theme }) => ({ |     position: "relative", | ||||||
|     position: 'relative', |  | ||||||
|     borderRadius: theme.shape.borderRadius, |     borderRadius: theme.shape.borderRadius, | ||||||
|     backgroundColor: alpha(theme.palette.common.white, 0.15), |     backgroundColor: alpha(theme.palette.common.white, 0.15), | ||||||
|     '&:hover': { |     "&:hover": { | ||||||
|         backgroundColor: alpha(theme.palette.common.white, 0.25), |         backgroundColor: alpha(theme.palette.common.white, 0.25), | ||||||
|     }, |     }, | ||||||
|     marginLeft: 0, |     marginLeft: 0, | ||||||
|     width: '100%', |     width: "100%", | ||||||
|     [theme.breakpoints.up('sm')]: { |     [theme.breakpoints.up("sm")]: { | ||||||
|         marginLeft: theme.spacing(1), |         marginLeft: theme.spacing(1), | ||||||
|         width: 'auto', |         width: "auto", | ||||||
|     }, |     }, | ||||||
| })); | })); | ||||||
| const StyledInputBase = styled(InputBase)(({ theme }) => ({ | const StyledInputBase = styled(InputBase)(({ theme }) => ({ | ||||||
|     color: 'inherit', |     color: "inherit", | ||||||
|     '& .MuiInputBase-input': { |     "& .MuiInputBase-input": { | ||||||
|         padding: theme.spacing(1, 1, 1, 0), |         padding: theme.spacing(1, 1, 1, 0), | ||||||
|         // vertical padding + font size from searchIcon
 |         // vertical padding + font size from searchIcon
 | ||||||
|         paddingLeft: `calc(1em + ${theme.spacing(4)})`, |         paddingLeft: `calc(1em + ${theme.spacing(4)})`, | ||||||
|         transition: theme.transitions.create('width'), |         transition: theme.transitions.create("width"), | ||||||
|         width: '100%', |         width: "100%", | ||||||
|         [theme.breakpoints.up('sm')]: { |         [theme.breakpoints.up("sm")]: { | ||||||
|             width: '12ch', |             width: "12ch", | ||||||
|             '&:focus': { |             "&:focus": { | ||||||
|                 width: '20ch', |                 width: "20ch", | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|     }, |     }, | ||||||
| })); | })); | ||||||
| 
 | 
 | ||||||
| const StyledNav = styled('nav')(({theme}) => ({ | const StyledNav = styled("nav")(({ theme }) => ({ | ||||||
|     [theme.breakpoints.up("sm")]: { |     [theme.breakpoints.up("sm")]: { | ||||||
|         width: theme.spacing(7) |         width: theme.spacing(7), | ||||||
|     } |     }, | ||||||
| })); | })); | ||||||
| 
 | 
 | ||||||
| const closedMixin = (theme: Theme) => ({ | const closedMixin = (theme: Theme) => ({ | ||||||
|     overflowX: 'hidden', |     overflowX: "hidden", | ||||||
|     width: `calc(${theme.spacing(7)} + 1px)`, |     width: `calc(${theme.spacing(7)} + 1px)`, | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| export const Headline = (prop: { | export const Headline = (prop: { | ||||||
|     children?: React.ReactNode, |     children?: React.ReactNode; | ||||||
|     classes?: { |     classes?: { | ||||||
|         content?: string, |         content?: string; | ||||||
|         toolbar?: string, |         toolbar?: string; | ||||||
|     }, |     }; | ||||||
|     menu: React.ReactNode |     menu: React.ReactNode; | ||||||
| }) => { | }) => { | ||||||
|     const [v, setv] = useState(false); |     const [v, setv] = useState(false); | ||||||
|     const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null); |     const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null); | ||||||
|  | @ -84,25 +96,36 @@ export const Headline = (prop: { | ||||||
|     const handleProfileMenuOpen = (e: React.MouseEvent<HTMLElement>) => setAnchorEl(e.currentTarget); |     const handleProfileMenuOpen = (e: React.MouseEvent<HTMLElement>) => setAnchorEl(e.currentTarget); | ||||||
|     const handleProfileMenuClose = () => setAnchorEl(null); |     const handleProfileMenuClose = () => setAnchorEl(null); | ||||||
|     const isProfileMenuOpened = Boolean(anchorEl); |     const isProfileMenuOpened = Boolean(anchorEl); | ||||||
|     const menuId = 'primary-search-account-menu'; |     const menuId = "primary-search-account-menu"; | ||||||
|     const user_ctx = useContext(UserContext); |     const user_ctx = useContext(UserContext); | ||||||
|     const isLogin = user_ctx.username !== ""; |     const isLogin = user_ctx.username !== ""; | ||||||
|     const navigate = useNavigate(); |     const navigate = useNavigate(); | ||||||
|     const [search, setSearch] = useState(""); |     const [search, setSearch] = useState(""); | ||||||
| 
 | 
 | ||||||
|     const renderProfileMenu = (<Menu |     const renderProfileMenu = ( | ||||||
|  |         <Menu | ||||||
|             anchorEl={anchorEl} |             anchorEl={anchorEl} | ||||||
|         anchorOrigin={{ horizontal: 'right', vertical: "top" }} |             anchorOrigin={{ horizontal: "right", vertical: "top" }} | ||||||
|             id={menuId} |             id={menuId} | ||||||
|             open={isProfileMenuOpened} |             open={isProfileMenuOpened} | ||||||
|             keepMounted |             keepMounted | ||||||
|         transformOrigin={{ horizontal: 'right', vertical: "top" }} |             transformOrigin={{ horizontal: "right", vertical: "top" }} | ||||||
|             onClose={handleProfileMenuClose} |             onClose={handleProfileMenuClose} | ||||||
|         > |         > | ||||||
|         <MenuItem component={RouterLink} to='/profile'>Profile</MenuItem> |             <MenuItem component={RouterLink} to="/profile">Profile</MenuItem> | ||||||
|         <MenuItem onClick={async () => { handleProfileMenuClose(); await doLogout(); user_ctx.setUsername(""); }}>Logout</MenuItem> |             <MenuItem | ||||||
|     </Menu>); |                 onClick={async () => { | ||||||
|     const drawer_contents = (<> |                     handleProfileMenuClose(); | ||||||
|  |                     await doLogout(); | ||||||
|  |                     user_ctx.setUsername(""); | ||||||
|  |                 }} | ||||||
|  |             > | ||||||
|  |                 Logout | ||||||
|  |             </MenuItem> | ||||||
|  |         </Menu> | ||||||
|  |     ); | ||||||
|  |     const drawer_contents = ( | ||||||
|  |         <> | ||||||
|             <DrawerHeader> |             <DrawerHeader> | ||||||
|                 <IconButton onClick={toggleV}> |                 <IconButton onClick={toggleV}> | ||||||
|                     {theme.direction === "ltr" ? <ChevronLeft /> : <ChevronRight />} |                     {theme.direction === "ltr" ? <ChevronLeft /> : <ChevronRight />} | ||||||
|  | @ -110,19 +133,25 @@ export const Headline = (prop: { | ||||||
|             </DrawerHeader> |             </DrawerHeader> | ||||||
|             <Divider /> |             <Divider /> | ||||||
|             {prop.menu} |             {prop.menu} | ||||||
|     </>); |         </> | ||||||
|  |     ); | ||||||
| 
 | 
 | ||||||
|     return (<div style={{ display: 'flex' }}> |     return ( | ||||||
|  |         <div style={{ display: "flex" }}> | ||||||
|             <CssBaseline /> |             <CssBaseline /> | ||||||
|         <AppBar position="fixed" sx={{ |             <AppBar | ||||||
|  |                 position="fixed" | ||||||
|  |                 sx={{ | ||||||
|                     zIndex: theme.zIndex.drawer + 1, |                     zIndex: theme.zIndex.drawer + 1, | ||||||
|             transition: theme.transitions.create(['width', 'margin'], { |                     transition: theme.transitions.create(["width", "margin"], { | ||||||
|                         easing: theme.transitions.easing.sharp, |                         easing: theme.transitions.easing.sharp, | ||||||
|                 duration: theme.transitions.duration.leavingScreen |                         duration: theme.transitions.duration.leavingScreen, | ||||||
|             }) |                     }), | ||||||
|         }}> |                 }} | ||||||
|  |             > | ||||||
|                 <Toolbar> |                 <Toolbar> | ||||||
|                 <IconButton color="inherit" |                     <IconButton | ||||||
|  |                         color="inherit" | ||||||
|                         aria-label="open drawer" |                         aria-label="open drawer" | ||||||
|                         onClick={toggleV} |                         onClick={toggleV} | ||||||
|                         edge="start" |                         edge="start" | ||||||
|  | @ -130,90 +159,114 @@ export const Headline = (prop: { | ||||||
|                     > |                     > | ||||||
|                         <MenuIcon></MenuIcon> |                         <MenuIcon></MenuIcon> | ||||||
|                     </IconButton> |                     </IconButton> | ||||||
|                 <Link variant="h5" noWrap sx={{ |                     <Link | ||||||
|                     display: 'none', |                         variant="h5" | ||||||
|  |                         noWrap | ||||||
|  |                         sx={{ | ||||||
|  |                             display: "none", | ||||||
|                             [theme.breakpoints.up("sm")]: { |                             [theme.breakpoints.up("sm")]: { | ||||||
|                         display: 'block' |                                 display: "block", | ||||||
|                     } |                             }, | ||||||
|                 }} color="inherit" component={RouterLink} to="/"> |                         }} | ||||||
|  |                         color="inherit" | ||||||
|  |                         component={RouterLink} | ||||||
|  |                         to="/" | ||||||
|  |                     > | ||||||
|                         Ionian |                         Ionian | ||||||
|                     </Link> |                     </Link> | ||||||
|                     <div style={{ flexGrow: 1 }}></div> |                     <div style={{ flexGrow: 1 }}></div> | ||||||
|                     <StyledSearchBar> |                     <StyledSearchBar> | ||||||
|                     <div style={{ |                         <div | ||||||
|  |                             style={{ | ||||||
|                                 padding: theme.spacing(0, 2), |                                 padding: theme.spacing(0, 2), | ||||||
|                         height: '100%', |                                 height: "100%", | ||||||
|                         position: 'absolute', |                                 position: "absolute", | ||||||
|                         pointerEvents: 'none', |                                 pointerEvents: "none", | ||||||
|                         display: 'flex', |                                 display: "flex", | ||||||
|                         alignItems: 'center', |                                 alignItems: "center", | ||||||
|                         justifyContent: 'center' |                                 justifyContent: "center", | ||||||
|                     }}> |                             }} | ||||||
|  |                         > | ||||||
|                             <SearchIcon onClick={() => navSearch(search)} /> |                             <SearchIcon onClick={() => navSearch(search)} /> | ||||||
|                         </div> |                         </div> | ||||||
|                     <StyledInputBase placeholder="search" |                         <StyledInputBase | ||||||
|  |                             placeholder="search" | ||||||
|                             onChange={(e) => setSearch(e.target.value)} |                             onChange={(e) => setSearch(e.target.value)} | ||||||
|                             onKeyUp={(e) => { |                             onKeyUp={(e) => { | ||||||
|                                 if (e.key === "Enter") { |                                 if (e.key === "Enter") { | ||||||
|                                     navSearch(search); |                                     navSearch(search); | ||||||
|                                 } |                                 } | ||||||
|                             }} |                             }} | ||||||
|                         value={search}></StyledInputBase> |                             value={search} | ||||||
|  |                         > | ||||||
|  |                         </StyledInputBase> | ||||||
|                     </StyledSearchBar> |                     </StyledSearchBar> | ||||||
|                 { |                     {isLogin | ||||||
|                     isLogin ? |                         ? ( | ||||||
|                             <IconButton |                             <IconButton | ||||||
|                                 edge="end" |                                 edge="end" | ||||||
|                                 aria-label="account of current user" |                                 aria-label="account of current user" | ||||||
|                                 aria-controls={menuId} |                                 aria-controls={menuId} | ||||||
|                                 aria-haspopup="true" |                                 aria-haspopup="true" | ||||||
|                                 onClick={handleProfileMenuOpen} |                                 onClick={handleProfileMenuOpen} | ||||||
|                             color="inherit"> |                                 color="inherit" | ||||||
|  |                             > | ||||||
|                                 <AccountCircle /> |                                 <AccountCircle /> | ||||||
|                             </IconButton> |                             </IconButton> | ||||||
|                         : <Button color="inherit" component={RouterLink} to="/login">Login</Button> |                         ) | ||||||
|                 } |                         : <Button color="inherit" component={RouterLink} to="/login">Login</Button>} | ||||||
|                 </Toolbar> |                 </Toolbar> | ||||||
|             </AppBar> |             </AppBar> | ||||||
|             {renderProfileMenu} |             {renderProfileMenu} | ||||||
|             <StyledNav> |             <StyledNav> | ||||||
|                 <Hidden smUp implementation="css"> |                 <Hidden smUp implementation="css"> | ||||||
|                 <StyledDrawer variant="temporary" anchor='left' open={v} onClose={toggleV} |                     <StyledDrawer | ||||||
|  |                         variant="temporary" | ||||||
|  |                         anchor="left" | ||||||
|  |                         open={v} | ||||||
|  |                         onClose={toggleV} | ||||||
|                         sx={{ |                         sx={{ | ||||||
|                         width: drawerWidth |                             width: drawerWidth, | ||||||
|                         }} |                         }} | ||||||
|                     > |                     > | ||||||
|                         {drawer_contents} |                         {drawer_contents} | ||||||
|                     </StyledDrawer> |                     </StyledDrawer> | ||||||
|                 </Hidden> |                 </Hidden> | ||||||
|                 <Hidden smDown implementation="css"> |                 <Hidden smDown implementation="css"> | ||||||
|                 <StyledDrawer variant='permanent' anchor='left' |                     <StyledDrawer | ||||||
|  |                         variant="permanent" | ||||||
|  |                         anchor="left" | ||||||
|                         sx={{ |                         sx={{ | ||||||
|                             ...closedMixin(theme), |                             ...closedMixin(theme), | ||||||
|                         '& .MuiDrawer-paper': closedMixin(theme), |                             "& .MuiDrawer-paper": closedMixin(theme), | ||||||
|                     }}> |                         }} | ||||||
|  |                     > | ||||||
|                         {drawer_contents} |                         {drawer_contents} | ||||||
|                     </StyledDrawer> |                     </StyledDrawer> | ||||||
|                 </Hidden> |                 </Hidden> | ||||||
|             </StyledNav> |             </StyledNav> | ||||||
|         <main style={{ |             <main | ||||||
|             display: 'flex', |                 style={{ | ||||||
|             flexFlow: 'column', |                     display: "flex", | ||||||
|  |                     flexFlow: "column", | ||||||
|                     flexGrow: 1, |                     flexGrow: 1, | ||||||
|                     padding: theme.spacing(3), |                     padding: theme.spacing(3), | ||||||
|                     marginTop: theme.spacing(6), |                     marginTop: theme.spacing(6), | ||||||
|         }}> |                 }} | ||||||
|             <div style={{ |             > | ||||||
|             }} ></div> |                 <div style={{}}></div> | ||||||
|                 {prop.children} |                 {prop.children} | ||||||
|             </main> |             </main> | ||||||
|     </div>); |         </div> | ||||||
|  |     ); | ||||||
|     function navSearch(search: string) { |     function navSearch(search: string) { | ||||||
|         let words = search.includes("&") ? search.split("&") : [search]; |         let words = search.includes("&") ? search.split("&") : [search]; | ||||||
|         words = words.map(w => w.trim()) |         words = words.map(w => w.trim()) | ||||||
|             .map(w => w.includes(":") ?  |             .map(w => | ||||||
|                 `allow_tag=${w}`  |                 w.includes(":") | ||||||
|                 : `word=${encodeURIComponent(w)}`); |                     ? `allow_tag=${w}` | ||||||
|  |                     : `word=${encodeURIComponent(w)}` | ||||||
|  |             ); | ||||||
|         navigate(`/search?${words.join("&")}`); |         navigate(`/search?${words.join("&")}`); | ||||||
|     } |     } | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -1,8 +1,10 @@ | ||||||
| import React from 'react'; | import { Box, CircularProgress } from "@mui/material"; | ||||||
| import {Box, CircularProgress} from '@mui/material'; | import React from "react"; | ||||||
| 
 | 
 | ||||||
| export const LoadingCircle = () => { | export const LoadingCircle = () => { | ||||||
|     return (<Box style={{position:"absolute", top:"50%", left:"50%", transform:"translate(-50%,-50%)"}}> |     return ( | ||||||
|  |         <Box style={{ position: "absolute", top: "50%", left: "50%", transform: "translate(-50%,-50%)" }}> | ||||||
|             <CircularProgress title="loading" /> |             <CircularProgress title="loading" /> | ||||||
|         </Box>); |         </Box> | ||||||
| } |     ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| export * from './contentinfo'; | export * from "./contentinfo"; | ||||||
| export * from './loading'; | export * from "./headline"; | ||||||
| export * from './tagchip'; | export * from "./loading"; | ||||||
| export * from './navlist'; | export * from "./navlist"; | ||||||
| export * from './headline'; | export * from "./tagchip"; | ||||||
|  |  | ||||||
|  | @ -1,36 +1,50 @@ | ||||||
| import React from 'react'; | import { | ||||||
| import {List, ListItem, ListItemIcon, Tooltip, ListItemText, Divider} from '@mui/material'; |     ArrowBack as ArrowBackIcon, | ||||||
| import {ArrowBack as ArrowBackIcon, Settings as SettingIcon,  |     Collections as CollectionIcon, | ||||||
|     Collections as CollectionIcon, VideoLibrary as VideoIcon, Home as HomeIcon, |     Folder as FolderIcon, | ||||||
|  |     Home as HomeIcon, | ||||||
|     List as ListIcon, |     List as ListIcon, | ||||||
|     Folder as FolderIcon } from '@mui/icons-material'; |     Settings as SettingIcon, | ||||||
| import {Link as RouterLink} from 'react-router-dom'; |     VideoLibrary as VideoIcon, | ||||||
|  | } from "@mui/icons-material"; | ||||||
|  | import { Divider, List, ListItem, ListItemIcon, ListItemText, Tooltip } from "@mui/material"; | ||||||
|  | import React from "react"; | ||||||
|  | import { Link as RouterLink } from "react-router-dom"; | ||||||
| 
 | 
 | ||||||
| export const NavItem = (props:{name:string,to:string, icon:React.ReactElement<any,any>})=>{ | export const NavItem = (props: { name: string; to: string; icon: React.ReactElement<any, any> }) => { | ||||||
|     return (<ListItem button key={props.name} component={RouterLink} to={props.to}> |     return ( | ||||||
|  |         <ListItem button key={props.name} component={RouterLink} to={props.to}> | ||||||
|             <ListItemIcon> |             <ListItemIcon> | ||||||
|                 <Tooltip title={props.name.toLocaleLowerCase()} placement="bottom"> |                 <Tooltip title={props.name.toLocaleLowerCase()} placement="bottom"> | ||||||
|                     {props.icon} |                     {props.icon} | ||||||
|                 </Tooltip> |                 </Tooltip> | ||||||
|             </ListItemIcon> |             </ListItemIcon> | ||||||
|             <ListItemText primary={props.name}></ListItemText> |             <ListItemText primary={props.name}></ListItemText> | ||||||
| </ListItem>); |         </ListItem> | ||||||
| } |     ); | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
| export const NavList = (props: { children?: React.ReactNode }) => { | export const NavList = (props: { children?: React.ReactNode }) => { | ||||||
|     return (<List> |     return ( | ||||||
|  |         <List> | ||||||
|             {props.children} |             {props.children} | ||||||
|         </List>); |         </List> | ||||||
| } |     ); | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
| export const BackItem = (props: { to?: string }) => { | export const BackItem = (props: { to?: string }) => { | ||||||
|     return <NavItem name="Back" to={props.to ?? "/"} icon={<ArrowBackIcon></ArrowBackIcon>} />; |     return <NavItem name="Back" to={props.to ?? "/"} icon={<ArrowBackIcon></ArrowBackIcon>} />; | ||||||
| } | }; | ||||||
| 
 | 
 | ||||||
| export function CommonMenuList(props?: { url?: string }) { | export function CommonMenuList(props?: { url?: string }) { | ||||||
|     let url = props?.url ?? ""; |     let url = props?.url ?? ""; | ||||||
|     return (<NavList> |     return ( | ||||||
|         {url !== "" && <><BackItem to={url} /> <Divider /></>} |         <NavList> | ||||||
|  |             {url !== "" && ( | ||||||
|  |                 <> | ||||||
|  |                     <BackItem to={url} /> <Divider /> | ||||||
|  |                 </> | ||||||
|  |             )} | ||||||
|             <NavItem name="All" to="/" icon={<HomeIcon />} /> |             <NavItem name="All" to="/" icon={<HomeIcon />} /> | ||||||
|             <NavItem name="Comic" to="/search?content_type=comic" icon={<CollectionIcon />}></NavItem> |             <NavItem name="Comic" to="/search?content_type=comic" icon={<CollectionIcon />}></NavItem> | ||||||
|             <NavItem name="Video" to="/search?content_type=video" icon={<VideoIcon />} /> |             <NavItem name="Video" to="/search?content_type=video" icon={<VideoIcon />} /> | ||||||
|  | @ -39,5 +53,6 @@ export function CommonMenuList(props?:{url?:string}) { | ||||||
|             <Divider /> |             <Divider /> | ||||||
|             <NavItem name="Difference" to="/difference" icon={<FolderIcon />}></NavItem> |             <NavItem name="Difference" to="/difference" icon={<FolderIcon />}></NavItem> | ||||||
|             <NavItem name="Settings" to="/setting" icon={<SettingIcon />} /> |             <NavItem name="Settings" to="/setting" icon={<SettingIcon />} /> | ||||||
|     </NavList>); |         </NavList> | ||||||
|  |     ); | ||||||
| } | } | ||||||
|  | @ -1,32 +1,32 @@ | ||||||
| import React from 'react'; | import { Chip, colors } from "@mui/material"; | ||||||
| import {ChipTypeMap} from '@mui/material/Chip'; | import { ChipTypeMap } from "@mui/material/Chip"; | ||||||
| import { Chip, colors } from '@mui/material'; | import { emphasize, Theme } from "@mui/material/styles"; | ||||||
| import { Theme, emphasize} from '@mui/material/styles'; | import React from "react"; | ||||||
| import {Link as RouterLink} from 'react-router-dom'; | import { Link as RouterLink } from "react-router-dom"; | ||||||
| 
 | 
 | ||||||
| type TagChipStyleProp = { | type TagChipStyleProp = { | ||||||
|     color: string |     color: string; | ||||||
| } | }; | ||||||
| 
 | 
 | ||||||
| const useTagStyles = ((theme:Theme)=>({ | const useTagStyles = (theme: Theme) => ({ | ||||||
|     root: (props: TagChipStyleProp) => ({ |     root: (props: TagChipStyleProp) => ({ | ||||||
|         color: theme.palette.getContrastText(props.color), |         color: theme.palette.getContrastText(props.color), | ||||||
|         backgroundColor: props.color, |         backgroundColor: props.color, | ||||||
|     }), |     }), | ||||||
|     clickable: (props: TagChipStyleProp) => ({ |     clickable: (props: TagChipStyleProp) => ({ | ||||||
|         '&:hover, &:focus':{ |         "&:hover, &:focus": { | ||||||
|             backgroundColor:emphasize(props.color,0.08) |             backgroundColor: emphasize(props.color, 0.08), | ||||||
|         } |         }, | ||||||
|     }), |     }), | ||||||
|     deletable: { |     deletable: { | ||||||
|         '&:focus': { |         "&:focus": { | ||||||
|             backgroundColor: (props: TagChipStyleProp) => emphasize(props.color, 0.2), |             backgroundColor: (props: TagChipStyleProp) => emphasize(props.color, 0.2), | ||||||
|         } |         }, | ||||||
|     }, |     }, | ||||||
|     outlined: { |     outlined: { | ||||||
|         color: (props: TagChipStyleProp) => props.color, |         color: (props: TagChipStyleProp) => props.color, | ||||||
|         border: (props: TagChipStyleProp) => `1px solid ${props.color}`, |         border: (props: TagChipStyleProp) => `1px solid ${props.color}`, | ||||||
|         '$clickable&:hover, $clickable&:focus, $deletable&:focus': { |         "$clickable&:hover, $clickable&:focus, $deletable&:focus": { | ||||||
|             // backgroundColor:(props:TagChipStyleProp)=> (props.color,theme.palette.action.hoverOpacity),
 |             // backgroundColor:(props:TagChipStyleProp)=> (props.color,theme.palette.action.hoverOpacity),
 | ||||||
|         }, |         }, | ||||||
|     }, |     }, | ||||||
|  | @ -37,35 +37,33 @@ const useTagStyles = ((theme:Theme)=>({ | ||||||
|         // color:(props:TagChipStyleProp)=> (theme.palette.getContrastText(props.color),0.7),
 |         // color:(props:TagChipStyleProp)=> (theme.palette.getContrastText(props.color),0.7),
 | ||||||
|         "&:hover, &:active": { |         "&:hover, &:active": { | ||||||
|             color: (props: TagChipStyleProp) => theme.palette.getContrastText(props.color), |             color: (props: TagChipStyleProp) => theme.palette.getContrastText(props.color), | ||||||
|         } |         }, | ||||||
|     } |     }, | ||||||
| })); | }); | ||||||
| 
 | 
 | ||||||
| const { blue, pink } = colors; | const { blue, pink } = colors; | ||||||
| const getTagColorName = (tagname: string): string => { | const getTagColorName = (tagname: string): string => { | ||||||
|     if (tagname.startsWith("female")) { |     if (tagname.startsWith("female")) { | ||||||
|         return pink[600]; |         return pink[600]; | ||||||
|     } |     } else if (tagname.startsWith("male")) { | ||||||
|     else if(tagname.startsWith("male")){ |  | ||||||
|         return blue[600]; |         return blue[600]; | ||||||
|     } |     } else return "default"; | ||||||
|     else return "default"; | }; | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| type ColorChipProp = Omit<ChipTypeMap['props'],"color"> & TagChipStyleProp & { | type ColorChipProp = Omit<ChipTypeMap["props"], "color"> & TagChipStyleProp & { | ||||||
|     component?: React.ElementType, |     component?: React.ElementType; | ||||||
|     to?: string |     to?: string; | ||||||
| } | }; | ||||||
| 
 | 
 | ||||||
| export const ColorChip = (props: ColorChipProp) => { | export const ColorChip = (props: ColorChipProp) => { | ||||||
|     const { color, ...rest } = props; |     const { color, ...rest } = props; | ||||||
|     // const classes = useTagStyles({color : color !== "default" ? color : "#000"});
 |     // const classes = useTagStyles({color : color !== "default" ? color : "#000"});
 | ||||||
|     return <Chip color="default" {...rest}></Chip>; |     return <Chip color="default" {...rest}></Chip>; | ||||||
| } | }; | ||||||
| 
 | 
 | ||||||
| type TagChipProp = Omit<ChipTypeMap['props'],"color"> & { | type TagChipProp = Omit<ChipTypeMap["props"], "color"> & { | ||||||
|     tagname:string |     tagname: string; | ||||||
| } | }; | ||||||
| 
 | 
 | ||||||
| export const TagChip = (props: TagChipProp) => { | export const TagChip = (props: TagChipProp) => { | ||||||
|     const { tagname, label, clickable, ...rest } = props; |     const { tagname, label, clickable, ...rest } = props; | ||||||
|  | @ -73,14 +71,25 @@ export const TagChip = (props:TagChipProp)=>{ | ||||||
|     if (typeof label === "string") { |     if (typeof label === "string") { | ||||||
|         if (label.startsWith("female:")) { |         if (label.startsWith("female:")) { | ||||||
|             newlabel = "♀ " + label.slice(7); |             newlabel = "♀ " + label.slice(7); | ||||||
|         } |         } else if (label.startsWith("male:")) { | ||||||
|         else if(label.startsWith("male:")){ |  | ||||||
|             newlabel = "♂ " + label.slice(5); |             newlabel = "♂ " + label.slice(5); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|     const inner = clickable ?  |     const inner = clickable | ||||||
|     (<ColorChip color={getTagColorName(tagname)} clickable={clickable} label={newlabel??label} {...rest}  |         ? ( | ||||||
|     component={RouterLink} to={`/search?allow_tag=${tagname}`}></ColorChip>): |             <ColorChip | ||||||
|     (<ColorChip color={getTagColorName(tagname)} clickable={clickable} label={newlabel??label} {...rest}></ColorChip>); |                 color={getTagColorName(tagname)} | ||||||
|  |                 clickable={clickable} | ||||||
|  |                 label={newlabel ?? label} | ||||||
|  |                 {...rest} | ||||||
|  |                 component={RouterLink} | ||||||
|  |                 to={`/search?allow_tag=${tagname}`} | ||||||
|  |             > | ||||||
|  |             </ColorChip> | ||||||
|  |         ) | ||||||
|  |         : ( | ||||||
|  |             <ColorChip color={getTagColorName(tagname)} clickable={clickable} label={newlabel ?? label} {...rest}> | ||||||
|  |             </ColorChip> | ||||||
|  |         ); | ||||||
|     return inner; |     return inner; | ||||||
| } | }; | ||||||
|  |  | ||||||
|  | @ -1,11 +1,13 @@ | ||||||
| import React from 'react'; | import { ArrowBack as ArrowBackIcon } from "@mui/icons-material"; | ||||||
| import {Typography} from '@mui/material'; | import { Typography } from "@mui/material"; | ||||||
| import {ArrowBack as ArrowBackIcon} from '@mui/icons-material'; | import React from "react"; | ||||||
| import { Headline, BackItem, NavList, CommonMenuList } from '../component/mod'; | import { BackItem, CommonMenuList, Headline, NavList } from "../component/mod"; | ||||||
| 
 | 
 | ||||||
| export const NotFoundPage = () => { | export const NotFoundPage = () => { | ||||||
|     const menu = CommonMenuList(); |     const menu = CommonMenuList(); | ||||||
|     return <Headline menu={menu}> |     return ( | ||||||
|         <Typography variant='h2'>404 Not Found</Typography> |         <Headline menu={menu}> | ||||||
|  |             <Typography variant="h2">404 Not Found</Typography> | ||||||
|         </Headline> |         </Headline> | ||||||
|  |     ); | ||||||
| }; | }; | ||||||
|  | @ -1,31 +1,31 @@ | ||||||
| import React, { useState, useEffect } from 'react'; | import { Theme, Typography } from "@mui/material"; | ||||||
| import { Route, Routes, useLocation, useParams } from 'react-router-dom'; | import React, { useEffect, useState } from "react"; | ||||||
| import DocumentAccessor, { Document } from '../accessor/document'; | import { Route, Routes, useLocation, useParams } from "react-router-dom"; | ||||||
| import { LoadingCircle } from '../component/loading'; | import DocumentAccessor, { Document } from "../accessor/document"; | ||||||
| import { Theme, Typography } from '@mui/material'; | import { LoadingCircle } from "../component/loading"; | ||||||
| import { getPresenter } from './reader/reader'; | import { CommonMenuList, ContentInfo, Headline } from "../component/mod"; | ||||||
| import { CommonMenuList, ContentInfo, Headline } from '../component/mod'; | import { NotFoundPage } from "./404"; | ||||||
| import { NotFoundPage } from './404'; | import { getPresenter } from "./reader/reader"; | ||||||
| 
 | 
 | ||||||
| export const makeContentInfoUrl = (id: number) => `/doc/${id}`; | export const makeContentInfoUrl = (id: number) => `/doc/${id}`; | ||||||
| export const makeComicReaderUrl = (id: number) => `/doc/${id}/reader`; | export const makeComicReaderUrl = (id: number) => `/doc/${id}/reader`; | ||||||
| 
 | 
 | ||||||
| type DocumentState = { | type DocumentState = { | ||||||
|     doc: Document | undefined, |     doc: Document | undefined; | ||||||
|     notfound: boolean, |     notfound: boolean; | ||||||
| } | }; | ||||||
| 
 | 
 | ||||||
| const styles = ((theme: Theme) => ({ | const styles = (theme: Theme) => ({ | ||||||
|     noPaddingContent: { |     noPaddingContent: { | ||||||
|         display: 'flex', |         display: "flex", | ||||||
|         flexDirection: 'column', |         flexDirection: "column", | ||||||
|         flexGrow: 1, |         flexGrow: 1, | ||||||
|     }, |     }, | ||||||
|     noPaddingToolbar: { |     noPaddingToolbar: { | ||||||
|         flex: '0 1 auto', |         flex: "0 1 auto", | ||||||
|         ...theme.mixins.toolbar, |         ...theme.mixins.toolbar, | ||||||
|     } |     }, | ||||||
| })); | }); | ||||||
| 
 | 
 | ||||||
| export function ReaderPage(props?: {}) { | export function ReaderPage(props?: {}) { | ||||||
|     const location = useLocation(); |     const location = useLocation(); | ||||||
|  | @ -49,28 +49,28 @@ export function ReaderPage(props?: {}) { | ||||||
|     if (isNaN(id)) { |     if (isNaN(id)) { | ||||||
|         return ( |         return ( | ||||||
|             <Headline menu={menu_list()}> |             <Headline menu={menu_list()}> | ||||||
|                 <Typography variant='h2'>Oops. Invalid ID</Typography> |                 <Typography variant="h2">Oops. Invalid ID</Typography> | ||||||
|             </Headline> |             </Headline> | ||||||
|         ); |         ); | ||||||
|     } |     } else if (info.notfound) { | ||||||
|     else if (info.notfound) { |  | ||||||
|         return ( |         return ( | ||||||
|             <Headline menu={menu_list()}> |             <Headline menu={menu_list()}> | ||||||
|                 <Typography variant='h2'>Content has been removed.</Typography> |                 <Typography variant="h2">Content has been removed.</Typography> | ||||||
|             </Headline> |             </Headline> | ||||||
|         ) |         ); | ||||||
|     } |     } else if (info.doc === undefined) { | ||||||
|     else if (info.doc === undefined) { |         return ( | ||||||
|         return (<Headline menu={menu_list()}> |             <Headline menu={menu_list()}> | ||||||
|                 <LoadingCircle /> |                 <LoadingCircle /> | ||||||
|             </Headline> |             </Headline> | ||||||
|         ); |         ); | ||||||
|     } |     } else { | ||||||
|     else { |  | ||||||
|         const ReaderPage = getPresenter(info.doc); |         const ReaderPage = getPresenter(info.doc); | ||||||
|         return <Headline menu={menu_list(location.pathname)}> |         return ( | ||||||
|  |             <Headline menu={menu_list(location.pathname)}> | ||||||
|                 <ReaderPage doc={info.doc}></ReaderPage> |                 <ReaderPage doc={info.doc}></ReaderPage> | ||||||
|             </Headline> |             </Headline> | ||||||
|  |         ); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -95,28 +95,26 @@ export const DocumentAbout = (prop?: {}) => { | ||||||
|     if (isNaN(id)) { |     if (isNaN(id)) { | ||||||
|         return ( |         return ( | ||||||
|             <Headline menu={menu_list()}> |             <Headline menu={menu_list()}> | ||||||
|                 <Typography variant='h2'>Oops. Invalid ID</Typography> |                 <Typography variant="h2">Oops. Invalid ID</Typography> | ||||||
|             </Headline> |             </Headline> | ||||||
|         ); |         ); | ||||||
|     } |     } else if (info.notfound) { | ||||||
|     else if (info.notfound) { |  | ||||||
|         return ( |         return ( | ||||||
|             <Headline menu={menu_list()}> |             <Headline menu={menu_list()}> | ||||||
|                 <Typography variant='h2'>Content has been removed.</Typography> |                 <Typography variant="h2">Content has been removed.</Typography> | ||||||
|             </Headline> |             </Headline> | ||||||
|         ) |         ); | ||||||
|     } |     } else if (info.doc === undefined) { | ||||||
|     else if (info.doc === undefined) { |         return ( | ||||||
|         return (<Headline menu={menu_list()}> |             <Headline menu={menu_list()}> | ||||||
|                 <LoadingCircle /> |                 <LoadingCircle /> | ||||||
|             </Headline> |             </Headline> | ||||||
|         ); |         ); | ||||||
|     } |     } else { | ||||||
|     else { |  | ||||||
|         return ( |         return ( | ||||||
|             <Headline menu={menu_list()}> |             <Headline menu={menu_list()}> | ||||||
|                 <ContentInfo document={info.doc}></ContentInfo> |                 <ContentInfo document={info.doc}></ContentInfo> | ||||||
|             </Headline> |             </Headline> | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
| } | }; | ||||||
|  |  | ||||||
|  | @ -1,60 +1,72 @@ | ||||||
| import React, { useContext, useEffect, useState } from 'react'; | import { Box, Button, Grid, Paper, Theme, Typography } from "@mui/material"; | ||||||
|  | import { Stack } from "@mui/material"; | ||||||
|  | import React, { useContext, useEffect, useState } from "react"; | ||||||
| import { CommonMenuList, Headline } from "../component/mod"; | import { CommonMenuList, Headline } from "../component/mod"; | ||||||
| import { UserContext } from "../state"; | import { UserContext } from "../state"; | ||||||
| import { Box, Grid, Paper, Typography,Button, Theme } from "@mui/material"; |  | ||||||
| import {Stack} from '@mui/material'; |  | ||||||
| 
 | 
 | ||||||
| const useStyles = ((theme:Theme)=>({ | const useStyles = (theme: Theme) => ({ | ||||||
|     paper: { |     paper: { | ||||||
|         padding: theme.spacing(2), |         padding: theme.spacing(2), | ||||||
|     }, |     }, | ||||||
|     commitable: { |     commitable: { | ||||||
|         display:'grid', |         display: "grid", | ||||||
|         gridTemplateColumns: `100px auto`, |         gridTemplateColumns: `100px auto`, | ||||||
|     }, |     }, | ||||||
|     contentTitle: { |     contentTitle: { | ||||||
|         marginLeft: theme.spacing(2) |         marginLeft: theme.spacing(2), | ||||||
|     } |     }, | ||||||
| })); | }); | ||||||
| type FileDifference = { | type FileDifference = { | ||||||
|     type:string, |     type: string; | ||||||
|     value: { |     value: { | ||||||
|         type:string, |         type: string; | ||||||
|         path:string, |         path: string; | ||||||
|     }[] |     }[]; | ||||||
| } | }; | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| function TypeDifference(prop: { | function TypeDifference(prop: { | ||||||
|     content:FileDifference, |     content: FileDifference; | ||||||
|     onCommit:(v:{type:string,path:string})=>void, |     onCommit: (v: { type: string; path: string }) => void; | ||||||
|     onCommitAll:(type:string) => void |     onCommitAll: (type: string) => void; | ||||||
| }) { | }) { | ||||||
|     // const classes = useStyles();
 |     // const classes = useStyles();
 | ||||||
|     const x = prop.content; |     const x = prop.content; | ||||||
|     const [button_disable, set_disable] = useState(false); |     const [button_disable, set_disable] = useState(false); | ||||||
| 
 | 
 | ||||||
|     return (<Paper /*className={classes.paper}*/> |     return ( | ||||||
|  |         <Paper /*className={classes.paper}*/> | ||||||
|             <Box /*className={classes.contentTitle}*/> |             <Box /*className={classes.contentTitle}*/> | ||||||
|                     <Typography variant='h3' >{x.type}</Typography> |                 <Typography variant="h3">{x.type}</Typography> | ||||||
|                     <Button variant="contained" key={x.type} onClick={()=>{ |                 <Button | ||||||
|  |                     variant="contained" | ||||||
|  |                     key={x.type} | ||||||
|  |                     onClick={() => { | ||||||
|                         set_disable(true); |                         set_disable(true); | ||||||
|                         prop.onCommitAll(x.type); |                         prop.onCommitAll(x.type); | ||||||
|                         set_disable(false); |                         set_disable(false); | ||||||
|                     }}>Commit all</Button> |                     }} | ||||||
|  |                 > | ||||||
|  |                     Commit all | ||||||
|  |                 </Button> | ||||||
|             </Box> |             </Box> | ||||||
|             {x.value.map(y => ( |             {x.value.map(y => ( | ||||||
|                 <Box sx={{ display: "flex" }} key={y.path}> |                 <Box sx={{ display: "flex" }} key={y.path}> | ||||||
|                                 <Button variant="contained" onClick={()=>{ |                     <Button | ||||||
|  |                         variant="contained" | ||||||
|  |                         onClick={() => { | ||||||
|                             set_disable(true); |                             set_disable(true); | ||||||
|                             prop.onCommit(y); |                             prop.onCommit(y); | ||||||
|                             set_disable(false); |                             set_disable(false); | ||||||
|                         }} |                         }} | ||||||
|                                     disabled={button_disable}>Commit</Button> |                         disabled={button_disable} | ||||||
|                                 <Typography variant='h5'>{y.path}</Typography> |                     > | ||||||
|  |                         Commit | ||||||
|  |                     </Button> | ||||||
|  |                     <Typography variant="h5">{y.path}</Typography> | ||||||
|                 </Box> |                 </Box> | ||||||
|             ))} |             ))} | ||||||
|                 </Paper>); |         </Paper> | ||||||
|  |     ); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function DifferencePage() { | export function DifferencePage() { | ||||||
|  | @ -64,64 +76,66 @@ export function DifferencePage(){ | ||||||
|         FileDifference[] |         FileDifference[] | ||||||
|     >([]); |     >([]); | ||||||
|     const doLoad = async () => { |     const doLoad = async () => { | ||||||
|         const list = await fetch('/api/diff/list'); |         const list = await fetch("/api/diff/list"); | ||||||
|         if (list.ok) { |         if (list.ok) { | ||||||
|             const inner = await list.json(); |             const inner = await list.json(); | ||||||
|             setDiffList(inner); |             setDiffList(inner); | ||||||
|         } |         } else { | ||||||
|         else{ |  | ||||||
|             // setDiffList([]);
 |             // setDiffList([]);
 | ||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
|     const Commit = async(x:{type:string,path:string})=>{ |     const Commit = async (x: { type: string; path: string }) => { | ||||||
|         const res = await fetch('/api/diff/commit',{ |         const res = await fetch("/api/diff/commit", { | ||||||
|             method:'POST', |             method: "POST", | ||||||
|             body: JSON.stringify([{ ...x }]), |             body: JSON.stringify([{ ...x }]), | ||||||
|             headers: { |             headers: { | ||||||
|                 'content-type':'application/json' |                 "content-type": "application/json", | ||||||
|             } |             }, | ||||||
|         }); |         }); | ||||||
|         const bb = await res.json(); |         const bb = await res.json(); | ||||||
|         if (bb.ok) { |         if (bb.ok) { | ||||||
|             doLoad(); |             doLoad(); | ||||||
|         } |         } else { | ||||||
|         else{ |  | ||||||
|             console.error("fail to add document"); |             console.error("fail to add document"); | ||||||
|         } |         } | ||||||
|     } |     }; | ||||||
|     const CommitAll = async (type: string) => { |     const CommitAll = async (type: string) => { | ||||||
|         const res = await fetch("/api/diff/commitall", { |         const res = await fetch("/api/diff/commitall", { | ||||||
|             method: "POST", |             method: "POST", | ||||||
|             body: JSON.stringify({ type: type }), |             body: JSON.stringify({ type: type }), | ||||||
|             headers: { |             headers: { | ||||||
|                 'content-type':'application/json' |                 "content-type": "application/json", | ||||||
|             } |             }, | ||||||
|         }); |         }); | ||||||
|         const bb = await res.json(); |         const bb = await res.json(); | ||||||
|         if (bb.ok) { |         if (bb.ok) { | ||||||
|             doLoad(); |             doLoad(); | ||||||
|         } |         } else { | ||||||
|         else{ |  | ||||||
|             console.error("fail to add document"); |             console.error("fail to add document"); | ||||||
|         } |         } | ||||||
|     } |     }; | ||||||
|     useEffect( |     useEffect( | ||||||
|         () => { |         () => { | ||||||
|             doLoad(); |             doLoad(); | ||||||
|             const i = setInterval(doLoad, 5000); |             const i = setInterval(doLoad, 5000); | ||||||
|             return () => { |             return () => { | ||||||
|                 clearInterval(i); |                 clearInterval(i); | ||||||
|             } |             }; | ||||||
|         },[] |         }, | ||||||
|     ) |         [], | ||||||
|  |     ); | ||||||
|     const menu = CommonMenuList(); |     const menu = CommonMenuList(); | ||||||
|     return (<Headline menu={menu}> |     return ( | ||||||
|         {(ctx.username == "admin") ? (<div> |         <Headline menu={menu}> | ||||||
|             {(diffList.map(x=> |             {(ctx.username == "admin") | ||||||
|                 <TypeDifference key={x.type} content={x} onCommit={Commit} onCommitAll={CommitAll}/>))} |                 ? ( | ||||||
|         </div>) |                     <div> | ||||||
|         :(<Typography variant='h2'>Not Allowed : please login as an admin</Typography>) |                         {diffList.map(x => ( | ||||||
|         } |                             <TypeDifference key={x.type} content={x} onCommit={Commit} onCommitAll={CommitAll} /> | ||||||
|      |                         ))} | ||||||
|     </Headline>) |                     </div> | ||||||
|  |                 ) | ||||||
|  |                 : <Typography variant="h2">Not Allowed : please login as an admin</Typography>} | ||||||
|  |         </Headline> | ||||||
|  |     ); | ||||||
| } | } | ||||||
|  | @ -1,15 +1,13 @@ | ||||||
| import React, { useContext, useEffect, useState } from 'react'; | import React, { useContext, useEffect, useState } from "react"; | ||||||
| import { Headline, CommonMenuList, LoadingCircle, ContentInfo, NavList, NavItem, TagChip } from '../component/mod'; | import { CommonMenuList, ContentInfo, Headline, LoadingCircle, NavItem, NavList, TagChip } from "../component/mod"; | ||||||
| 
 |  | ||||||
| import { Box, Typography, Chip, Pagination, Button } from '@mui/material'; |  | ||||||
| import ContentAccessor, { QueryListOption, Document } from '../accessor/document'; |  | ||||||
| import { toQueryString } from '../accessor/util'; |  | ||||||
| 
 |  | ||||||
| import { useLocation } from 'react-router-dom'; |  | ||||||
| import { QueryStringToMap } from '../accessor/util'; |  | ||||||
| import { useIsElementInViewport } from './reader/reader'; |  | ||||||
| 
 | 
 | ||||||
|  | import { Box, Button, Chip, Pagination, Typography } from "@mui/material"; | ||||||
|  | import ContentAccessor, { Document, QueryListOption } from "../accessor/document"; | ||||||
|  | import { toQueryString } from "../accessor/util"; | ||||||
| 
 | 
 | ||||||
|  | import { useLocation } from "react-router-dom"; | ||||||
|  | import { QueryStringToMap } from "../accessor/util"; | ||||||
|  | import { useIsElementInViewport } from "./reader/reader"; | ||||||
| 
 | 
 | ||||||
| export type GalleryProp = { | export type GalleryProp = { | ||||||
|     option?: QueryListOption; |     option?: QueryListOption; | ||||||
|  | @ -17,7 +15,7 @@ export type GalleryProp = { | ||||||
| }; | }; | ||||||
| type GalleryState = { | type GalleryState = { | ||||||
|     documents: Document[] | undefined; |     documents: Document[] | undefined; | ||||||
| } | }; | ||||||
| 
 | 
 | ||||||
| export const GalleryInfo = (props: GalleryProp) => { | export const GalleryInfo = (props: GalleryProp) => { | ||||||
|     const [state, setState] = useState<GalleryState>({ documents: undefined }); |     const [state, setState] = useState<GalleryState>({ documents: undefined }); | ||||||
|  | @ -33,60 +31,72 @@ export const GalleryInfo = (props: GalleryProp) => { | ||||||
| 
 | 
 | ||||||
|     useEffect(() => { |     useEffect(() => { | ||||||
|         const abortController = new AbortController(); |         const abortController = new AbortController(); | ||||||
|         console.log('load first',props.option); |         console.log("load first", props.option); | ||||||
|         const load = (async () => { |         const load = async () => { | ||||||
|             try { |             try { | ||||||
|                 const c = await ContentAccessor.findList(props.option); |                 const c = await ContentAccessor.findList(props.option); | ||||||
|                 // todo : if c is undefined, retry to fetch 3 times. and show error message.
 |                 // todo : if c is undefined, retry to fetch 3 times. and show error message.
 | ||||||
|                 setState({ documents: c }); |                 setState({ documents: c }); | ||||||
|                 setLoadAll(c.length == 0); |                 setLoadAll(c.length == 0); | ||||||
|             } |             } catch (e) { | ||||||
|             catch(e){ |  | ||||||
|                 if (e instanceof Error) { |                 if (e instanceof Error) { | ||||||
|                     setError(e.message); |                     setError(e.message); | ||||||
|                 } |                 } else { | ||||||
|                 else{ |  | ||||||
|                     setError("unknown error"); |                     setError("unknown error"); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         }); |         }; | ||||||
|         load(); |         load(); | ||||||
|     }, [props.diff]); |     }, [props.diff]); | ||||||
|     const queryString = toQueryString(props.option ?? {}); |     const queryString = toQueryString(props.option ?? {}); | ||||||
|     if (state.documents === undefined && error == null) { |     if (state.documents === undefined && error == null) { | ||||||
|         return (<LoadingCircle />); |         return <LoadingCircle />; | ||||||
|     } |     } else { | ||||||
|     else { |  | ||||||
|         return ( |         return ( | ||||||
|             <Box sx={{ |             <Box | ||||||
|                 display: 'grid', |                 sx={{ | ||||||
|                 gridRowGap: '1rem' |                     display: "grid", | ||||||
|             }}> |                     gridRowGap: "1rem", | ||||||
|                 {props.option !== undefined && props.diff !== "" && <Box> |                 }} | ||||||
|  |             > | ||||||
|  |                 {props.option !== undefined && props.diff !== "" && ( | ||||||
|  |                     <Box> | ||||||
|                         <Typography variant="h6">search for</Typography> |                         <Typography variant="h6">search for</Typography> | ||||||
|                     {props.option.word !== undefined && <Chip label={"search : " + decodeURIComponent(props.option.word)}></Chip>} |                         {props.option.word !== undefined && ( | ||||||
|                     {props.option.content_type !== undefined && <Chip label={"type : " + props.option.content_type}></Chip>} |                             <Chip label={"search : " + decodeURIComponent(props.option.word)}></Chip> | ||||||
|                     {props.option.allow_tag !== undefined && props.option.allow_tag.map(x => ( |                         )} | ||||||
|                         <TagChip key={x} tagname={decodeURIComponent(x)} label={decodeURIComponent(x)}></TagChip>))} |                         {props.option.content_type !== undefined && ( | ||||||
|                 </Box>} |                             <Chip label={"type : " + props.option.content_type}></Chip> | ||||||
|                 { |                         )} | ||||||
|                     state.documents && state.documents.map(x => { |                         {props.option.allow_tag !== undefined | ||||||
|                         return (<ContentInfo document={x} key={x.id} |                             && props.option.allow_tag.map(x => ( | ||||||
|                             gallery={`/search?${queryString}`} short />); |                                 <TagChip key={x} tagname={decodeURIComponent(x)} label={decodeURIComponent(x)}> | ||||||
|                     }) |                                 </TagChip> | ||||||
|                 } |                             ))} | ||||||
|                 {(error && <Typography variant="h5">Error : {error}</Typography>)} |                     </Box> | ||||||
|                 <Typography variant="body1" sx={{ |                 )} | ||||||
|  |                 {state.documents && state.documents.map(x => { | ||||||
|  |                     return <ContentInfo document={x} key={x.id} gallery={`/search?${queryString}`} short />; | ||||||
|  |                 })} | ||||||
|  |                 {error && <Typography variant="h5">Error : {error}</Typography>} | ||||||
|  |                 <Typography | ||||||
|  |                     variant="body1" | ||||||
|  |                     sx={{ | ||||||
|                         justifyContent: "center", |                         justifyContent: "center", | ||||||
|                     textAlign:"center" |                         textAlign: "center", | ||||||
|                 }}>{state.documents ? state.documents.length : "null"} loaded...</Typography> |                     }} | ||||||
|                 <Button onClick={()=>loadMore()} disabled={loadAll} ref={elementRef} >{loadAll ? "Load All" : "Load More"}</Button> |                 > | ||||||
|  |                     {state.documents ? state.documents.length : "null"} loaded... | ||||||
|  |                 </Typography> | ||||||
|  |                 <Button onClick={() => loadMore()} disabled={loadAll} ref={elementRef}> | ||||||
|  |                     {loadAll ? "Load All" : "Load More"} | ||||||
|  |                 </Button> | ||||||
|             </Box> |             </Box> | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
|     function loadMore() { |     function loadMore() { | ||||||
|         let option = { ...props.option }; |         let option = { ...props.option }; | ||||||
|         console.log(elementRef) |         console.log(elementRef); | ||||||
|         if (state.documents === undefined || state.documents.length === 0) { |         if (state.documents === undefined || state.documents.length === 0) { | ||||||
|             console.log("loadall"); |             console.log("loadall"); | ||||||
|             setLoadAll(true); |             setLoadAll(true); | ||||||
|  | @ -95,18 +105,17 @@ export const GalleryInfo = (props: GalleryProp) => { | ||||||
|         const prev_documents = state.documents; |         const prev_documents = state.documents; | ||||||
|         option.cursor = prev_documents[prev_documents.length - 1].id; |         option.cursor = prev_documents[prev_documents.length - 1].id; | ||||||
|         console.log("load more", option); |         console.log("load more", option); | ||||||
|         const load = (async () => { |         const load = async () => { | ||||||
|             const c = await ContentAccessor.findList(option); |             const c = await ContentAccessor.findList(option); | ||||||
|             if (c.length === 0) { |             if (c.length === 0) { | ||||||
|                 setLoadAll(true); |                 setLoadAll(true); | ||||||
|             } |             } else { | ||||||
|             else{ |  | ||||||
|                 setState({ documents: [...prev_documents, ...c] }); |                 setState({ documents: [...prev_documents, ...c] }); | ||||||
|             } |             } | ||||||
|         }); |         }; | ||||||
|         load(); |         load(); | ||||||
|     } |     } | ||||||
| } | }; | ||||||
| 
 | 
 | ||||||
| export const Gallery = () => { | export const Gallery = () => { | ||||||
|     const location = useLocation(); |     const location = useLocation(); | ||||||
|  | @ -114,8 +123,10 @@ export const Gallery = () => { | ||||||
|     const menu_list = CommonMenuList({ url: location.search }); |     const menu_list = CommonMenuList({ url: location.search }); | ||||||
|     let option: QueryListOption = query; |     let option: QueryListOption = query; | ||||||
|     option.allow_tag = typeof option.allow_tag === "string" ? [option.allow_tag] : option.allow_tag; |     option.allow_tag = typeof option.allow_tag === "string" ? [option.allow_tag] : option.allow_tag; | ||||||
|     option.limit = typeof query['limit'] === "string" ? parseInt(query['limit']) : undefined; |     option.limit = typeof query["limit"] === "string" ? parseInt(query["limit"]) : undefined; | ||||||
|     return (<Headline menu={menu_list}> |     return ( | ||||||
|  |         <Headline menu={menu_list}> | ||||||
|             <GalleryInfo diff={location.search} option={query}></GalleryInfo> |             <GalleryInfo diff={location.search} option={query}></GalleryInfo> | ||||||
|     </Headline>) |         </Headline> | ||||||
| } |     ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | @ -1,10 +1,21 @@ | ||||||
| import React, { useContext, useState } from 'react'; | import { | ||||||
| import {CommonMenuList, Headline} from '../component/mod'; |     Button, | ||||||
| import { Button, Dialog, DialogActions, DialogContent, DialogContentText, |     Dialog, | ||||||
|      DialogTitle, MenuList, Paper, TextField, Typography, useTheme } from '@mui/material'; |     DialogActions, | ||||||
| import { UserContext } from '../state'; |     DialogContent, | ||||||
| import { useNavigate } from 'react-router-dom'; |     DialogContentText, | ||||||
| import {doLogin as doSessionLogin} from '../state'; |     DialogTitle, | ||||||
|  |     MenuList, | ||||||
|  |     Paper, | ||||||
|  |     TextField, | ||||||
|  |     Typography, | ||||||
|  |     useTheme, | ||||||
|  | } from "@mui/material"; | ||||||
|  | import React, { useContext, useState } from "react"; | ||||||
|  | import { useNavigate } from "react-router-dom"; | ||||||
|  | import { CommonMenuList, Headline } from "../component/mod"; | ||||||
|  | import { UserContext } from "../state"; | ||||||
|  | import { doLogin as doSessionLogin } from "../state"; | ||||||
| 
 | 
 | ||||||
| export const LoginPage = () => { | export const LoginPage = () => { | ||||||
|     const theme = useTheme(); |     const theme = useTheme(); | ||||||
|  | @ -14,7 +25,7 @@ export const LoginPage = ()=>{ | ||||||
|     const navigate = useNavigate(); |     const navigate = useNavigate(); | ||||||
|     const handleDialogClose = () => { |     const handleDialogClose = () => { | ||||||
|         setOpenDialog({ ...openDialog, open: false }); |         setOpenDialog({ ...openDialog, open: false }); | ||||||
|     } |     }; | ||||||
|     const doLogin = async () => { |     const doLogin = async () => { | ||||||
|         try { |         try { | ||||||
|             const b = await doSessionLogin(userLoginInfo); |             const b = await doSessionLogin(userLoginInfo); | ||||||
|  | @ -25,35 +36,43 @@ export const LoginPage = ()=>{ | ||||||
|             console.log(`login as ${b.username}`); |             console.log(`login as ${b.username}`); | ||||||
|             setUsername(b.username); |             setUsername(b.username); | ||||||
|             setPermission(b.permission); |             setPermission(b.permission); | ||||||
|         } |         } catch (e) { | ||||||
|         catch(e){ |  | ||||||
|             if (e instanceof Error) { |             if (e instanceof Error) { | ||||||
|                 console.error(e); |                 console.error(e); | ||||||
|                 setOpenDialog({ open: true, message: e.message }); |                 setOpenDialog({ open: true, message: e.message }); | ||||||
|             } |             } else console.error(e); | ||||||
|             else console.error(e); |  | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|         navigate("/"); |         navigate("/"); | ||||||
|     } |     }; | ||||||
|     const menu = CommonMenuList(); |     const menu = CommonMenuList(); | ||||||
|     return <Headline menu={menu}> |     return ( | ||||||
|         <Paper style={{ width: theme.spacing(40), padding: theme.spacing(4), alignSelf:'center'}}> |         <Headline menu={menu}> | ||||||
|  |             <Paper style={{ width: theme.spacing(40), padding: theme.spacing(4), alignSelf: "center" }}> | ||||||
|                 <Typography variant="h4">Login</Typography> |                 <Typography variant="h4">Login</Typography> | ||||||
|                 <div style={{ minHeight: theme.spacing(2) }}></div> |                 <div style={{ minHeight: theme.spacing(2) }}></div> | ||||||
|         <form style={{display:'flex', flexFlow:'column', alignItems:'stretch'}}> |                 <form style={{ display: "flex", flexFlow: "column", alignItems: "stretch" }}> | ||||||
|             <TextField label="username" onChange={(e)=>setUserLoginInfo({...userLoginInfo,username:e.target.value ?? "",})}></TextField> |                     <TextField | ||||||
|             <TextField label="password" type="password"  onKeyDown={(e)=>{if(e.key === 'Enter') doLogin();}} |                         label="username" | ||||||
|                 onChange={(e)=>setUserLoginInfo({...userLoginInfo,password:e.target.value ?? "",})}/> |                         onChange={(e) => setUserLoginInfo({ ...userLoginInfo, username: e.target.value ?? "" })} | ||||||
|  |                     > | ||||||
|  |                     </TextField> | ||||||
|  |                     <TextField | ||||||
|  |                         label="password" | ||||||
|  |                         type="password" | ||||||
|  |                         onKeyDown={(e) => { | ||||||
|  |                             if (e.key === "Enter") doLogin(); | ||||||
|  |                         }} | ||||||
|  |                         onChange={(e) => setUserLoginInfo({ ...userLoginInfo, password: e.target.value ?? "" })} | ||||||
|  |                     /> | ||||||
|                     <div style={{ minHeight: theme.spacing(2) }}></div> |                     <div style={{ minHeight: theme.spacing(2) }}></div> | ||||||
|             <div style={{display:'flex'}}> |                     <div style={{ display: "flex" }}> | ||||||
|                         <Button onClick={doLogin}>login</Button> |                         <Button onClick={doLogin}>login</Button> | ||||||
|                         <Button>signin</Button> |                         <Button>signin</Button> | ||||||
|                     </div> |                     </div> | ||||||
|                 </form> |                 </form> | ||||||
|             </Paper> |             </Paper> | ||||||
|         <Dialog open={openDialog.open} |             <Dialog open={openDialog.open} onClose={handleDialogClose}> | ||||||
|             onClose={handleDialogClose}> |  | ||||||
|                 <DialogTitle>Login Failed</DialogTitle> |                 <DialogTitle>Login Failed</DialogTitle> | ||||||
|                 <DialogContent> |                 <DialogContent> | ||||||
|                     <DialogContentText>detail : {openDialog.message}</DialogContentText> |                     <DialogContentText>detail : {openDialog.message}</DialogContentText> | ||||||
|  | @ -63,4 +82,5 @@ export const LoginPage = ()=>{ | ||||||
|                 </DialogActions> |                 </DialogActions> | ||||||
|             </Dialog> |             </Dialog> | ||||||
|         </Headline> |         </Headline> | ||||||
| } |     ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | @ -1,8 +1,8 @@ | ||||||
| export * from './contentinfo'; | export * from "./404"; | ||||||
| export * from './gallery'; | export * from "./contentinfo"; | ||||||
| export * from './login'; | export * from "./difference"; | ||||||
| export * from './404'; | export * from "./gallery"; | ||||||
| export * from './profile'; | export * from "./login"; | ||||||
| export * from './difference'; | export * from "./profile"; | ||||||
| export * from './setting'; | export * from "./setting"; | ||||||
| export * from './tags'; | export * from "./tags"; | ||||||
|  |  | ||||||
|  | @ -1,19 +1,32 @@ | ||||||
|  | import { | ||||||
|  |     Button, | ||||||
|  |     Chip, | ||||||
|  |     Dialog, | ||||||
|  |     DialogActions, | ||||||
|  |     DialogContent, | ||||||
|  |     DialogContentText, | ||||||
|  |     DialogTitle, | ||||||
|  |     Divider, | ||||||
|  |     Grid, | ||||||
|  |     Paper, | ||||||
|  |     TextField, | ||||||
|  |     Theme, | ||||||
|  |     Typography, | ||||||
|  | } from "@mui/material"; | ||||||
|  | import React, { useContext, useState } from "react"; | ||||||
| import { CommonMenuList, Headline } from "../component/mod"; | import { CommonMenuList, Headline } from "../component/mod"; | ||||||
| import React, { useContext, useState } from 'react'; |  | ||||||
| import { UserContext } from "../state"; | import { UserContext } from "../state"; | ||||||
| import { Chip, Grid, Paper, Theme, Typography, Divider, Button, |  | ||||||
|      Dialog, DialogTitle, DialogContentText, DialogContent, TextField, DialogActions } from "@mui/material"; |  | ||||||
| 
 | 
 | ||||||
| const useStyles = ((theme:Theme)=>({ | const useStyles = (theme: Theme) => ({ | ||||||
|     paper: { |     paper: { | ||||||
|         alignSelf: "center", |         alignSelf: "center", | ||||||
|         padding: theme.spacing(2), |         padding: theme.spacing(2), | ||||||
|     }, |     }, | ||||||
|     formfield: { |     formfield: { | ||||||
|         display:'flex', |         display: "flex", | ||||||
|         flexFlow:'column', |         flexFlow: "column", | ||||||
|     } |     }, | ||||||
| })); | }); | ||||||
| 
 | 
 | ||||||
| export function ProfilePage() { | export function ProfilePage() { | ||||||
|     const userctx = useContext(UserContext); |     const userctx = useContext(UserContext); | ||||||
|  | @ -24,10 +37,8 @@ export function ProfilePage(){ | ||||||
|     const [newpw, setNewpw] = useState(""); |     const [newpw, setNewpw] = useState(""); | ||||||
|     const [newpwch, setNewpwch] = useState(""); |     const [newpwch, setNewpwch] = useState(""); | ||||||
|     const [msg_dialog, set_msg_dialog] = useState({ opened: false, msg: "" }); |     const [msg_dialog, set_msg_dialog] = useState({ opened: false, msg: "" }); | ||||||
|     const permission_list =userctx.permission.map(p=>( |     const permission_list = userctx.permission.map(p => <Chip key={p} label={p}></Chip>); | ||||||
|         <Chip key={p} label={p}></Chip> |     const isElectronContent = ((window["electron"] as any) !== undefined) as boolean; | ||||||
|     )); |  | ||||||
|     const isElectronContent = (((window['electron'] as any) !== undefined) as boolean); |  | ||||||
|     const handle_open = () => set_pw_open(true); |     const handle_open = () => set_pw_open(true); | ||||||
|     const handle_close = () => { |     const handle_close = () => { | ||||||
|         set_pw_open(false); |         set_pw_open(false); | ||||||
|  | @ -41,35 +52,35 @@ export function ProfilePage(){ | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|         if (isElectronContent) { |         if (isElectronContent) { | ||||||
|             const elec = window['electron'] as any; |             const elec = window["electron"] as any; | ||||||
|             const success = elec.passwordReset(userctx.username, newpw); |             const success = elec.passwordReset(userctx.username, newpw); | ||||||
|             if (!success) { |             if (!success) { | ||||||
|                 set_msg_dialog({ opened: true, msg: "user not exist." }); |                 set_msg_dialog({ opened: true, msg: "user not exist." }); | ||||||
|             } |             } | ||||||
|         } |         } else { | ||||||
|         else{ |  | ||||||
|             const res = await fetch("/user/reset", { |             const res = await fetch("/user/reset", { | ||||||
|                 method: 'POST', |                 method: "POST", | ||||||
|                 body: JSON.stringify({ |                 body: JSON.stringify({ | ||||||
|                     username: userctx.username, |                     username: userctx.username, | ||||||
|                     oldpassword: oldpw, |                     oldpassword: oldpw, | ||||||
|                     newpassword: newpw, |                     newpassword: newpw, | ||||||
|                 }), |                 }), | ||||||
|                 headers: { |                 headers: { | ||||||
|                     "content-type":"application/json" |                     "content-type": "application/json", | ||||||
|                 } |                 }, | ||||||
|             }); |             }); | ||||||
|             if (res.status != 200) { |             if (res.status != 200) { | ||||||
|                 set_msg_dialog({ opened: true, msg: "failed to change password." }); |                 set_msg_dialog({ opened: true, msg: "failed to change password." }); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         handle_close(); |         handle_close(); | ||||||
|     } |     }; | ||||||
|     return (<Headline menu={menu}> |     return ( | ||||||
|  |         <Headline menu={menu}> | ||||||
|             <Paper /*className={classes.paper}*/> |             <Paper /*className={classes.paper}*/> | ||||||
|                 <Grid container direction="column" alignItems="center"> |                 <Grid container direction="column" alignItems="center"> | ||||||
|                     <Grid item> |                     <Grid item> | ||||||
|                     <Typography variant='h4'>{userctx.username}</Typography> |                         <Typography variant="h4">{userctx.username}</Typography> | ||||||
|                     </Grid> |                     </Grid> | ||||||
|                     <Divider></Divider> |                     <Divider></Divider> | ||||||
|                     <Grid item> |                     <Grid item> | ||||||
|  | @ -88,12 +99,33 @@ export function ProfilePage(){ | ||||||
|                 <DialogContent> |                 <DialogContent> | ||||||
|                     <Typography>type the old and new password</Typography> |                     <Typography>type the old and new password</Typography> | ||||||
|                     <div /*className={classes.formfield}*/> |                     <div /*className={classes.formfield}*/> | ||||||
|                 {(!isElectronContent) && (<TextField autoFocus margin='dense' type="password" label="old password" |                         {(!isElectronContent) && ( | ||||||
|                  value={oldpw} onChange={(e)=>setOldpw(e.target.value)}></TextField>)} |                             <TextField | ||||||
|                 <TextField margin='dense' type="password" label="new password"  |                                 autoFocus | ||||||
|                 value={newpw} onChange={e=>setNewpw(e.target.value)}></TextField> |                                 margin="dense" | ||||||
|                 <TextField margin='dense' type="password" label="new password check" |                                 type="password" | ||||||
|                 value={newpwch} onChange={e=>setNewpwch(e.target.value)}></TextField> |                                 label="old password" | ||||||
|  |                                 value={oldpw} | ||||||
|  |                                 onChange={(e) => setOldpw(e.target.value)} | ||||||
|  |                             > | ||||||
|  |                             </TextField> | ||||||
|  |                         )} | ||||||
|  |                         <TextField | ||||||
|  |                             margin="dense" | ||||||
|  |                             type="password" | ||||||
|  |                             label="new password" | ||||||
|  |                             value={newpw} | ||||||
|  |                             onChange={e => setNewpw(e.target.value)} | ||||||
|  |                         > | ||||||
|  |                         </TextField> | ||||||
|  |                         <TextField | ||||||
|  |                             margin="dense" | ||||||
|  |                             type="password" | ||||||
|  |                             label="new password check" | ||||||
|  |                             value={newpwch} | ||||||
|  |                             onChange={e => setNewpwch(e.target.value)} | ||||||
|  |                         > | ||||||
|  |                         </TextField> | ||||||
|                     </div> |                     </div> | ||||||
|                 </DialogContent> |                 </DialogContent> | ||||||
|                 <DialogActions> |                 <DialogActions> | ||||||
|  | @ -110,5 +142,6 @@ export function ProfilePage(){ | ||||||
|                     <Button onClick={() => set_msg_dialog({ opened: false, msg: "" })} color="primary">Close</Button> |                     <Button onClick={() => set_msg_dialog({ opened: false, msg: "" })} color="primary">Close</Button> | ||||||
|                 </DialogActions> |                 </DialogActions> | ||||||
|             </Dialog> |             </Dialog> | ||||||
|     </Headline>) |         </Headline> | ||||||
|  |     ); | ||||||
| } | } | ||||||
|  | @ -1,47 +1,52 @@ | ||||||
| import React, {useState, useEffect} from 'react'; | import { Typography, useTheme } from "@mui/material"; | ||||||
| import {  Typography, useTheme } from '@mui/material'; | import React, { useEffect, useState } from "react"; | ||||||
| import { Document } from '../../accessor/document'; | import { Document } from "../../accessor/document"; | ||||||
| 
 | 
 | ||||||
| type ComicType = "comic" | "artist cg" | "donjinshi" | "western"; | type ComicType = "comic" | "artist cg" | "donjinshi" | "western"; | ||||||
| 
 | 
 | ||||||
| export type PresentableTag = { | export type PresentableTag = { | ||||||
|     artist:string[], |     artist: string[]; | ||||||
|     group: string[], |     group: string[]; | ||||||
|     series: string[], |     series: string[]; | ||||||
|     type: ComicType, |     type: ComicType; | ||||||
|     character: string[], |     character: string[]; | ||||||
|     tags: string[], |     tags: string[]; | ||||||
| } | }; | ||||||
| 
 | 
 | ||||||
| export const ComicReader = (props: { doc: Document }) => { | export const ComicReader = (props: { doc: Document }) => { | ||||||
|     const additional = props.doc.additional; |     const additional = props.doc.additional; | ||||||
|     const [curPage, setCurPage] = useState(0); |     const [curPage, setCurPage] = useState(0); | ||||||
|     if(!('page' in additional)){ |     if (!("page" in additional)) { | ||||||
|         console.error("invalid content : page read fail : " + JSON.stringify(additional)); |         console.error("invalid content : page read fail : " + JSON.stringify(additional)); | ||||||
|         return <Typography>Error. DB error. page restriction</Typography> |         return <Typography>Error. DB error. page restriction</Typography>; | ||||||
|     } |     } | ||||||
|     const PageDown = () => setCurPage(Math.max(curPage - 1, 0)); |     const PageDown = () => setCurPage(Math.max(curPage - 1, 0)); | ||||||
|     const PageUP = () => setCurPage(Math.min(curPage + 1, page - 1)); |     const PageUP = () => setCurPage(Math.min(curPage + 1, page - 1)); | ||||||
|     const page:number = additional['page'] as number; |     const page: number = additional["page"] as number; | ||||||
|     const onKeyUp = (e: KeyboardEvent) => { |     const onKeyUp = (e: KeyboardEvent) => { | ||||||
|         if (e.code === "ArrowLeft") { |         if (e.code === "ArrowLeft") { | ||||||
|             PageDown(); |             PageDown(); | ||||||
|         } |         } else if (e.code === "ArrowRight") { | ||||||
|         else if(e.code === "ArrowRight"){ |  | ||||||
|             PageUP(); |             PageUP(); | ||||||
|         } |         } | ||||||
|     } |     }; | ||||||
|     useEffect(() => { |     useEffect(() => { | ||||||
|         document.addEventListener("keydown", onKeyUp); |         document.addEventListener("keydown", onKeyUp); | ||||||
|         return () => { |         return () => { | ||||||
|             document.removeEventListener("keydown", onKeyUp); |             document.removeEventListener("keydown", onKeyUp); | ||||||
|         } |         }; | ||||||
|     }); |     }); | ||||||
|     // theme.mixins.toolbar.minHeight;
 |     // theme.mixins.toolbar.minHeight;
 | ||||||
|     return (<div style={{overflow: 'hidden', alignSelf:'center'}}> |     return ( | ||||||
|             <img onClick={PageUP} src={`/api/doc/${props.doc.id}/comic/${curPage}`} |         <div style={{ overflow: "hidden", alignSelf: "center" }}> | ||||||
|                 style={{maxWidth:'100%', maxHeight:'calc(100vh - 64px)'}}></img> |             <img | ||||||
|         </div>); |                 onClick={PageUP} | ||||||
| } |                 src={`/api/doc/${props.doc.id}/comic/${curPage}`} | ||||||
|  |                 style={{ maxWidth: "100%", maxHeight: "calc(100vh - 64px)" }} | ||||||
|  |             > | ||||||
|  |             </img> | ||||||
|  |         </div> | ||||||
|  |     ); | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
| export default ComicReader; | export default ComicReader; | ||||||
|  | @ -1,15 +1,15 @@ | ||||||
| import { Typography, styled } from '@mui/material'; | import { styled, Typography } from "@mui/material"; | ||||||
| import React from 'react'; | import React from "react"; | ||||||
| import { Document, makeThumbnailUrl } from '../../accessor/document'; | import { Document, makeThumbnailUrl } from "../../accessor/document"; | ||||||
| import {ComicReader} from './comic'; | import { ComicReader } from "./comic"; | ||||||
| import {VideoReader} from './video' | import { VideoReader } from "./video"; | ||||||
| 
 | 
 | ||||||
| export interface PagePresenterProp { | export interface PagePresenterProp { | ||||||
|     doc:Document, |     doc: Document; | ||||||
|     className?:string |     className?: string; | ||||||
| } | } | ||||||
| interface PagePresenter { | interface PagePresenter { | ||||||
|     (prop:PagePresenterProp):JSX.Element |     (prop: PagePresenterProp): JSX.Element; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const getPresenter = (content: Document): PagePresenter => { | export const getPresenter = (content: Document): PagePresenter => { | ||||||
|  | @ -19,20 +19,19 @@ export const getPresenter = (content:Document):PagePresenter => { | ||||||
|         case "video": |         case "video": | ||||||
|             return VideoReader; |             return VideoReader; | ||||||
|     } |     } | ||||||
|     return ()=><Typography variant='h2'>Not implemented reader</Typography>; |     return () => <Typography variant="h2">Not implemented reader</Typography>; | ||||||
| } | }; | ||||||
| const BackgroundDiv = styled("div")({ | const BackgroundDiv = styled("div")({ | ||||||
|     height: '400px', |     height: "400px", | ||||||
|     width:'300px', |     width: "300px", | ||||||
|     backgroundColor: "#272733", |     backgroundColor: "#272733", | ||||||
|     display: "flex", |     display: "flex", | ||||||
|     alignItems: "center", |     alignItems: "center", | ||||||
|     justifyContent:"center"} |     justifyContent: "center", | ||||||
|     ); | }); | ||||||
| 
 | 
 | ||||||
| 
 | import { useEffect, useRef, useState } from "react"; | ||||||
| import { useRef, useState, useEffect } from 'react'; | import "./thumbnail.css"; | ||||||
| import "./thumbnail.css" |  | ||||||
| 
 | 
 | ||||||
| export function useIsElementInViewport<T extends HTMLElement>(options?: IntersectionObserverInit) { | export function useIsElementInViewport<T extends HTMLElement>(options?: IntersectionObserverInit) { | ||||||
|     const elementRef = useRef<T>(null); |     const elementRef = useRef<T>(null); | ||||||
|  | @ -50,11 +49,11 @@ export function useIsElementInViewport<T extends HTMLElement>(options?: Intersec | ||||||
|     }, [elementRef, options]); |     }, [elementRef, options]); | ||||||
| 
 | 
 | ||||||
|     return { elementRef, isVisible }; |     return { elementRef, isVisible }; | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| export function ThumbnailContainer(props: { | export function ThumbnailContainer(props: { | ||||||
|     content:Document, |     content: Document; | ||||||
|     className?:string, |     className?: string; | ||||||
| }) { | }) { | ||||||
|     const { elementRef, isVisible } = useIsElementInViewport<HTMLDivElement>({}); |     const { elementRef, isVisible } = useIsElementInViewport<HTMLDivElement>({}); | ||||||
|     const [loaded, setLoaded] = useState(false); |     const [loaded, setLoaded] = useState(false); | ||||||
|  | @ -62,19 +61,17 @@ export function ThumbnailContainer(props:{ | ||||||
|         if (isVisible) { |         if (isVisible) { | ||||||
|             setLoaded(true); |             setLoaded(true); | ||||||
|         } |         } | ||||||
|     },[isVisible]) |     }, [isVisible]); | ||||||
|     const style = { |     const style = { | ||||||
|         maxHeight: '400px', |         maxHeight: "400px", | ||||||
|         maxWidth: 'min(400px, 100vw)', |         maxWidth: "min(400px, 100vw)", | ||||||
|     }; |     }; | ||||||
|     const thumbnailurl = makeThumbnailUrl(props.content); |     const thumbnailurl = makeThumbnailUrl(props.content); | ||||||
|     if (props.content.content_type === "video") { |     if (props.content.content_type === "video") { | ||||||
|         return (<video src={thumbnailurl} muted autoPlay loop className={props.className} style={style}></video>) |         return <video src={thumbnailurl} muted autoPlay loop className={props.className} style={style}></video>; | ||||||
|     } |     } else {return ( | ||||||
|     else return (<BackgroundDiv ref={elementRef}> |             <BackgroundDiv ref={elementRef}> | ||||||
|         {loaded && <img src={thumbnailurl}  |                 {loaded && <img src={thumbnailurl} className={props.className + " thumbnail_img"} loading="lazy"></img>} | ||||||
|         className={props.className + " thumbnail_img"} |             </BackgroundDiv> | ||||||
|           |         );} | ||||||
|           loading="lazy"></img>} |  | ||||||
|         </BackgroundDiv>) |  | ||||||
| } | } | ||||||
|  | @ -1,7 +1,10 @@ | ||||||
| import React from 'react'; | import React from "react"; | ||||||
| import { Document } from '../../accessor/document'; | import { Document } from "../../accessor/document"; | ||||||
| 
 | 
 | ||||||
| export const VideoReader = (props: { doc: Document }) => { | export const VideoReader = (props: { doc: Document }) => { | ||||||
|     const id = props.doc.id; |     const id = props.doc.id; | ||||||
|     return <video controls autoPlay src={`/api/doc/${props.doc.id}/video`} style={{maxHeight:'100%',maxWidth:'100%'}}></video>; |     return ( | ||||||
| } |         <video controls autoPlay src={`/api/doc/${props.doc.id}/video`} style={{ maxHeight: "100%", maxWidth: "100%" }}> | ||||||
|  |         </video> | ||||||
|  |     ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | @ -1,13 +1,15 @@ | ||||||
| import React from 'react'; | import { ArrowBack as ArrowBackIcon } from "@mui/icons-material"; | ||||||
| import {Typography, Paper} from '@mui/material'; | import { Paper, Typography } from "@mui/material"; | ||||||
| import {ArrowBack as ArrowBackIcon} from '@mui/icons-material'; | import React from "react"; | ||||||
| import { Headline, BackItem, NavList, CommonMenuList } from '../component/mod'; | import { BackItem, CommonMenuList, Headline, NavList } from "../component/mod"; | ||||||
| 
 | 
 | ||||||
| export const SettingPage = () => { | export const SettingPage = () => { | ||||||
|     const menu = CommonMenuList(); |     const menu = CommonMenuList(); | ||||||
|     return (<Headline menu={menu}> |     return ( | ||||||
|  |         <Headline menu={menu}> | ||||||
|             <Paper> |             <Paper> | ||||||
|                 <Typography variant='h2'>Setting</Typography> |                 <Typography variant="h2">Setting</Typography> | ||||||
|             </Paper> |             </Paper> | ||||||
|     </Headline>); |         </Headline> | ||||||
|  |     ); | ||||||
| }; | }; | ||||||
|  | @ -1,8 +1,8 @@ | ||||||
| import React, { useEffect, useState } from 'react'; | import { Box, Paper, Typography } from "@mui/material"; | ||||||
| import {Typography, Box, Paper} from '@mui/material'; | import { DataGrid, GridColDef } from "@mui/x-data-grid"; | ||||||
|  | import React, { useEffect, useState } from "react"; | ||||||
| import { LoadingCircle } from "../component/loading"; | import { LoadingCircle } from "../component/loading"; | ||||||
| import { Headline, CommonMenuList } from '../component/mod'; | import { CommonMenuList, Headline } from "../component/mod"; | ||||||
| import {DataGrid, GridColDef} from "@mui/x-data-grid" |  | ||||||
| 
 | 
 | ||||||
| type TagCount = { | type TagCount = { | ||||||
|     tag_name: string; |     tag_name: string; | ||||||
|  | @ -19,9 +19,9 @@ const tagTableColumn: GridColDef[] = [ | ||||||
|         field: "occurs", |         field: "occurs", | ||||||
|         headerName: "Occurs", |         headerName: "Occurs", | ||||||
|         width: 100, |         width: 100, | ||||||
|     type:"number" |         type: "number", | ||||||
| } |     }, | ||||||
| ] | ]; | ||||||
| 
 | 
 | ||||||
| function TagTable() { | function TagTable() { | ||||||
|     const [data, setData] = useState<TagCount[] | undefined>(); |     const [data, setData] = useState<TagCount[] | undefined>(); | ||||||
|  | @ -36,26 +36,26 @@ function TagTable(){ | ||||||
|         return <LoadingCircle />; |         return <LoadingCircle />; | ||||||
|     } |     } | ||||||
|     if (error !== undefined) { |     if (error !== undefined) { | ||||||
|         return <Typography variant="h3">{error}</Typography> |         return <Typography variant="h3">{error}</Typography>; | ||||||
|     } |     } | ||||||
|     return <Box sx={{height:"400px",width:"100%"}}> |     return ( | ||||||
|  |         <Box sx={{ height: "400px", width: "100%" }}> | ||||||
|             <Paper sx={{ height: "100%" }} elevation={2}> |             <Paper sx={{ height: "100%" }} elevation={2}> | ||||||
|                 <DataGrid rows={data} columns={tagTableColumn} getRowId={(t) => t.tag_name}></DataGrid> |                 <DataGrid rows={data} columns={tagTableColumn} getRowId={(t) => t.tag_name}></DataGrid> | ||||||
|             </Paper> |             </Paper> | ||||||
|         </Box> |         </Box> | ||||||
|  |     ); | ||||||
| 
 | 
 | ||||||
|     async function loadData() { |     async function loadData() { | ||||||
|         try { |         try { | ||||||
|             const res = await fetch("/api/tags?withCount=true"); |             const res = await fetch("/api/tags?withCount=true"); | ||||||
|             const data = await res.json(); |             const data = await res.json(); | ||||||
|             setData(data); |             setData(data); | ||||||
|         } |         } catch (e) { | ||||||
|         catch(e){ |  | ||||||
|             setData([]); |             setData([]); | ||||||
|             if (e instanceof Error) { |             if (e instanceof Error) { | ||||||
|                 setErrorMsg(e.message); |                 setErrorMsg(e.message); | ||||||
|             } |             } else { | ||||||
|             else{ |  | ||||||
|                 console.log(e); |                 console.log(e); | ||||||
|                 setErrorMsg(""); |                 setErrorMsg(""); | ||||||
|             } |             } | ||||||
|  | @ -65,7 +65,9 @@ function TagTable(){ | ||||||
| 
 | 
 | ||||||
| export const TagsPage = () => { | export const TagsPage = () => { | ||||||
|     const menu = CommonMenuList(); |     const menu = CommonMenuList(); | ||||||
|     return <Headline menu={menu}> |     return ( | ||||||
|  |         <Headline menu={menu}> | ||||||
|             <TagTable></TagTable> |             <TagTable></TagTable> | ||||||
|         </Headline> |         </Headline> | ||||||
|  |     ); | ||||||
| }; | }; | ||||||
|  | @ -1,16 +1,16 @@ | ||||||
| import React, { createContext, useRef, useState } from 'react'; | import React, { createContext, useRef, useState } from "react"; | ||||||
| export const BackLinkContext = createContext({ backLink: "", setBackLink: (s: string) => {} }); | export const BackLinkContext = createContext({ backLink: "", setBackLink: (s: string) => {} }); | ||||||
| export const UserContext = createContext({ | export const UserContext = createContext({ | ||||||
|     username: "", |     username: "", | ||||||
|     permission: [] as string[], |     permission: [] as string[], | ||||||
|     setUsername: (s: string) => {}, |     setUsername: (s: string) => {}, | ||||||
|     setPermission: (permission: string[]) => { } |     setPermission: (permission: string[]) => {}, | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| type LoginLocalStorage = { | type LoginLocalStorage = { | ||||||
|     username: string, |     username: string; | ||||||
|     permission: string[], |     permission: string[]; | ||||||
|     accessExpired: number |     accessExpired: number; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| let localObj: LoginLocalStorage | null = null; | let localObj: LoginLocalStorage | null = null; | ||||||
|  | @ -25,65 +25,64 @@ export const getInitialValue = async () => { | ||||||
|         return { |         return { | ||||||
|             username: localObj.username, |             username: localObj.username, | ||||||
|             permission: localObj.permission, |             permission: localObj.permission, | ||||||
|  |         }; | ||||||
|     } |     } | ||||||
|     } |     const res = await fetch("/user/refresh", { | ||||||
|     const res = await fetch('/user/refresh', { |         method: "POST", | ||||||
|         method: 'POST', |  | ||||||
|     }); |     }); | ||||||
|     if (res.status !== 200) throw new Error("Maybe Network Error") |     if (res.status !== 200) throw new Error("Maybe Network Error"); | ||||||
|     const r = await res.json() as LoginLocalStorage & { refresh: boolean }; |     const r = await res.json() as LoginLocalStorage & { refresh: boolean }; | ||||||
|     if (r.refresh) { |     if (r.refresh) { | ||||||
|         localObj = { |         localObj = { | ||||||
|             username: r.username, |             username: r.username, | ||||||
|             permission: r.permission, |             permission: r.permission, | ||||||
|             accessExpired: r.accessExpired |             accessExpired: r.accessExpired, | ||||||
|         } |         }; | ||||||
|     } |     } else { | ||||||
|     else { |  | ||||||
|         localObj = { |         localObj = { | ||||||
|             accessExpired: 0, |             accessExpired: 0, | ||||||
|             username: "", |             username: "", | ||||||
|             permission: r.permission |             permission: r.permission, | ||||||
|         } |         }; | ||||||
|     } |     } | ||||||
|     window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj)); |     window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj)); | ||||||
|     return { |     return { | ||||||
|         username: r.username, |         username: r.username, | ||||||
|         permission: r.permission |         permission: r.permission, | ||||||
|     } |     }; | ||||||
| } | }; | ||||||
| export const doLogout = async () => { | export const doLogout = async () => { | ||||||
|     const req = await fetch('/user/logout', { |     const req = await fetch("/user/logout", { | ||||||
|         method: 'POST' |         method: "POST", | ||||||
|     }); |     }); | ||||||
|     try { |     try { | ||||||
|         const res = await req.json(); |         const res = await req.json(); | ||||||
|         localObj = { |         localObj = { | ||||||
|             accessExpired: 0, |             accessExpired: 0, | ||||||
|             username: "", |             username: "", | ||||||
|             permission: res["permission"] |             permission: res["permission"], | ||||||
|         } |         }; | ||||||
|         window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj)); |         window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj)); | ||||||
|         return { |         return { | ||||||
|             username: localObj.username, |             username: localObj.username, | ||||||
|             permission: localObj.permission, |             permission: localObj.permission, | ||||||
|         } |         }; | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|         console.error(`Server Error ${error}`); |         console.error(`Server Error ${error}`); | ||||||
|         return { |         return { | ||||||
|             username: "", |             username: "", | ||||||
|             permission: [], |             permission: [], | ||||||
|  |         }; | ||||||
|     } |     } | ||||||
|     } | }; | ||||||
| } |  | ||||||
| export const doLogin = async (userLoginInfo: { | export const doLogin = async (userLoginInfo: { | ||||||
|     username:string, |     username: string; | ||||||
|     password:string, |     password: string; | ||||||
| }): Promise<string | LoginLocalStorage> => { | }): Promise<string | LoginLocalStorage> => { | ||||||
|     const res = await fetch('/user/login',{ |     const res = await fetch("/user/login", { | ||||||
|         method:'POST', |         method: "POST", | ||||||
|         body: JSON.stringify(userLoginInfo), |         body: JSON.stringify(userLoginInfo), | ||||||
|         headers:{"content-type":"application/json"} |         headers: { "content-type": "application/json" }, | ||||||
|     }); |     }); | ||||||
|     const b = await res.json(); |     const b = await res.json(); | ||||||
|     if (res.status !== 200) { |     if (res.status !== 200) { | ||||||
|  | @ -92,4 +91,4 @@ export const doLogin = async (userLoginInfo:{ | ||||||
|     localObj = b; |     localObj = b; | ||||||
|     window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj)); |     window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj)); | ||||||
|     return b; |     return b; | ||||||
| } | }; | ||||||
|  |  | ||||||
|  | @ -2,21 +2,21 @@ import {Knex as k} from "knex"; | ||||||
| 
 | 
 | ||||||
| export namespace Knex { | export namespace Knex { | ||||||
|     export const config: { |     export const config: { | ||||||
|     development: k.Config, |         development: k.Config; | ||||||
|     production: k.Config |         production: k.Config; | ||||||
|     } = { |     } = { | ||||||
|         development: { |         development: { | ||||||
|         client: 'sqlite3', |             client: "sqlite3", | ||||||
|             connection: { |             connection: { | ||||||
|           filename: './devdb.sqlite3' |                 filename: "./devdb.sqlite3", | ||||||
|             }, |             }, | ||||||
|             debug: true, |             debug: true, | ||||||
|         }, |         }, | ||||||
|         production: { |         production: { | ||||||
|         client: 'sqlite3', |             client: "sqlite3", | ||||||
|             connection: { |             connection: { | ||||||
|           filename: './db.sqlite3', |                 filename: "./db.sqlite3", | ||||||
|  |             }, | ||||||
|         }, |         }, | ||||||
|       } |  | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,19 +1,19 @@ | ||||||
| import {ContentFile, createDefaultClass,registerContentReferrer, ContentConstructOption} from './file'; | import { extname } from "path"; | ||||||
| import {readZip, readAllFromZip} from '../util/zipwrap'; | import { DocumentBody } from "../model/doc"; | ||||||
| import { DocumentBody } from '../model/doc'; | import { readAllFromZip, readZip } from "../util/zipwrap"; | ||||||
| import {extname} from 'path'; | import { ContentConstructOption, ContentFile, createDefaultClass, registerContentReferrer } from "./file"; | ||||||
| 
 | 
 | ||||||
| type ComicType = "doujinshi" | "artist cg" | "manga" | "western"; | type ComicType = "doujinshi" | "artist cg" | "manga" | "western"; | ||||||
| interface ComicDesc { | interface ComicDesc { | ||||||
|     title:string, |     title: string; | ||||||
|     artist?:string[], |     artist?: string[]; | ||||||
|     group?:string[], |     group?: string[]; | ||||||
|     series?:string[], |     series?: string[]; | ||||||
|     type:ComicType|[ComicType], |     type: ComicType | [ComicType]; | ||||||
|     character?:string[], |     character?: string[]; | ||||||
|     tags?:string[] |     tags?: string[]; | ||||||
| } | } | ||||||
| const ImageExt = ['.gif', '.png', '.jpeg', '.bmp', '.webp', '.jpg']; | const ImageExt = [".gif", ".png", ".jpeg", ".bmp", ".webp", ".jpg"]; | ||||||
| export class ComicReferrer extends createDefaultClass("comic") { | export class ComicReferrer extends createDefaultClass("comic") { | ||||||
|     desc: ComicDesc | undefined; |     desc: ComicDesc | undefined; | ||||||
|     pagenum: number; |     pagenum: number; | ||||||
|  | @ -32,11 +32,12 @@ export class ComicReferrer extends createDefaultClass("comic"){ | ||||||
|         if (entry === undefined) { |         if (entry === undefined) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|         const data = (await readAllFromZip(zip,entry)).toString('utf-8'); |         const data = (await readAllFromZip(zip, entry)).toString("utf-8"); | ||||||
|         this.desc = JSON.parse(data); |         this.desc = JSON.parse(data); | ||||||
|         if(this.desc === undefined) |         if (this.desc === undefined) { | ||||||
|             throw new Error(`JSON.parse is returning undefined. ${this.path} desc.json format error`); |             throw new Error(`JSON.parse is returning undefined. ${this.path} desc.json format error`); | ||||||
|         } |         } | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     async createDocumentBody(): Promise<DocumentBody> { |     async createDocumentBody(): Promise<DocumentBody> { | ||||||
|         await this.initDesc(); |         await this.initDesc(); | ||||||
|  | @ -56,10 +57,10 @@ export class ComicReferrer extends createDefaultClass("comic"){ | ||||||
|             ...basebody, |             ...basebody, | ||||||
|             title: this.desc.title, |             title: this.desc.title, | ||||||
|             additional: { |             additional: { | ||||||
|                 page:this.pagenum |                 page: this.pagenum, | ||||||
|             }, |             }, | ||||||
|             tags:tags |             tags: tags, | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
| }; | } | ||||||
| registerContentReferrer(ComicReferrer); | registerContentReferrer(ComicReferrer); | ||||||
|  | @ -1,10 +1,10 @@ | ||||||
| import {Context, DefaultState, DefaultContext, Middleware, Next} from 'koa'; | import { createHash } from "crypto"; | ||||||
| import Router from 'koa-router'; | import { promises, Stats } from "fs"; | ||||||
| import {createHash} from 'crypto'; | import { Context, DefaultContext, DefaultState, Middleware, Next } from "koa"; | ||||||
| import {promises, Stats} from 'fs' | import Router from "koa-router"; | ||||||
| import {extname} from 'path'; | import { extname } from "path"; | ||||||
| import { DocumentBody } from '../model/mod'; | import path from "path"; | ||||||
| import path from 'path'; | import { DocumentBody } from "../model/mod"; | ||||||
| /** | /** | ||||||
|  * content file or directory referrer |  * content file or directory referrer | ||||||
|  */ |  */ | ||||||
|  | @ -15,9 +15,11 @@ export interface ContentFile{ | ||||||
|     readonly type: string; |     readonly type: string; | ||||||
| } | } | ||||||
| export type ContentConstructOption = { | export type ContentConstructOption = { | ||||||
|     hash: string, |     hash: string; | ||||||
| } | }; | ||||||
| type ContentFileConstructor =  (new (path:string,option?:ContentConstructOption) => ContentFile)&{content_type:string}; | type ContentFileConstructor = (new(path: string, option?: ContentConstructOption) => ContentFile) & { | ||||||
|  |     content_type: string; | ||||||
|  | }; | ||||||
| export const createDefaultClass = (type: string): ContentFileConstructor => { | export const createDefaultClass = (type: string): ContentFileConstructor => { | ||||||
|     let cons = class implements ContentFile { |     let cons = class implements ContentFile { | ||||||
|         readonly path: string; |         readonly path: string; | ||||||
|  | @ -68,10 +70,10 @@ export const createDefaultClass = (type:string):ContentFileConstructor=>{ | ||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
|     return cons; |     return cons; | ||||||
| } | }; | ||||||
| let ContstructorTable: { [k: string]: ContentFileConstructor } = {}; | let ContstructorTable: { [k: string]: ContentFileConstructor } = {}; | ||||||
| export function registerContentReferrer(s: ContentFileConstructor) { | export function registerContentReferrer(s: ContentFileConstructor) { | ||||||
|     console.log(`registered content type: ${s.content_type}`) |     console.log(`registered content type: ${s.content_type}`); | ||||||
|     ContstructorTable[s.content_type] = s; |     ContstructorTable[s.content_type] = s; | ||||||
| } | } | ||||||
| export function createContentFile(type: string, path: string, option?: ContentConstructOption) { | export function createContentFile(type: string, path: string, option?: ContentConstructOption) { | ||||||
|  |  | ||||||
|  | @ -1,3 +1,3 @@ | ||||||
| import './comic'; | import "./comic"; | ||||||
| import './video'; | import "./video"; | ||||||
| export {ContentFile, createContentFile} from './file'; | export { ContentFile, createContentFile } from "./file"; | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import {ContentFile, registerContentReferrer, ContentConstructOption} from './file'; | import { ContentConstructOption, ContentFile, registerContentReferrer } from "./file"; | ||||||
| import {createDefaultClass} from './file'; | import { createDefaultClass } from "./file"; | ||||||
| 
 | 
 | ||||||
| export class VideoReferrer extends createDefaultClass("video") { | export class VideoReferrer extends createDefaultClass("video") { | ||||||
|     constructor(path: string, desc?: ContentConstructOption) { |     constructor(path: string, desc?: ContentConstructOption) { | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| import { existsSync } from 'fs'; | import { existsSync } from "fs"; | ||||||
| import Knex from 'knex'; | import Knex from "knex"; | ||||||
| import {Knex as KnexConfig} from './config'; | import { Knex as KnexConfig } from "./config"; | ||||||
| import { get_setting } from './SettingConfig'; | import { get_setting } from "./SettingConfig"; | ||||||
| 
 | 
 | ||||||
| export async function connectDB() { | export async function connectDB() { | ||||||
|     const env = get_setting().mode; |     const env = get_setting().mode; | ||||||
|  | @ -9,7 +9,7 @@ export async function connectDB(){ | ||||||
|     if (!config.connection) { |     if (!config.connection) { | ||||||
|         throw new Error("connection options required."); |         throw new Error("connection options required."); | ||||||
|     } |     } | ||||||
|     const connection = config.connection |     const connection = config.connection; | ||||||
|     if (typeof connection === "string") { |     if (typeof connection === "string") { | ||||||
|         throw new Error("unknown connection options"); |         throw new Error("unknown connection options"); | ||||||
|     } |     } | ||||||
|  | @ -25,16 +25,14 @@ export async function connectDB(){ | ||||||
|     for (;;) { |     for (;;) { | ||||||
|         try { |         try { | ||||||
|             console.log("try to connect db"); |             console.log("try to connect db"); | ||||||
|             await knex.raw('select 1 + 1;'); |             await knex.raw("select 1 + 1;"); | ||||||
|             console.log("connect success"); |             console.log("connect success"); | ||||||
|         } |         } catch (err) { | ||||||
|         catch(err){ |  | ||||||
|             if (tries < 3) { |             if (tries < 3) { | ||||||
|                 tries++; |                 tries++; | ||||||
|                 console.error(`connection fail ${err} retry...`); |                 console.error(`connection fail ${err} retry...`); | ||||||
|                 continue; |                 continue; | ||||||
|             } |             } else { | ||||||
|             else{ |  | ||||||
|                 throw err; |                 throw err; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -1,12 +1,12 @@ | ||||||
| import { Document, DocumentBody, DocumentAccessor, QueryListOption } from '../model/doc'; | import { Knex } from "knex"; | ||||||
| import {Knex} from 'knex'; | import { Document, DocumentAccessor, DocumentBody, QueryListOption } from "../model/doc"; | ||||||
| import {createKnexTagController} from './tag'; | import { TagAccessor } from "../model/tag"; | ||||||
| import { TagAccessor } from '../model/tag'; | import { createKnexTagController } from "./tag"; | ||||||
| 
 | 
 | ||||||
| export type DBTagContentRelation = { | export type DBTagContentRelation = { | ||||||
|     doc_id:number, |     doc_id: number; | ||||||
|     tag_name:string |     tag_name: string; | ||||||
| } | }; | ||||||
| 
 | 
 | ||||||
| class KnexDocumentAccessor implements DocumentAccessor { | class KnexDocumentAccessor implements DocumentAccessor { | ||||||
|     knex: Knex; |     knex: Knex; | ||||||
|  | @ -45,14 +45,14 @@ class KnexDocumentAccessor implements DocumentAccessor{ | ||||||
|                 const id_lst = await trx.insert({ |                 const id_lst = await trx.insert({ | ||||||
|                     additional: JSON.stringify(additional), |                     additional: JSON.stringify(additional), | ||||||
|                     created_at: Date.now(), |                     created_at: Date.now(), | ||||||
|                         ...rest |                     ...rest, | ||||||
|                 }).into("document"); |                 }).into("document"); | ||||||
|                 const id = id_lst[0]; |                 const id = id_lst[0]; | ||||||
|                 if (tags.length > 0) { |                 if (tags.length > 0) { | ||||||
|                     await trx.insert(tags.map(y => ({ |                     await trx.insert(tags.map(y => ({ | ||||||
|                         doc_id: id, |                         doc_id: id, | ||||||
|                         tag_name:y |                         tag_name: y, | ||||||
|                     }))).into('doc_tag_relation'); |                     }))).into("doc_tag_relation"); | ||||||
|                 } |                 } | ||||||
|                 ret.push(id); |                 ret.push(id); | ||||||
|             } |             } | ||||||
|  | @ -64,19 +64,19 @@ class KnexDocumentAccessor implements DocumentAccessor{ | ||||||
|         const id_lst = await this.knex.insert({ |         const id_lst = await this.knex.insert({ | ||||||
|             additional: JSON.stringify(additional), |             additional: JSON.stringify(additional), | ||||||
|             created_at: Date.now(), |             created_at: Date.now(), | ||||||
|             ...rest |             ...rest, | ||||||
|         }).into('document'); |         }).into("document"); | ||||||
|         const id = id_lst[0]; |         const id = id_lst[0]; | ||||||
|         for (const it of tags) { |         for (const it of tags) { | ||||||
|             this.tagController.addTag({ name: it }); |             this.tagController.addTag({ name: it }); | ||||||
|         } |         } | ||||||
|         if (tags.length > 0) { |         if (tags.length > 0) { | ||||||
|             await this.knex.insert<DBTagContentRelation>( |             await this.knex.insert<DBTagContentRelation>( | ||||||
|                 tags.map(x=>({doc_id:id,tag_name:x})) |                 tags.map(x => ({ doc_id: id, tag_name: x })), | ||||||
|             ).into("doc_tag_relation"); |             ).into("doc_tag_relation"); | ||||||
|         } |         } | ||||||
|         return id; |         return id; | ||||||
|     }; |     } | ||||||
|     async del(id: number) { |     async del(id: number) { | ||||||
|         if (await this.findById(id) !== undefined) { |         if (await this.findById(id) !== undefined) { | ||||||
|             await this.knex.delete().from("doc_tag_relation").where({ doc_id: id }); |             await this.knex.delete().from("doc_tag_relation").where({ doc_id: id }); | ||||||
|  | @ -84,12 +84,12 @@ class KnexDocumentAccessor implements DocumentAccessor{ | ||||||
|             return true; |             return true; | ||||||
|         } |         } | ||||||
|         return false; |         return false; | ||||||
|     }; |     } | ||||||
|     async findById(id: number, tagload?: boolean): Promise<Document | undefined> { |     async findById(id: number, tagload?: boolean): Promise<Document | undefined> { | ||||||
|         const s = await this.knex.select("*").from("document").where({ id: id }); |         const s = await this.knex.select("*").from("document").where({ id: id }); | ||||||
|         if (s.length === 0) return undefined; |         if (s.length === 0) return undefined; | ||||||
|         const first = s[0]; |         const first = s[0]; | ||||||
|         let ret_tags:string[] = [] |         let ret_tags: string[] = []; | ||||||
|         if (tagload === true) { |         if (tagload === true) { | ||||||
|             const tags: DBTagContentRelation[] = await this.knex.select("*") |             const tags: DBTagContentRelation[] = await this.knex.select("*") | ||||||
|                 .from("doc_tag_relation").where({ doc_id: first.id }); |                 .from("doc_tag_relation").where({ doc_id: first.id }); | ||||||
|  | @ -100,7 +100,7 @@ class KnexDocumentAccessor implements DocumentAccessor{ | ||||||
|             tags: ret_tags, |             tags: ret_tags, | ||||||
|             additional: first.additional !== null ? JSON.parse(first.additional) : {}, |             additional: first.additional !== null ? JSON.parse(first.additional) : {}, | ||||||
|         }; |         }; | ||||||
|     }; |     } | ||||||
|     async findDeleted(content_type: string) { |     async findDeleted(content_type: string) { | ||||||
|         const s = await this.knex.select("*") |         const s = await this.knex.select("*") | ||||||
|             .where({ content_type: content_type }) |             .where({ content_type: content_type }) | ||||||
|  | @ -109,7 +109,7 @@ class KnexDocumentAccessor implements DocumentAccessor{ | ||||||
|         return s.map(x => ({ |         return s.map(x => ({ | ||||||
|             ...x, |             ...x, | ||||||
|             tags: [], |             tags: [], | ||||||
|             additional:{} |             additional: {}, | ||||||
|         })); |         })); | ||||||
|     } |     } | ||||||
|     async findList(option?: QueryListOption) { |     async findList(option?: QueryListOption) { | ||||||
|  | @ -130,33 +130,35 @@ class KnexDocumentAccessor implements DocumentAccessor{ | ||||||
|                 query = query.where("tags_0.tag_name", "=", allow_tag[0]); |                 query = query.where("tags_0.tag_name", "=", allow_tag[0]); | ||||||
|                 for (let index = 1; index < allow_tag.length; index++) { |                 for (let index = 1; index < allow_tag.length; index++) { | ||||||
|                     const element = allow_tag[index]; |                     const element = allow_tag[index]; | ||||||
|                     query = query.innerJoin(`doc_tag_relation as tags_${index}`,`tags_${index}.doc_id`,"tags_0.doc_id"); |                     query = query.innerJoin( | ||||||
|                     query = query.where(`tags_${index}.tag_name`,'=',element); |                         `doc_tag_relation as tags_${index}`, | ||||||
|  |                         `tags_${index}.doc_id`, | ||||||
|  |                         "tags_0.doc_id", | ||||||
|  |                     ); | ||||||
|  |                     query = query.where(`tags_${index}.tag_name`, "=", element); | ||||||
|                 } |                 } | ||||||
|                 query = query.innerJoin("document", "tags_0.doc_id", "document.id"); |                 query = query.innerJoin("document", "tags_0.doc_id", "document.id"); | ||||||
|             } |             } else { | ||||||
|             else{ |  | ||||||
|                 query = query.from("document"); |                 query = query.from("document"); | ||||||
|             } |             } | ||||||
|             if (word !== undefined) { |             if (word !== undefined) { | ||||||
|                 // don't worry about sql injection.
 |                 // don't worry about sql injection.
 | ||||||
|                 query = query.where('title','like',`%${word}%`); |                 query = query.where("title", "like", `%${word}%`); | ||||||
|             } |             } | ||||||
|             if (content_type !== undefined) { |             if (content_type !== undefined) { | ||||||
|                 query = query.where('content_type','=',content_type); |                 query = query.where("content_type", "=", content_type); | ||||||
|             } |             } | ||||||
|             if (use_offset) { |             if (use_offset) { | ||||||
|                 query = query.offset(offset); |                 query = query.offset(offset); | ||||||
|             } |             } else { | ||||||
|             else{ |  | ||||||
|                 if (cursor !== undefined) { |                 if (cursor !== undefined) { | ||||||
|                     query = query.where('id','<',cursor); |                     query = query.where("id", "<", cursor); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             query = query.limit(limit); |             query = query.limit(limit); | ||||||
|             query = query.orderBy('id',"desc"); |             query = query.orderBy("id", "desc"); | ||||||
|             return query; |             return query; | ||||||
|         } |         }; | ||||||
|         let query = buildquery(); |         let query = buildquery(); | ||||||
|         // console.log(query.toSQL());
 |         // console.log(query.toSQL());
 | ||||||
|         let result: Document[] = await query; |         let result: Document[] = await query; | ||||||
|  | @ -173,24 +175,25 @@ class KnexDocumentAccessor implements DocumentAccessor{ | ||||||
|             let tagquery = this.knex.select("id", "doc_tag_relation.tag_name").from(subquery) |             let tagquery = this.knex.select("id", "doc_tag_relation.tag_name").from(subquery) | ||||||
|                 .innerJoin("doc_tag_relation", "doc_tag_relation.doc_id", "id"); |                 .innerJoin("doc_tag_relation", "doc_tag_relation.doc_id", "id"); | ||||||
|             // console.log(tagquery.toSQL());
 |             // console.log(tagquery.toSQL());
 | ||||||
|             let tagresult:{id:number,tag_name:string}[] = await tagquery; |             let tagresult: { id: number; tag_name: string }[] = await tagquery; | ||||||
|             for (const { id, tag_name } of tagresult) { |             for (const { id, tag_name } of tagresult) { | ||||||
|                 idmap[id].tags.push(tag_name); |                 idmap[id].tags.push(tag_name); | ||||||
|             } |             } | ||||||
|         } |         } else { | ||||||
|         else{ |             result.forEach(v => { | ||||||
|             result.forEach(v=>{v.tags = [];}); |                 v.tags = []; | ||||||
|  |             }); | ||||||
|         } |         } | ||||||
|         return result; |         return result; | ||||||
|     }; |     } | ||||||
|     async findByPath(path: string, filename?: string): Promise<Document[]> { |     async findByPath(path: string, filename?: string): Promise<Document[]> { | ||||||
|         const e = filename == undefined ? {} : {filename:filename} |         const e = filename == undefined ? {} : { filename: filename }; | ||||||
|         const results = await this.knex.select("*").from("document").where({ basepath: path, ...e }); |         const results = await this.knex.select("*").from("document").where({ basepath: path, ...e }); | ||||||
|         return results.map(x => ({ |         return results.map(x => ({ | ||||||
|             ...x, |             ...x, | ||||||
|             tags: [], |             tags: [], | ||||||
|             additional:{} |             additional: {}, | ||||||
|         })) |         })); | ||||||
|     } |     } | ||||||
|     async update(c: Partial<Document> & { id: number }) { |     async update(c: Partial<Document> & { id: number }) { | ||||||
|         const { id, tags, ...rest } = c; |         const { id, tags, ...rest } = c; | ||||||
|  | @ -217,4 +220,4 @@ class KnexDocumentAccessor implements DocumentAccessor{ | ||||||
| } | } | ||||||
| export const createKnexDocumentAccessor = (knex: Knex): DocumentAccessor => { | export const createKnexDocumentAccessor = (knex: Knex): DocumentAccessor => { | ||||||
|     return new KnexDocumentAccessor(knex); |     return new KnexDocumentAccessor(knex); | ||||||
| } | }; | ||||||
|  |  | ||||||
|  | @ -1,3 +1,3 @@ | ||||||
| export * from './doc'; | export * from "./doc"; | ||||||
| export * from './tag'; | export * from "./tag"; | ||||||
| export * from './user'; | export * from "./user"; | ||||||
|  |  | ||||||
|  | @ -1,14 +1,14 @@ | ||||||
| import {Tag, TagAccessor, TagCount} from '../model/tag'; | import { Knex } from "knex"; | ||||||
| import {Knex} from 'knex'; | import { Tag, TagAccessor, TagCount } from "../model/tag"; | ||||||
| import {DBTagContentRelation} from './doc'; | import { DBTagContentRelation } from "./doc"; | ||||||
| 
 | 
 | ||||||
| type DBTags = { | type DBTags = { | ||||||
|     name: string, |     name: string; | ||||||
|     description?: string |     description?: string; | ||||||
| } | }; | ||||||
| 
 | 
 | ||||||
| class KnexTagAccessor implements TagAccessor { | class KnexTagAccessor implements TagAccessor { | ||||||
|     knex:Knex<DBTags> |     knex: Knex<DBTags>; | ||||||
|     constructor(knex: Knex) { |     constructor(knex: Knex) { | ||||||
|         this.knex = knex; |         this.knex = knex; | ||||||
|     } |     } | ||||||
|  | @ -19,11 +19,11 @@ class KnexTagAccessor implements TagAccessor{ | ||||||
|     } |     } | ||||||
|     async getAllTagList(onlyname?: boolean) { |     async getAllTagList(onlyname?: boolean) { | ||||||
|         onlyname = onlyname ?? false; |         onlyname = onlyname ?? false; | ||||||
|         const t:DBTags[] = await this.knex.select(onlyname ? "*" : "name").from("tags") |         const t: DBTags[] = await this.knex.select(onlyname ? "*" : "name").from("tags"); | ||||||
|         return t; |         return t; | ||||||
|     } |     } | ||||||
|     async getTagByName(name: string) { |     async getTagByName(name: string) { | ||||||
|         const t:DBTags[] = await this.knex.select('*').from("tags").where({name: name}); |         const t: DBTags[] = await this.knex.select("*").from("tags").where({ name: name }); | ||||||
|         if (t.length === 0) return undefined; |         if (t.length === 0) return undefined; | ||||||
|         return t[0]; |         return t[0]; | ||||||
|     } |     } | ||||||
|  | @ -31,7 +31,7 @@ class KnexTagAccessor implements TagAccessor{ | ||||||
|         if (await this.getTagByName(tag.name) === undefined) { |         if (await this.getTagByName(tag.name) === undefined) { | ||||||
|             await this.knex.insert<DBTags>({ |             await this.knex.insert<DBTags>({ | ||||||
|                 name: tag.name, |                 name: tag.name, | ||||||
|                 description:tag.description === undefined ? "" : tag.description |                 description: tag.description === undefined ? "" : tag.description, | ||||||
|             }).into("tags"); |             }).into("tags"); | ||||||
|             return true; |             return true; | ||||||
|         } |         } | ||||||
|  | @ -51,7 +51,7 @@ class KnexTagAccessor implements TagAccessor{ | ||||||
|         } |         } | ||||||
|         return false; |         return false; | ||||||
|     } |     } | ||||||
| }; | } | ||||||
| export const createKnexTagController = (knex: Knex): TagAccessor => { | export const createKnexTagController = (knex: Knex): TagAccessor => { | ||||||
|     return new KnexTagAccessor(knex); |     return new KnexTagAccessor(knex); | ||||||
| } | }; | ||||||
|  |  | ||||||
|  | @ -1,15 +1,15 @@ | ||||||
| import {Knex} from 'knex'; | import { Knex } from "knex"; | ||||||
| import {IUser,UserCreateInput, UserAccessor, Password} from '../model/user'; | import { IUser, Password, UserAccessor, UserCreateInput } from "../model/user"; | ||||||
| 
 | 
 | ||||||
| type PermissionTable = { | type PermissionTable = { | ||||||
|     username:string, |     username: string; | ||||||
|     name:string |     name: string; | ||||||
| }; | }; | ||||||
| type DBUser = { | type DBUser = { | ||||||
|     username : string, |     username: string; | ||||||
|     password_hash: string, |     password_hash: string; | ||||||
|     password_salt: string |     password_salt: string; | ||||||
| } | }; | ||||||
| class KnexUser implements IUser { | class KnexUser implements IUser { | ||||||
|     private knex: Knex; |     private knex: Knex; | ||||||
|     readonly username: string; |     readonly username: string; | ||||||
|  | @ -27,7 +27,7 @@ class KnexUser implements IUser{ | ||||||
|             .update({ password_hash: this.password.hash, password_salt: this.password.salt }); |             .update({ password_hash: this.password.hash, password_salt: this.password.salt }); | ||||||
|     } |     } | ||||||
|     async get_permissions() { |     async get_permissions() { | ||||||
|         let b = (await this.knex.select('*').from("permissions") |         let b = (await this.knex.select("*").from("permissions") | ||||||
|             .where({ username: this.username })) as PermissionTable[]; |             .where({ username: this.username })) as PermissionTable[]; | ||||||
|         return b.map(x => x.name); |         return b.map(x => x.name); | ||||||
|     } |     } | ||||||
|  | @ -35,7 +35,7 @@ class KnexUser implements IUser{ | ||||||
|         if (!(await this.get_permissions()).includes(name)) { |         if (!(await this.get_permissions()).includes(name)) { | ||||||
|             const r = await this.knex.insert({ |             const r = await this.knex.insert({ | ||||||
|                 username: this.username, |                 username: this.username, | ||||||
|                 name: name |                 name: name, | ||||||
|             }).into("permissions"); |             }).into("permissions"); | ||||||
|             return true; |             return true; | ||||||
|         } |         } | ||||||
|  | @ -45,7 +45,8 @@ class KnexUser implements IUser{ | ||||||
|         const r = await this.knex |         const r = await this.knex | ||||||
|             .from("permissions") |             .from("permissions") | ||||||
|             .where({ |             .where({ | ||||||
|                 username:this.username, name:name |                 username: this.username, | ||||||
|  |                 name: name, | ||||||
|             }).delete(); |             }).delete(); | ||||||
|         return r !== 0; |         return r !== 0; | ||||||
|     } |     } | ||||||
|  | @ -60,23 +61,27 @@ export const createKnexUserController = (knex: Knex):UserAccessor=>{ | ||||||
|         await knex.insert<DBUser>({ |         await knex.insert<DBUser>({ | ||||||
|             username: user.username, |             username: user.username, | ||||||
|             password_hash: user.password.hash, |             password_hash: user.password.hash, | ||||||
|             password_salt: user.password.salt}).into("users"); |             password_salt: user.password.salt, | ||||||
|  |         }).into("users"); | ||||||
|         return user; |         return user; | ||||||
|     }; |     }; | ||||||
|     const findUserKenx = async (id: string) => { |     const findUserKenx = async (id: string) => { | ||||||
|         let user: DBUser[] = await knex.select("*").from("users").where({ username: id }); |         let user: DBUser[] = await knex.select("*").from("users").where({ username: id }); | ||||||
|         if (user.length == 0) return undefined; |         if (user.length == 0) return undefined; | ||||||
|         const first = user[0]; |         const first = user[0]; | ||||||
|         return new KnexUser(first.username, |         return new KnexUser( | ||||||
|             new Password({hash: first.password_hash, salt: first.password_salt}), knex); |             first.username, | ||||||
|     } |             new Password({ hash: first.password_hash, salt: first.password_salt }), | ||||||
|  |             knex, | ||||||
|  |         ); | ||||||
|  |     }; | ||||||
|     const delUserKnex = async (id: string) => { |     const delUserKnex = async (id: string) => { | ||||||
|         let r = await knex.delete().from("users").where({ username: id }); |         let r = await knex.delete().from("users").where({ username: id }); | ||||||
|         return r === 0; |         return r === 0; | ||||||
|     } |     }; | ||||||
|     return { |     return { | ||||||
|         createUser: createUserKnex, |         createUser: createUserKnex, | ||||||
|         findUser: findUserKenx, |         findUser: findUserKenx, | ||||||
|         delUser: delUserKnex, |         delUser: delUserKnex, | ||||||
|     }; |     }; | ||||||
| } | }; | ||||||
|  |  | ||||||
|  | @ -1,8 +1,8 @@ | ||||||
| import { basename, dirname, join as pathjoin } from 'path'; | import { basename, dirname, join as pathjoin } from "path"; | ||||||
| import { Document, DocumentAccessor } from '../model/mod'; | import { ContentFile, createContentFile } from "../content/mod"; | ||||||
| import { ContentFile, createContentFile } from '../content/mod'; | import { Document, DocumentAccessor } from "../model/mod"; | ||||||
| import { IDiffWatcher } from './watcher'; | import { ContentList } from "./content_list"; | ||||||
| import { ContentList } from './content_list'; | import { IDiffWatcher } from "./watcher"; | ||||||
| 
 | 
 | ||||||
| // refactoring needed.
 | // refactoring needed.
 | ||||||
| export class ContentDiffHandler { | export class ContentDiffHandler { | ||||||
|  | @ -26,9 +26,9 @@ export class ContentDiffHandler { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|     register(diff: IDiffWatcher) { |     register(diff: IDiffWatcher) { | ||||||
|         diff.on('create', (path) => this.OnCreated(path)) |         diff.on("create", (path) => this.OnCreated(path)) | ||||||
|             .on('delete', (path) => this.OnDeleted(path)) |             .on("delete", (path) => this.OnDeleted(path)) | ||||||
|             .on('change', (prev, cur) => this.OnChanged(prev, cur)); |             .on("change", (prev, cur) => this.OnChanged(prev, cur)); | ||||||
|     } |     } | ||||||
|     private async OnDeleted(cpath: string) { |     private async OnDeleted(cpath: string) { | ||||||
|         const basepath = dirname(cpath); |         const basepath = dirname(cpath); | ||||||
|  | @ -83,14 +83,13 @@ export class ContentDiffHandler { | ||||||
|                 id: c.id, |                 id: c.id, | ||||||
|                 deleted_at: null, |                 deleted_at: null, | ||||||
|                 filename: filename, |                 filename: filename, | ||||||
|                 basepath: basepath |                 basepath: basepath, | ||||||
|             }); |             }); | ||||||
|         } |         } | ||||||
|         if (this.waiting_list.hasByHash(hash)) { |         if (this.waiting_list.hasByHash(hash)) { | ||||||
|             console.log("Hash Conflict!!!"); |             console.log("Hash Conflict!!!"); | ||||||
|         } |         } | ||||||
|         this.waiting_list.set(content); |         this.waiting_list.set(content); | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
|     private async OnChanged(prev_path: string, cur_path: string) { |     private async OnChanged(prev_path: string, cur_path: string) { | ||||||
|         const prev_basepath = dirname(prev_path); |         const prev_basepath = dirname(prev_path); | ||||||
|  | @ -115,7 +114,7 @@ export class ContentDiffHandler { | ||||||
|         await this.doc_cntr.update({ |         await this.doc_cntr.update({ | ||||||
|             ...doc[0], |             ...doc[0], | ||||||
|             basepath: cur_basepath, |             basepath: cur_basepath, | ||||||
|             filename: cur_filename |             filename: cur_filename, | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { ContentFile } from '../content/mod'; | import { ContentFile } from "../content/mod"; | ||||||
| 
 | 
 | ||||||
| export class ContentList { | export class ContentList { | ||||||
|     /** path map */ |     /** path map */ | ||||||
|  | @ -7,8 +7,8 @@ export class ContentList{ | ||||||
|     private hl: Map<string, ContentFile>; |     private hl: Map<string, ContentFile>; | ||||||
| 
 | 
 | ||||||
|     constructor() { |     constructor() { | ||||||
|         this.cl = new Map; |         this.cl = new Map(); | ||||||
|         this.hl = new Map; |         this.hl = new Map(); | ||||||
|     } |     } | ||||||
|     hasByHash(s: string) { |     hasByHash(s: string) { | ||||||
|         return this.hl.has(s); |         return this.hl.has(s); | ||||||
|  | @ -17,7 +17,7 @@ export class ContentList{ | ||||||
|         return this.cl.has(p); |         return this.cl.has(p); | ||||||
|     } |     } | ||||||
|     getByHash(s: string) { |     getByHash(s: string) { | ||||||
|         return this.hl.get(s) |         return this.hl.get(s); | ||||||
|     } |     } | ||||||
|     getByPath(p: string) { |     getByPath(p: string) { | ||||||
|         return this.cl.get(p); |         return this.cl.get(p); | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| import { DocumentAccessor } from '../model/doc'; | import asyncPool from "tiny-async-pool"; | ||||||
| import {ContentDiffHandler} from './content_handler'; | import { DocumentAccessor } from "../model/doc"; | ||||||
| import { IDiffWatcher } from './watcher'; | import { ContentDiffHandler } from "./content_handler"; | ||||||
| import asyncPool from 'tiny-async-pool'; | import { IDiffWatcher } from "./watcher"; | ||||||
| 
 | 
 | ||||||
| export class DiffManager { | export class DiffManager { | ||||||
|     watching: { [content_type: string]: ContentDiffHandler }; |     watching: { [content_type: string]: ContentDiffHandler }; | ||||||
|  | @ -42,4 +42,4 @@ export class DiffManager{ | ||||||
|             value: this.watching[x].waiting_list.getAll(), |             value: this.watching[x].waiting_list.getAll(), | ||||||
|         })); |         })); | ||||||
|     } |     } | ||||||
| }; | } | ||||||
|  |  | ||||||
|  | @ -1,2 +1,2 @@ | ||||||
| export * from './router'; | export * from "./diff"; | ||||||
| export * from './diff'; | export * from "./router"; | ||||||
|  |  | ||||||
|  | @ -1,9 +1,9 @@ | ||||||
| import Koa from 'koa'; | import Koa from "koa"; | ||||||
| import Router from 'koa-router'; | import Router from "koa-router"; | ||||||
| import { ContentFile } from '../content/mod'; | import { ContentFile } from "../content/mod"; | ||||||
| import { sendError } from '../route/error_handler'; | import { AdminOnlyMiddleware } from "../permission/permission"; | ||||||
| import {DiffManager} from './diff'; | import { sendError } from "../route/error_handler"; | ||||||
| import {AdminOnlyMiddleware} from '../permission/permission'; | import { DiffManager } from "./diff"; | ||||||
| 
 | 
 | ||||||
| function content_file_to_return(x: ContentFile) { | function content_file_to_return(x: ContentFile) { | ||||||
|     return { path: x.path, type: x.type }; |     return { path: x.path, type: x.type }; | ||||||
|  | @ -15,17 +15,17 @@ export const getAdded = (diffmgr:DiffManager)=> (ctx:Koa.Context,next:Koa.Next)= | ||||||
|         type: x.type, |         type: x.type, | ||||||
|         value: x.value.map(x => ({ path: x.path, type: x.type })), |         value: x.value.map(x => ({ path: x.path, type: x.type })), | ||||||
|     })); |     })); | ||||||
|     ctx.type = 'json'; |     ctx.type = "json"; | ||||||
| } | }; | ||||||
| 
 | 
 | ||||||
| type PostAddedBody = { | type PostAddedBody = { | ||||||
|     type:string, |     type: string; | ||||||
|     path:string, |     path: string; | ||||||
| }[]; | }[]; | ||||||
| 
 | 
 | ||||||
| function checkPostAddedBody(body: any): body is PostAddedBody { | function checkPostAddedBody(body: any): body is PostAddedBody { | ||||||
|     if (body instanceof Array) { |     if (body instanceof Array) { | ||||||
|         return body.map(x=> 'type' in x && 'path' in x).every(x=>x); |         return body.map(x => "type" in x && "path" in x).every(x => x); | ||||||
|     } |     } | ||||||
|     return false; |     return false; | ||||||
| } | } | ||||||
|  | @ -41,11 +41,11 @@ export const postAdded = (diffmgr:DiffManager) => async (ctx:Router.IRouterConte | ||||||
|     ctx.body = { |     ctx.body = { | ||||||
|         ok: true, |         ok: true, | ||||||
|         docs: results, |         docs: results, | ||||||
|     } |     }; | ||||||
|     ctx.type = 'json'; |     ctx.type = "json"; | ||||||
| } | }; | ||||||
| export const postAddedAll = (diffmgr: DiffManager) => async (ctx: Router.IRouterContext, next: Koa.Next) => { | export const postAddedAll = (diffmgr: DiffManager) => async (ctx: Router.IRouterContext, next: Koa.Next) => { | ||||||
|     if (!ctx.is('json')){ |     if (!ctx.is("json")) { | ||||||
|         sendError(400, "format exception"); |         sendError(400, "format exception"); | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
|  | @ -61,10 +61,10 @@ export const postAddedAll = (diffmgr: DiffManager) => async (ctx:Router.IRouterC | ||||||
|     } |     } | ||||||
|     await diffmgr.commitAll(t); |     await diffmgr.commitAll(t); | ||||||
|     ctx.body = { |     ctx.body = { | ||||||
|         ok:true |         ok: true, | ||||||
|  |     }; | ||||||
|  |     ctx.type = "json"; | ||||||
| }; | }; | ||||||
|     ctx.type = 'json'; |  | ||||||
| } |  | ||||||
| /* | /* | ||||||
| export const getNotWatched = (diffmgr : DiffManager)=> (ctx:Router.IRouterContext,next:Koa.Next)=>{ | export const getNotWatched = (diffmgr : DiffManager)=> (ctx:Router.IRouterContext,next:Koa.Next)=>{ | ||||||
|     ctx.body = { |     ctx.body = { | ||||||
|  |  | ||||||
|  | @ -1,15 +1,15 @@ | ||||||
| import { FSWatcher, watch } from 'fs'; | import event from "events"; | ||||||
| import { promises } from 'fs'; | import { FSWatcher, watch } from "fs"; | ||||||
| import event from 'events'; | import { promises } from "fs"; | ||||||
| import { join } from 'path'; | import { join } from "path"; | ||||||
| import { DocumentAccessor } from '../model/doc'; | import { DocumentAccessor } from "../model/doc"; | ||||||
| 
 | 
 | ||||||
| const readdir = promises.readdir; | const readdir = promises.readdir; | ||||||
| 
 | 
 | ||||||
| export interface DiffWatcherEvent { | export interface DiffWatcherEvent { | ||||||
|     'create':(path:string)=>void, |     "create": (path: string) => void; | ||||||
|     'delete':(path:string)=>void, |     "delete": (path: string) => void; | ||||||
|     'change':(prev_path:string,cur_path:string)=>void, |     "change": (prev_path: string, cur_path: string) => void; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface IDiffWatcher extends event.EventEmitter { | export interface IDiffWatcher extends event.EventEmitter { | ||||||
|  |  | ||||||
|  | @ -1 +1,12 @@ | ||||||
| {"$schema":"http://json-schema.org/draft-07/schema#","$ref":"#/definitions/ComicConfig","definitions":{"ComicConfig":{"type":"object","properties":{"watch":{"type":"array","items":{"type":"string"}},"$schema":{"type":"string"}},"required":["watch"],"additionalProperties":false}}} | { | ||||||
|  |   "$schema": "http://json-schema.org/draft-07/schema#", | ||||||
|  |   "$ref": "#/definitions/ComicConfig", | ||||||
|  |   "definitions": { | ||||||
|  |     "ComicConfig": { | ||||||
|  |       "type": "object", | ||||||
|  |       "properties": { "watch": { "type": "array", "items": { "type": "string" } }, "$schema": { "type": "string" } }, | ||||||
|  |       "required": ["watch"], | ||||||
|  |       "additionalProperties": false | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -1,8 +1,7 @@ | ||||||
| import {ConfigManager} from '../../util/configRW'; | import { ConfigManager } from "../../util/configRW"; | ||||||
| import ComicSchema from "./ComicConfig.schema.json" | import ComicSchema from "./ComicConfig.schema.json"; | ||||||
| export interface ComicConfig { | export interface ComicConfig { | ||||||
|     watch:string[] |     watch: string[]; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const ComicConfig = new ConfigManager<ComicConfig>("comic_config.json", { watch: [] }, ComicSchema); | export const ComicConfig = new ConfigManager<ComicConfig>("comic_config.json", { watch: [] }, ComicSchema); | ||||||
| 
 |  | ||||||
|  |  | ||||||
|  | @ -1,17 +1,16 @@ | ||||||
| import {IDiffWatcher, DiffWatcherEvent} from '../watcher'; | import { EventEmitter } from "events"; | ||||||
| import {EventEmitter} from 'events'; | import { DocumentAccessor } from "../../model/doc"; | ||||||
| import { DocumentAccessor } from '../../model/doc'; | import { DiffWatcherEvent, IDiffWatcher } from "../watcher"; | ||||||
| import { WatcherFilter } from './watcher_filter'; | import { ComicConfig } from "./ComicConfig"; | ||||||
| import { RecursiveWatcher } from './recursive_watcher'; | import { WatcherCompositer } from "./compositer"; | ||||||
| import { ComicConfig } from './ComicConfig'; | import { RecursiveWatcher } from "./recursive_watcher"; | ||||||
| import {WatcherCompositer} from './compositer' | import { WatcherFilter } from "./watcher_filter"; | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| const createComicWatcherBase = (path: string) => { | const createComicWatcherBase = (path: string) => { | ||||||
|     return new WatcherFilter(new RecursiveWatcher(path), (x) => x.endsWith(".zip")); |     return new WatcherFilter(new RecursiveWatcher(path), (x) => x.endsWith(".zip")); | ||||||
| } | }; | ||||||
| export const createComicWatcher = () => { | export const createComicWatcher = () => { | ||||||
|     const file = ComicConfig.get_config_file(); |     const file = ComicConfig.get_config_file(); | ||||||
|     console.log(`register comic ${file.watch.join(",")}`) |     console.log(`register comic ${file.watch.join(",")}`); | ||||||
|     return new WatcherCompositer(file.watch.map(path => createComicWatcherBase(path))); |     return new WatcherCompositer(file.watch.map(path => createComicWatcherBase(path))); | ||||||
| } | }; | ||||||
|  |  | ||||||
|  | @ -1,9 +1,9 @@ | ||||||
| import event from 'events'; | import event from "events"; | ||||||
| import {FSWatcher,watch,promises} from 'fs'; | import { FSWatcher, promises, watch } from "fs"; | ||||||
| import {IDiffWatcher, DiffWatcherEvent} from '../watcher'; | import { join } from "path"; | ||||||
| import {join} from 'path'; | import { DocumentAccessor } from "../../model/doc"; | ||||||
| import { DocumentAccessor } from '../../model/doc'; | import { DiffWatcherEvent, IDiffWatcher } from "../watcher"; | ||||||
| import { setupHelp } from './util'; | import { setupHelp } from "./util"; | ||||||
| 
 | 
 | ||||||
| const { readdir } = promises; | const { readdir } = promises; | ||||||
| 
 | 
 | ||||||
|  | @ -25,10 +25,9 @@ export class CommonDiffWatcher extends event.EventEmitter implements IDiffWatche | ||||||
|                 const cur = await readdir(this._path); |                 const cur = await readdir(this._path); | ||||||
|                 // add
 |                 // add
 | ||||||
|                 if (cur.includes(filename)) { |                 if (cur.includes(filename)) { | ||||||
|                     this.emit('create',join(this.path,filename)); |                     this.emit("create", join(this.path, filename)); | ||||||
|                 } |                 } else { | ||||||
|                 else{ |                     this.emit("delete", join(this.path, filename)); | ||||||
|                     this.emit('delete',join(this.path,filename)) |  | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|  | @ -40,6 +39,6 @@ export class CommonDiffWatcher extends event.EventEmitter implements IDiffWatche | ||||||
|         return this._path; |         return this._path; | ||||||
|     } |     } | ||||||
|     watchClose() { |     watchClose() { | ||||||
|         this._watcher.close() |         this._watcher.close(); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -2,7 +2,6 @@ import { EventEmitter } from "events"; | ||||||
| import { DocumentAccessor } from "../../model/doc"; | import { DocumentAccessor } from "../../model/doc"; | ||||||
| import { DiffWatcherEvent, IDiffWatcher, linkWatcher } from "../watcher"; | import { DiffWatcherEvent, IDiffWatcher, linkWatcher } from "../watcher"; | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| export class WatcherCompositer extends EventEmitter implements IDiffWatcher { | export class WatcherCompositer extends EventEmitter implements IDiffWatcher { | ||||||
|     refWatchers: IDiffWatcher[]; |     refWatchers: IDiffWatcher[]; | ||||||
|     on<U extends keyof DiffWatcherEvent>(event: U, listener: DiffWatcherEvent[U]): this { |     on<U extends keyof DiffWatcherEvent>(event: U, listener: DiffWatcherEvent[U]): this { | ||||||
|  |  | ||||||
|  | @ -1,16 +1,16 @@ | ||||||
| import {watch, FSWatcher} from 'chokidar'; | import { FSWatcher, watch } from "chokidar"; | ||||||
| import { EventEmitter } from 'events'; | import { EventEmitter } from "events"; | ||||||
| import { join } from 'path'; | import { join } from "path"; | ||||||
| import { DocumentAccessor } from '../../model/doc'; | import { DocumentAccessor } from "../../model/doc"; | ||||||
| import { DiffWatcherEvent, IDiffWatcher } from '../watcher'; | import { DiffWatcherEvent, IDiffWatcher } from "../watcher"; | ||||||
| import { setupHelp, setupRecursive } from './util'; | import { setupHelp, setupRecursive } from "./util"; | ||||||
| 
 | 
 | ||||||
| type RecursiveWatcherOption = { | type RecursiveWatcherOption = { | ||||||
|     /** @default true */ |     /** @default true */ | ||||||
|     watchFile?:boolean, |     watchFile?: boolean; | ||||||
|     /** @default false */ |     /** @default false */ | ||||||
|     watchDir?:boolean, |     watchDir?: boolean; | ||||||
| } | }; | ||||||
| 
 | 
 | ||||||
| export class RecursiveWatcher extends EventEmitter implements IDiffWatcher { | export class RecursiveWatcher extends EventEmitter implements IDiffWatcher { | ||||||
|     on<U extends keyof DiffWatcherEvent>(event: U, listener: DiffWatcherEvent[U]): this { |     on<U extends keyof DiffWatcherEvent>(event: U, listener: DiffWatcherEvent[U]): this { | ||||||
|  | @ -20,7 +20,7 @@ export class RecursiveWatcher extends EventEmitter implements IDiffWatcher  { | ||||||
|         return super.emit(event, ...arg); |         return super.emit(event, ...arg); | ||||||
|     } |     } | ||||||
|     readonly path: string; |     readonly path: string; | ||||||
|     private watcher: FSWatcher |     private watcher: FSWatcher; | ||||||
| 
 | 
 | ||||||
|     constructor(path: string, option: RecursiveWatcherOption = { |     constructor(path: string, option: RecursiveWatcherOption = { | ||||||
|         watchDir: false, |         watchDir: false, | ||||||
|  | @ -52,7 +52,7 @@ export class RecursiveWatcher extends EventEmitter implements IDiffWatcher  { | ||||||
|             }).on("unlinkDir", path => { |             }).on("unlinkDir", path => { | ||||||
|                 const cpath = path; |                 const cpath = path; | ||||||
|                 this.emit("delete", cpath); |                 this.emit("delete", cpath); | ||||||
|             }) |             }); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|     async setup(cntr: DocumentAccessor): Promise<void> { |     async setup(cntr: DocumentAccessor): Promise<void> { | ||||||
|  |  | ||||||
|  | @ -5,18 +5,17 @@ const {readdir} = promises; | ||||||
| import { DocumentAccessor } from "../../model/doc"; | import { DocumentAccessor } from "../../model/doc"; | ||||||
| import { IDiffWatcher } from "../watcher"; | import { IDiffWatcher } from "../watcher"; | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| function setupCommon(watcher: IDiffWatcher, basepath: string, initial_filenames: string[], cur: string[]) { | function setupCommon(watcher: IDiffWatcher, basepath: string, initial_filenames: string[], cur: string[]) { | ||||||
|     // Todo : reduce O(nm) to O(n+m) using hash map.
 |     // Todo : reduce O(nm) to O(n+m) using hash map.
 | ||||||
|     let added = cur.filter(x => !initial_filenames.includes(x)); |     let added = cur.filter(x => !initial_filenames.includes(x)); | ||||||
|     let deleted = initial_filenames.filter(x => !cur.includes(x)); |     let deleted = initial_filenames.filter(x => !cur.includes(x)); | ||||||
|     for (const it of added) { |     for (const it of added) { | ||||||
|         const cpath = join(basepath, it); |         const cpath = join(basepath, it); | ||||||
|         watcher.emit('create',cpath); |         watcher.emit("create", cpath); | ||||||
|     } |     } | ||||||
|     for (const it of deleted) { |     for (const it of deleted) { | ||||||
|         const cpath = join(basepath, it); |         const cpath = join(basepath, it); | ||||||
|         watcher.emit('delete',cpath); |         watcher.emit("delete", cpath); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| export async function setupHelp(watcher: IDiffWatcher, basepath: string, cntr: DocumentAccessor) { | export async function setupHelp(watcher: IDiffWatcher, basepath: string, cntr: DocumentAccessor) { | ||||||
|  | @ -30,6 +29,8 @@ export async function setupRecursive(watcher:IDiffWatcher,basepath:string,cntr:D | ||||||
|     const initial_filenames = initial_document.map(x => x.filename); |     const initial_filenames = initial_document.map(x => x.filename); | ||||||
|     const cur = await readdir(basepath, { withFileTypes: true }); |     const cur = await readdir(basepath, { withFileTypes: true }); | ||||||
|     setupCommon(watcher, basepath, initial_filenames, cur.map(x => x.name)); |     setupCommon(watcher, basepath, initial_filenames, cur.map(x => x.name)); | ||||||
|     await Promise.all([cur.filter(x=>x.isDirectory()) |     await Promise.all([ | ||||||
|         .map(x=>setupHelp(watcher,join(basepath,x.name),cntr))]); |         cur.filter(x => x.isDirectory()) | ||||||
|  |             .map(x => setupHelp(watcher, join(basepath, x.name), cntr)), | ||||||
|  |     ]); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -2,10 +2,9 @@ import { EventEmitter } from "events"; | ||||||
| import { DocumentAccessor } from "../../model/doc"; | import { DocumentAccessor } from "../../model/doc"; | ||||||
| import { DiffWatcherEvent, IDiffWatcher, linkWatcher } from "../watcher"; | import { DiffWatcherEvent, IDiffWatcher, linkWatcher } from "../watcher"; | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| export class WatcherFilter extends EventEmitter implements IDiffWatcher { | export class WatcherFilter extends EventEmitter implements IDiffWatcher { | ||||||
|     refWatcher: IDiffWatcher; |     refWatcher: IDiffWatcher; | ||||||
|     filter : (filename:string)=>boolean;; |     filter: (filename: string) => boolean; | ||||||
|     on<U extends keyof DiffWatcherEvent>(event: U, listener: DiffWatcherEvent[U]): this { |     on<U extends keyof DiffWatcherEvent>(event: U, listener: DiffWatcherEvent[U]): this { | ||||||
|         return super.on(event, listener); |         return super.on(event, listener); | ||||||
|     } |     } | ||||||
|  | @ -22,22 +21,18 @@ export class WatcherFilter extends EventEmitter implements IDiffWatcher{ | ||||||
|             if (this.filter(prev)) { |             if (this.filter(prev)) { | ||||||
|                 if (this.filter(cur)) { |                 if (this.filter(cur)) { | ||||||
|                     return super.emit("change", prev, cur); |                     return super.emit("change", prev, cur); | ||||||
|                 } |                 } else { | ||||||
|                 else{ |  | ||||||
|                     return super.emit("delete", cur); |                     return super.emit("delete", cur); | ||||||
|                 } |                 } | ||||||
|             } |             } else { | ||||||
|             else{ |  | ||||||
|                 if (this.filter(cur)) { |                 if (this.filter(cur)) { | ||||||
|                     return super.emit("create", cur); |                     return super.emit("create", cur); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             return false; |             return false; | ||||||
|         } |         } else if (!this.filter(arg[0])) { | ||||||
|         else if(!this.filter(arg[0])){ |  | ||||||
|             return false; |             return false; | ||||||
|         } |         } else return super.emit(event, ...arg); | ||||||
|         else return super.emit(event,...arg); |  | ||||||
|     } |     } | ||||||
|     constructor(refWatcher: IDiffWatcher, filter: (filename: string) => boolean) { |     constructor(refWatcher: IDiffWatcher, filter: (filename: string) => boolean) { | ||||||
|         super(); |         super(); | ||||||
|  |  | ||||||
							
								
								
									
										70
									
								
								src/login.ts
									
										
									
									
									
								
							
							
						
						
									
										70
									
								
								src/login.ts
									
										
									
									
									
								
							|  | @ -1,12 +1,12 @@ | ||||||
|  | import { request } from "http"; | ||||||
| import { decode, sign, TokenExpiredError, verify } from "jsonwebtoken"; | import { decode, sign, TokenExpiredError, verify } from "jsonwebtoken"; | ||||||
|  | import Knex from "knex"; | ||||||
| import Koa from "koa"; | import Koa from "koa"; | ||||||
| import Router from "koa-router"; | import Router from "koa-router"; | ||||||
| import { sendError } from "./route/error_handler"; |  | ||||||
| import Knex from "knex"; |  | ||||||
| import { createKnexUserController } from "./db/mod"; | import { createKnexUserController } from "./db/mod"; | ||||||
| import { request } from "http"; |  | ||||||
| import { get_setting } from "./SettingConfig"; |  | ||||||
| import { IUser, UserAccessor } from "./model/mod"; | import { IUser, UserAccessor } from "./model/mod"; | ||||||
|  | import { sendError } from "./route/error_handler"; | ||||||
|  | import { get_setting } from "./SettingConfig"; | ||||||
| 
 | 
 | ||||||
| type PayloadInfo = { | type PayloadInfo = { | ||||||
|     username: string; |     username: string; | ||||||
|  | @ -19,14 +19,14 @@ export type UserState = { | ||||||
| 
 | 
 | ||||||
| const isUserState = (obj: object | string): obj is PayloadInfo => { | const isUserState = (obj: object | string): obj is PayloadInfo => { | ||||||
|     if (typeof obj === "string") return false; |     if (typeof obj === "string") return false; | ||||||
|   return "username" in obj && "permission" in obj && |     return "username" in obj && "permission" in obj | ||||||
|     (obj as { permission: unknown }).permission instanceof Array; |         && (obj as { permission: unknown }).permission instanceof Array; | ||||||
| }; | }; | ||||||
| type RefreshPayloadInfo = { username: string }; | type RefreshPayloadInfo = { username: string }; | ||||||
| const isRefreshToken = (obj: object | string): obj is RefreshPayloadInfo => { | const isRefreshToken = (obj: object | string): obj is RefreshPayloadInfo => { | ||||||
|     if (typeof obj === "string") return false; |     if (typeof obj === "string") return false; | ||||||
|   return "username" in obj && |     return "username" in obj | ||||||
|     typeof (obj as { username: unknown }).username === "string"; |         && typeof (obj as { username: unknown }).username === "string"; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const accessTokenName = "access_token"; | export const accessTokenName = "access_token"; | ||||||
|  | @ -86,9 +86,8 @@ function setToken( | ||||||
|         sameSite: "strict", |         sameSite: "strict", | ||||||
|         expires: new Date(Date.now() + expiredtime * 1000), |         expires: new Date(Date.now() + expiredtime * 1000), | ||||||
|     }); |     }); | ||||||
| }; | } | ||||||
| export const createLoginMiddleware = (userController: UserAccessor) => | export const createLoginMiddleware = (userController: UserAccessor) => async (ctx: Koa.Context, _next: Koa.Next) => { | ||||||
|   async (ctx: Koa.Context, _next: Koa.Next) => { |  | ||||||
|     const setting = get_setting(); |     const setting = get_setting(); | ||||||
|     const secretKey = setting.jwt_secretkey; |     const secretKey = setting.jwt_secretkey; | ||||||
|     const body = ctx.request.body; |     const body = ctx.request.body; | ||||||
|  | @ -144,18 +143,18 @@ export const createLoginMiddleware = (userController: UserAccessor) => | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const LogoutMiddleware = (ctx: Koa.Context, next: Koa.Next) => { | export const LogoutMiddleware = (ctx: Koa.Context, next: Koa.Next) => { | ||||||
|   const setting = get_setting() |     const setting = get_setting(); | ||||||
|     ctx.cookies.set(accessTokenName, null); |     ctx.cookies.set(accessTokenName, null); | ||||||
|     ctx.cookies.set(refreshTokenName, null); |     ctx.cookies.set(refreshTokenName, null); | ||||||
|     ctx.body = { |     ctx.body = { | ||||||
|         ok: true, |         ok: true, | ||||||
|         username: "", |         username: "", | ||||||
|     permission: setting.guest |         permission: setting.guest, | ||||||
|     }; |     }; | ||||||
|     return; |     return; | ||||||
| }; | }; | ||||||
| export const createUserMiddleWare = (userController: UserAccessor) => | export const createUserMiddleWare = | ||||||
|   async (ctx: Koa.ParameterizedContext<UserState>, next: Koa.Next) => { |     (userController: UserAccessor) => async (ctx: Koa.ParameterizedContext<UserState>, next: Koa.Next) => { | ||||||
|         const refreshToken = refreshTokenHandler(userController); |         const refreshToken = refreshTokenHandler(userController); | ||||||
|         const setting = get_setting(); |         const setting = get_setting(); | ||||||
|         const setGuest = async () => { |         const setGuest = async () => { | ||||||
|  | @ -166,8 +165,7 @@ export const createUserMiddleWare = (userController: UserAccessor) => | ||||||
|         }; |         }; | ||||||
|         return await refreshToken(ctx, setGuest, next); |         return await refreshToken(ctx, setGuest, next); | ||||||
|     }; |     }; | ||||||
| const refreshTokenHandler = (cntr: UserAccessor) => | const refreshTokenHandler = (cntr: UserAccessor) => async (ctx: Koa.Context, fail: Koa.Next, next: Koa.Next) => { | ||||||
|   async (ctx: Koa.Context, fail: Koa.Next, next: Koa.Next) => { |  | ||||||
|     const accessPayload = ctx.cookies.get(accessTokenName); |     const accessPayload = ctx.cookies.get(accessTokenName); | ||||||
|     const setting = get_setting(); |     const setting = get_setting(); | ||||||
|     const secretKey = setting.jwt_secretkey; |     const secretKey = setting.jwt_secretkey; | ||||||
|  | @ -218,10 +216,9 @@ const refreshTokenHandler = (cntr: UserAccessor) => | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         return await next(); |         return await next(); | ||||||
|  |     } | ||||||
| }; | }; | ||||||
|   }; | export const createRefreshTokenMiddleware = (cntr: UserAccessor) => async (ctx: Koa.Context, next: Koa.Next) => { | ||||||
| export const createRefreshTokenMiddleware = (cntr: UserAccessor) => |  | ||||||
|   async (ctx: Koa.Context, next: Koa.Next) => { |  | ||||||
|     const handler = refreshTokenHandler(cntr); |     const handler = refreshTokenHandler(cntr); | ||||||
|     await handler(ctx, fail, success); |     await handler(ctx, fail, success); | ||||||
|     async function fail() { |     async function fail() { | ||||||
|  | @ -231,7 +228,7 @@ export const createRefreshTokenMiddleware = (cntr: UserAccessor) => | ||||||
|             ...user, |             ...user, | ||||||
|         }; |         }; | ||||||
|         ctx.type = "json"; |         ctx.type = "json"; | ||||||
|     }; |     } | ||||||
|     async function success() { |     async function success() { | ||||||
|         const user = ctx.state.user as PayloadInfo; |         const user = ctx.state.user as PayloadInfo; | ||||||
|         ctx.body = { |         ctx.body = { | ||||||
|  | @ -240,17 +237,16 @@ export const createRefreshTokenMiddleware = (cntr: UserAccessor) => | ||||||
|             refreshExpired: Math.floor(Date.now() / 1000 + accessExpiredTime), |             refreshExpired: Math.floor(Date.now() / 1000 + accessExpiredTime), | ||||||
|         }; |         }; | ||||||
|         ctx.type = "json"; |         ctx.type = "json"; | ||||||
|  |     } | ||||||
| }; | }; | ||||||
|   }; | export const resetPasswordMiddleware = (cntr: UserAccessor) => async (ctx: Koa.Context, next: Koa.Next) => { | ||||||
| export const resetPasswordMiddleware = (cntr: UserAccessor) => |  | ||||||
|   async (ctx: Koa.Context, next: Koa.Next) => { |  | ||||||
|     const body = ctx.request.body; |     const body = ctx.request.body; | ||||||
|     if (typeof body !== "object" || !('username' in body) || !('oldpassword' in body) || !('newpassword' in body)) { |     if (typeof body !== "object" || !("username" in body) || !("oldpassword" in body) || !("newpassword" in body)) { | ||||||
|         return sendError(400, "request body is invalid format"); |         return sendError(400, "request body is invalid format"); | ||||||
|     } |     } | ||||||
|     const username = body['username']; |     const username = body["username"]; | ||||||
|     const oldpw = body['oldpassword']; |     const oldpw = body["oldpassword"]; | ||||||
|     const newpw = body['newpassword']; |     const newpw = body["newpassword"]; | ||||||
|     if (typeof username !== "string" || typeof oldpw !== "string" || typeof newpw !== "string") { |     if (typeof username !== "string" || typeof oldpw !== "string" || typeof newpw !== "string") { | ||||||
|         return sendError(400, "request body is invalid format"); |         return sendError(400, "request body is invalid format"); | ||||||
|     } |     } | ||||||
|  | @ -262,16 +258,16 @@ export const resetPasswordMiddleware = (cntr: UserAccessor) => | ||||||
|         return sendError(403, "not authorized"); |         return sendError(403, "not authorized"); | ||||||
|     } |     } | ||||||
|     user.reset_password(newpw); |     user.reset_password(newpw); | ||||||
|     ctx.body = { ok: true } |     ctx.body = { ok: true }; | ||||||
|     ctx.type = 'json'; |     ctx.type = "json"; | ||||||
|   } | }; | ||||||
| 
 | 
 | ||||||
| export function createLoginRouter(userController: UserAccessor) { | export function createLoginRouter(userController: UserAccessor) { | ||||||
|     const router = new Router(); |     const router = new Router(); | ||||||
|   router.post('/login', createLoginMiddleware(userController)); |     router.post("/login", createLoginMiddleware(userController)); | ||||||
|   router.post('/logout', LogoutMiddleware); |     router.post("/logout", LogoutMiddleware); | ||||||
|   router.post('/refresh', createRefreshTokenMiddleware(userController)); |     router.post("/refresh", createRefreshTokenMiddleware(userController)); | ||||||
|   router.post('/reset', resetPasswordMiddleware(userController)); |     router.post("/reset", resetPasswordMiddleware(userController)); | ||||||
|     return router; |     return router; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -284,6 +280,6 @@ export const getAdmin = async (cntr: UserAccessor) => { | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const isAdminFirst = (admin: IUser) => { | export const isAdminFirst = (admin: IUser) => { | ||||||
|   return admin.password.hash === "unchecked" && |     return admin.password.hash === "unchecked" | ||||||
|     admin.password.salt === "unchecked"; |         && admin.password.salt === "unchecked"; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -1,16 +1,16 @@ | ||||||
| import {TagAccessor} from './tag'; | import { JSONMap } from "../types/json"; | ||||||
| import {check_type} from '../util/type_check' | import { check_type } from "../util/type_check"; | ||||||
| import {JSONMap} from '../types/json'; | import { TagAccessor } from "./tag"; | ||||||
| 
 | 
 | ||||||
| export interface DocumentBody { | export interface DocumentBody { | ||||||
|     title           : string, |     title: string; | ||||||
|     content_type    : string, |     content_type: string; | ||||||
|     basepath        : string, |     basepath: string; | ||||||
|     filename        : string, |     filename: string; | ||||||
|     modified_at     : number, |     modified_at: number; | ||||||
|     content_hash    : string, |     content_hash: string; | ||||||
|     additional      : JSONMap, |     additional: JSONMap; | ||||||
|     tags            : string[],//eager loading 
 |     tags: string[]; // eager loading
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const MetaContentBody = { | export const MetaContentBody = { | ||||||
|  | @ -21,71 +21,71 @@ export const MetaContentBody = { | ||||||
|     content_hash: "string", |     content_hash: "string", | ||||||
|     additional: "object", |     additional: "object", | ||||||
|     tags: "string[]", |     tags: "string[]", | ||||||
| } | }; | ||||||
| 
 | 
 | ||||||
| export const isDocBody = (c: any): c is DocumentBody => { | export const isDocBody = (c: any): c is DocumentBody => { | ||||||
|     return check_type<DocumentBody>(c, MetaContentBody); |     return check_type<DocumentBody>(c, MetaContentBody); | ||||||
| } | }; | ||||||
| 
 | 
 | ||||||
| export interface Document extends DocumentBody { | export interface Document extends DocumentBody { | ||||||
|     readonly id: number; |     readonly id: number; | ||||||
|     readonly created_at: number; |     readonly created_at: number; | ||||||
|     readonly deleted_at: number | null; |     readonly deleted_at: number | null; | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| export const isDoc = (c: any): c is Document => { | export const isDoc = (c: any): c is Document => { | ||||||
|     if('id' in c && typeof c['id'] === "number"){ |     if ("id" in c && typeof c["id"] === "number") { | ||||||
|         const { id, ...rest } = c; |         const { id, ...rest } = c; | ||||||
|         return isDocBody(rest); |         return isDocBody(rest); | ||||||
|     } |     } | ||||||
|     return false; |     return false; | ||||||
| } | }; | ||||||
| 
 | 
 | ||||||
| export type QueryListOption = { | export type QueryListOption = { | ||||||
|     /** |     /** | ||||||
|      * search word |      * search word | ||||||
|      */ |      */ | ||||||
|     word?:string, |     word?: string; | ||||||
|     allow_tag?:string[], |     allow_tag?: string[]; | ||||||
|     /** |     /** | ||||||
|      * limit of list |      * limit of list | ||||||
|      * @default 20 |      * @default 20 | ||||||
|      */ |      */ | ||||||
|     limit?:number, |     limit?: number; | ||||||
|     /** |     /** | ||||||
|      * use offset if true, otherwise |      * use offset if true, otherwise | ||||||
|      * @default false |      * @default false | ||||||
|      */ |      */ | ||||||
|     use_offset?:boolean, |     use_offset?: boolean; | ||||||
|     /** |     /** | ||||||
|      * cursor of documents |      * cursor of documents | ||||||
|      */ |      */ | ||||||
|     cursor?:number, |     cursor?: number; | ||||||
|     /** |     /** | ||||||
|      * offset of documents |      * offset of documents | ||||||
|      */ |      */ | ||||||
|     offset?:number, |     offset?: number; | ||||||
|     /** |     /** | ||||||
|      * tag eager loading |      * tag eager loading | ||||||
|      * @default true |      * @default true | ||||||
|      */ |      */ | ||||||
|     eager_loading?:boolean, |     eager_loading?: boolean; | ||||||
|     /** |     /** | ||||||
|      * content type |      * content type | ||||||
|      */ |      */ | ||||||
|     content_type?:string |     content_type?: string; | ||||||
| } | }; | ||||||
| 
 | 
 | ||||||
| export interface DocumentAccessor { | export interface DocumentAccessor { | ||||||
|     /** |     /** | ||||||
|      * find list by option |      * find list by option | ||||||
|      * @returns documents list |      * @returns documents list | ||||||
|      */ |      */ | ||||||
|     findList: (option?:QueryListOption)=>Promise<Document[]>, |     findList: (option?: QueryListOption) => Promise<Document[]>; | ||||||
|     /** |     /** | ||||||
|      * @returns document if exist, otherwise undefined |      * @returns document if exist, otherwise undefined | ||||||
|      */ |      */ | ||||||
|     findById: (id:number,tagload?:boolean)=> Promise<Document| undefined>, |     findById: (id: number, tagload?: boolean) => Promise<Document | undefined>; | ||||||
|     /** |     /** | ||||||
|      * find by base path and filename. |      * find by base path and filename. | ||||||
|      * if you call this function with filename, its return array length is 0 or 1. |      * if you call this function with filename, its return array length is 0 or 1. | ||||||
|  | @ -98,7 +98,7 @@ export interface DocumentAccessor{ | ||||||
|     /** |     /** | ||||||
|      * search by in document |      * search by in document | ||||||
|      */ |      */ | ||||||
|     search:(search_word:string)=>Promise<Document[]> |     search: (search_word: string) => Promise<Document[]>; | ||||||
|     /** |     /** | ||||||
|      * update document except tag. |      * update document except tag. | ||||||
|      */ |      */ | ||||||
|  | @ -126,4 +126,4 @@ export interface DocumentAccessor{ | ||||||
|      * @returns if success, return true |      * @returns if success, return true | ||||||
|      */ |      */ | ||||||
|     delTag: (c: Document, tag_name: string) => Promise<boolean>; |     delTag: (c: Document, tag_name: string) => Promise<boolean>; | ||||||
| }; | } | ||||||
|  |  | ||||||
|  | @ -1,3 +1,3 @@ | ||||||
| export * from './doc'; | export * from "./doc"; | ||||||
| export * from './tag'; | export * from "./tag"; | ||||||
| export * from './user'; | export * from "./user"; | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| export interface Tag { | export interface Tag { | ||||||
|     readonly name: string, |     readonly name: string; | ||||||
|     description?: string |     description?: string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface TagCount { | export interface TagCount { | ||||||
|  |  | ||||||
|  | @ -1,20 +1,20 @@ | ||||||
| import { createHmac, randomBytes } from 'crypto'; | import { createHmac, randomBytes } from "crypto"; | ||||||
| 
 | 
 | ||||||
| function hashForPassword(salt: string, password: string) { | function hashForPassword(salt: string, password: string) { | ||||||
|     return createHmac('sha256', salt).update(password).digest('hex') |     return createHmac("sha256", salt).update(password).digest("hex"); | ||||||
| } | } | ||||||
| function createPasswordHashAndSalt(password: string):{salt:string,hash:string}{ | function createPasswordHashAndSalt(password: string): { salt: string; hash: string } { | ||||||
|     const secret = randomBytes(32).toString('hex'); |     const secret = randomBytes(32).toString("hex"); | ||||||
|     return { |     return { | ||||||
|         salt: secret, |         salt: secret, | ||||||
|         hash: hashForPassword(secret,password) |         hash: hashForPassword(secret, password), | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class Password { | export class Password { | ||||||
|     private _salt: string; |     private _salt: string; | ||||||
|     private _hash: string; |     private _hash: string; | ||||||
|     constructor(pw : string|{salt:string,hash:string}){ |     constructor(pw: string | { salt: string; hash: string }) { | ||||||
|         const { salt, hash } = typeof pw === "string" ? createPasswordHashAndSalt(pw) : pw; |         const { salt, hash } = typeof pw === "string" ? createPasswordHashAndSalt(pw) : pw; | ||||||
|         this._hash = hash; |         this._hash = hash; | ||||||
|         this._salt = salt; |         this._salt = salt; | ||||||
|  | @ -27,13 +27,17 @@ export class Password{ | ||||||
|     check_password(password: string): boolean { |     check_password(password: string): boolean { | ||||||
|         return this._hash === hashForPassword(this._salt, password); |         return this._hash === hashForPassword(this._salt, password); | ||||||
|     } |     } | ||||||
|     get salt(){return this._salt;} |     get salt() { | ||||||
|     get hash(){return this._hash;} |         return this._salt; | ||||||
|  |     } | ||||||
|  |     get hash() { | ||||||
|  |         return this._hash; | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface UserCreateInput { | export interface UserCreateInput { | ||||||
|     username: string, |     username: string; | ||||||
|     password: string |     password: string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface IUser { | export interface IUser { | ||||||
|  | @ -60,21 +64,21 @@ export interface IUser{ | ||||||
|      * @param password password to set |      * @param password password to set | ||||||
|      */ |      */ | ||||||
|     reset_password(password: string): Promise<void>; |     reset_password(password: string): Promise<void>; | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| export interface UserAccessor { | export interface UserAccessor { | ||||||
|     /** |     /** | ||||||
|      * create user |      * create user | ||||||
|      * @returns if user exist, return undefined |      * @returns if user exist, return undefined | ||||||
|      */ |      */ | ||||||
|     createUser: (input :UserCreateInput)=> Promise<IUser|undefined>, |     createUser: (input: UserCreateInput) => Promise<IUser | undefined>; | ||||||
|     /** |     /** | ||||||
|      * find user |      * find user | ||||||
|      */ |      */ | ||||||
|     findUser: (username: string)=> Promise<IUser|undefined>, |     findUser: (username: string) => Promise<IUser | undefined>; | ||||||
|     /** |     /** | ||||||
|      * remove user |      * remove user | ||||||
|      * @returns if user exist, true |      * @returns if user exist, true | ||||||
|      */ |      */ | ||||||
|     delUser: (username: string)=>Promise<boolean> |     delUser: (username: string) => Promise<boolean>; | ||||||
| }; | } | ||||||
|  |  | ||||||
|  | @ -1,7 +1,6 @@ | ||||||
| import Koa from 'koa'; | import Koa from "koa"; | ||||||
| import { UserState } from '../login'; | import { UserState } from "../login"; | ||||||
| import { sendError } from '../route/error_handler'; | import { sendError } from "../route/error_handler"; | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| export enum Permission { | export enum Permission { | ||||||
|     // ========
 |     // ========
 | ||||||
|  | @ -21,7 +20,7 @@ export enum Permission{ | ||||||
|     /** remove tag from document */ |     /** remove tag from document */ | ||||||
|     // removeTagContent = 'removeTagContent',
 |     // removeTagContent = 'removeTagContent',
 | ||||||
|     /** ModifyTagInDoc */ |     /** ModifyTagInDoc */ | ||||||
|     ModifyTag = 'ModifyTag', |     ModifyTag = "ModifyTag", | ||||||
| 
 | 
 | ||||||
|     /** find documents with query */ |     /** find documents with query */ | ||||||
|     // findAllContent = 'findAllContent',
 |     // findAllContent = 'findAllContent',
 | ||||||
|  | @ -29,15 +28,15 @@ export enum Permission{ | ||||||
|     // findOneContent = 'findOneContent',
 |     // findOneContent = 'findOneContent',
 | ||||||
|     /** view content*/ |     /** view content*/ | ||||||
|     // viewContent = 'viewContent',
 |     // viewContent = 'viewContent',
 | ||||||
|     QueryContent = 'QueryContent', |     QueryContent = "QueryContent", | ||||||
| 
 | 
 | ||||||
|     /** modify description about the one tag. */ |     /** modify description about the one tag. */ | ||||||
|     modifyTagDesc = 'ModifyTagDesc', |     modifyTagDesc = "ModifyTagDesc", | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const createPermissionCheckMiddleware = (...permissions:string[]) =>  | export const createPermissionCheckMiddleware = | ||||||
|     async (ctx: Koa.ParameterizedContext<UserState>,next:Koa.Next) => { |     (...permissions: string[]) => async (ctx: Koa.ParameterizedContext<UserState>, next: Koa.Next) => { | ||||||
|     const user = ctx.state['user']; |         const user = ctx.state["user"]; | ||||||
|         if (user.username === "admin") { |         if (user.username === "admin") { | ||||||
|             return await next(); |             return await next(); | ||||||
|         } |         } | ||||||
|  | @ -46,15 +45,14 @@ export const createPermissionCheckMiddleware = (...permissions:string[]) => | ||||||
|         if (!permissions.map(p => user_permission.includes(p)).every(x => x)) { |         if (!permissions.map(p => user_permission.includes(p)).every(x => x)) { | ||||||
|             if (user.username === "") { |             if (user.username === "") { | ||||||
|                 return sendError(401, "you are guest. login needed."); |                 return sendError(401, "you are guest. login needed."); | ||||||
|         } |             } else return sendError(403, "do not have permission"); | ||||||
|         else return sendError(403,"do not have permission"); |  | ||||||
|         } |         } | ||||||
|         await next(); |         await next(); | ||||||
| } |     }; | ||||||
| export const AdminOnlyMiddleware = async (ctx: Koa.ParameterizedContext<UserState>, next: Koa.Next) => { | export const AdminOnlyMiddleware = async (ctx: Koa.ParameterizedContext<UserState>, next: Koa.Next) => { | ||||||
|     const user = ctx.state['user']; |     const user = ctx.state["user"]; | ||||||
|     if (user.username !== "admin") { |     if (user.username !== "admin") { | ||||||
|         return sendError(403, "admin only"); |         return sendError(403, "admin only"); | ||||||
|     } |     } | ||||||
|     await next(); |     await next(); | ||||||
| } | }; | ||||||
|  |  | ||||||
|  | @ -1,21 +1,23 @@ | ||||||
| import { DefaultContext, Middleware, Next, ParameterizedContext } from 'koa'; | import { DefaultContext, Middleware, Next, ParameterizedContext } from "koa"; | ||||||
| import compose from 'koa-compose'; | import compose from "koa-compose"; | ||||||
| import Router, { IParamMiddleware } from 'koa-router'; | import Router, { IParamMiddleware } from "koa-router"; | ||||||
| import { ContentContext } from './context'; | import ComicRouter from "./comic"; | ||||||
| import ComicRouter from './comic'; | import { ContentContext } from "./context"; | ||||||
| import VideoRouter from './video'; | import VideoRouter from "./video"; | ||||||
| 
 | 
 | ||||||
| const table: { [s: string]: Router | undefined } = { | const table: { [s: string]: Router | undefined } = { | ||||||
|     "comic": new ComicRouter, |     "comic": new ComicRouter(), | ||||||
|     "video": new VideoRouter |     "video": new VideoRouter(), | ||||||
| } | }; | ||||||
| const all_middleware = (cont: string|undefined, restarg: string|undefined)=>async (ctx:ParameterizedContext<ContentContext,DefaultContext>,next:Next)=>{ | const all_middleware = | ||||||
|  |     (cont: string | undefined, restarg: string | undefined) => | ||||||
|  |     async (ctx: ParameterizedContext<ContentContext, DefaultContext>, next: Next) => { | ||||||
|         if (cont == undefined) { |         if (cont == undefined) { | ||||||
|             ctx.status = 404; |             ctx.status = 404; | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|         if (ctx.state.location.type != cont) { |         if (ctx.state.location.type != cont) { | ||||||
|         console.error("not matched") |             console.error("not matched"); | ||||||
|             ctx.status = 404; |             ctx.status = 404; | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  | @ -44,12 +46,12 @@ const all_middleware = (cont: string|undefined, restarg: string|undefined)=>asyn | ||||||
| export class AllContentRouter extends Router<ContentContext> { | export class AllContentRouter extends Router<ContentContext> { | ||||||
|     constructor() { |     constructor() { | ||||||
|         super(); |         super(); | ||||||
|         this.get('/:content_type',async (ctx,next)=>{ |         this.get("/:content_type", async (ctx, next) => { | ||||||
|             return await (all_middleware(ctx.params["content_type"], undefined))(ctx, next); |             return await (all_middleware(ctx.params["content_type"], undefined))(ctx, next); | ||||||
|         }); |         }); | ||||||
|         this.get('/:content_type/:rest(.*)', async (ctx,next) => { |         this.get("/:content_type/:rest(.*)", async (ctx, next) => { | ||||||
|             const cont = ctx.params["content_type"] as string; |             const cont = ctx.params["content_type"] as string; | ||||||
|             return await (all_middleware(cont, ctx.params["rest"]))(ctx, next); |             return await (all_middleware(cont, ctx.params["rest"]))(ctx, next); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| }; | } | ||||||
|  |  | ||||||
|  | @ -1,13 +1,8 @@ | ||||||
| import { Context, DefaultContext, DefaultState, Next } from "koa"; | import { Context, DefaultContext, DefaultState, Next } from "koa"; | ||||||
| import { |  | ||||||
|   createReadableStreamFromZip, |  | ||||||
|   entriesByNaturalOrder, |  | ||||||
|   readZip, |  | ||||||
|   ZipAsync, |  | ||||||
| } from "../util/zipwrap"; |  | ||||||
| import { since_last_modified } from "./util"; |  | ||||||
| import { ContentContext } from "./context"; |  | ||||||
| import Router from "koa-router"; | import Router from "koa-router"; | ||||||
|  | import { createReadableStreamFromZip, entriesByNaturalOrder, readZip, ZipAsync } from "../util/zipwrap"; | ||||||
|  | import { ContentContext } from "./context"; | ||||||
|  | import { since_last_modified } from "./util"; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * zip stream cache. |  * zip stream cache. | ||||||
|  | @ -21,8 +16,7 @@ async function acquireZip(path: string) { | ||||||
|         ZipStreamCache[path] = [ret, 1]; |         ZipStreamCache[path] = [ret, 1]; | ||||||
|         // console.log(`acquire ${path} 1`);
 |         // console.log(`acquire ${path} 1`);
 | ||||||
|         return ret; |         return ret; | ||||||
|   } |     } else { | ||||||
|   else { |  | ||||||
|         const [ret, refCount] = ZipStreamCache[path]; |         const [ret, refCount] = ZipStreamCache[path]; | ||||||
|         ZipStreamCache[path] = [ret, refCount + 1]; |         ZipStreamCache[path] = [ret, refCount + 1]; | ||||||
|         // console.log(`acquire ${path} ${refCount + 1}`);
 |         // console.log(`acquire ${path} ${refCount + 1}`);
 | ||||||
|  | @ -38,8 +32,7 @@ function releaseZip(path: string) { | ||||||
|     if (refCount === 1) { |     if (refCount === 1) { | ||||||
|         ref.close(); |         ref.close(); | ||||||
|         delete ZipStreamCache[path]; |         delete ZipStreamCache[path]; | ||||||
|   } |     } else { | ||||||
|   else{ |  | ||||||
|         ZipStreamCache[path] = [ref, refCount - 1]; |         ZipStreamCache[path] = [ref, refCount - 1]; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -58,7 +51,7 @@ async function renderZipImage(ctx: Context, path: string, page: number) { | ||||||
|         if (since_last_modified(ctx, last_modified)) { |         if (since_last_modified(ctx, last_modified)) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|     const read_stream = (await createReadableStreamFromZip(zip, entry)); |         const read_stream = await createReadableStreamFromZip(zip, entry); | ||||||
|         /** Exceptions (ECONNRESET, ECONNABORTED) may be thrown when processing this request |         /** Exceptions (ECONNRESET, ECONNABORTED) may be thrown when processing this request | ||||||
|          * for reasons such as when the browser unexpectedly closes the connection. |          * for reasons such as when the browser unexpectedly closes the connection. | ||||||
|          * Once such an exception is raised, the stream is not properly destroyed, |          * Once such an exception is raised, the stream is not properly destroyed, | ||||||
|  |  | ||||||
|  | @ -1,47 +1,52 @@ | ||||||
| import { Context, Next } from 'koa'; | import { Context, Next } from "koa"; | ||||||
| import Router from 'koa-router'; | import Router from "koa-router"; | ||||||
| import {Document, DocumentAccessor, isDocBody} from '../model/doc'; | import { join } from "path"; | ||||||
| import {QueryListOption} from '../model/doc'; | import { Document, DocumentAccessor, isDocBody } from "../model/doc"; | ||||||
| import {ParseQueryNumber, ParseQueryArray, ParseQueryBoolean, ParseQueryArgString} from './util' | import { QueryListOption } from "../model/doc"; | ||||||
| import {sendError} from './error_handler'; | import { | ||||||
| import { join } from 'path'; |     AdminOnlyMiddleware as AdminOnly, | ||||||
| import {AllContentRouter} from './all'; |     createPermissionCheckMiddleware as PerCheck, | ||||||
| import {createPermissionCheckMiddleware as PerCheck, Permission as Per, AdminOnlyMiddleware as AdminOnly} from '../permission/permission'; |     Permission as Per, | ||||||
| import {ContentLocation} from './context' | } from "../permission/permission"; | ||||||
|  | import { AllContentRouter } from "./all"; | ||||||
|  | import { ContentLocation } from "./context"; | ||||||
|  | import { sendError } from "./error_handler"; | ||||||
|  | import { ParseQueryArgString, ParseQueryArray, ParseQueryBoolean, ParseQueryNumber } from "./util"; | ||||||
| 
 | 
 | ||||||
| const ContentIDHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { | const ContentIDHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { | ||||||
|     const num = Number.parseInt(ctx.params['num']); |     const num = Number.parseInt(ctx.params["num"]); | ||||||
|     let document = await controller.findById(num, true); |     let document = await controller.findById(num, true); | ||||||
|     if (document == undefined) { |     if (document == undefined) { | ||||||
|         return sendError(404, "document does not exist."); |         return sendError(404, "document does not exist."); | ||||||
|     } |     } | ||||||
|     ctx.body = document; |     ctx.body = document; | ||||||
|     ctx.type = 'json'; |     ctx.type = "json"; | ||||||
|     console.log(document.additional); |     console.log(document.additional); | ||||||
| }; | }; | ||||||
| const ContentTagIDHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { | const ContentTagIDHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { | ||||||
|     const num = Number.parseInt(ctx.params['num']); |     const num = Number.parseInt(ctx.params["num"]); | ||||||
|     let document = await controller.findById(num, true); |     let document = await controller.findById(num, true); | ||||||
|     if (document == undefined) { |     if (document == undefined) { | ||||||
|         return sendError(404, "document does not exist."); |         return sendError(404, "document does not exist."); | ||||||
|     } |     } | ||||||
|     ctx.body = document.tags; |     ctx.body = document.tags; | ||||||
|     ctx.type = 'json'; |     ctx.type = "json"; | ||||||
| }; | }; | ||||||
| const ContentQueryHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { | const ContentQueryHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { | ||||||
| 
 |     let query_limit = ctx.query["limit"]; | ||||||
|     let query_limit = (ctx.query['limit']); |     let query_cursor = ctx.query["cursor"]; | ||||||
|     let query_cursor = (ctx.query['cursor']); |     let query_word = ctx.query["word"]; | ||||||
|     let query_word = (ctx.query['word']); |     let query_content_type = ctx.query["content_type"]; | ||||||
|     let query_content_type = (ctx.query['content_type']); |     let query_offset = ctx.query["offset"]; | ||||||
|     let query_offset = (ctx.query['offset']); |     let query_use_offset = ctx.query["use_offset"]; | ||||||
|     let query_use_offset = ctx.query['use_offset']; |     if ( | ||||||
|     if(query_limit instanceof Array  |         query_limit instanceof Array | ||||||
|         || query_cursor instanceof Array |         || query_cursor instanceof Array | ||||||
|         || query_word instanceof Array |         || query_word instanceof Array | ||||||
|         || query_content_type instanceof Array |         || query_content_type instanceof Array | ||||||
|         || query_offset instanceof Array |         || query_offset instanceof Array | ||||||
|         || query_use_offset instanceof Array){ |         || query_use_offset instanceof Array | ||||||
|  |     ) { | ||||||
|         return sendError(400, "paramter can not be array"); |         return sendError(400, "paramter can not be array"); | ||||||
|     } |     } | ||||||
|     const limit = ParseQueryNumber(query_limit); |     const limit = ParseQueryNumber(query_limit); | ||||||
|  | @ -52,7 +57,7 @@ const ContentQueryHandler = (controller : DocumentAccessor) => async (ctx: Conte | ||||||
|     if (limit === NaN || cursor === NaN || offset === NaN) { |     if (limit === NaN || cursor === NaN || offset === NaN) { | ||||||
|         return sendError(400, "parameter limit, cursor or offset is not a number"); |         return sendError(400, "parameter limit, cursor or offset is not a number"); | ||||||
|     } |     } | ||||||
|     const allow_tag = ParseQueryArray(ctx.query['allow_tag']); |     const allow_tag = ParseQueryArray(ctx.query["allow_tag"]); | ||||||
|     const [ok, use_offset] = ParseQueryBoolean(query_use_offset); |     const [ok, use_offset] = ParseQueryBoolean(query_use_offset); | ||||||
|     if (!ok) { |     if (!ok) { | ||||||
|         return sendError(400, "use_offset must be true or false."); |         return sendError(400, "use_offset must be true or false."); | ||||||
|  | @ -69,28 +74,29 @@ const ContentQueryHandler = (controller : DocumentAccessor) => async (ctx: Conte | ||||||
|     }; |     }; | ||||||
|     let document = await controller.findList(option); |     let document = await controller.findList(option); | ||||||
|     ctx.body = document; |     ctx.body = document; | ||||||
|     ctx.type = 'json'; |     ctx.type = "json"; | ||||||
| } | }; | ||||||
| const UpdateContentHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { | const UpdateContentHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { | ||||||
|     const num = Number.parseInt(ctx.params['num']); |     const num = Number.parseInt(ctx.params["num"]); | ||||||
| 
 | 
 | ||||||
|     if(ctx.request.type !== 'json'){ |     if (ctx.request.type !== "json") { | ||||||
|         return sendError(400, "update fail. invalid document type: it is not json."); |         return sendError(400, "update fail. invalid document type: it is not json."); | ||||||
|     } |     } | ||||||
|     if (typeof ctx.request.body !== "object") { |     if (typeof ctx.request.body !== "object") { | ||||||
|         return sendError(400, "update fail. invalid argument: not"); |         return sendError(400, "update fail. invalid argument: not"); | ||||||
|     } |     } | ||||||
|     const content_desc: Partial<Document> & { id: number } = { |     const content_desc: Partial<Document> & { id: number } = { | ||||||
|         id:num,...ctx.request.body |         id: num, | ||||||
|  |         ...ctx.request.body, | ||||||
|     }; |     }; | ||||||
|     const success = await controller.update(content_desc); |     const success = await controller.update(content_desc); | ||||||
|     ctx.body = JSON.stringify(success); |     ctx.body = JSON.stringify(success); | ||||||
|     ctx.type = 'json'; |     ctx.type = "json"; | ||||||
| } | }; | ||||||
| 
 | 
 | ||||||
| const AddTagHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { | const AddTagHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { | ||||||
|     let tag_name = ctx.params['tag']; |     let tag_name = ctx.params["tag"]; | ||||||
|     const num = Number.parseInt(ctx.params['num']); |     const num = Number.parseInt(ctx.params["num"]); | ||||||
|     if (typeof tag_name === undefined) { |     if (typeof tag_name === undefined) { | ||||||
|         return sendError(400, "??? Unreachable"); |         return sendError(400, "??? Unreachable"); | ||||||
|     } |     } | ||||||
|  | @ -101,11 +107,11 @@ const AddTagHandler = (controller: DocumentAccessor)=>async (ctx: Context, next: | ||||||
|     } |     } | ||||||
|     const r = await controller.addTag(c, tag_name); |     const r = await controller.addTag(c, tag_name); | ||||||
|     ctx.body = JSON.stringify(r); |     ctx.body = JSON.stringify(r); | ||||||
|     ctx.type = 'json'; |     ctx.type = "json"; | ||||||
| }; | }; | ||||||
| const DelTagHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { | const DelTagHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { | ||||||
|     let tag_name = ctx.params['tag']; |     let tag_name = ctx.params["tag"]; | ||||||
|     const num = Number.parseInt(ctx.params['num']); |     const num = Number.parseInt(ctx.params["num"]); | ||||||
|     if (typeof tag_name === undefined) { |     if (typeof tag_name === undefined) { | ||||||
|         return sendError(400, "?? Unreachable"); |         return sendError(400, "?? Unreachable"); | ||||||
|     } |     } | ||||||
|  | @ -116,16 +122,16 @@ const DelTagHandler = (controller: DocumentAccessor)=>async (ctx: Context, next: | ||||||
|     } |     } | ||||||
|     const r = await controller.delTag(c, tag_name); |     const r = await controller.delTag(c, tag_name); | ||||||
|     ctx.body = JSON.stringify(r); |     ctx.body = JSON.stringify(r); | ||||||
|     ctx.type = 'json'; |     ctx.type = "json"; | ||||||
| } | }; | ||||||
| const DeleteContentHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { | const DeleteContentHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { | ||||||
|     const num = Number.parseInt(ctx.params['num']); |     const num = Number.parseInt(ctx.params["num"]); | ||||||
|     const r = await controller.del(num); |     const r = await controller.del(num); | ||||||
|     ctx.body = JSON.stringify(r); |     ctx.body = JSON.stringify(r); | ||||||
|     ctx.type = 'json'; |     ctx.type = "json"; | ||||||
| }; | }; | ||||||
| const ContentHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { | const ContentHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { | ||||||
|     const num = Number.parseInt(ctx.params['num']); |     const num = Number.parseInt(ctx.params["num"]); | ||||||
|     let document = await controller.findById(num, true); |     let document = await controller.findById(num, true); | ||||||
|     if (document == undefined) { |     if (document == undefined) { | ||||||
|         return sendError(404, "document does not exist."); |         return sendError(404, "document does not exist."); | ||||||
|  | @ -134,7 +140,7 @@ const ContentHandler = (controller : DocumentAccessor) => async (ctx:Context, ne | ||||||
|         return sendError(404, "document has been removed."); |         return sendError(404, "document has been removed."); | ||||||
|     } |     } | ||||||
|     const path = join(document.basepath, document.filename); |     const path = join(document.basepath, document.filename); | ||||||
|     ctx.state['location'] = { |     ctx.state["location"] = { | ||||||
|         path: path, |         path: path, | ||||||
|         type: document.content_type, |         type: document.content_type, | ||||||
|         additional: document.additional, |         additional: document.additional, | ||||||
|  | @ -154,8 +160,8 @@ export const getContentRouter = (controller: DocumentAccessor)=>{ | ||||||
|     ret.del("/:num(\\d+)/tags/:tag", PerCheck(Per.ModifyTag), DelTagHandler(controller)); |     ret.del("/:num(\\d+)/tags/:tag", PerCheck(Per.ModifyTag), DelTagHandler(controller)); | ||||||
|     ret.del("/:num(\\d+)", AdminOnly, DeleteContentHandler(controller)); |     ret.del("/:num(\\d+)", AdminOnly, DeleteContentHandler(controller)); | ||||||
|     ret.all("/:num(\\d+)/(.*)", PerCheck(Per.QueryContent), ContentHandler(controller)); |     ret.all("/:num(\\d+)/(.*)", PerCheck(Per.QueryContent), ContentHandler(controller)); | ||||||
|     ret.use("/:num(\\d+)",PerCheck(Per.QueryContent),(new AllContentRouter).routes()); |     ret.use("/:num(\\d+)", PerCheck(Per.QueryContent), (new AllContentRouter()).routes()); | ||||||
|     return ret; |     return ret; | ||||||
| } | }; | ||||||
| 
 | 
 | ||||||
| export default getContentRouter; | export default getContentRouter; | ||||||
|  | @ -1,8 +1,8 @@ | ||||||
| export type ContentLocation = { | export type ContentLocation = { | ||||||
|     path:string, |     path: string; | ||||||
|     type:string, |     type: string; | ||||||
|     additional:object|undefined, |     additional: object | undefined; | ||||||
| } | }; | ||||||
| export interface ContentContext { | export interface ContentContext { | ||||||
|     location:ContentLocation |     location: ContentLocation; | ||||||
| } | } | ||||||
|  | @ -1,9 +1,9 @@ | ||||||
| import {Context, Next} from 'koa'; | import { Context, Next } from "koa"; | ||||||
| 
 | 
 | ||||||
| export interface ErrorFormat { | export interface ErrorFormat { | ||||||
|     code: number, |     code: number; | ||||||
|     message: string, |     message: string; | ||||||
|     detail?: string |     detail?: string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| class ClientRequestError implements Error { | class ClientRequestError implements Error { | ||||||
|  | @ -21,8 +21,8 @@ class ClientRequestError implements Error{ | ||||||
| 
 | 
 | ||||||
| const code_to_message_table: { [key: number]: string | undefined } = { | const code_to_message_table: { [key: number]: string | undefined } = { | ||||||
|     400: "BadRequest", |     400: "BadRequest", | ||||||
|     404:"NotFound" |     404: "NotFound", | ||||||
| } | }; | ||||||
| 
 | 
 | ||||||
| export const error_handler = async (ctx: Context, next: Next) => { | export const error_handler = async (ctx: Context, next: Next) => { | ||||||
|     try { |     try { | ||||||
|  | @ -32,19 +32,18 @@ export const error_handler = async (ctx:Context,next: Next)=>{ | ||||||
|             const body: ErrorFormat = { |             const body: ErrorFormat = { | ||||||
|                 code: err.code, |                 code: err.code, | ||||||
|                 message: code_to_message_table[err.code] ?? "", |                 message: code_to_message_table[err.code] ?? "", | ||||||
|                 detail: err.message |                 detail: err.message, | ||||||
|             } |             }; | ||||||
|             ctx.status = err.code; |             ctx.status = err.code; | ||||||
|             ctx.body = body; |             ctx.body = body; | ||||||
|         } |         } else { | ||||||
|         else{ |  | ||||||
|             throw err; |             throw err; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | }; | ||||||
| 
 | 
 | ||||||
| export const sendError = (code: number, message?: string) => { | export const sendError = (code: number, message?: string) => { | ||||||
|     throw new ClientRequestError(code, message ?? ""); |     throw new ClientRequestError(code, message ?? ""); | ||||||
| } | }; | ||||||
| 
 | 
 | ||||||
| export default error_handler; | export default error_handler; | ||||||
|  | @ -1,25 +1,22 @@ | ||||||
| import { Context, Next } from "koa"; | import { Context, Next } from "koa"; | ||||||
| import Router, { RouterContext } from "koa-router"; | import Router, { RouterContext } from "koa-router"; | ||||||
| import { TagAccessor } from "../model/tag"; | import { TagAccessor } from "../model/tag"; | ||||||
| import { sendError } from "./error_handler"; |  | ||||||
| import { createPermissionCheckMiddleware as PerCheck, Permission } from "../permission/permission"; | import { createPermissionCheckMiddleware as PerCheck, Permission } from "../permission/permission"; | ||||||
|  | import { sendError } from "./error_handler"; | ||||||
| 
 | 
 | ||||||
| export function getTagRounter(tagController: TagAccessor) { | export function getTagRounter(tagController: TagAccessor) { | ||||||
|     let router = new Router(); |     let router = new Router(); | ||||||
|     router.get("/",PerCheck(Permission.QueryContent), |     router.get("/", PerCheck(Permission.QueryContent), async (ctx: Context) => { | ||||||
|         async (ctx: Context)=>{ |  | ||||||
|         if (ctx.query["withCount"]) { |         if (ctx.query["withCount"]) { | ||||||
|             const c = await tagController.getAllTagCount(); |             const c = await tagController.getAllTagCount(); | ||||||
|             ctx.body = c; |             ctx.body = c; | ||||||
|             } |         } else { | ||||||
|             else { |  | ||||||
|             const c = await tagController.getAllTagList(); |             const c = await tagController.getAllTagList(); | ||||||
|             ctx.body = c; |             ctx.body = c; | ||||||
|         } |         } | ||||||
|         ctx.type = "json"; |         ctx.type = "json"; | ||||||
|     }); |     }); | ||||||
|     router.get("/:tag_name", PerCheck(Permission.QueryContent), |     router.get("/:tag_name", PerCheck(Permission.QueryContent), async (ctx: RouterContext) => { | ||||||
|         async (ctx: RouterContext)=>{ |  | ||||||
|         const tag_name = ctx.params["tag_name"]; |         const tag_name = ctx.params["tag_name"]; | ||||||
|         const c = await tagController.getTagByName(tag_name); |         const c = await tagController.getTagByName(tag_name); | ||||||
|         if (!c) { |         if (!c) { | ||||||
|  |  | ||||||
|  | @ -1,6 +1,4 @@ | ||||||
| 
 | import { Context } from "koa"; | ||||||
| import {Context} from 'koa'; |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| export function ParseQueryNumber(s: string[] | string | undefined): number | undefined { | export function ParseQueryNumber(s: string[] | string | undefined): number | undefined { | ||||||
|     if (s === undefined) return undefined; |     if (s === undefined) return undefined; | ||||||
|  | @ -19,14 +17,14 @@ export function ParseQueryArgString(s: string[]|string|undefined){ | ||||||
| export function ParseQueryBoolean(s: string[] | string | undefined): [boolean, boolean | undefined] { | export function ParseQueryBoolean(s: string[] | string | undefined): [boolean, boolean | undefined] { | ||||||
|     let value: boolean | undefined; |     let value: boolean | undefined; | ||||||
| 
 | 
 | ||||||
|     if(s === "true") |     if (s === "true") { | ||||||
|         value = true; |         value = true; | ||||||
|     else if(s === "false") |     } else if (s === "false") { | ||||||
|         value = false; |         value = false; | ||||||
|     else if(s === undefined) |     } else if (s === undefined) { | ||||||
|         value = undefined; |         value = undefined; | ||||||
|     else return [false,undefined] |     } else return [false, undefined]; | ||||||
|     return [true,value] |     return [true, value]; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function since_last_modified(ctx: Context, last_modified: Date): boolean { | export function since_last_modified(ctx: Context, last_modified: Date): boolean { | ||||||
|  |  | ||||||
|  | @ -1,13 +1,13 @@ | ||||||
| import {Context } from 'koa'; | import { createReadStream, promises } from "fs"; | ||||||
| import {promises, createReadStream} from "fs"; | import { Context } from "koa"; | ||||||
| import {ContentContext} from './context'; | import Router from "koa-router"; | ||||||
| import Router from 'koa-router'; | import { ContentContext } from "./context"; | ||||||
| 
 | 
 | ||||||
| export async function renderVideo(ctx: Context, path: string) { | export async function renderVideo(ctx: Context, path: string) { | ||||||
|     const ext = path.trim().split('.').pop(); |     const ext = path.trim().split(".").pop(); | ||||||
|     if (ext === undefined) { |     if (ext === undefined) { | ||||||
|         // ctx.status = 404;
 |         // ctx.status = 404;
 | ||||||
|         console.error(`${path}:${ext}`) |         console.error(`${path}:${ext}`); | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
|     ctx.response.type = ext; |     ctx.response.type = ext; | ||||||
|  | @ -15,10 +15,10 @@ export async function renderVideo(ctx: Context,path : string){ | ||||||
|     const stat = await promises.stat(path); |     const stat = await promises.stat(path); | ||||||
|     let start = 0; |     let start = 0; | ||||||
|     let end = 0; |     let end = 0; | ||||||
|     ctx.set('Last-Modified',(new Date(stat.mtime).toUTCString())); |     ctx.set("Last-Modified", new Date(stat.mtime).toUTCString()); | ||||||
|     ctx.set('Date', new Date().toUTCString()); |     ctx.set("Date", new Date().toUTCString()); | ||||||
|     ctx.set("Accept-Ranges", "bytes"); |     ctx.set("Accept-Ranges", "bytes"); | ||||||
|     if(range_text === ''){ |     if (range_text === "") { | ||||||
|         end = 1024 * 512; |         end = 1024 * 512; | ||||||
|         end = Math.min(end, stat.size - 1); |         end = Math.min(end, stat.size - 1); | ||||||
|         if (start > end) { |         if (start > end) { | ||||||
|  | @ -29,8 +29,7 @@ export async function renderVideo(ctx: Context,path : string){ | ||||||
|         ctx.length = stat.size; |         ctx.length = stat.size; | ||||||
|         let stream = createReadStream(path); |         let stream = createReadStream(path); | ||||||
|         ctx.body = stream; |         ctx.body = stream; | ||||||
|     } |     } else { | ||||||
|     else{ |  | ||||||
|         const m = range_text.match(/^bytes=(\d+)-(\d*)/); |         const m = range_text.match(/^bytes=(\d+)-(\d*)/); | ||||||
|         if (m === null) { |         if (m === null) { | ||||||
|             ctx.status = 416; |             ctx.status = 416; | ||||||
|  | @ -48,7 +47,7 @@ export async function renderVideo(ctx: Context,path : string){ | ||||||
|         ctx.response.set("Content-Range", `bytes ${start}-${end}/${stat.size}`); |         ctx.response.set("Content-Range", `bytes ${start}-${end}/${stat.size}`); | ||||||
|         ctx.body = createReadStream(path, { |         ctx.body = createReadStream(path, { | ||||||
|             start: start, |             start: start, | ||||||
|             end:end |             end: end, | ||||||
|         }); // inclusive range.
 |         }); // inclusive range.
 | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -61,7 +60,7 @@ export class VideoRouter extends Router<ContentContext>{ | ||||||
|         }); |         }); | ||||||
|         this.get("/thumbnail", async (ctx, next) => { |         this.get("/thumbnail", async (ctx, next) => { | ||||||
|             await renderVideo(ctx, ctx.state.location.path); |             await renderVideo(ctx, ctx.state.location.path); | ||||||
|         }) |         }); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,4 +1,3 @@ | ||||||
| 
 |  | ||||||
| export interface PaginationOption { | export interface PaginationOption { | ||||||
|     cursor: number; |     cursor: number; | ||||||
|     limit: number; |     limit: number; | ||||||
|  |  | ||||||
|  | @ -1,4 +1,3 @@ | ||||||
| 
 |  | ||||||
| export interface ITokenizer { | export interface ITokenizer { | ||||||
|     tokenize(s: string): string[]; |     tokenize(s: string): string[]; | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										147
									
								
								src/server.ts
									
										
									
									
									
								
							
							
						
						
									
										147
									
								
								src/server.ts
									
										
									
									
									
								
							|  | @ -1,22 +1,21 @@ | ||||||
| import Koa from 'koa'; | import Koa from "koa"; | ||||||
| import Router from 'koa-router'; | import Router from "koa-router"; | ||||||
| 
 | 
 | ||||||
| import {get_setting, SettingConfig} from './SettingConfig'; | import { connectDB } from "./database"; | ||||||
| import {connectDB} from './database'; | import { createDiffRouter, DiffManager } from "./diff/mod"; | ||||||
| import {DiffManager, createDiffRouter} from './diff/mod'; | import { get_setting, SettingConfig } from "./SettingConfig"; | ||||||
| 
 | 
 | ||||||
| import { createReadStream, readFileSync } from 'fs'; | import { createReadStream, readFileSync } from "fs"; | ||||||
| import getContentRouter from './route/contents'; | import bodyparser from "koa-bodyparser"; | ||||||
| import { createKnexDocumentAccessor, createKnexTagController, createKnexUserController } from './db/mod'; | import { createKnexDocumentAccessor, createKnexTagController, createKnexUserController } from "./db/mod"; | ||||||
| import bodyparser from 'koa-bodyparser'; | import { createLoginRouter, createUserMiddleWare, getAdmin, isAdminFirst } from "./login"; | ||||||
| import {error_handler} from './route/error_handler'; | import getContentRouter from "./route/contents"; | ||||||
| import {createUserMiddleWare, createLoginRouter, isAdminFirst, getAdmin} from './login'; | import { error_handler } from "./route/error_handler"; | ||||||
| 
 |  | ||||||
| import {createInterface as createReadlineInterface} from 'readline'; |  | ||||||
| import { DocumentAccessor, UserAccessor, TagAccessor } from './model/mod'; |  | ||||||
| import { createComicWatcher } from './diff/watcher/comic_watcher'; |  | ||||||
| import { getTagRounter } from './route/tags'; |  | ||||||
| 
 | 
 | ||||||
|  | import { createInterface as createReadlineInterface } from "readline"; | ||||||
|  | import { createComicWatcher } from "./diff/watcher/comic_watcher"; | ||||||
|  | import { DocumentAccessor, TagAccessor, UserAccessor } from "./model/mod"; | ||||||
|  | import { getTagRounter } from "./route/tags"; | ||||||
| 
 | 
 | ||||||
| class ServerApplication { | class ServerApplication { | ||||||
|     readonly userController: UserAccessor; |     readonly userController: UserAccessor; | ||||||
|  | @ -26,9 +25,10 @@ class ServerApplication{ | ||||||
|     readonly app: Koa; |     readonly app: Koa; | ||||||
|     private index_html: string; |     private index_html: string; | ||||||
|     private constructor(controller: { |     private constructor(controller: { | ||||||
|             userController: UserAccessor, |         userController: UserAccessor; | ||||||
|             documentController:DocumentAccessor, |         documentController: DocumentAccessor; | ||||||
|             tagController: TagAccessor}){ |         tagController: TagAccessor; | ||||||
|  |     }) { | ||||||
|         this.userController = controller.userController; |         this.userController = controller.userController; | ||||||
|         this.documentController = controller.documentController; |         this.documentController = controller.documentController; | ||||||
|         this.tagController = controller.tagController; |         this.tagController = controller.tagController; | ||||||
|  | @ -46,7 +46,7 @@ class ServerApplication{ | ||||||
|             if (await isAdminFirst(userAdmin)) { |             if (await isAdminFirst(userAdmin)) { | ||||||
|                 const rl = createReadlineInterface({ |                 const rl = createReadlineInterface({ | ||||||
|                     input: process.stdin, |                     input: process.stdin, | ||||||
|                     output:process.stdout |                     output: process.stdout, | ||||||
|                 }); |                 }); | ||||||
|                 const pw = await new Promise((res: (data: string) => void, err) => { |                 const pw = await new Promise((res: (data: string) => void, err) => { | ||||||
|                     rl.question("put admin password :", (data) => { |                     rl.question("put admin password :", (data) => { | ||||||
|  | @ -73,39 +73,36 @@ class ServerApplication{ | ||||||
|             await next(); |             await next(); | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         router.use('/api/diff',diff_router.routes()); |         router.use("/api/diff", diff_router.routes()); | ||||||
|         router.use('/api/diff',diff_router.allowedMethods()); |         router.use("/api/diff", diff_router.allowedMethods()); | ||||||
| 
 | 
 | ||||||
|         const content_router = getContentRouter(this.documentController); |         const content_router = getContentRouter(this.documentController); | ||||||
|         router.use('/api/doc',content_router.routes()); |         router.use("/api/doc", content_router.routes()); | ||||||
|         router.use('/api/doc',content_router.allowedMethods()); |         router.use("/api/doc", content_router.allowedMethods()); | ||||||
| 
 | 
 | ||||||
|         const tags_router = getTagRounter(this.tagController); |         const tags_router = getTagRounter(this.tagController); | ||||||
|         router.use("/api/tags", tags_router.allowedMethods()); |         router.use("/api/tags", tags_router.allowedMethods()); | ||||||
|         router.use("/api/tags", tags_router.routes()); |         router.use("/api/tags", tags_router.routes()); | ||||||
| 
 | 
 | ||||||
|          |  | ||||||
|          |  | ||||||
|         this.serve_with_meta_index(router); |         this.serve_with_meta_index(router); | ||||||
|         this.serve_index(router); |         this.serve_index(router); | ||||||
|         this.serve_static_file(router); |         this.serve_static_file(router); | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         const login_router = createLoginRouter(this.userController); |         const login_router = createLoginRouter(this.userController); | ||||||
|         router.use('/user',login_router.routes()); |         router.use("/user", login_router.routes()); | ||||||
|         router.use('/user',login_router.allowedMethods()); |         router.use("/user", login_router.allowedMethods()); | ||||||
|          |  | ||||||
| 
 | 
 | ||||||
|         if (setting.mode == "development") { |         if (setting.mode == "development") { | ||||||
|             let mm_count = 0; |             let mm_count = 0; | ||||||
|             app.use(async (ctx, next) => { |             app.use(async (ctx, next) => { | ||||||
|                 console.log(`==========================${mm_count++}`); |                 console.log(`==========================${mm_count++}`); | ||||||
|                 const ip = (ctx.get("X-Real-IP")) ?? ctx.ip; |                 const ip = (ctx.get("X-Real-IP")) ?? ctx.ip; | ||||||
|             const fromClient = ctx.state['user'].username === "" ? ip : ctx.state['user'].username; |                 const fromClient = ctx.state["user"].username === "" ? ip : ctx.state["user"].username; | ||||||
|                 console.log(`${fromClient} : ${ctx.method} ${ctx.url}`); |                 console.log(`${fromClient} : ${ctx.method} ${ctx.url}`); | ||||||
|                 await next(); |                 await next(); | ||||||
|                 // console.log(`404`);
 |                 // console.log(`404`);
 | ||||||
|         });} |             }); | ||||||
|  |         } | ||||||
|         app.use(router.routes()); |         app.use(router.routes()); | ||||||
|         app.use(router.allowedMethods()); |         app.use(router.allowedMethods()); | ||||||
|         console.log("setup done"); |         console.log("setup done"); | ||||||
|  | @ -113,25 +110,25 @@ class ServerApplication{ | ||||||
|     private serve_index(router: Router) { |     private serve_index(router: Router) { | ||||||
|         const serveindex = (url: string) => { |         const serveindex = (url: string) => { | ||||||
|             router.get(url, (ctx) => { |             router.get(url, (ctx) => { | ||||||
|                 ctx.type = 'html'; ctx.body = this.index_html; |                 ctx.type = "html"; | ||||||
|  |                 ctx.body = this.index_html; | ||||||
|                 const setting = get_setting(); |                 const setting = get_setting(); | ||||||
|                 ctx.set('x-content-type-options','no-sniff'); |                 ctx.set("x-content-type-options", "no-sniff"); | ||||||
|                 if (setting.mode === "development") { |                 if (setting.mode === "development") { | ||||||
|                     ctx.set('cache-control','no-cache');  |                     ctx.set("cache-control", "no-cache"); | ||||||
|  |                 } else { | ||||||
|  |                     ctx.set("cache-control", "public, max-age=3600"); | ||||||
|                 } |                 } | ||||||
|                 else{ |             }); | ||||||
|                     ctx.set('cache-control','public, max-age=3600'); |         }; | ||||||
|                 } |         serveindex("/"); | ||||||
|             }) |         serveindex("/doc/:rest(.*)"); | ||||||
|         } |         serveindex("/search"); | ||||||
|         serveindex('/'); |         serveindex("/login"); | ||||||
|         serveindex('/doc/:rest(.*)'); |         serveindex("/profile"); | ||||||
|         serveindex('/search'); |         serveindex("/difference"); | ||||||
|         serveindex('/login'); |         serveindex("/setting"); | ||||||
|         serveindex('/profile'); |         serveindex("/tags"); | ||||||
|         serveindex('/difference'); |  | ||||||
|         serveindex('/setting'); |  | ||||||
|         serveindex('/tags'); |  | ||||||
|     } |     } | ||||||
|     private serve_with_meta_index(router: Router) { |     private serve_with_meta_index(router: Router) { | ||||||
|         const DocMiddleware = async (ctx: Koa.ParameterizedContext) => { |         const DocMiddleware = async (ctx: Koa.ParameterizedContext) => { | ||||||
|  | @ -141,16 +138,18 @@ class ServerApplication{ | ||||||
|             if (doc === undefined) { |             if (doc === undefined) { | ||||||
|                 ctx.status = 404; |                 ctx.status = 404; | ||||||
|                 meta = NotFoundContent(); |                 meta = NotFoundContent(); | ||||||
|             } |             } else { | ||||||
|             else { |  | ||||||
|                 ctx.status = 200; |                 ctx.status = 200; | ||||||
|                 meta = createOgTagContent(doc.title,doc.tags.join(", "), |                 meta = createOgTagContent( | ||||||
|                 `https://aeolian.prelude.duckdns.org/api/doc/${docId}/comic/thumbnail`); |                     doc.title, | ||||||
|  |                     doc.tags.join(", "), | ||||||
|  |                     `https://aeolian.prelude.duckdns.org/api/doc/${docId}/comic/thumbnail`, | ||||||
|  |                 ); | ||||||
|             } |             } | ||||||
|             const html = makeMetaTagInjectedHTML(this.index_html, meta); |             const html = makeMetaTagInjectedHTML(this.index_html, meta); | ||||||
|             serveHTML(ctx, html); |             serveHTML(ctx, html); | ||||||
|         } |         }; | ||||||
|         router.get('/doc/:id(\\d+)',DocMiddleware); |         router.get("/doc/:id(\\d+)", DocMiddleware); | ||||||
| 
 | 
 | ||||||
|         function NotFoundContent() { |         function NotFoundContent() { | ||||||
|             return createOgTagContent("Not Found Doc", "Not Found", ""); |             return createOgTagContent("Not Found Doc", "Not Found", ""); | ||||||
|  | @ -159,14 +158,14 @@ class ServerApplication{ | ||||||
|             return html.replace("<!--MetaTag-Outlet-->", tagContent); |             return html.replace("<!--MetaTag-Outlet-->", tagContent); | ||||||
|         } |         } | ||||||
|         function serveHTML(ctx: Koa.Context, file: string) { |         function serveHTML(ctx: Koa.Context, file: string) { | ||||||
|             ctx.type = 'html'; ctx.body = file; |             ctx.type = "html"; | ||||||
|  |             ctx.body = file; | ||||||
|             const setting = get_setting(); |             const setting = get_setting(); | ||||||
|             ctx.set('x-content-type-options','no-sniff'); |             ctx.set("x-content-type-options", "no-sniff"); | ||||||
|             if (setting.mode === "development") { |             if (setting.mode === "development") { | ||||||
|                 ctx.set('cache-control','no-cache');  |                 ctx.set("cache-control", "no-cache"); | ||||||
|             } |             } else { | ||||||
|             else{ |                 ctx.set("cache-control", "public, max-age=3600"); | ||||||
|                 ctx.set('cache-control','public, max-age=3600'); |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -174,7 +173,8 @@ class ServerApplication{ | ||||||
|             return `<meta property="${key}" content="${value}">`; |             return `<meta property="${key}" content="${value}">`; | ||||||
|         } |         } | ||||||
|         function createOgTagContent(title: string, description: string, image: string) { |         function createOgTagContent(title: string, description: string, image: string) { | ||||||
|             return [createMetaTagContent("og:title",title), |             return [ | ||||||
|  |                 createMetaTagContent("og:title", title), | ||||||
|                 createMetaTagContent("og:type", "website"), |                 createMetaTagContent("og:type", "website"), | ||||||
|                 createMetaTagContent("og:description", description), |                 createMetaTagContent("og:description", description), | ||||||
|                 createMetaTagContent("og:image", image), |                 createMetaTagContent("og:image", image), | ||||||
|  | @ -190,23 +190,24 @@ class ServerApplication{ | ||||||
|     } |     } | ||||||
|     private serve_static_file(router: Router) { |     private serve_static_file(router: Router) { | ||||||
|         const static_file_server = (path: string, type: string) => { |         const static_file_server = (path: string, type: string) => { | ||||||
|             router.get('/'+path,async (ctx,next)=>{ |             router.get("/" + path, async (ctx, next) => { | ||||||
|                 const setting = get_setting(); |                 const setting = get_setting(); | ||||||
|                 ctx.type = type; ctx.body = createReadStream(path); |                 ctx.type = type; | ||||||
|                 ctx.set('x-content-type-options','no-sniff'); |                 ctx.body = createReadStream(path); | ||||||
|  |                 ctx.set("x-content-type-options", "no-sniff"); | ||||||
|                 if (setting.mode === "development") { |                 if (setting.mode === "development") { | ||||||
|                     ctx.set('cache-control','no-cache');  |                     ctx.set("cache-control", "no-cache"); | ||||||
|  |                 } else { | ||||||
|  |                     ctx.set("cache-control", "public, max-age=3600"); | ||||||
|                 } |                 } | ||||||
|                 else{ |             }); | ||||||
|                     ctx.set('cache-control','public, max-age=3600'); |         }; | ||||||
|                 } |  | ||||||
|             })}; |  | ||||||
|         const setting = get_setting(); |         const setting = get_setting(); | ||||||
|         static_file_server('dist/bundle.css','css'); |         static_file_server("dist/bundle.css", "css"); | ||||||
|         static_file_server('dist/bundle.js','js'); |         static_file_server("dist/bundle.js", "js"); | ||||||
|         if (setting.mode === "development") { |         if (setting.mode === "development") { | ||||||
|             static_file_server('dist/bundle.js.map','text');         |             static_file_server("dist/bundle.js.map", "text"); | ||||||
|             static_file_server('dist/bundle.css.map','text'); |             static_file_server("dist/bundle.css.map", "text"); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|     start_server() { |     start_server() { | ||||||
|  |  | ||||||
|  | @ -1,5 +1,4 @@ | ||||||
| 
 |  | ||||||
| export type JSONPrimitive = null | boolean | number | string; | export type JSONPrimitive = null | boolean | number | string; | ||||||
| export interface JSONMap extends Record<string, JSONType> {} | export interface JSONMap extends Record<string, JSONType> {} | ||||||
| export interface JSONArray extends Array<JSONType>{}; | export interface JSONArray extends Array<JSONType> {} | ||||||
| export type JSONType = JSONMap | JSONPrimitive | JSONArray; | export type JSONType = JSONMap | JSONPrimitive | JSONArray; | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import {readFileSync, existsSync, writeFileSync, promises as fs} from 'fs'; | import { existsSync, promises as fs, readFileSync, writeFileSync } from "fs"; | ||||||
| import {validate} from 'jsonschema'; | import { validate } from "jsonschema"; | ||||||
| 
 | 
 | ||||||
| export class ConfigManager<T> { | export class ConfigManager<T> { | ||||||
|     path: string; |     path: string; | ||||||
|  |  | ||||||
|  | @ -7,10 +7,9 @@ export function check_type<T>(obj: any,check_proto:Record<string,string|undefine | ||||||
|             if (!(obj[it] instanceof Array)) { |             if (!(obj[it] instanceof Array)) { | ||||||
|                 return false; |                 return false; | ||||||
|             } |             } | ||||||
|         } |         } else if (defined !== typeof obj[it]) { | ||||||
|         else if(defined !== typeof obj[it]){ |  | ||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|     return true; |     return true; | ||||||
| }; | } | ||||||
|  |  | ||||||
|  | @ -1,14 +1,14 @@ | ||||||
| import { ZipEntry } from 'node-stream-zip'; | import { ZipEntry } from "node-stream-zip"; | ||||||
| 
 | 
 | ||||||
| import {orderBy} from 'natural-orderby'; | import { ReadStream } from "fs"; | ||||||
| import { ReadStream } from 'fs'; | import { orderBy } from "natural-orderby"; | ||||||
| import StreamZip from 'node-stream-zip'; | import StreamZip from "node-stream-zip"; | ||||||
| 
 | 
 | ||||||
| export type ZipAsync = InstanceType<typeof StreamZip.async>; | export type ZipAsync = InstanceType<typeof StreamZip.async>; | ||||||
| export async function readZip(path: string): Promise<ZipAsync> { | export async function readZip(path: string): Promise<ZipAsync> { | ||||||
|     return new StreamZip.async({ |     return new StreamZip.async({ | ||||||
|         file: path, |         file: path, | ||||||
|         storeEntries: true |         storeEntries: true, | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
| export async function entriesByNaturalOrder(zip: ZipAsync) { | export async function entriesByNaturalOrder(zip: ZipAsync) { | ||||||
|  | @ -24,8 +24,10 @@ export async function readAllFromZip(zip:ZipAsync,entry: ZipEntry):Promise<Buffe | ||||||
|     const stream = await createReadableStreamFromZip(zip, entry); |     const stream = await createReadableStreamFromZip(zip, entry); | ||||||
|     const chunks: Uint8Array[] = []; |     const chunks: Uint8Array[] = []; | ||||||
|     return new Promise((resolve, reject) => { |     return new Promise((resolve, reject) => { | ||||||
|         stream.on('data',(data)=>{chunks.push(data)}); |         stream.on("data", (data) => { | ||||||
|         stream.on('error', (err)=>reject(err)); |             chunks.push(data); | ||||||
|         stream.on('end',()=>resolve(Buffer.concat(chunks))); |         }); | ||||||
|  |         stream.on("error", (err) => reject(err)); | ||||||
|  |         stream.on("end", () => resolve(Buffer.concat(chunks))); | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
|  | @ -65,8 +65,8 @@ | ||||||
| 
 | 
 | ||||||
|     /* Advanced Options */ |     /* Advanced Options */ | ||||||
|     "skipLibCheck": true, /* Skip type checking of declaration files. */ |     "skipLibCheck": true, /* Skip type checking of declaration files. */ | ||||||
|     "forceConsistentCasingInFileNames": true,  /* Disallow inconsistently-cased references to the same file. */ |     "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ | ||||||
|   }, |   }, | ||||||
|   "include": ["./"], |   "include": ["./"], | ||||||
|   "exclude": ["src/client","app","seeds"], |   "exclude": ["src/client", "app", "seeds"] | ||||||
| } | } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue