Compare commits

...

5 Commits

Author SHA1 Message Date
5670a12910 add: restore colored tag chip 2023-06-01 17:50:23 +09:00
edc6104a09 add: dprint fmt 2023-06-01 14:18:53 +09:00
04ab39a3ec chore: remove useless comments 2023-06-01 14:10:41 +09:00
a2a2407af6 fix: drawer for mobile view 2023-06-01 13:28:00 +09:00
65192c6c72 use pnpm 2023-06-01 11:09:00 +09:00
85 changed files with 4236 additions and 3593 deletions

View File

@ -6,4 +6,4 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -4,25 +4,29 @@ Content File Management Program.
For study about nodejs, typescript and react. For study about nodejs, typescript and react.
### deployment ### deployment
```
$ npm run app:build ```bash
pnpm run app:build
``` ```
### test ### test
```
$ npm run app ```bash
$ pnpm run app
``` ```
### server build ### server build
```
$ npm run compile ```bash
$ pnpm run compile
``` ```
### client build ### client build
```
$ npm run build ```bash
$ pnpm run build
``` ```
## License ## License
MIT License MIT License

196
app.ts
View File

@ -1,115 +1,113 @@
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;
} }
user.reset_password(password); user.reset_password(password);
return true; return true;
}); });
} }
const setting = get_setting(); const setting = get_setting();
if (!setting.cli) { if (!setting.cli) {
let wnd: BrowserWindow | null = null; let wnd: BrowserWindow | null = null;
const createWindow = async () => { const createWindow = async () => {
wnd = new BrowserWindow({ wnd = new BrowserWindow({
width: 800, width: 800,
height: 600, height: 600,
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('../loading.html');
//set admin cookies.
await session.defaultSession.cookies.set({
url:`http://localhost:${setting.port}`,
name:accessTokenName,
value:getAdminAccessTokenValue(),
httpOnly: true,
secure: false,
sameSite:"strict"
});
await session.defaultSession.cookies.set({
url:`http://localhost:${setting.port}`,
name:refreshTokenName,
value:getAdminRefreshTokenValue(),
httpOnly: true,
secure: false,
sameSite:"strict"
});
try{
const server = await create_server();
const app = server.start_server();
registerChannel(server.userController);
await wnd.loadURL(`http://localhost:${setting.port}`);
}
catch(e){
if(e instanceof Error){
await dialog.showMessageBox({
type: "error",
title:"error!",
message:e.message,
}); });
} await wnd.loadURL(`data:text/html;base64,` + Buffer.from(loading_html).toString("base64"));
else{ // await wnd.loadURL('../loading.html');
await dialog.showMessageBox({ // set admin cookies.
type: "error", await session.defaultSession.cookies.set({
title:"error!", url: `http://localhost:${setting.port}`,
message:String(e), name: accessTokenName,
value: getAdminAccessTokenValue(),
httpOnly: true,
secure: false,
sameSite: "strict",
}); });
} await session.defaultSession.cookies.set({
url: `http://localhost:${setting.port}`,
name: refreshTokenName,
value: getAdminRefreshTokenValue(),
httpOnly: true,
secure: false,
sameSite: "strict",
});
try {
const server = await create_server();
const app = server.start_server();
registerChannel(server.userController);
await wnd.loadURL(`http://localhost:${setting.port}`);
} catch (e) {
if (e instanceof Error) {
await dialog.showMessageBox({
type: "error",
title: "error!",
message: e.message,
});
} else {
await dialog.showMessageBox({
type: "error",
title: "error!",
message: String(e),
});
}
}
wnd.on("closed", () => {
wnd = null;
});
};
const isPrimary = app.requestSingleInstanceLock();
if (!isPrimary) {
app.quit(); // exit window
app.exit();
} }
wnd.on("closed", () => { app.on("second-instance", () => {
wnd = null; if (wnd != null) {
if (wnd.isMinimized()) {
wnd.restore();
}
wnd.focus();
}
});
app.on("ready", (event, info) => {
createWindow();
}); });
};
const isPrimary = app.requestSingleInstanceLock(); app.on("window-all-closed", () => { // quit when all windows are closed
if (!isPrimary) { if (process.platform != "darwin") app.quit(); // (except leave MacOS app active until Cmd+Q)
app.quit(); //exit window });
app.exit();
}
app.on("second-instance", () => {
if (wnd != null) {
if (wnd.isMinimized()) {
wnd.restore();
}
wnd.focus();
}
});
app.on("ready", (event, info) => {
createWindow();
});
app.on("window-all-closed", () => { // quit when all windows are closed app.on("activate", () => { // re-recreate window when dock icon is clicked and no other windows open
if (process.platform != "darwin") app.quit(); // (except leave MacOS app active until Cmd+Q) if (wnd == null) createWindow();
}); });
app.on("activate", () => { // re-recreate window when dock icon is clicked and no other windows open
if (wnd == null) createWindow();
});
} else { } else {
(async () => { (async () => {
try { try {
const server = await create_server(); const server = await create_server();
server.start_server(); server.start_server();
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
})(); })();
} }
const loading_html = `<!DOCTYPE html> const loading_html = `<!DOCTYPE html>
<html lang="ko"><head> <html lang="ko"><head>
@ -142,4 +140,4 @@ h1 {
<h1>Loading...</h1> <h1>Loading...</h1>
<div id="loading"></div> <div id="loading"></div>
</body> </body>
</html>`; </html>`;

23
dprint.json Normal file
View 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"
]
}

View File

@ -1,48 +1,48 @@
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) {
const definitions = schema.definitions; const definitions = schema.definitions;
const definition = definitions[typename]; const definition = definitions[typename];
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",
}; };
} }
} }
} }
const text = JSON.stringify(schema); const text = JSON.stringify(schema);
await writeFile(join(dirname(path),`${typename}.schema.json`),text); await writeFile(join(dirname(path), `${typename}.schema.json`), text);
} }
function capitalize(s:string){ 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;
const m = /(.+)\.ts/.exec(name); const m = /(.+)\.ts/.exec(name);
if(m !== null){ if (m !== null) {
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");

View File

@ -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;

View File

@ -1,54 +1,54 @@
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) => {
b.string("version"); b.string("version");
b.boolean("dirty"); b.boolean("dirty");
}); });
await knex.schema.createTable("users",(b)=>{ await knex.schema.createTable("users", (b) => {
b.string("username").primary().comment("user's login id"); b.string("username").primary().comment("user's login id");
b.string("password_hash",64).notNullable(); b.string("password_hash", 64).notNullable();
b.string("password_salt",64).notNullable(); b.string("password_salt", 64).notNullable();
}); });
await knex.schema.createTable("document",(b)=>{ await knex.schema.createTable("document", (b) => {
b.increments("id").primary(); b.increments("id").primary();
b.string("title").notNullable(); b.string("title").notNullable();
b.string("content_type",16).notNullable(); b.string("content_type", 16).notNullable();
b.string("basepath",256).notNullable().comment("directory path for resource"); b.string("basepath", 256).notNullable().comment("directory path for resource");
b.string("filename",256).notNullable().comment("filename"); b.string("filename", 256).notNullable().comment("filename");
b.string("content_hash").nullable(); b.string("content_hash").nullable();
b.json("additional").nullable(); b.json("additional").nullable();
b.integer("created_at").notNullable(); b.integer("created_at").notNullable();
b.integer("modified_at").notNullable(); b.integer("modified_at").notNullable();
b.integer("deleted_at"); b.integer("deleted_at");
b.index("content_type","content_type_index"); b.index("content_type", "content_type_index");
}); });
await knex.schema.createTable("tags", (b)=>{ await knex.schema.createTable("tags", (b) => {
b.string("name").primary(); b.string("name").primary();
b.text("description"); b.text("description");
}); });
await knex.schema.createTable("doc_tag_relation",(b)=>{ await knex.schema.createTable("doc_tag_relation", (b) => {
b.integer("doc_id").unsigned().notNullable(); b.integer("doc_id").unsigned().notNullable();
b.string("tag_name").notNullable(); b.string("tag_name").notNullable();
b.foreign("doc_id").references("document.id"); b.foreign("doc_id").references("document.id");
b.foreign("tag_name").references("tags.name"); b.foreign("tag_name").references("tags.name");
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.");
}; }

View File

@ -6,8 +6,9 @@
"scripts": { "scripts": {
"compile": "tsc", "compile": "tsc",
"compile:watch": "tsc -w", "compile:watch": "tsc -w",
"build": "cd src/client && npm run build:prod", "build": "cd src/client && pnpm run build:prod",
"build:watch": "cd src/client && npm 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",

View File

@ -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
@ -44,6 +46,6 @@
add URL Render page 바꾸기 add URL Render page 바꾸기
add modified_time add modified_time
add support robots.txt add support robots.txt
add vite ssr add vite ssr

File diff suppressed because it is too large Load Diff

View File

@ -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);
} },
}); });

View File

@ -1,66 +1,66 @@
{ {
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"$ref": "#/definitions/SettingConfig", "$ref": "#/definitions/SettingConfig",
"definitions": { "definitions": {
"SettingConfig": { "SettingConfig": {
"type": "object", "type": "object",
"properties": { "properties": {
"localmode": { "localmode": {
"type": "boolean", "type": "boolean",
"description": "if true, server will bind on '127.0.0.1' rather than '0.0.0.0'" "description": "if true, server will bind on '127.0.0.1' rather than '0.0.0.0'"
},
"guest": {
"type": "array",
"items": {
"$ref": "#/definitions/Permission"
},
"description": "guest permission"
},
"jwt_secretkey": {
"type": "string",
"description": "JWT secret key. if you change its value, all access tokens are invalidated."
},
"port": {
"type": "number",
"description": "the port which running server is binding on."
},
"mode": {
"type": "string",
"enum": [
"development",
"production"
]
},
"cli": {
"type": "boolean",
"description": "if true, do not show 'electron' window and show terminal only."
},
"forbid_remote_admin_login": {
"type": "boolean",
"description": "forbid to login admin from remote client. but, it do not invalidate access token. \r if you want to invalidate access token, change 'jwt_secretkey'."
},
"$schema": {
"type": "string"
}
},
"required": [
"localmode",
"guest",
"jwt_secretkey",
"port",
"mode",
"cli",
"forbid_remote_admin_login"
],
"additionalProperties": false
}, },
"Permission": { "guest": {
"type": "string", "type": "array",
"enum": [ "items": {
"ModifyTag", "$ref": "#/definitions/Permission"
"QueryContent", },
"ModifyTagDesc" "description": "guest permission"
] },
"jwt_secretkey": {
"type": "string",
"description": "JWT secret key. if you change its value, all access tokens are invalidated."
},
"port": {
"type": "number",
"description": "the port which running server is binding on."
},
"mode": {
"type": "string",
"enum": [
"development",
"production"
]
},
"cli": {
"type": "boolean",
"description": "if true, do not show 'electron' window and show terminal only."
},
"forbid_remote_admin_login": {
"type": "boolean",
"description": "forbid to login admin from remote client. but, it do not invalidate access token. \r if you want to invalidate access token, change 'jwt_secretkey'."
},
"$schema": {
"type": "string"
} }
},
"required": [
"localmode",
"guest",
"jwt_secretkey",
"port",
"mode",
"cli",
"forbid_remote_admin_login"
],
"additionalProperties": false
},
"Permission": {
"type": "string",
"enum": [
"ModifyTag",
"QueryContent",
"ModifyTagDesc"
]
} }
} }
}

View File

@ -1,76 +1,76 @@
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,
secure: true, secure: true,
guest:[], guest: [],
jwt_secretkey:"itsRandom", jwt_secretkey: "itsRandom",
port:8080, port: 8080,
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) => {
let diff_occur = false; let diff_occur = false;
for(const key in default_table){ for (const key in default_table) {
if(key === undefined || key in target){ if (key === undefined || key in target) {
continue; continue;
} }
target[key] = default_table[key as keyof SettingConfig]; target[key] = default_table[key as keyof 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" })) : {};
const partial_occur = setEmptyToDefault(ret,default_setting); const partial_occur = setEmptyToDefault(ret, default_setting);
if(partial_occur){ if (partial_occur) {
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();
const env = process.env.NODE_ENV; const env = process.env.NODE_ENV;
if(env !== undefined && (env != "production" && env != "development")){ if (env !== undefined && (env != "production" && env != "development")) {
throw new Error("process unknown value in NODE_ENV: must be either \"development\" or \"production\""); throw new Error("process unknown value in NODE_ENV: must be either \"development\" or \"production\"");
} }
setting.mode = env ?? setting.mode; setting.mode = env ?? setting.mode;

View File

@ -1,99 +1,99 @@
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";
export class FetchFailError extends Error{} export class FetchFailError extends Error {}
export class ClientDocumentAccessor implements DocumentAccessor{ export class ClientDocumentAccessor implements DocumentAccessor {
search: (search_word: string) => Promise<Document[]>; search: (search_word: string) => Promise<Document[]>;
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;
} }
/** /**
* not implement * not implement
*/ */
async findListByBasePath(basepath: string): Promise<Document[]>{ async findListByBasePath(basepath: string): Promise<Document[]> {
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;
} }
async add(c: DocumentBody): Promise<number>{ async add(c: DocumentBody): Promise<number> {
throw new Error("not allow"); throw new Error("not allow");
const res = await fetch(`${baseurl}`,{ const res = await fetch(`${baseurl}`, {
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;
} }
async addTag(c: Document, tag_name: string): Promise<boolean>{ async addTag(c: Document, tag_name: string): Promise<boolean> {
const {id,...rest} = c; const { id, ...rest } = c;
const res = await fetch(`${baseurl}/${id}/tags/${tag_name}`,{ const res = await fetch(`${baseurl}/${id}/tags/${tag_name}`, {
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;
} }
async delTag(c: Document, tag_name: string): Promise<boolean>{ async delTag(c: Document, tag_name: string): Promise<boolean> {
const {id,...rest} = c; const { id, ...rest } = c;
const res = await fetch(`${baseurl}/${id}/tags/${tag_name}`,{ const res = await fetch(`${baseurl}/${id}/tags/${tag_name}`, {
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;

View File

@ -1,35 +1,32 @@
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[] } = {};
keyValue.forEach((p)=>{ keyValue.forEach((p) => {
const [k,v] = p.split("="); const [k, v] = p.split("=");
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;
} };

View File

@ -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();
@ -29,18 +29,20 @@ const App = () => {
setUserPermission(permission); setUserPermission(permission);
} }
})(); })();
//useEffect(()=>{}); // useEffect(()=>{});
return ( return (
<UserContext.Provider value={{ <UserContext.Provider
username: user, value={{
setUsername: setUser, username: user,
permission: userPermission, setUsername: setUser,
setPermission: setUserPermission permission: userPermission,
}}> 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"),
); );

View File

@ -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 {
console.log("watch build success");
} }
else{ },
console.log('watch build success'); },
}
}
}
}); });
console.log("watching..."); console.log("watching...");
return result; return result;
@ -30,4 +29,4 @@ async function main() {
} }
main().then((res) => { main().then((res) => {
}); });

View File

@ -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,115 +32,133 @@ 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 classes = useStyles();
const theme = useTheme(); const theme = useTheme();
const document = props.document; const document = props.document;
/*const rootName = props.short ? classes.short_root : classes.root;
const thumbnail_anchor = props.short ? classes.short_thumbnail_anchor : "";
const thumbnail_content = props.short ? classes.short_thumbnail_content :
classes.thumbnail_content;
const subinfoContainer = props.short ? classes.short_subinfoContainer :
classes.subinfoContainer;*/
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 (
display: "flex", <Paper
height: "400px", sx={{
[theme.breakpoints.down("sm")]: { display: "flex",
flexDirection: "column", height: "400px",
alignItems: "center", [theme.breakpoints.down("sm")]: {
} flexDirection: "column",
}} elevation={4}> alignItems: "center",
<Link /*className={propclasses.thumbnail_anchor ?? thumbnail_anchor}*/ component={RouterLink} to={{ height: "auto",
pathname: makeContentReaderUrl(document.id) },
}}> }}
{document.deleted_at === null ? elevation={4}
(<ThumbnailContainer content={document}/>) >
: (<Typography/* className={propclasses.thumbnail_content ?? thumbnail_content} */ variant='h4'>Deleted</Typography>)} <Link
</Link> component={RouterLink}
<Box /*className={propclasses.infoContainer ?? classes.infoContainer}*/> to={{
<Link variant='h5' color='inherit' component={RouterLink} to={{pathname: url}} pathname: makeContentReaderUrl(document.id),
/*className={propclasses.title ?? classes.title}*/> }}
{document.title} >
{document.deleted_at === null
? <ThumbnailContainer content={document} />
: <Typography variant="h4">Deleted</Typography>}
</Link> </Link>
<Box /*className={propclasses.subinfoContainer ?? subinfoContainer}*/> <Box>
{props.short ? (<Box /*className={propclasses.tag_list ?? classes.tag_list}*/>{document.tags.map(x => <Link variant="h5" color="inherit" component={RouterLink} to={{ pathname: url }}>
(<TagChip key={x} label={x} clickable tagname={x} size="small"></TagChip>) {document.title}
)}</Box>) : ( </Link>
<ComicDetailTag tags={document.tags} path={document.basepath+"/"+document.filename} <Box>
createdAt={document.created_at} {props.short
deletedAt={document.deleted_at != null ? document.deleted_at : undefined} ? (
/* classes={({tag_list:classes.tag_list})}*/ ></ComicDetailTag>) <Box>
} {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}
deletedAt={document.deleted_at != null ? document.deleted_at : undefined}
>
</ComicDetailTag>
)}
</Box>
{document.deleted_at != null
&& (
<Button
onClick={() => {
documentDelete(document.id);
}}
>
Delete
</Button>
)}
</Box> </Box>
{document.deleted_at != null && </Paper>
<Button onClick={()=>{documentDelete(document.id);}}>Delete</Button> );
} };
</Box> async function documentDelete(id: number) {
</Paper>);
}
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.");
} }
} }
function ComicDetailTag(prop: { function ComicDetailTag(prop: {
tags: string[];/*classes:{ tags: string[]; /*classes:{
tag_list:string tag_list:string
}*/ }*/
path?: string; path?: string;
createdAt?: number; createdAt?: number;
deletedAt?: number; deletedAt?: number;
@ -153,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 (
{tagKind.map(key => ( <Grid container>
<React.Fragment key={key}> {tagKind.map(key => (
<Grid item xs={3}> <React.Fragment key={key}>
<Typography variant='subtitle1'>{key}</Typography> <Grid item xs={3}>
</Grid> <Typography variant="subtitle1">{key}</Typography>
<Grid item xs={9}> </Grid>
<Box>{tagTable[key].length !== 0 ? tagTable[key].join(", ") : "N/A"}</Box> <Grid item xs={9}>
</Grid> <Box>{tagTable[key].length !== 0 ? tagTable[key].join(", ") : "N/A"}</Box>
</React.Fragment> </Grid>
))} </React.Fragment>
{ prop.path != undefined && <><Grid item xs={3}> ))}
<Typography variant='subtitle1'>Path</Typography> {prop.path != undefined && (
</Grid><Grid item xs={9}> <>
<Box>{prop.path}</Box> <Grid item xs={3}>
</Grid></> <Typography variant="subtitle1">Path</Typography>
} </Grid>
{ prop.createdAt != undefined && <><Grid item xs={3}> <Grid item xs={9}>
<Typography variant='subtitle1'>CreatedAt</Typography> <Box>{prop.path}</Box>
</Grid><Grid item xs={9}> </Grid>
<Box>{new Date(prop.createdAt).toUTCString()}</Box> </>
</Grid></> )}
} {prop.createdAt != undefined && (
{ prop.deletedAt != undefined && <><Grid item xs={3}> <>
<Typography variant='subtitle1'>DeletedAt</Typography> <Grid item xs={3}>
</Grid><Grid item xs={9}> <Typography variant="subtitle1">CreatedAt</Typography>
<Box>{new Date(prop.deletedAt).toUTCString()}</Box> </Grid>
</Grid></> <Grid item xs={9}>
} <Box>{new Date(prop.createdAt).toUTCString()}</Box>
<Grid item xs={3}> </Grid>
<Typography variant='subtitle1'>Tags</Typography> </>
)}
{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> </Grid>
<Grid item xs={9}> );
{allTag.map(x => (<TagChip key={x} label={x} clickable tagname={x} size="small"></TagChip>))} }
</Grid>
</Grid>);
}

View File

@ -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,51 +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 }) => ({
[theme.breakpoints.up("sm")]: {
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);
@ -77,138 +96,179 @@ 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 = (
anchorEl={anchorEl} <Menu
anchorOrigin={{ horizontal: 'right', vertical: "top" }} anchorEl={anchorEl}
id={menuId} anchorOrigin={{ horizontal: "right", vertical: "top" }}
open={isProfileMenuOpened} id={menuId}
keepMounted open={isProfileMenuOpened}
transformOrigin={{ horizontal: 'right', vertical: "top" }} keepMounted
onClose={handleProfileMenuClose} transformOrigin={{ horizontal: "right", vertical: "top" }}
> onClose={handleProfileMenuClose}
<MenuItem component={RouterLink} to='/profile'>Profile</MenuItem> >
<MenuItem onClick={async () => { handleProfileMenuClose(); await doLogout(); user_ctx.setUsername(""); }}>Logout</MenuItem> <MenuItem component={RouterLink} to="/profile">Profile</MenuItem>
</Menu>); <MenuItem
const drawer_contents = (<> onClick={async () => {
<DrawerHeader> handleProfileMenuClose();
<IconButton onClick={toggleV}> await doLogout();
{theme.direction === "ltr" ? <ChevronLeft /> : <ChevronRight />} user_ctx.setUsername("");
</IconButton> }}
</DrawerHeader> >
<Divider /> Logout
{prop.menu} </MenuItem>
</>); </Menu>
);
return (<div style={{ display: 'flex' }}> const drawer_contents = (
<CssBaseline /> <>
<AppBar position="fixed" sx={{ <DrawerHeader>
zIndex: theme.zIndex.drawer + 1, <IconButton onClick={toggleV}>
transition: theme.transitions.create(['width', 'margin'], { {theme.direction === "ltr" ? <ChevronLeft /> : <ChevronRight />}
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
})
}}>
<Toolbar>
<IconButton color="inherit"
aria-label="open drawer"
onClick={toggleV}
edge="start"
style={{ marginRight: 36 }}
>
<MenuIcon></MenuIcon>
</IconButton> </IconButton>
<Link variant="h5" noWrap sx={{ </DrawerHeader>
display: 'none', <Divider />
[theme.breakpoints.up("sm")]: { {prop.menu}
display: 'block' </>
} );
}} color="inherit" component={RouterLink} to="/">
Ionian return (
</Link> <div style={{ display: "flex" }}>
<div style={{ flexGrow: 1 }}></div> <CssBaseline />
<StyledSearchBar > <AppBar
<div style={{ position="fixed"
padding: theme.spacing(0, 2), sx={{
height: '100%', zIndex: theme.zIndex.drawer + 1,
position: 'absolute', transition: theme.transitions.create(["width", "margin"], {
pointerEvents: 'none', easing: theme.transitions.easing.sharp,
display: 'flex', duration: theme.transitions.duration.leavingScreen,
alignItems: 'center', }),
justifyContent: 'center' }}
}}> >
<SearchIcon onClick={() => navSearch(search)} /> <Toolbar>
</div> <IconButton
<StyledInputBase placeholder="search" color="inherit"
onChange={(e) => setSearch(e.target.value)} aria-label="open drawer"
onKeyUp={(e) => { onClick={toggleV}
if (e.key === "Enter") { edge="start"
navSearch(search); style={{ marginRight: 36 }}
} >
<MenuIcon></MenuIcon>
</IconButton>
<Link
variant="h5"
noWrap
sx={{
display: "none",
[theme.breakpoints.up("sm")]: {
display: "block",
},
}} }}
value={search}></StyledInputBase> color="inherit"
</StyledSearchBar> component={RouterLink}
{ to="/"
isLogin ? >
<IconButton Ionian
edge="end" </Link>
aria-label="account of current user" <div style={{ flexGrow: 1 }}></div>
aria-controls={menuId} <StyledSearchBar>
aria-haspopup="true" <div
onClick={handleProfileMenuOpen} style={{
color="inherit"> padding: theme.spacing(0, 2),
<AccountCircle /> height: "100%",
</IconButton> position: "absolute",
: <Button color="inherit" component={RouterLink} to="/login">Login</Button> pointerEvents: "none",
} display: "flex",
</Toolbar> alignItems: "center",
</AppBar> justifyContent: "center",
{renderProfileMenu} }}
<nav style={{ width: theme.spacing(7) }}> >
<Hidden smUp implementation="css"> <SearchIcon onClick={() => navSearch(search)} />
<StyledDrawer variant="temporary" anchor='left' open={v} onClose={toggleV} </div>
sx={{ <StyledInputBase
width: drawerWidth placeholder="search"
}} onChange={(e) => setSearch(e.target.value)}
> onKeyUp={(e) => {
{drawer_contents} if (e.key === "Enter") {
</StyledDrawer> navSearch(search);
</Hidden> }
<Hidden xsDown implementation="css"> }}
<StyledDrawer variant='permanent' anchor='left' value={search}
sx={{ >
...closedMixin(theme), </StyledInputBase>
'& .MuiDrawer-paper': closedMixin(theme), </StyledSearchBar>
}}> {isLogin
{drawer_contents} ? (
</StyledDrawer> <IconButton
</Hidden> edge="end"
</nav> aria-label="account of current user"
<main style={{ aria-controls={menuId}
display: 'flex', aria-haspopup="true"
flexFlow: 'column', onClick={handleProfileMenuOpen}
flexGrow: 1, color="inherit"
padding: theme.spacing(3), >
marginTop: theme.spacing(6), <AccountCircle />
}}> </IconButton>
<div style={{ )
}} ></div> : <Button color="inherit" component={RouterLink} to="/login">Login</Button>}
{prop.children} </Toolbar>
</main> </AppBar>
</div>); {renderProfileMenu}
function navSearch(search: string){ <StyledNav>
<Hidden smUp implementation="css">
<StyledDrawer
variant="temporary"
anchor="left"
open={v}
onClose={toggleV}
sx={{
width: drawerWidth,
}}
>
{drawer_contents}
</StyledDrawer>
</Hidden>
<Hidden smDown implementation="css">
<StyledDrawer
variant="permanent"
anchor="left"
sx={{
...closedMixin(theme),
"& .MuiDrawer-paper": closedMixin(theme),
}}
>
{drawer_contents}
</StyledDrawer>
</Hidden>
</StyledNav>
<main
style={{
display: "flex",
flexFlow: "column",
flexGrow: 1,
padding: theme.spacing(3),
marginTop: theme.spacing(6),
}}
>
<div style={{}}></div>
{prop.children}
</main>
</div>
);
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}`
navigate(`/search?${words.join("&")}`); : `word=${encodeURIComponent(w)}`
);
navigate(`/search?${words.join("&")}`);
} }
}; };
export default Headline; export default Headline;

View File

@ -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 (
<CircularProgress title="loading" /> <Box style={{ position: "absolute", top: "50%", left: "50%", transform: "translate(-50%,-50%)" }}>
</Box>); <CircularProgress title="loading" />
} </Box>
);
};

View File

@ -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";

View File

@ -1,43 +1,58 @@
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 (
<ListItemIcon> <ListItem button key={props.name} component={RouterLink} to={props.to}>
<Tooltip title={props.name.toLocaleLowerCase()} placement="bottom"> <ListItemIcon>
{props.icon} <Tooltip title={props.name.toLocaleLowerCase()} placement="bottom">
</Tooltip> {props.icon}
</ListItemIcon> </Tooltip>
<ListItemText primary={props.name}></ListItemText> </ListItemIcon>
</ListItem>); <ListItemText primary={props.name}></ListItemText>
} </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>
<NavItem name="All" to="/" icon={<HomeIcon />} /> {url !== "" && (
<NavItem name="Comic" to="/search?content_type=comic" icon={<CollectionIcon />}></NavItem> <>
<NavItem name="Video" to="/search?content_type=video" icon={<VideoIcon />} /> <BackItem to={url} /> <Divider />
<Divider /> </>
<NavItem name="Tags" to="/tags" icon={<ListIcon/>}/> )}
<Divider /> <NavItem name="All" to="/" icon={<HomeIcon />} />
<NavItem name="Difference" to="/difference" icon={<FolderIcon/>}></NavItem> <NavItem name="Comic" to="/search?content_type=comic" icon={<CollectionIcon />}></NavItem>
<NavItem name="Settings" to="/setting" icon={<SettingIcon />} /> <NavItem name="Video" to="/search?content_type=video" icon={<VideoIcon />} />
</NavList>); <Divider />
} <NavItem name="Tags" to="/tags" icon={<ListIcon />} />
<Divider />
<NavItem name="Difference" to="/difference" icon={<FolderIcon />}></NavItem>
<NavItem name="Settings" to="/setting" icon={<SettingIcon />} />
</NavList>
);
}

View File

@ -1,86 +1,78 @@
import React from 'react'; import * as colors from "@mui/material/colors";
import {ChipTypeMap} from '@mui/material/Chip'; import Chip, { ChipTypeMap } from "@mui/material/Chip";
import { Chip, colors } from '@mui/material'; import { emphasize, styled, Theme, useTheme } 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: `rgba(${number},${number},${number},${number})` | `#${string}` | 'default';
} };
const useTagStyles = ((theme:Theme)=>({ const { blue, pink } = colors;
root:(props:TagChipStyleProp)=>({ const getTagColorName = (tagname: string): TagChipStyleProp['color'] => {
color: theme.palette.getContrastText(props.color), if (tagname.startsWith("female")) {
backgroundColor: props.color,
}),
clickable:(props:TagChipStyleProp)=>({
'&:hover, &:focus':{
backgroundColor:emphasize(props.color,0.08)
}
}),
deletable: {
'&:focus': {
backgroundColor: (props:TagChipStyleProp)=>emphasize(props.color, 0.2),
}
},
outlined:{
color: (props:TagChipStyleProp)=>props.color,
border: (props:TagChipStyleProp)=> `1px solid ${props.color}`,
'$clickable&:hover, $clickable&:focus, $deletable&:focus': {
//backgroundColor:(props:TagChipStyleProp)=> (props.color,theme.palette.action.hoverOpacity),
},
},
icon:{
color:"inherit",
},
deleteIcon:{
//color:(props:TagChipStyleProp)=> (theme.palette.getContrastText(props.color),0.7),
"&:hover, &:active":{
color:(props:TagChipStyleProp)=>theme.palette.getContrastText(props.color),
}
}
}));
const {blue, pink} = colors;
const getTagColorName = (tagname :string):string=>{
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";
};
type ColorChipProp = Omit<ChipTypeMap["props"], "color"> & TagChipStyleProp & {
component?: React.ElementType;
to?: string;
};
export const ColorChip = (props: ColorChipProp) => {
const { color, ...rest } = props;
const theme = useTheme();
let newcolor = color;
if (color === "default"){
newcolor = "#ebebeb";
} }
else return "default"; return <Chip
} sx={{
color: theme.palette.getContrastText(newcolor),
backgroundColor: newcolor,
["&:hover, &:focus"]: {
backgroundColor: emphasize(newcolor, 0.08),
},
}}
{...rest}></Chip>;
};
type ColorChipProp = Omit<ChipTypeMap['props'],"color"> & TagChipStyleProp & { type TagChipProp = Omit<ChipTypeMap["props"], "color"> & {
component?: React.ElementType, tagname: string;
to?: string };
}
export const ColorChip = (props:ColorChipProp)=>{ export const TagChip = (props: TagChipProp) => {
const {color,...rest} = props; const { tagname, label, clickable, ...rest } = props;
//const classes = useTagStyles({color : color !== "default" ? color : "#000"}); const colorName = getTagColorName(tagname);
return <Chip color="default" {...rest}></Chip>;
}
type TagChipProp = Omit<ChipTypeMap['props'],"color"> & { let newlabel: React.ReactNode = label;
tagname:string if (typeof label === "string") {
} const female = "female:";
const male = "male:";
export const TagChip = (props:TagChipProp)=>{ if (label.startsWith(female)) {
const {tagname,label,clickable,...rest} = props; newlabel = "♀ " + label.slice(female.length);
let newlabel:string|undefined = undefined; } else if (label.startsWith(male)) {
if(typeof label === "string"){ newlabel = "♂ " + label.slice(male.length);
if(label.startsWith("female:")){
newlabel ="♀ "+label.slice(7);
}
else if(label.startsWith("male:")){
newlabel = "♂ "+label.slice(5);
} }
} }
const inner = clickable ?
(<ColorChip color={getTagColorName(tagname)} clickable={clickable} label={newlabel??label} {...rest} const inner = clickable
component={RouterLink} to={`/search?allow_tag=${tagname}`}></ColorChip>): ? (
(<ColorChip color={getTagColorName(tagname)} clickable={clickable} label={newlabel??label} {...rest}></ColorChip>); <ColorChip
color={colorName}
clickable={clickable}
label={newlabel ?? label}
{...rest}
component={RouterLink}
to={`/search?allow_tag=${tagname}`}
/>
)
: (
<ColorChip color={colorName} clickable={clickable} label={newlabel ?? label} {...rest}/>
);
return inner; return inner;
} };

View File

@ -1,24 +1,24 @@
{ {
"name": "ionian_client", "name": "ionian_client",
"version": "0.0.1", "version": "0.0.1",
"description": "client of ionian", "description": "client of ionian",
"scripts": { "scripts": {
"build:watch": "ts-node build.ts" "build:watch": "ts-node build.ts"
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.9.0", "@emotion/react": "^11.9.0",
"@emotion/styled": "^11.8.1", "@emotion/styled": "^11.8.1",
"@mui/icons-material": "^5.6.2", "@mui/icons-material": "^5.6.2",
"@mui/material": "^5.6.2", "@mui/material": "^5.6.2",
"@mui/x-data-grid": "^5.12.3", "@mui/x-data-grid": "^5.12.3",
"@types/react": "^18.0.5", "@types/react": "^18.0.5",
"@types/react-dom": "^18.0.1", "@types/react-dom": "^18.0.1",
"react": "^18.0.0", "react": "^18.0.0",
"react-dom": "^18.0.0", "react-dom": "^18.0.0",
"react-router-dom": "^6.3.0" "react-router-dom": "^6.3.0"
}, },
"devDependencies": { "devDependencies": {
"esbuild": "^0.14.36", "esbuild": "^0.14.36",
"ts-node": "^10.7.0" "ts-node": "^10.7.0"
} }
} }

View File

@ -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}>
</Headline> <Typography variant="h2">404 Not Found</Typography>
}; </Headline>
);
};

View File

@ -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) {
return (<Headline menu={menu_list()}>
<LoadingCircle />
</Headline>
); );
} } else if (info.doc === undefined) {
else { return (
<Headline menu={menu_list()}>
<LoadingCircle />
</Headline>
);
} else {
const ReaderPage = getPresenter(info.doc); const ReaderPage = getPresenter(info.doc);
return <Headline menu={menu_list(location.pathname)}> return (
<ReaderPage doc={info.doc}></ReaderPage> <Headline menu={menu_list(location.pathname)}>
</Headline> <ReaderPage doc={info.doc}></ReaderPage>
</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) {
return (<Headline menu={menu_list()}>
<LoadingCircle />
</Headline>
); );
} } else if (info.doc === undefined) {
else { return (
<Headline menu={menu_list()}>
<LoadingCircle />
</Headline>
);
} else {
return ( return (
<Headline menu={menu_list()}> <Headline menu={menu_list()}>
<ContentInfo document={info.doc}></ContentInfo> <ContentInfo document={info.doc}></ContentInfo>
</Headline> </Headline>
); );
} }
} };

View File

@ -1,127 +1,141 @@
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 (
<Box /*className={classes.contentTitle}*/> <Paper /*className={classes.paper}*/>
<Typography variant='h3' >{x.type}</Typography> <Box /*className={classes.contentTitle}*/>
<Button variant="contained" key={x.type} onClick={()=>{ <Typography variant="h3">{x.type}</Typography>
<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> }}
</Box> >
{x.value.map(y=>( Commit all
<Box sx={{display:"flex"}} key={y.path}> </Button>
<Button variant="contained" onClick={()=>{ </Box>
set_disable(true); {x.value.map(y => (
prop.onCommit(y); <Box sx={{ display: "flex" }} key={y.path}>
set_disable(false); <Button
}} variant="contained"
disabled={button_disable}>Commit</Button> onClick={() => {
<Typography variant='h5'>{y.path}</Typography> set_disable(true);
</Box> prop.onCommit(y);
))} set_disable(false);
</Paper>); }}
disabled={button_disable}
>
Commit
</Button>
<Typography variant="h5">{y.path}</Typography>
</Box>
))}
</Paper>
);
} }
export function DifferencePage(){ export function DifferencePage() {
const ctx = useContext(UserContext); const ctx = useContext(UserContext);
//const classes = useStyles(); // const classes = useStyles();
const [diffList,setDiffList] = useState< const [diffList, setDiffList] = useState<
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>
);
}

View File

@ -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,77 +15,89 @@ 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 });
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [loadAll, setLoadAll] = useState(false); const [loadAll, setLoadAll] = useState(false);
const {elementRef, isVisible: isLoadVisible} = useIsElementInViewport<HTMLButtonElement>({}); const { elementRef, isVisible: isLoadVisible } = useIsElementInViewport<HTMLButtonElement>({});
useEffect(()=>{ useEffect(() => {
if(isLoadVisible && (!loadAll) && (state.documents != undefined)){ if (isLoadVisible && (!loadAll) && (state.documents != undefined)) {
loadMore(); loadMore();
} }
},[isLoadVisible]); }, [isLoadVisible]);
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> }}
<Typography variant="h6">search for</Typography> >
{props.option.word !== undefined && <Chip label={"search : " + decodeURIComponent(props.option.word)}></Chip>} {props.option !== undefined && props.diff !== "" && (
{props.option.content_type !== undefined && <Chip label={"type : " + props.option.content_type}></Chip>} <Box>
{props.option.allow_tag !== undefined && props.option.allow_tag.map(x => ( <Typography variant="h6">search for</Typography>
<TagChip key={x} tagname={decodeURIComponent(x)} label={decodeURIComponent(x)}></TagChip>))} {props.option.word !== undefined && (
</Box>} <Chip label={"search : " + decodeURIComponent(props.option.word)}></Chip>
{ )}
state.documents && state.documents.map(x => { {props.option.content_type !== undefined && (
return (<ContentInfo document={x} key={x.id} <Chip label={"type : " + props.option.content_type}></Chip>
gallery={`/search?${queryString}`} short />); )}
}) {props.option.allow_tag !== undefined
} && props.option.allow_tag.map(x => (
{(error && <Typography variant="h5">Error : {error}</Typography>)} <TagChip key={x} tagname={decodeURIComponent(x)} label={decodeURIComponent(x)}>
<Typography variant="body1" sx={{ </TagChip>
justifyContent: "center", ))}
textAlign:"center" </Box>
}}>{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.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",
textAlign: "center",
}}
>
{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);
return; return;
@ -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 (
<GalleryInfo diff={location.search} option={query}></GalleryInfo> <Headline menu={menu_list}>
</Headline>) <GalleryInfo diff={location.search} option={query}></GalleryInfo>
} </Headline>
);
};

View File

@ -1,66 +1,86 @@
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();
const [userLoginInfo,setUserLoginInfo]= useState({username:"",password:""}); const [userLoginInfo, setUserLoginInfo] = useState({ username: "", password: "" });
const [openDialog,setOpenDialog] = useState({open:false,message:""}); const [openDialog, setOpenDialog] = useState({ open: false, message: "" });
const {setUsername,setPermission} = useContext(UserContext); const { setUsername, setPermission } = useContext(UserContext);
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);
if(typeof b === "string"){ if (typeof b === "string") {
setOpenDialog({open:true,message: b}); setOpenDialog({ open: true, message: b });
return; return;
} }
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}>
<Typography variant="h4">Login</Typography> <Paper style={{ width: theme.spacing(40), padding: theme.spacing(4), alignSelf: "center" }}>
<div style={{minHeight:theme.spacing(2)}}></div> <Typography variant="h4">Login</Typography>
<form style={{display:'flex', flexFlow:'column', alignItems:'stretch'}}> <div style={{ minHeight: theme.spacing(2) }}></div>
<TextField label="username" onChange={(e)=>setUserLoginInfo({...userLoginInfo,username:e.target.value ?? "",})}></TextField> <form style={{ display: "flex", flexFlow: "column", alignItems: "stretch" }}>
<TextField label="password" type="password" onKeyDown={(e)=>{if(e.key === 'Enter') doLogin();}} <TextField
onChange={(e)=>setUserLoginInfo({...userLoginInfo,password:e.target.value ?? "",})}/> label="username"
<div style={{minHeight:theme.spacing(2)}}></div> onChange={(e) => setUserLoginInfo({ ...userLoginInfo, username: e.target.value ?? "" })}
<div style={{display:'flex'}}> >
<Button onClick={doLogin}>login</Button> </TextField>
<Button>signin</Button> <TextField
</div> label="password"
</form> type="password"
</Paper> onKeyDown={(e) => {
<Dialog open={openDialog.open} if (e.key === "Enter") doLogin();
onClose={handleDialogClose}> }}
<DialogTitle>Login Failed</DialogTitle> onChange={(e) => setUserLoginInfo({ ...userLoginInfo, password: e.target.value ?? "" })}
<DialogContent> />
<DialogContentText>detail : {openDialog.message}</DialogContentText> <div style={{ minHeight: theme.spacing(2) }}></div>
</DialogContent> <div style={{ display: "flex" }}>
<DialogActions> <Button onClick={doLogin}>login</Button>
<Button onClick={handleDialogClose} color="primary" autoFocus>Close</Button> <Button>signin</Button>
</DialogActions> </div>
</Dialog> </form>
</Headline> </Paper>
} <Dialog open={openDialog.open} onClose={handleDialogClose}>
<DialogTitle>Login Failed</DialogTitle>
<DialogContent>
<DialogContentText>detail : {openDialog.message}</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleDialogClose} color="primary" autoFocus>Close</Button>
</DialogActions>
</Dialog>
</Headline>
);
};

View File

@ -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";

View File

@ -1,114 +1,147 @@
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);
//const classes = useStyles(); // const classes = useStyles();
const menu = CommonMenuList(); const menu = CommonMenuList();
const [pw_open,set_pw_open] = useState(false); const [pw_open, set_pw_open] = useState(false);
const [oldpw,setOldpw] = useState(""); const [oldpw, setOldpw] = useState("");
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 handle_open = () => set_pw_open(true);
const isElectronContent = (((window['electron'] as any) !== undefined) as boolean); const handle_close = () => {
const handle_open = ()=>set_pw_open(true);
const handle_close = ()=>{
set_pw_open(false); set_pw_open(false);
setNewpw(""); setNewpw("");
setNewpwch(""); setNewpwch("");
}; };
const handle_ok= async ()=>{ const handle_ok = async () => {
if(newpw != newpwch){ if (newpw != newpwch) {
set_msg_dialog({opened:true,msg:"password and password check is not equal."}); set_msg_dialog({ opened: true, msg: "password and password check is not equal." });
handle_close(); handle_close();
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 (
<Paper /*className={classes.paper}*/> <Headline menu={menu}>
<Grid container direction="column" alignItems="center"> <Paper /*className={classes.paper}*/>
<Grid item> <Grid container direction="column" alignItems="center">
<Typography variant='h4'>{userctx.username}</Typography> <Grid item>
<Typography variant="h4">{userctx.username}</Typography>
</Grid>
<Divider></Divider>
<Grid item>
Permission
</Grid>
<Grid item>
{permission_list.length == 0 ? "-" : permission_list}
</Grid>
<Grid item>
<Button onClick={handle_open}>Password Reset</Button>
</Grid>
</Grid> </Grid>
<Divider></Divider> </Paper>
<Grid item> <Dialog open={pw_open} onClose={handle_close}>
Permission <DialogTitle>Password Reset</DialogTitle>
</Grid> <DialogContent>
<Grid item> <Typography>type the old and new password</Typography>
{permission_list.length == 0 ? "-" : permission_list} <div /*className={classes.formfield}*/>
</Grid> {(!isElectronContent) && (
<Grid item> <TextField
<Button onClick={handle_open}>Password Reset</Button> autoFocus
</Grid> margin="dense"
</Grid> type="password"
</Paper> label="old password"
<Dialog open={pw_open} onClose={handle_close}> value={oldpw}
<DialogTitle>Password Reset</DialogTitle> onChange={(e) => setOldpw(e.target.value)}
<DialogContent> >
<Typography>type the old and new password</Typography> </TextField>
<div /*className={classes.formfield}*/> )}
{(!isElectronContent) && (<TextField autoFocus margin='dense' type="password" label="old password" <TextField
value={oldpw} onChange={(e)=>setOldpw(e.target.value)}></TextField>)} margin="dense"
<TextField margin='dense' type="password" label="new password" type="password"
value={newpw} onChange={e=>setNewpw(e.target.value)}></TextField> label="new password"
<TextField margin='dense' type="password" label="new password check" value={newpw}
value={newpwch} onChange={e=>setNewpwch(e.target.value)}></TextField> onChange={e => setNewpw(e.target.value)}
</div> >
</DialogContent> </TextField>
<DialogActions> <TextField
<Button onClick={handle_close} color="primary">Cancel</Button> margin="dense"
<Button onClick={handle_ok} color="primary">Ok</Button> type="password"
</DialogActions> label="new password check"
</Dialog> value={newpwch}
<Dialog open={msg_dialog.opened} onClose={()=>set_msg_dialog({opened:false,msg:""})}> onChange={e => setNewpwch(e.target.value)}
<DialogTitle>Alert!</DialogTitle> >
<DialogContent> </TextField>
<DialogContentText>{msg_dialog.msg}</DialogContentText> </div>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={()=>set_msg_dialog({opened:false,msg:""})} color="primary">Close</Button> <Button onClick={handle_close} color="primary">Cancel</Button>
</DialogActions> <Button onClick={handle_ok} color="primary">Ok</Button>
</Dialog> </DialogActions>
</Headline>) </Dialog>
} <Dialog open={msg_dialog.opened} onClose={() => set_msg_dialog({ opened: false, msg: "" })}>
<DialogTitle>Alert!</DialogTitle>
<DialogContent>
<DialogContentText>{msg_dialog.msg}</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => set_msg_dialog({ opened: false, msg: "" })} color="primary">Close</Button>
</DialogActions>
</Dialog>
</Headline>
);
}

View File

@ -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;

View File

@ -1,80 +1,77 @@
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 => {
switch (content.content_type) { switch (content.content_type) {
case "comic": case "comic":
return ComicReader; return ComicReader;
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);
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
const callback = (entries: IntersectionObserverEntry[]) => { const callback = (entries: IntersectionObserverEntry[]) => {
const [entry] = entries; const [entry] = entries;
setIsVisible(entry.isIntersecting); setIsVisible(entry.isIntersecting);
}; };
useEffect(() => { useEffect(() => {
const observer = new IntersectionObserver(callback, options); const observer = new IntersectionObserver(callback, options);
elementRef.current && observer.observe(elementRef.current); elementRef.current && observer.observe(elementRef.current);
return () => observer.disconnect(); return () => observer.disconnect();
}, [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);
useEffect(()=>{ useEffect(() => {
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>)
}

View File

@ -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>
);
};

View File

@ -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>
}; );
};

View File

@ -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 {LoadingCircle} from "../component/loading"; import React, { useEffect, useState } from "react";
import { Headline, CommonMenuList } from '../component/mod'; import { LoadingCircle } from "../component/loading";
import {DataGrid, GridColDef} from "@mui/x-data-grid" import { CommonMenuList, Headline } from "../component/mod";
type TagCount = { type TagCount = {
tag_name: string; tag_name: string;
@ -10,52 +10,52 @@ type TagCount = {
}; };
const tagTableColumn: GridColDef[] = [ const tagTableColumn: GridColDef[] = [
{ {
field:"tag_name", field: "tag_name",
headerName:"Tag Name", headerName: "Tag Name",
width: 200, width: 200,
}, },
{ {
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>();
const [error, setErrorMsg] = useState<string|undefined>(undefined); const [error, setErrorMsg] = useState<string | undefined>(undefined);
const isLoading = data === undefined; const isLoading = data === undefined;
useEffect(()=>{ useEffect(() => {
loadData(); loadData();
},[]); }, []);
if(isLoading){ if (isLoading) {
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 (
<Paper sx={{height:"100%"}} elevation={2}> <Box sx={{ height: "400px", width: "100%" }}>
<DataGrid rows={data} columns={tagTableColumn} getRowId={(t)=>t.tag_name} ></DataGrid> <Paper sx={{ height: "100%" }} elevation={2}>
</Paper> <DataGrid rows={data} columns={tagTableColumn} getRowId={(t) => t.tag_name}></DataGrid>
</Paper>
</Box> </Box>
);
async function loadData(){
try{ async function loadData() {
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("");
} }
@ -63,9 +63,11 @@ function TagTable(){
} }
} }
export const TagsPage = ()=>{ export const TagsPage = () => {
const menu = CommonMenuList(); const menu = CommonMenuList();
return <Headline menu={menu}> return (
<TagTable></TagTable> <Headline menu={menu}>
</Headline> <TagTable></TagTable>
}; </Headline>
);
};

File diff suppressed because it is too large Load Diff

View File

@ -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;
@ -20,76 +20,75 @@ export const getInitialValue = async () => {
const storagestr = window.localStorage.getItem("UserLoginContext") as string | null; const storagestr = window.localStorage.getItem("UserLoginContext") as string | null;
const storage = storagestr !== null ? JSON.parse(storagestr) as LoginLocalStorage | null : null; const storage = storagestr !== null ? JSON.parse(storagestr) as LoginLocalStorage | null : null;
localObj = storage; localObj = storage;
} }
if (localObj !== null && localObj.accessExpired > Math.floor(Date.now() / 1000)) { if (localObj !== null && localObj.accessExpired > Math.floor(Date.now() / 1000)) {
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) {
return b.detail as string; return b.detail as string;
} }
localObj = b; localObj = b;
window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj)); window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj));
return b; return b;
} };

View File

@ -1,22 +1,22 @@
import {Knex as k} from "knex"; 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: {
}, client: "sqlite3",
production: { connection: {
client: 'sqlite3', filename: "./db.sqlite3",
connection: { },
filename: './db.sqlite3',
}, },
} };
};
} }

View File

@ -1,65 +1,66 @@
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;
additional: ContentConstructOption| undefined; additional: ContentConstructOption | undefined;
constructor(path:string,option?:ContentConstructOption){ constructor(path: string, option?: ContentConstructOption) {
super(path); super(path);
this.additional = option; this.additional = option;
this.pagenum = 0; this.pagenum = 0;
} }
async initDesc():Promise<void>{ async initDesc(): Promise<void> {
if(this.desc !== undefined) return; if (this.desc !== undefined) return;
const zip = await readZip(this.path); const zip = await readZip(this.path);
const entries = await zip.entries(); const entries = await zip.entries();
this.pagenum = Object.keys(entries).filter(x=>ImageExt.includes(extname(x))).length; this.pagenum = Object.keys(entries).filter(x => ImageExt.includes(extname(x))).length;
const entry = entries["desc.json"]; const entry = entries["desc.json"];
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();
const basebody = await super.createDocumentBody(); const basebody = await super.createDocumentBody();
this.desc?.title; this.desc?.title;
if(this.desc === undefined){ if (this.desc === undefined) {
return basebody; return basebody;
} }
let tags:string[] = this.desc.tags ?? []; let tags: string[] = this.desc.tags ?? [];
tags = tags.concat(this.desc.artist?.map(x=>`artist:${x}`) ?? []); tags = tags.concat(this.desc.artist?.map(x => `artist:${x}`) ?? []);
tags = tags.concat(this.desc.character?.map(x=>`character:${x}`) ?? []); tags = tags.concat(this.desc.character?.map(x => `character:${x}`) ?? []);
tags = tags.concat(this.desc.group?.map(x=>`group:${x}`) ?? []); tags = tags.concat(this.desc.group?.map(x => `group:${x}`) ?? []);
tags = tags.concat(this.desc.series?.map(x=>`series:${x}`) ?? []); tags = tags.concat(this.desc.series?.map(x => `series:${x}`) ?? []);
const type = this.desc.type instanceof Array ? this.desc.type[0]: this.desc.type; const type = this.desc.type instanceof Array ? this.desc.type[0] : this.desc.type;
tags.push(`type:${type}`); tags.push(`type:${type}`);
return { return {
...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);

View File

@ -1,42 +1,44 @@
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
*/ */
export interface ContentFile{ export interface ContentFile {
getHash():Promise<string>; getHash(): Promise<string>;
createDocumentBody():Promise<DocumentBody>; createDocumentBody(): Promise<DocumentBody>;
readonly path: string; readonly path: string;
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) & {
export const createDefaultClass = (type:string):ContentFileConstructor=>{ content_type: string;
let cons = class implements ContentFile{ };
export const createDefaultClass = (type: string): ContentFileConstructor => {
let cons = class implements ContentFile {
readonly path: string; readonly path: string;
//type = type; // type = type;
static content_type = type; static content_type = type;
protected hash: string| undefined; protected hash: string | undefined;
protected stat: Stats| undefined; protected stat: Stats | undefined;
constructor(path:string,option?:ContentConstructOption){ constructor(path: string, option?: ContentConstructOption) {
this.path = path; this.path = path;
this.hash = option?.hash; this.hash = option?.hash;
this.stat = undefined; this.stat = undefined;
} }
async createDocumentBody(): Promise<DocumentBody> { async createDocumentBody(): Promise<DocumentBody> {
const {base,dir, name} = path.parse(this.path); const { base, dir, name } = path.parse(this.path);
const ret = { const ret = {
title : name, title: name,
basepath : dir, basepath: dir,
additional: {}, additional: {},
content_type: cons.content_type, content_type: cons.content_type,
filename: base, filename: base,
@ -46,43 +48,43 @@ export const createDefaultClass = (type:string):ContentFileConstructor=>{
} as DocumentBody; } as DocumentBody;
return ret; return ret;
} }
get type():string{ get type(): string {
return cons.content_type; return cons.content_type;
} }
async getHash():Promise<string>{ async getHash(): Promise<string> {
if(this.hash !== undefined) return this.hash; if (this.hash !== undefined) return this.hash;
this.stat = await promises.stat(this.path); this.stat = await promises.stat(this.path);
const hash = createHash("sha512"); const hash = createHash("sha512");
hash.update(extname(this.path)); hash.update(extname(this.path));
hash.update(this.stat.mode.toString()); hash.update(this.stat.mode.toString());
//if(this.desc !== undefined) // if(this.desc !== undefined)
// hash.update(JSON.stringify(this.desc)); // hash.update(JSON.stringify(this.desc));
hash.update(this.stat.size.toString()); hash.update(this.stat.size.toString());
this.hash = hash.digest("base64"); this.hash = hash.digest("base64");
return this.hash; return this.hash;
} }
async getMtime():Promise<number>{ async getMtime(): Promise<number> {
if(this.stat !== undefined) return this.stat.mtimeMs; if (this.stat !== undefined) return this.stat.mtimeMs;
await this.getHash(); await this.getHash();
return this.stat!.mtimeMs; return this.stat!.mtimeMs;
} }
}; };
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) {
const constructorMethod = ContstructorTable[type]; const constructorMethod = ContstructorTable[type];
if(constructorMethod === undefined){ if (constructorMethod === undefined) {
console.log(`${type} are not in ${JSON.stringify(ContstructorTable)}`); console.log(`${type} are not in ${JSON.stringify(ContstructorTable)}`);
throw new Error("construction method of the content type is undefined"); throw new Error("construction method of the content type is undefined");
} }
return new constructorMethod(path,option); return new constructorMethod(path, option);
} }
export function getContentFileConstructor(type:string): ContentFileConstructor|undefined{ export function getContentFileConstructor(type: string): ContentFileConstructor | undefined {
const ret = ContstructorTable[type]; const ret = ContstructorTable[type];
return ret; return ret;
} }

View File

@ -1,3 +1,3 @@
import './comic'; import "./comic";
import './video'; import "./video";
export {ContentFile, createContentFile} from './file'; export { ContentFile, createContentFile } from "./file";

View File

@ -1,9 +1,9 @@
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) {
super(path,desc); super(path, desc);
} }
} }
registerContentReferrer(VideoReferrer); registerContentReferrer(VideoReferrer);

View File

@ -1,49 +1,47 @@
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;
const config = KnexConfig.config[env]; const config = KnexConfig.config[env];
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");
} }
if(typeof connection === "function"){ if (typeof connection === "function") {
throw new Error("connection provider not supported..."); throw new Error("connection provider not supported...");
} }
if(!("filename" in connection) ){ if (!("filename" in connection)) {
throw new Error("sqlite3 config need"); throw new Error("sqlite3 config need");
} }
const init_need = !existsSync(connection.filename); const init_need = !existsSync(connection.filename);
const knex = Knex(config); const knex = Knex(config);
let tries = 0; let tries = 0;
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;
} }
} }
break; break;
} }
if(init_need){ if (init_need) {
console.log("first execute: initialize database..."); console.log("first execute: initialize database...");
const migrate = await import("../migrations/initial"); const migrate = await import("../migrations/initial");
await migrate.up(knex); await migrate.up(knex);
} }
return knex; return knex;
} }

View File

@ -1,118 +1,118 @@
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;
tagController: TagAccessor; tagController: TagAccessor;
constructor(knex : Knex){ constructor(knex: Knex) {
this.knex = knex; this.knex = knex;
this.tagController = createKnexTagController(knex); this.tagController = createKnexTagController(knex);
} }
async search(search_word: string): Promise<Document[]>{ async search(search_word: string): Promise<Document[]> {
throw new Error("Method not implemented."); throw new Error("Method not implemented.");
const sw = `%${search_word}%`; const sw = `%${search_word}%`;
const docs = await this.knex.select("*").from("document") const docs = await this.knex.select("*").from("document")
.where("title","like",sw); .where("title", "like", sw);
return docs; return docs;
} }
async addList(content_list: DocumentBody[]):Promise<number[]>{ async addList(content_list: DocumentBody[]): Promise<number[]> {
return await this.knex.transaction(async (trx)=>{ return await this.knex.transaction(async (trx) => {
//add tags // add tags
const tagCollected = new Set<string>(); const tagCollected = new Set<string>();
content_list.map(x=>x.tags).forEach((x)=>{ content_list.map(x => x.tags).forEach((x) => {
x.forEach(x=>{ x.forEach(x => {
tagCollected.add(x); tagCollected.add(x);
}); });
}); });
const tagCollectPromiseList = []; const tagCollectPromiseList = [];
const tagController = createKnexTagController(trx); const tagController = createKnexTagController(trx);
for (const it of tagCollected){ for (const it of tagCollected) {
const p = tagController.addTag({name:it}); const p = tagController.addTag({ name: it });
tagCollectPromiseList.push(p); tagCollectPromiseList.push(p);
} }
await Promise.all(tagCollectPromiseList); await Promise.all(tagCollectPromiseList);
//add for each contents // add for each contents
const ret = []; const ret = [];
for (const content of content_list) { for (const content of content_list) {
const {tags,additional, ...rest} = content; const { tags, additional, ...rest } = content;
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);
} }
return ret; return ret;
}); });
} }
async add(c: DocumentBody){ async add(c: DocumentBody) {
const {tags,additional, ...rest} = c; const { tags, additional, ...rest } = c;
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 });
await this.knex.delete().from("document").where({id:id}); await this.knex.delete().from("document").where({ id: id });
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 });
ret_tags = tags.map(x=>x.tag_name); ret_tags = tags.map(x => x.tag_name);
} }
return { return {
...first, ...first,
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 })
.whereNotNull("update_at") .whereNotNull("update_at")
.from("document"); .from("document");
return s.map(x=>({ return s.map(x => ({
...x, ...x,
tags:[], tags: [],
additional:{} additional: {},
})); }));
} }
async findList(option?:QueryListOption){ async findList(option?: QueryListOption) {
option = option ?? {}; option = option ?? {};
const allow_tag = option.allow_tag ?? []; const allow_tag = option.allow_tag ?? [];
const eager_loading = option.eager_loading ?? true; const eager_loading = option.eager_loading ?? true;
@ -123,98 +123,101 @@ class KnexDocumentAccessor implements DocumentAccessor{
const content_type = option.content_type; const content_type = option.content_type;
const cursor = option.cursor; const cursor = option.cursor;
const buildquery = ()=>{ const buildquery = () => {
let query = this.knex.select("document.*"); let query = this.knex.select("document.*");
if(allow_tag.length > 0){ if (allow_tag.length > 0) {
query = query.from("doc_tag_relation as tags_0"); query = query.from("doc_tag_relation as tags_0");
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;
for(let i of result){ for (let i of result) {
i.additional = JSON.parse((i.additional as unknown) as string); i.additional = JSON.parse((i.additional as unknown) as string);
} }
if(eager_loading){ if (eager_loading) {
let idmap: {[index:number]:Document} = {}; let idmap: { [index: number]: Document } = {};
for(const r of result){ for (const r of result) {
idmap[r.id] = r; idmap[r.id] = r;
r.tags = []; r.tags = [];
} }
let subquery = buildquery(); let subquery = buildquery();
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[]>{
const e = filename == undefined ? {} : {filename:filename}
const results = await this.knex.select("*").from("document").where({basepath:path,...e});
return results.map(x=>({
...x,
tags:[],
additional:{}
}))
} }
async update(c:Partial<Document> & { id:number }){ async findByPath(path: string, filename?: string): Promise<Document[]> {
const {id,tags,...rest} = c; const e = filename == undefined ? {} : { filename: filename };
if (await this.findById(id) !== undefined){ const results = await this.knex.select("*").from("document").where({ basepath: path, ...e });
await this.knex.update(rest).where({id: id}).from("document"); return results.map(x => ({
...x,
tags: [],
additional: {},
}));
}
async update(c: Partial<Document> & { id: number }) {
const { id, tags, ...rest } = c;
if (await this.findById(id) !== undefined) {
await this.knex.update(rest).where({ id: id }).from("document");
return true; return true;
} }
return false; return false;
} }
async addTag(c: Document,tag_name:string){ async addTag(c: Document, tag_name: string) {
if (c.tags.includes(tag_name)) return false; if (c.tags.includes(tag_name)) return false;
this.tagController.addTag({name:tag_name}); this.tagController.addTag({ name: tag_name });
await this.knex.insert<DBTagContentRelation>({tag_name: tag_name, doc_id: c.id}) await this.knex.insert<DBTagContentRelation>({ tag_name: tag_name, doc_id: c.id })
.into("doc_tag_relation"); .into("doc_tag_relation");
c.tags.push(tag_name); c.tags.push(tag_name);
return true; return true;
} }
async delTag(c: Document,tag_name:string){ async delTag(c: Document, tag_name: string) {
if (c.tags.includes(tag_name)) return false; if (c.tags.includes(tag_name)) return false;
await this.knex.delete().where({tag_name: tag_name,doc_id: c.id}).from("doc_tag_relation"); await this.knex.delete().where({ tag_name: tag_name, doc_id: c.id }).from("doc_tag_relation");
c.tags.push(tag_name); c.tags.push(tag_name);
return true; return true;
} }
} }
export const createKnexDocumentAccessor = (knex:Knex): DocumentAccessor=>{ export const createKnexDocumentAccessor = (knex: Knex): DocumentAccessor => {
return new KnexDocumentAccessor(knex); return new KnexDocumentAccessor(knex);
} };

View File

@ -1,3 +1,3 @@
export * from './doc'; export * from "./doc";
export * from './tag'; export * from "./tag";
export * from './user'; export * from "./user";

View File

@ -1,57 +1,57 @@
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;
} }
async getAllTagCount(): Promise<TagCount[]> { async getAllTagCount(): Promise<TagCount[]> {
const result = await this.knex<DBTagContentRelation>("doc_tag_relation").select("tag_name") const result = await this.knex<DBTagContentRelation>("doc_tag_relation").select("tag_name")
.count("*",{as:"occurs"}).groupBy<TagCount[]>("tag_name"); .count("*", { as: "occurs" }).groupBy<TagCount[]>("tag_name");
return result; return result;
} }
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];
} }
async addTag(tag: Tag){ async addTag(tag: Tag) {
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;
} }
return false; return false;
} }
async delTag(name:string){ async delTag(name: string) {
if(await this.getTagByName(name) !== undefined){ if (await this.getTagByName(name) !== undefined) {
await this.knex.delete().where({name:name}).from("tags"); await this.knex.delete().where({ name: name }).from("tags");
return true; return true;
} }
return false; return false;
} }
async updateTag(name:string,desc:string){ async updateTag(name: string, desc: string) {
if(await this.getTagByName(name) !== undefined){ if (await this.getTagByName(name) !== undefined) {
await this.knex.update({description:desc}).where({name:name}).from("tags"); await this.knex.update({ description: desc }).where({ name: name }).from("tags");
return true; return true;
} }
return false; return false;
} }
}; }
export const createKnexTagController = (knex:Knex):TagAccessor=>{ export const createKnexTagController = (knex: Knex): TagAccessor => {
return new KnexTagAccessor(knex); return new KnexTagAccessor(knex);
} };

View File

@ -1,41 +1,41 @@
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;
readonly password: Password; readonly password: Password;
constructor(username: string, pw: Password,knex:Knex){ constructor(username: string, pw: Password, knex: Knex) {
this.username = username; this.username = username;
this.password = pw; this.password = pw;
this.knex = knex; this.knex = knex;
} }
async reset_password(password: string){ async reset_password(password: string) {
this.password.set_password(password); this.password.set_password(password);
await this.knex.from("users") await this.knex.from("users")
.where({username:this.username}) .where({ username: this.username })
.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);
} }
async add(name: string) { async add(name: string) {
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,38 +45,43 @@ 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;
} }
} }
export const createKnexUserController = (knex: Knex):UserAccessor=>{ export const createKnexUserController = (knex: Knex): UserAccessor => {
const createUserKnex = async (input:UserCreateInput)=>{ const createUserKnex = async (input: UserCreateInput) => {
if(undefined !== (await findUserKenx(input.username))){ if (undefined !== (await findUserKenx(input.username))) {
return undefined; return undefined;
} }
const user = new KnexUser(input.username,new Password(input.password),knex); const user = new KnexUser(input.username, new Password(input.password), knex);
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 }),
const delUserKnex = async (id:string) => { knex,
let r = await knex.delete().from("users").where({username:id}); );
return r===0; };
} const delUserKnex = async (id: string) => {
let r = await knex.delete().from("users").where({ username: id });
return r === 0;
};
return { return {
createUser: createUserKnex, createUser: createUserKnex,
findUser: findUserKenx, findUser: findUserKenx,
delUser: delUserKnex, delUser: delUserKnex,
}; };
} };

View File

@ -1,15 +1,15 @@
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 {
/** content file list waiting to add */ /** content file list waiting to add */
waiting_list: ContentList; waiting_list: ContentList;
/** deleted contents */ /** deleted contents */
tombstone: Map<string, Document>;//hash, contentfile tombstone: Map<string, Document>; // hash, contentfile
doc_cntr: DocumentAccessor; doc_cntr: DocumentAccessor;
/** content type of handle */ /** content type of handle */
content_type: string; content_type: string;
@ -26,21 +26,21 @@ 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);
const filename = basename(cpath); const filename = basename(cpath);
console.log("deleted ", cpath); console.log("deleted ", cpath);
//if it wait to add, delete it from waiting list. // if it wait to add, delete it from waiting list.
if (this.waiting_list.hasByPath(cpath)) { if (this.waiting_list.hasByPath(cpath)) {
this.waiting_list.deleteByPath(cpath); this.waiting_list.deleteByPath(cpath);
return; return;
} }
const dbc = await this.doc_cntr.findByPath(basepath, filename); const dbc = await this.doc_cntr.findByPath(basepath, filename);
//when there is no related content in db, ignore. // when there is no related content in db, ignore.
if (dbc.length === 0) { if (dbc.length === 0) {
console.log("its not in waiting_list and db!!!: ", cpath); console.log("its not in waiting_list and db!!!: ", cpath);
return; return;
@ -48,10 +48,10 @@ export class ContentDiffHandler {
const content_hash = dbc[0].content_hash; const content_hash = dbc[0].content_hash;
// When a path is changed, it takes into account when the // When a path is changed, it takes into account when the
// creation event occurs first and the deletion occurs, not // creation event occurs first and the deletion occurs, not
// the change event. // the change event.
const cf = this.waiting_list.getByHash(content_hash); const cf = this.waiting_list.getByHash(content_hash);
if (cf) { if (cf) {
//if a path is changed, update the changed path. // if a path is changed, update the changed path.
console.log("update path from", cpath, "to", cf.path); console.log("update path from", cpath, "to", cf.path);
const newFilename = basename(cf.path); const newFilename = basename(cf.path);
const newBasepath = dirname(cf.path); const newBasepath = dirname(cf.path);
@ -64,7 +64,7 @@ export class ContentDiffHandler {
}); });
return; return;
} }
//invalidate db and add it to tombstone. // invalidate db and add it to tombstone.
await this.doc_cntr.update({ await this.doc_cntr.update({
id: dbc[0].id, id: dbc[0].id,
deleted_at: Date.now(), deleted_at: Date.now(),
@ -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);
@ -99,7 +98,7 @@ export class ContentDiffHandler {
const cur_filename = basename(cur_path); const cur_filename = basename(cur_path);
console.log("modify", cur_path, "from", prev_path); console.log("modify", cur_path, "from", prev_path);
const c = this.waiting_list.getByPath(prev_path); const c = this.waiting_list.getByPath(prev_path);
if(c !== undefined){ if (c !== undefined) {
await this.waiting_list.delete(c); await this.waiting_list.delete(c);
const content = createContentFile(this.content_type, cur_path); const content = createContentFile(this.content_type, cur_path);
await this.waiting_list.set(content); await this.waiting_list.set(content);
@ -107,7 +106,7 @@ export class ContentDiffHandler {
} }
const doc = await this.doc_cntr.findByPath(prev_basepath, prev_filename); const doc = await this.doc_cntr.findByPath(prev_basepath, prev_filename);
if(doc.length === 0){ if (doc.length === 0) {
await this.OnCreated(cur_path); await this.OnCreated(cur_path);
return; return;
} }
@ -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,
}); });
} }
} }

View File

@ -1,59 +1,59 @@
import { ContentFile } from '../content/mod'; import { ContentFile } from "../content/mod";
export class ContentList{ export class ContentList {
/** path map */ /** path map */
private cl:Map<string,ContentFile>; private cl: Map<string, ContentFile>;
/** hash map */ /** hash map */
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);
} }
hasByPath(p:string){ hasByPath(p: string) {
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);
} }
async set(c:ContentFile){ async set(c: ContentFile) {
const path = c.path; const path = c.path;
const hash = await c.getHash(); const hash = await c.getHash();
this.cl.set(path,c); this.cl.set(path, c);
this.hl.set(hash,c); this.hl.set(hash, c);
} }
/** delete content file */ /** delete content file */
async delete(c:ContentFile){ async delete(c: ContentFile) {
const hash = await c.getHash(); const hash = await c.getHash();
let r = true; let r = true;
r = this.cl.delete(c.path) && r; r = this.cl.delete(c.path) && r;
r = this.hl.delete(hash) && r; r = this.hl.delete(hash) && r;
return r; return r;
} }
async deleteByPath(p:string){ async deleteByPath(p: string) {
const o = this.getByPath(p); const o = this.getByPath(p);
if(o === undefined) return false; if (o === undefined) return false;
return await this.delete(o); return await this.delete(o);
} }
deleteByHash(s:string){ deleteByHash(s: string) {
const o = this.getByHash(s); const o = this.getByHash(s);
if(o === undefined) return false; if (o === undefined) return false;
let r = true; let r = true;
r = this.cl.delete(o.path) && r; r = this.cl.delete(o.path) && r;
r = this.hl.delete(s) && r; r = this.hl.delete(s) && r;
return r; return r;
} }
clear(){ clear() {
this.cl.clear(); this.cl.clear();
this.hl.clear(); this.hl.clear();
} }
getAll(){ getAll() {
return [...this.cl.values()]; return [...this.cl.values()];
} }
} }

View File

@ -1,26 +1,26 @@
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 };
doc_cntr: DocumentAccessor; doc_cntr: DocumentAccessor;
constructor(contorller: DocumentAccessor){ constructor(contorller: DocumentAccessor) {
this.watching = {}; this.watching = {};
this.doc_cntr = contorller; this.doc_cntr = contorller;
} }
async register(content_type:string,watcher:IDiffWatcher){ async register(content_type: string, watcher: IDiffWatcher) {
if(this.watching[content_type] === undefined){ if (this.watching[content_type] === undefined) {
this.watching[content_type] = new ContentDiffHandler(this.doc_cntr,content_type); this.watching[content_type] = new ContentDiffHandler(this.doc_cntr, content_type);
} }
this.watching[content_type].register(watcher); this.watching[content_type].register(watcher);
await watcher.setup(this.doc_cntr); await watcher.setup(this.doc_cntr);
} }
async commit(type:string,path:string){ async commit(type: string, path: string) {
const list = this.watching[type].waiting_list; const list = this.watching[type].waiting_list;
const c = list.getByPath(path); const c = list.getByPath(path);
if(c===undefined){ if (c === undefined) {
throw new Error("path is not exist"); throw new Error("path is not exist");
} }
await list.delete(c); await list.delete(c);
@ -28,18 +28,18 @@ export class DiffManager{
const id = await this.doc_cntr.add(body); const id = await this.doc_cntr.add(body);
return id; return id;
} }
async commitAll(type:string){ async commitAll(type: string) {
const list = this.watching[type].waiting_list; const list = this.watching[type].waiting_list;
const contentFiles = list.getAll(); const contentFiles = list.getAll();
list.clear(); list.clear();
const bodies = await asyncPool(30,contentFiles,async (x)=>await x.createDocumentBody()); const bodies = await asyncPool(30, contentFiles, async (x) => await x.createDocumentBody());
const ids = await this.doc_cntr.addList(bodies); const ids = await this.doc_cntr.addList(bodies);
return ids; return ids;
} }
getAdded(){ getAdded() {
return Object.keys(this.watching).map(x=>({ return Object.keys(this.watching).map(x => ({
type:x, type: x,
value:this.watching[x].waiting_list.getAll(), value: this.watching[x].waiting_list.getAll(),
})); }));
} }
}; }

View File

@ -1,2 +1,2 @@
export * from './router'; export * from "./diff";
export * from './diff'; export * from "./router";

View File

@ -1,70 +1,70 @@
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 };
} }
export const getAdded = (diffmgr:DiffManager)=> (ctx:Koa.Context,next:Koa.Next)=>{ export const getAdded = (diffmgr: DiffManager) => (ctx: Koa.Context, next: Koa.Next) => {
const ret = diffmgr.getAdded(); const ret = diffmgr.getAdded();
ctx.body = ret.map(x=>({ ctx.body = ret.map(x => ({
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;
} }
export const postAdded = (diffmgr:DiffManager) => async (ctx:Router.IRouterContext,next:Koa.Next)=>{ export const postAdded = (diffmgr: DiffManager) => async (ctx: Router.IRouterContext, next: Koa.Next) => {
const reqbody = ctx.request.body; const reqbody = ctx.request.body;
if(!checkPostAddedBody(reqbody)){ if (!checkPostAddedBody(reqbody)) {
sendError(400,"format exception"); sendError(400, "format exception");
return; return;
} }
const allWork = reqbody.map(op=>diffmgr.commit(op.type,op.path)); const allWork = reqbody.map(op => diffmgr.commit(op.type, op.path));
const results = await Promise.all(allWork); const results = await Promise.all(allWork);
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;
} }
const reqbody = ctx.request.body as Record<string,unknown>; const reqbody = ctx.request.body as Record<string, unknown>;
if(!("type" in reqbody)){ if (!("type" in reqbody)) {
sendError(400,"format exception: there is no \"type\""); sendError(400, "format exception: there is no \"type\"");
return; return;
} }
const t = reqbody["type"]; const t = reqbody["type"];
if(typeof t !== "string"){ if (typeof t !== "string") {
sendError(400,"format exception: invalid type of \"type\""); sendError(400, "format exception: invalid type of \"type\"");
return; return;
} }
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 = {
@ -74,10 +74,10 @@ export const getNotWatched = (diffmgr : DiffManager)=> (ctx:Router.IRouterContex
ctx.type = 'json'; ctx.type = 'json';
}*/ }*/
export function createDiffRouter(diffmgr: DiffManager){ export function createDiffRouter(diffmgr: DiffManager) {
const ret = new Router(); const ret = new Router();
ret.get("/list",AdminOnlyMiddleware,getAdded(diffmgr)); ret.get("/list", AdminOnlyMiddleware, getAdded(diffmgr));
ret.post("/commit",AdminOnlyMiddleware,postAdded(diffmgr)); ret.post("/commit", AdminOnlyMiddleware, postAdded(diffmgr));
ret.post("/commitall",AdminOnlyMiddleware,postAddedAll(diffmgr)); ret.post("/commitall", AdminOnlyMiddleware, postAddedAll(diffmgr));
return ret; return ret;
} }

View File

@ -1,25 +1,25 @@
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 {
on<U extends keyof DiffWatcherEvent>(event:U,listener:DiffWatcherEvent[U]): this; on<U extends keyof DiffWatcherEvent>(event: U, listener: DiffWatcherEvent[U]): this;
emit<U extends keyof DiffWatcherEvent>(event:U,...arg:Parameters<DiffWatcherEvent[U]>): boolean; emit<U extends keyof DiffWatcherEvent>(event: U, ...arg: Parameters<DiffWatcherEvent[U]>): boolean;
setup(cntr:DocumentAccessor):Promise<void>; setup(cntr: DocumentAccessor): Promise<void>;
} }
export function linkWatcher(fromWatcher :IDiffWatcher, toWatcher: IDiffWatcher){ export function linkWatcher(fromWatcher: IDiffWatcher, toWatcher: IDiffWatcher) {
fromWatcher.on("create",p=>toWatcher.emit("create",p)); fromWatcher.on("create", p => toWatcher.emit("create", p));
fromWatcher.on("delete",p=>toWatcher.emit("delete",p)); fromWatcher.on("delete", p => toWatcher.emit("delete", p));
fromWatcher.on("change",(p,c)=>toWatcher.emit("change",p,c)); fromWatcher.on("change", (p, c) => toWatcher.emit("change", p, c));
} }

View File

@ -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
}
}
}

View File

@ -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);

View File

@ -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)));
} };

View File

@ -1,45 +1,44 @@
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;
export class CommonDiffWatcher extends event.EventEmitter implements IDiffWatcher{ export class CommonDiffWatcher extends event.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 {
return super.on(event,listener); return super.on(event, listener);
} }
emit<U extends keyof DiffWatcherEvent>(event:U,...arg:Parameters<DiffWatcherEvent[U]>): boolean{ emit<U extends keyof DiffWatcherEvent>(event: U, ...arg: Parameters<DiffWatcherEvent[U]>): boolean {
return super.emit(event,...arg); return super.emit(event, ...arg);
} }
private _path:string; private _path: string;
private _watcher: FSWatcher; private _watcher: FSWatcher;
constructor(path:string){ constructor(path: string) {
super(); super();
this._path = path; this._path = path;
this._watcher = watch(this._path,{persistent: true, recursive:false},async (eventType,filename)=>{ this._watcher = watch(this._path, { persistent: true, recursive: false }, async (eventType, filename) => {
if(eventType === "rename"){ if (eventType === "rename") {
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))
} }
} }
}); });
} }
async setup(cntr: DocumentAccessor): Promise<void> { async setup(cntr: DocumentAccessor): Promise<void> {
await setupHelp(this,this.path,cntr); await setupHelp(this, this.path, cntr);
} }
public get path(){ public get path() {
return this._path; return this._path;
} }
watchClose(){ watchClose() {
this._watcher.close() this._watcher.close();
} }
} }

View File

@ -2,23 +2,22 @@ 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{ return super.on(event, listener);
return super.on(event,listener);
} }
emit<U extends keyof DiffWatcherEvent>(event:U,...arg:Parameters<DiffWatcherEvent[U]>): boolean{ emit<U extends keyof DiffWatcherEvent>(event: U, ...arg: Parameters<DiffWatcherEvent[U]>): boolean {
return super.emit(event,...arg); return super.emit(event, ...arg);
} }
constructor(refWatchers:IDiffWatcher[]){ constructor(refWatchers: IDiffWatcher[]) {
super(); super();
this.refWatchers = refWatchers; this.refWatchers = refWatchers;
for(const refWatcher of this.refWatchers){ for (const refWatcher of this.refWatchers) {
linkWatcher(refWatcher,this); linkWatcher(refWatcher, this);
} }
} }
async setup(cntr: DocumentAccessor): Promise<void> { async setup(cntr: DocumentAccessor): Promise<void> {
await Promise.all(this.refWatchers.map(x=>x.setup(cntr))); await Promise.all(this.refWatchers.map(x => x.setup(cntr)));
} }
} }

View File

@ -1,61 +1,61 @@
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 {
return super.on(event,listener); return super.on(event, listener);
} }
emit<U extends keyof DiffWatcherEvent>(event:U,...arg:Parameters<DiffWatcherEvent[U]>): boolean{ emit<U extends keyof DiffWatcherEvent>(event: U, ...arg: Parameters<DiffWatcherEvent[U]>): boolean {
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,
watchFile:true, watchFile: true,
}){ }) {
super(); super();
this.path = path; this.path = path;
this.watcher = watch(path,{ this.watcher = watch(path, {
persistent:true, persistent: true,
ignoreInitial:true, ignoreInitial: true,
depth:100, depth: 100,
}); });
option.watchFile ??= true; option.watchFile ??= true;
if(option.watchFile){ if (option.watchFile) {
this.watcher.on("add",path=>{ this.watcher.on("add", path => {
const cpath = path; const cpath = path;
//console.log("add ", cpath); // console.log("add ", cpath);
this.emit("create",cpath); this.emit("create", cpath);
}).on("unlink",path=>{ }).on("unlink", path => {
const cpath = path; const cpath = path;
//console.log("unlink ", cpath); // console.log("unlink ", cpath);
this.emit("delete",cpath); this.emit("delete", cpath);
}); });
} }
if(option.watchDir){ if (option.watchDir) {
this.watcher.on("addDir",path=>{ this.watcher.on("addDir", path => {
const cpath = path; const cpath = path;
this.emit("create",cpath); this.emit("create", cpath);
}).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> {
await setupRecursive(this,this.path,cntr); await setupRecursive(this, this.path, cntr);
} }
} }

View File

@ -1,35 +1,36 @@
import { EventEmitter } from "events"; import { EventEmitter } from "events";
import { promises } from "fs"; import { promises } from "fs";
import { join } from "path"; import { join } from "path";
const {readdir} = promises; 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) {
const initial_document = await cntr.findByPath(basepath); const initial_document = await cntr.findByPath(basepath);
const initial_filenames = initial_document.map(x=>x.filename); const initial_filenames = initial_document.map(x => x.filename);
const cur = await readdir(basepath); const cur = await readdir(basepath);
setupCommon(watcher,basepath,initial_filenames,cur); setupCommon(watcher, basepath, initial_filenames, cur);
} }
export async function setupRecursive(watcher:IDiffWatcher,basepath:string,cntr:DocumentAccessor){ export async function setupRecursive(watcher: IDiffWatcher, basepath: string, cntr: DocumentAccessor) {
const initial_document = await cntr.findByPath(basepath); const initial_document = await cntr.findByPath(basepath);
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)),
]);
} }

View File

@ -2,50 +2,45 @@ 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);
} }
/** /**
* emit event * emit event
* @param event * @param event
* @param arg * @param arg
* @returns `true` if the event had listeners, `false` otherwise. * @returns `true` if the event had listeners, `false` otherwise.
*/ */
emit<U extends keyof DiffWatcherEvent>(event:U,...arg:Parameters<DiffWatcherEvent[U]>): boolean{ emit<U extends keyof DiffWatcherEvent>(event: U, ...arg: Parameters<DiffWatcherEvent[U]>): boolean {
if(event === "change"){ if (event === "change") {
const prev = arg[0]; const prev = arg[0];
const cur = arg[1] as string; const cur = arg[1] as string;
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 {
return super.emit("delete", cur);
} }
else{ } else {
return super.emit("delete",cur); if (this.filter(cur)) {
} return super.emit("create", cur);
}
else{
if(this.filter(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();
this.refWatcher = refWatcher; this.refWatcher = refWatcher;
this.filter = filter; this.filter = filter;
linkWatcher(refWatcher,this); linkWatcher(refWatcher, this);
} }
setup(cntr:DocumentAccessor): Promise<void> { setup(cntr: DocumentAccessor): Promise<void> {
return this.refWatcher.setup(cntr); return this.refWatcher.setup(cntr);
} }
} }

View File

@ -1,289 +1,285 @@
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;
permission: string[]; permission: string[];
}; };
export type UserState = { export type UserState = {
user: PayloadInfo; user: PayloadInfo;
}; };
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";
export const refreshTokenName = "refresh_token"; export const refreshTokenName = "refresh_token";
const accessExpiredTime = 60 * 60; //1 hour const accessExpiredTime = 60 * 60; // 1 hour
const refreshExpiredTime = 60 * 60 * 24 * 14; // 14 day; const refreshExpiredTime = 60 * 60 * 24 * 14; // 14 day;
export const getAdminAccessTokenValue = () => { export const getAdminAccessTokenValue = () => {
const { jwt_secretkey } = get_setting(); const { jwt_secretkey } = get_setting();
return publishAccessToken(jwt_secretkey, "admin", [], accessExpiredTime); return publishAccessToken(jwt_secretkey, "admin", [], accessExpiredTime);
}; };
export const getAdminRefreshTokenValue = () => { export const getAdminRefreshTokenValue = () => {
const { jwt_secretkey } = get_setting(); const { jwt_secretkey } = get_setting();
return publishRefreshToken(jwt_secretkey, "admin", refreshExpiredTime); return publishRefreshToken(jwt_secretkey, "admin", refreshExpiredTime);
}; };
const publishAccessToken = ( const publishAccessToken = (
secretKey: string, secretKey: string,
username: string, username: string,
permission: string[], permission: string[],
expiredtime: number, expiredtime: number,
) => { ) => {
const payload = sign( const payload = sign(
{ {
username: username, username: username,
permission: permission, permission: permission,
}, },
secretKey, secretKey,
{ expiresIn: expiredtime }, { expiresIn: expiredtime },
); );
return payload; return payload;
}; };
const publishRefreshToken = ( const publishRefreshToken = (
secretKey: string, secretKey: string,
username: string, username: string,
expiredtime: number, expiredtime: number,
) => { ) => {
const payload = sign( const payload = sign(
{ username: username }, { username: username },
secretKey, secretKey,
{ expiresIn: expiredtime }, { expiresIn: expiredtime },
); );
return payload; return payload;
}; };
function setToken( function setToken(
ctx: Koa.Context, ctx: Koa.Context,
token_name: string, token_name: string,
token_payload: string | null, token_payload: string | null,
expiredtime: number, expiredtime: number,
) { ) {
const setting = get_setting(); const setting = get_setting();
if (token_payload === null && !!!ctx.cookies.get(token_name)) { if (token_payload === null && !!!ctx.cookies.get(token_name)) {
return; return;
} }
ctx.cookies.set(token_name, token_payload, { ctx.cookies.set(token_name, token_payload, {
httpOnly: true, httpOnly: true,
secure: setting.secure, secure: setting.secure,
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;
//check format // check format
if (typeof body == "string" || !("username" in body) || !("password" in body)) { if (typeof body == "string" || !("username" in body) || !("password" in body)) {
return sendError( return sendError(
400, 400,
"invalid form : username or password is not found in query.", "invalid form : username or password is not found in query.",
); );
} }
const username = body["username"]; const username = body["username"];
const password = body["password"]; const password = body["password"];
//check type // check type
if (typeof username !== "string" || typeof password !== "string") { if (typeof username !== "string" || typeof password !== "string") {
return sendError( return sendError(
400, 400,
"invalid form : username or password is not string", "invalid form : username or password is not string",
); );
} }
//if admin login is forbidden? // if admin login is forbidden?
if (username === "admin" && setting.forbid_remote_admin_login) { if (username === "admin" && setting.forbid_remote_admin_login) {
return sendError(403, "forbidden remote admin login"); return sendError(403, "forbidden remote admin login");
} }
const user = await userController.findUser(username); const user = await userController.findUser(username);
//username not exist // username not exist
if (user === undefined) return sendError(401, "not authorized"); if (user === undefined) return sendError(401, "not authorized");
//password not matched // password not matched
if (!user.password.check_password(password)) { if (!user.password.check_password(password)) {
return sendError(401, "not authorized"); return sendError(401, "not authorized");
} }
//create token // create token
const userPermission = await user.get_permissions(); const userPermission = await user.get_permissions();
const payload = publishAccessToken( const payload = publishAccessToken(
secretKey, secretKey,
user.username, user.username,
userPermission, userPermission,
accessExpiredTime, accessExpiredTime,
); );
const payload2 = publishRefreshToken( const payload2 = publishRefreshToken(
secretKey, secretKey,
user.username, user.username,
refreshExpiredTime, refreshExpiredTime,
); );
setToken(ctx, accessTokenName, payload, accessExpiredTime); setToken(ctx, accessTokenName, payload, accessExpiredTime);
setToken(ctx, refreshTokenName, payload2, refreshExpiredTime); setToken(ctx, refreshTokenName, payload2, refreshExpiredTime);
ctx.body = { ctx.body = {
username: user.username, username: user.username,
permission: userPermission, permission: userPermission,
accessExpired: (Math.floor(Date.now() / 1000) + accessExpiredTime), accessExpired: (Math.floor(Date.now() / 1000) + accessExpiredTime),
}; };
console.log(`${username} logined`); console.log(`${username} logined`);
return; return;
}; };
export const LogoutMiddleware = (ctx: Koa.Context, next: Koa.Next) => { export const LogoutMiddleware = (ctx: Koa.Context, next: Koa.Next) => {
const setting = get_setting()
ctx.cookies.set(accessTokenName, null);
ctx.cookies.set(refreshTokenName, null);
ctx.body = {
ok: true,
username: "",
permission: setting.guest
};
return;
};
export const createUserMiddleWare = (userController: UserAccessor) =>
async (ctx: Koa.ParameterizedContext<UserState>, next: Koa.Next) => {
const refreshToken = refreshTokenHandler(userController);
const setting = get_setting(); const setting = get_setting();
const setGuest = async () => { ctx.cookies.set(accessTokenName, null);
setToken(ctx, accessTokenName, null, 0); ctx.cookies.set(refreshTokenName, null);
setToken(ctx, refreshTokenName, null, 0); ctx.body = {
ctx.state["user"] = { username: "", permission: setting.guest }; ok: true,
return await next(); username: "",
permission: setting.guest,
}; };
return await refreshToken(ctx, setGuest, next); return;
}; };
const refreshTokenHandler = (cntr: UserAccessor) => export const createUserMiddleWare =
async (ctx: Koa.Context, fail: Koa.Next, next: Koa.Next) => { (userController: UserAccessor) => async (ctx: Koa.ParameterizedContext<UserState>, next: Koa.Next) => {
const refreshToken = refreshTokenHandler(userController);
const setting = get_setting();
const setGuest = async () => {
setToken(ctx, accessTokenName, null, 0);
setToken(ctx, refreshTokenName, null, 0);
ctx.state["user"] = { username: "", permission: setting.guest };
return await next();
};
return await refreshToken(ctx, setGuest, next);
};
const refreshTokenHandler = (cntr: UserAccessor) => 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;
if (accessPayload == undefined) { if (accessPayload == undefined) {
return await checkRefreshAndUpdate(); return await checkRefreshAndUpdate();
} }
try { try {
const o = verify(accessPayload, secretKey); const o = verify(accessPayload, secretKey);
if (isUserState(o)) { if (isUserState(o)) {
ctx.state.user = o; ctx.state.user = o;
return await next(); return await next();
} else { } else {
console.error("invalid token detected");
throw new Error("token form invalid");
}
} catch (e) {
if (e instanceof TokenExpiredError) {
return await checkRefreshAndUpdate();
} else throw e;
}
async function checkRefreshAndUpdate() {
const refreshPayload = ctx.cookies.get(refreshTokenName);
if (refreshPayload === undefined) {
return await fail(); // refresh token doesn't exist
} else {
try {
const o = verify(refreshPayload, secretKey);
if (isRefreshToken(o)) {
const user = await cntr.findUser(o.username);
if (user === undefined) return await fail(); //already non-existence user
const perm = await user.get_permissions();
const payload = publishAccessToken(
secretKey,
user.username,
perm,
accessExpiredTime,
);
setToken(ctx, accessTokenName, payload, accessExpiredTime);
ctx.state.user = { username: o.username, permission: perm };
} else {
console.error("invalid token detected"); console.error("invalid token detected");
throw new Error("token form invalid"); throw new Error("token form invalid");
}
} catch (e) {
if (e instanceof TokenExpiredError) { // refresh token is expired.
return await fail();
} else throw e;
} }
} } catch (e) {
return await next(); if (e instanceof TokenExpiredError) {
}; return await checkRefreshAndUpdate();
}; } else throw e;
export const createRefreshTokenMiddleware = (cntr: UserAccessor) => }
async (ctx: Koa.Context, next: Koa.Next) => { async function checkRefreshAndUpdate() {
const refreshPayload = ctx.cookies.get(refreshTokenName);
if (refreshPayload === undefined) {
return await fail(); // refresh token doesn't exist
} else {
try {
const o = verify(refreshPayload, secretKey);
if (isRefreshToken(o)) {
const user = await cntr.findUser(o.username);
if (user === undefined) return await fail(); // already non-existence user
const perm = await user.get_permissions();
const payload = publishAccessToken(
secretKey,
user.username,
perm,
accessExpiredTime,
);
setToken(ctx, accessTokenName, payload, accessExpiredTime);
ctx.state.user = { username: o.username, permission: perm };
} else {
console.error("invalid token detected");
throw new Error("token form invalid");
}
} catch (e) {
if (e instanceof TokenExpiredError) { // refresh token is expired.
return await fail();
} else throw e;
}
}
return await 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() {
const user = ctx.state.user as PayloadInfo; const user = ctx.state.user as PayloadInfo;
ctx.body = { ctx.body = {
refresh: false, refresh: false,
...user, ...user,
}; };
ctx.type = "json"; ctx.type = "json";
};
async function success() {
const user = ctx.state.user as PayloadInfo;
ctx.body = {
...user,
refresh: true,
refreshExpired: Math.floor(Date.now() / 1000 + accessExpiredTime),
};
ctx.type = "json";
};
};
export const resetPasswordMiddleware = (cntr: UserAccessor) =>
async (ctx: Koa.Context, next: Koa.Next) => {
const body = ctx.request.body;
if (typeof body !== "object" || !('username' in body) || !('oldpassword' in body) || !('newpassword' in body)) {
return sendError(400, "request body is invalid format");
} }
const username = body['username']; async function success() {
const oldpw = body['oldpassword']; const user = ctx.state.user as PayloadInfo;
const newpw = body['newpassword']; ctx.body = {
...user,
refresh: true,
refreshExpired: Math.floor(Date.now() / 1000 + accessExpiredTime),
};
ctx.type = "json";
}
};
export const resetPasswordMiddleware = (cntr: UserAccessor) => async (ctx: Koa.Context, next: Koa.Next) => {
const body = ctx.request.body;
if (typeof body !== "object" || !("username" in body) || !("oldpassword" in body) || !("newpassword" in body)) {
return sendError(400, "request body is invalid format");
}
const username = body["username"];
const oldpw = body["oldpassword"];
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");
} }
const user = await cntr.findUser(username); const user = await cntr.findUser(username);
if (user === undefined) { if (user === undefined) {
return sendError(403, "not authorized"); return sendError(403, "not authorized");
} }
if (!user.password.check_password(oldpw)) { if (!user.password.check_password(oldpw)) {
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;
} }
export const getAdmin = async (cntr: UserAccessor) => { export const getAdmin = async (cntr: UserAccessor) => {
const admin = await cntr.findUser("admin"); const admin = await cntr.findUser("admin");
if (admin === undefined) { if (admin === undefined) {
throw new Error("initial process failed!"); //??? throw new Error("initial process failed!"); // ???
} }
return admin; return admin;
}; };
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";
}; };

View File

@ -1,129 +1,129 @@
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 = {
title : "string", title: "string",
content_type : "string", content_type: "string",
basepath : "string", basepath: "string",
filename : "string", filename: "string",
content_hash : "string", content_hash: "string",
additional : "object", additional: "object",
tags : "string[]", tags: "string[]",
}
export const isDocBody = (c : any):c is DocumentBody =>{
return check_type<DocumentBody>(c,MetaContentBody);
}
export interface Document extends DocumentBody{
readonly id: number;
readonly created_at:number;
readonly deleted_at:number|null;
}; };
export const isDoc = (c: any):c is Document =>{ export const isDocBody = (c: any): c is DocumentBody => {
if('id' in c && typeof c['id'] === "number"){ return check_type<DocumentBody>(c, MetaContentBody);
const {id, ...rest} = c; };
export interface Document extends DocumentBody {
readonly id: number;
readonly created_at: number;
readonly deleted_at: number | null;
}
export const isDoc = (c: any): c is Document => {
if ("id" in c && typeof c["id"] === "number") {
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.
*/ */
findByPath:(basepath: string,filename?:string)=>Promise<Document[]>; findByPath: (basepath: string, filename?: string) => Promise<Document[]>;
/** /**
* find deleted content * find deleted content
*/ */
findDeleted:(content_type:string)=>Promise<Document[]>; findDeleted: (content_type: string) => Promise<Document[]>;
/** /**
* 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.
*/ */
update:(c:Partial<Document> & { id:number })=>Promise<boolean>; update: (c: Partial<Document> & { id: number }) => Promise<boolean>;
/** /**
* add document * add document
*/ */
add:(c:DocumentBody)=>Promise<number>; add: (c: DocumentBody) => Promise<number>;
/** /**
* add document list * add document list
*/ */
addList:(content_list:DocumentBody[]) => Promise<number[]>; addList: (content_list: DocumentBody[]) => Promise<number[]>;
/** /**
* delete document * delete document
* @returns if it exists, return true. * @returns if it exists, return true.
*/ */
del:(id:number)=>Promise<boolean>; del: (id: number) => Promise<boolean>;
/** /**
* @param c Valid Document * @param c Valid Document
* @param tagname tag name to add * @param tagname tag name to add
* @returns if success, return true * @returns if success, return true
*/ */
addTag:(c:Document,tag_name:string)=>Promise<boolean>; addTag: (c: Document, tag_name: string) => Promise<boolean>;
/** /**
* @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>;
}; }

View File

@ -1,3 +1,3 @@
export * from './doc'; export * from "./doc";
export * from './tag'; export * from "./tag";
export * from './user'; export * from "./user";

View File

@ -1,18 +1,18 @@
export interface Tag{ export interface Tag {
readonly name: string, readonly name: string;
description?: string description?: string;
} }
export interface TagCount{ export interface TagCount {
tag_name: string; tag_name: string;
occurs: number; occurs: number;
} }
export interface TagAccessor{ export interface TagAccessor {
getAllTagList: (onlyname?:boolean)=> Promise<Tag[]>; getAllTagList: (onlyname?: boolean) => Promise<Tag[]>;
getAllTagCount(): Promise<TagCount[]>; getAllTagCount(): Promise<TagCount[]>;
getTagByName: (name:string)=>Promise<Tag|undefined>; getTagByName: (name: string) => Promise<Tag | undefined>;
addTag: (tag:Tag)=>Promise<boolean>; addTag: (tag: Tag) => Promise<boolean>;
delTag: (name:string) => Promise<boolean>; delTag: (name: string) => Promise<boolean>;
updateTag: (name:string,tag:string) => Promise<boolean>; updateTag: (name: string, tag: string) => Promise<boolean>;
} }

View File

@ -1,80 +1,84 @@
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;
} }
set_password(password: string){ set_password(password: string) {
const {salt,hash} = createPasswordHashAndSalt(password); const { salt, hash } = createPasswordHashAndSalt(password);
this._hash = hash; this._hash = hash;
this._salt = salt; this._salt = salt;
} }
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 hash() {
return this._hash;
} }
get salt(){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 {
readonly username : string; readonly username: string;
readonly password : Password; readonly password: Password;
/** /**
* return user's permission list. * return user's permission list.
*/ */
get_permissions():Promise<string[]>; get_permissions(): Promise<string[]>;
/** /**
* add permission * add permission
* @param name permission name to add * @param name permission name to add
* @returns if `name` doesn't exist, return true * @returns if `name` doesn't exist, return true
*/ */
add(name :string):Promise<boolean>; add(name: string): Promise<boolean>;
/** /**
* remove permission * remove permission
* @param name permission name to remove * @param name permission name to remove
* @returns if `name` exist, return true * @returns if `name` exist, return true
*/ */
remove(name :string):Promise<boolean>; remove(name: string): Promise<boolean>;
/** /**
* reset password. * reset password.
* @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>;
}; }

View File

@ -1,60 +1,58 @@
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{ // ========
//======== // not implemented
//not implemented // admin only
//admin only
/** remove document */ /** remove document */
//removeContent = 'removeContent', // removeContent = 'removeContent',
/** upload document */ /** upload document */
//uploadContent = 'uploadContent', // uploadContent = 'uploadContent',
/** modify document except base path, filename, content_hash. but admin can modify all. */ /** modify document except base path, filename, content_hash. but admin can modify all. */
//modifyContent = 'modifyContent', // modifyContent = 'modifyContent',
/** add tag into document */ /** add tag into document */
//addTagContent = 'addTagContent', // addTagContent = 'addTagContent',
/** 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',
/** find one document. */ /** find one document. */
//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();
}
const user_permission = user.permission;
//if permissions is not subset of user permission
if(!permissions.map(p=>user_permission.includes(p)).every(x=>x)){
if(user.username === ""){
return sendError(401,"you are guest. login needed.");
} }
else return sendError(403,"do not have permission"); const user_permission = user.permission;
// if permissions is not subset of user permission
if (!permissions.map(p => user_permission.includes(p)).every(x => x)) {
if (user.username === "") {
return sendError(401, "you are guest. login needed.");
} else return sendError(403, "do not have permission");
}
await next();
};
export const AdminOnlyMiddleware = async (ctx: Koa.ParameterizedContext<UserState>, next: Koa.Next) => {
const user = ctx.state["user"];
if (user.username !== "admin") {
return sendError(403, "admin only");
} }
await next(); await next();
} };
export const AdminOnlyMiddleware = async (ctx: Koa.ParameterizedContext<UserState>,next:Koa.Next)=>{
const user = ctx.state['user'];
if(user.username !== "admin"){
return sendError(403,"admin only");
}
await next();
}

View File

@ -1,55 +1,57 @@
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)=>{
if(cont == undefined){
ctx.status = 404;
return;
}
if(ctx.state.location.type != cont){
console.error("not matched")
ctx.status = 404;
return;
}
const router = table[cont];
if(router == undefined){
ctx.status = 404;
return;
}
const rest = "/"+(restarg ?? "");
const result = router.match(rest,"GET");
if(!result.route){
return await next();
}
const chain = result.pathAndMethod.reduce((combination : Middleware<any& DefaultContext,any>[],cur)=>{
combination.push(async (ctx,next)=>{
const captures = cur.captures(rest);
ctx.params = cur.params(rest,captures);
ctx.request.params = ctx.params;
ctx.routerPath = cur.path;
return await next();
});
return combination.concat(cur.stack);
},[]);
return await compose(chain)(ctx,next);
}; };
export class AllContentRouter extends Router<ContentContext>{ const all_middleware =
constructor(){ (cont: string | undefined, restarg: string | undefined) =>
async (ctx: ParameterizedContext<ContentContext, DefaultContext>, next: Next) => {
if (cont == undefined) {
ctx.status = 404;
return;
}
if (ctx.state.location.type != cont) {
console.error("not matched");
ctx.status = 404;
return;
}
const router = table[cont];
if (router == undefined) {
ctx.status = 404;
return;
}
const rest = "/" + (restarg ?? "");
const result = router.match(rest, "GET");
if (!result.route) {
return await next();
}
const chain = result.pathAndMethod.reduce((combination: Middleware<any & DefaultContext, any>[], cur) => {
combination.push(async (ctx, next) => {
const captures = cur.captures(rest);
ctx.params = cur.params(rest, captures);
ctx.request.params = ctx.params;
ctx.routerPath = cur.path;
return await next();
});
return combination.concat(cur.stack);
}, []);
return await compose(chain)(ctx, next);
};
export class AllContentRouter extends Router<ContentContext> {
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);
}); });
} }
}; }

View File

@ -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.
@ -16,88 +11,86 @@ import Router from "koa-router";
let ZipStreamCache: { [path: string]: [ZipAsync, number] } = {}; let ZipStreamCache: { [path: string]: [ZipAsync, number] } = {};
async function acquireZip(path: string) { async function acquireZip(path: string) {
if (!(path in ZipStreamCache)) { if (!(path in ZipStreamCache)) {
const ret = await readZip(path); const ret = await readZip(path);
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}`); return ret;
return ret; }
}
} }
function releaseZip(path: string) { function releaseZip(path: string) {
const obj = ZipStreamCache[path]; const obj = ZipStreamCache[path];
if (obj === undefined) throw new Error("error! key invalid"); if (obj === undefined) throw new Error("error! key invalid");
const [ref, refCount] = obj; const [ref, refCount] = obj;
//console.log(`release ${path} : ${refCount}`); // console.log(`release ${path} : ${refCount}`);
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]; }
}
} }
async function renderZipImage(ctx: Context, path: string, page: number) { async function renderZipImage(ctx: Context, path: string, page: number) {
const image_ext = ["gif", "png", "jpeg", "bmp", "webp", "jpg"]; const image_ext = ["gif", "png", "jpeg", "bmp", "webp", "jpg"];
//console.log(`opened ${page}`); // console.log(`opened ${page}`);
let zip = await acquireZip(path); let zip = await acquireZip(path);
const entries = (await entriesByNaturalOrder(zip)).filter((x) => { const entries = (await entriesByNaturalOrder(zip)).filter((x) => {
const ext = x.name.split(".").pop(); const ext = x.name.split(".").pop();
return ext !== undefined && image_ext.includes(ext); return ext !== undefined && image_ext.includes(ext);
});
if (0 <= page && page < entries.length) {
const entry = entries[page];
const last_modified = new Date(entry.time);
if (since_last_modified(ctx, last_modified)) {
return;
}
const read_stream = (await createReadableStreamFromZip(zip, entry));
/**Exceptions (ECONNRESET, ECONNABORTED) may be thrown when processing this request
* for reasons such as when the browser unexpectedly closes the connection.
* Once such an exception is raised, the stream is not properly destroyed,
* so there is a problem with the zlib stream being accessed even after the stream is closed.
* So it waits for 100 ms and releases it.
* Additionaly, there is a risk of memory leak becuase zlib stream is not properly destroyed.
* @todo modify function 'stream' in 'node-stream-zip' library to prevent memory leak*/
read_stream.once("close", () => {
setTimeout(() => {
releaseZip(path);
}, 100);
}); });
if (0 <= page && page < entries.length) {
const entry = entries[page];
const last_modified = new Date(entry.time);
if (since_last_modified(ctx, last_modified)) {
return;
}
const read_stream = await createReadableStreamFromZip(zip, entry);
/** Exceptions (ECONNRESET, ECONNABORTED) may be thrown when processing this request
* for reasons such as when the browser unexpectedly closes the connection.
* Once such an exception is raised, the stream is not properly destroyed,
* so there is a problem with the zlib stream being accessed even after the stream is closed.
* So it waits for 100 ms and releases it.
* Additionaly, there is a risk of memory leak becuase zlib stream is not properly destroyed.
* @todo modify function 'stream' in 'node-stream-zip' library to prevent memory leak */
read_stream.once("close", () => {
setTimeout(() => {
releaseZip(path);
}, 100);
});
ctx.body = read_stream; ctx.body = read_stream;
ctx.response.length = entry.size; ctx.response.length = entry.size;
//console.log(`${entry.name}'s ${page}:${entry.size}`); // console.log(`${entry.name}'s ${page}:${entry.size}`);
ctx.response.type = entry.name.split(".").pop() as string; ctx.response.type = entry.name.split(".").pop() as string;
ctx.status = 200; ctx.status = 200;
ctx.set("Date", new Date().toUTCString()); ctx.set("Date", new Date().toUTCString());
ctx.set("Last-Modified", last_modified.toUTCString()); ctx.set("Last-Modified", last_modified.toUTCString());
} else { } else {
ctx.status = 404; ctx.status = 404;
} }
} }
export class ComicRouter extends Router<ContentContext> { export class ComicRouter extends Router<ContentContext> {
constructor() { constructor() {
super(); super();
this.get("/", async (ctx, next) => { this.get("/", async (ctx, next) => {
await renderZipImage(ctx, ctx.state.location.path, 0); await renderZipImage(ctx, ctx.state.location.path, 0);
}); });
this.get("/:page(\\d+)", async (ctx, next) => { this.get("/:page(\\d+)", async (ctx, next) => {
const page = Number.parseInt(ctx.params["page"]); const page = Number.parseInt(ctx.params["page"]);
await renderZipImage(ctx, ctx.state.location.path, page); await renderZipImage(ctx, ctx.state.location.path, page);
}); });
this.get("/thumbnail", async (ctx, next) => { this.get("/thumbnail", async (ctx, next) => {
await renderZipImage(ctx, ctx.state.location.path, 0); await renderZipImage(ctx, ctx.state.location.path, 0);
}); });
} }
} }
export default ComicRouter; export default ComicRouter;

View File

@ -1,63 +1,68 @@
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);
const cursor = ParseQueryNumber(query_cursor); const cursor = ParseQueryNumber(query_cursor);
const word = ParseQueryArgString(query_word); const word = ParseQueryArgString(query_word);
const content_type = ParseQueryArgString(query_content_type); const content_type = ParseQueryArgString(query_content_type);
const offset = ParseQueryNumber(query_offset); const offset = ParseQueryNumber(query_offset);
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.");
} }
const option :QueryListOption = { const option: QueryListOption = {
limit: limit, limit: limit,
allow_tag: allow_tag, allow_tag: allow_tag,
word: word, word: word,
@ -69,93 +74,94 @@ 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)=>{
let tag_name = ctx.params['tag'];
const num = Number.parseInt(ctx.params['num']);
if(typeof tag_name === undefined){
return sendError(400,"??? Unreachable");
}
tag_name = String(tag_name);
const c = await controller.findById(num);
if(c === undefined){
return sendError(404);
}
const r = await controller.addTag(c,tag_name);
ctx.body = JSON.stringify(r);
ctx.type = 'json';
}; };
const DelTagHandler = (controller: DocumentAccessor)=>async (ctx: Context, next: Next)=>{
let tag_name = ctx.params['tag']; const AddTagHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => {
const num = Number.parseInt(ctx.params['num']); let tag_name = ctx.params["tag"];
if(typeof tag_name === undefined){ const num = Number.parseInt(ctx.params["num"]);
return sendError(400,"?? Unreachable"); if (typeof tag_name === undefined) {
return sendError(400, "??? Unreachable");
} }
tag_name = String(tag_name); tag_name = String(tag_name);
const c = await controller.findById(num); const c = await controller.findById(num);
if(c === undefined){ if (c === undefined) {
return sendError(404); return sendError(404);
} }
const r = await controller.delTag(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 DeleteContentHandler = (controller : DocumentAccessor) => async (ctx: Context, next: Next) => { const DelTagHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => {
const num = Number.parseInt(ctx.params['num']); let tag_name = ctx.params["tag"];
const num = Number.parseInt(ctx.params["num"]);
if (typeof tag_name === undefined) {
return sendError(400, "?? Unreachable");
}
tag_name = String(tag_name);
const c = await controller.findById(num);
if (c === undefined) {
return sendError(404);
}
const r = await controller.delTag(c, tag_name);
ctx.body = JSON.stringify(r);
ctx.type = "json";
};
const DeleteContentHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => {
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.");
} }
if(document.deleted_at !== null){ if (document.deleted_at !== null) {
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,
} as ContentLocation; } as ContentLocation;
await next(); await next();
}; };
export const getContentRouter = (controller: DocumentAccessor)=>{ export const getContentRouter = (controller: DocumentAccessor) => {
const ret = new Router(); const ret = new Router();
ret.get("/search",PerCheck(Per.QueryContent),ContentQueryHandler(controller)); ret.get("/search", PerCheck(Per.QueryContent), ContentQueryHandler(controller));
ret.get("/:num(\\d+)",PerCheck(Per.QueryContent),ContentIDHandler(controller)); ret.get("/:num(\\d+)", PerCheck(Per.QueryContent), ContentIDHandler(controller));
ret.post("/:num(\\d+)",AdminOnly,UpdateContentHandler(controller)); ret.post("/:num(\\d+)", AdminOnly, UpdateContentHandler(controller));
//ret.use("/:num(\\d+)/:content_type"); // ret.use("/:num(\\d+)/:content_type");
//ret.post("/",AdminOnly,CreateContentHandler(controller)); // ret.post("/",AdminOnly,CreateContentHandler(controller));
ret.get("/:num(\\d+)/tags",PerCheck(Per.QueryContent),ContentTagIDHandler(controller)); ret.get("/:num(\\d+)/tags", PerCheck(Per.QueryContent), ContentTagIDHandler(controller));
ret.post("/:num(\\d+)/tags/:tag",PerCheck(Per.ModifyTag),AddTagHandler(controller)); ret.post("/:num(\\d+)/tags/:tag", PerCheck(Per.ModifyTag), AddTagHandler(controller));
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;

View File

@ -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 {
location: ContentLocation;
} }
export interface ContentContext{
location:ContentLocation
}

View File

@ -1,50 +1,49 @@
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 {
name: string; name: string;
message: string; message: string;
stack?: string | undefined; stack?: string | undefined;
code: number; code: number;
constructor(code : number,message: string){ constructor(code: number, message: string) {
this.name = "client request error"; this.name = "client request error";
this.message = message; this.message = message;
this.code = code; this.code = code;
} }
} }
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 {
await next(); await next();
} catch (err) { } catch (err) {
if(err instanceof ClientRequestError){ if (err instanceof ClientRequestError) {
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;

View File

@ -1,32 +1,29 @@
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;
}
else {
const c = await tagController.getAllTagList();
ctx.body = c;
}
ctx.type = "json";
});
router.get("/:tag_name", PerCheck(Permission.QueryContent),
async (ctx: RouterContext)=>{
const tag_name = ctx.params["tag_name"];
const c = await tagController.getTagByName(tag_name);
if (!c){
sendError(404, "tags not found");
}
ctx.body = c; ctx.body = c;
ctx.type = "json"; } else {
}); const c = await tagController.getAllTagList();
ctx.body = c;
}
ctx.type = "json";
});
router.get("/:tag_name", PerCheck(Permission.QueryContent), async (ctx: RouterContext) => {
const tag_name = ctx.params["tag_name"];
const c = await tagController.getTagByName(tag_name);
if (!c) {
sendError(404, "tags not found");
}
ctx.body = c;
ctx.type = "json";
});
return router; return router;
} }

View File

@ -1,39 +1,37 @@
import { Context } from "koa";
import {Context} from 'koa'; export function ParseQueryNumber(s: string[] | string | undefined): number | undefined {
if (s === undefined) return undefined;
else if (typeof s === "object") return undefined;
export function ParseQueryNumber(s: string[] | string|undefined): number| undefined{
if(s === undefined) return undefined;
else if(typeof s === "object") return undefined;
else return Number.parseInt(s); else return Number.parseInt(s);
} }
export function ParseQueryArray(s: string[]|string|undefined){ export function ParseQueryArray(s: string[] | string | undefined) {
s = s ?? []; s = s ?? [];
const r = s instanceof Array ? s : [s]; const r = s instanceof Array ? s : [s];
return r.map(x=>decodeURIComponent(x)); return r.map(x => decodeURIComponent(x));
} }
export function ParseQueryArgString(s: string[]|string|undefined){ export function ParseQueryArgString(s: string[] | string | undefined) {
if(typeof s === "object") return undefined; if (typeof s === "object") return undefined;
return s === undefined ? s : decodeURIComponent(s); return s === undefined ? s : decodeURIComponent(s);
} }
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 {
const con = ctx.get("If-Modified-Since"); const con = ctx.get("If-Modified-Since");
if(con === "") return false; if (con === "") return false;
const mdate = new Date(con); const mdate = new Date(con);
if(last_modified > mdate) return false; if (last_modified > mdate) return false;
ctx.status = 304; ctx.status = 304;
return true; return true;
} }

View File

@ -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,13 +15,13 @@ 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) {
ctx.status = 416; ctx.status = 416;
return; return;
} }
@ -29,40 +29,39 @@ 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;
return; return;
} }
start = parseInt(m[1]); start = parseInt(m[1]);
end = m[2].length > 0 ? parseInt(m[2]) : start + 1024*1024; end = m[2].length > 0 ? parseInt(m[2]) : start + 1024 * 1024;
end = Math.min(end,stat.size-1); end = Math.min(end, stat.size - 1);
if(start > end){ if (start > end) {
ctx.status = 416; ctx.status = 416;
return; return;
} }
ctx.status = 206; ctx.status = 206;
ctx.length = end - start + 1; ctx.length = end - start + 1;
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.
} }
} }
export class VideoRouter extends Router<ContentContext>{ export class VideoRouter extends Router<ContentContext> {
constructor(){ constructor() {
super(); super();
this.get("/", async (ctx,next)=>{ this.get("/", async (ctx, next) => {
await renderVideo(ctx,ctx.state.location.path); await renderVideo(ctx, ctx.state.location.path);
});
this.get("/thumbnail", async (ctx, next) => {
await renderVideo(ctx, ctx.state.location.path);
}); });
this.get("/thumbnail", async (ctx,next)=>{
await renderVideo(ctx,ctx.state.location.path);
})
} }
} }
export default VideoRouter; export default VideoRouter;

View File

@ -1,13 +1,12 @@
export interface PaginationOption {
export interface PaginationOption{ cursor: number;
cursor:number; limit: number;
limit:number;
} }
export interface IIndexer{ export interface IIndexer {
indexDoc(word:string,doc_id:number):boolean; indexDoc(word: string, doc_id: number): boolean;
indexDoc(word:string[],doc_id:number):boolean; indexDoc(word: string[], doc_id: number): boolean;
getDoc(word:string,option?:PaginationOption):number[]; getDoc(word: string, option?: PaginationOption): number[];
getDoc(word:string[],option?:PaginationOption):number[]; getDoc(word: string[], option?: PaginationOption): number[];
} }

View File

@ -1,10 +1,9 @@
export interface ITokenizer {
export interface ITokenizer{ tokenize(s: string): string[];
tokenize(s:string):string[];
} }
export class DefaultTokenizer implements ITokenizer{ export class DefaultTokenizer implements ITokenizer {
tokenize(s: string): string[] { tokenize(s: string): string[] {
return s.split(" "); return s.split(" ");
} }
} }

View File

@ -1,55 +1,55 @@
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 { createInterface as createReadlineInterface } from "readline";
import { DocumentAccessor, UserAccessor, TagAccessor } from './model/mod'; import { createComicWatcher } from "./diff/watcher/comic_watcher";
import { createComicWatcher } from './diff/watcher/comic_watcher'; import { DocumentAccessor, TagAccessor, UserAccessor } from "./model/mod";
import { getTagRounter } from './route/tags'; import { getTagRounter } from "./route/tags";
class ServerApplication {
class ServerApplication{
readonly userController: UserAccessor; readonly userController: UserAccessor;
readonly documentController: DocumentAccessor; readonly documentController: DocumentAccessor;
readonly tagController: TagAccessor; readonly tagController: TagAccessor;
readonly diffManger: DiffManager; readonly diffManger: DiffManager;
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;
this.diffManger = new DiffManager(this.documentController); this.diffManger = new DiffManager(this.documentController);
this.app = new Koa(); this.app = new Koa();
this.index_html = readFileSync("index.html","utf-8"); this.index_html = readFileSync("index.html", "utf-8");
} }
private async setup(){ private async setup() {
const setting = get_setting(); const setting = get_setting();
const app = this.app; const app = this.app;
if(setting.cli){ if (setting.cli) {
const userAdmin = await getAdmin(this.userController); const userAdmin = await getAdmin(this.userController);
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) => {
res(data); res(data);
}); });
}); });
@ -60,160 +60,168 @@ class ServerApplication{
app.use(bodyparser()); app.use(bodyparser());
app.use(error_handler); app.use(error_handler);
app.use(createUserMiddleWare(this.userController)); app.use(createUserMiddleWare(this.userController));
let diff_router = createDiffRouter(this.diffManger); let diff_router = createDiffRouter(this.diffManger);
this.diffManger.register("comic",createComicWatcher()); this.diffManger.register("comic", createComicWatcher());
console.log("setup router");
let router = new Router(); let router = new Router();
router.use("/api/*", async (ctx,next)=>{ router.use("/api/(.*)", async (ctx, next) => {
//For CORS // For CORS
ctx.res.setHeader("access-control-allow-origin", "*"); ctx.res.setHeader("access-control-allow-origin", "*");
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);
router.use("/api/doc", content_router.routes());
router.use("/api/doc", content_router.allowedMethods());
const tags_router = getTagRounter(this.tagController);
router.use("/api/tags", tags_router.allowedMethods());
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 content_router = getContentRouter(this.documentController);
router.use('/api/doc',content_router.routes());
router.use('/api/doc',content_router.allowedMethods());
const tags_router = getTagRounter(this.tagController);
router.use("/api/tags",tags_router.allowedMethods());
router.use("/api/tags",tags_router.routes());
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");
} }
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) => {
const docId = Number.parseInt(ctx.params["id"]); const docId = Number.parseInt(ctx.params["id"]);
const doc = await this.documentController.findById(docId,true); const doc = await this.documentController.findById(docId, true);
let meta; let meta;
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);
function NotFoundContent() {
return createOgTagContent("Not Found Doc", "Not Found", "");
} }
router.get('/doc/:id(\\d+)',DocMiddleware); function makeMetaTagInjectedHTML(html: string, tagContent: string) {
return html.replace("<!--MetaTag-Outlet-->", tagContent);
function NotFoundContent(){
return createOgTagContent("Not Found Doc","Not Found","");
} }
function makeMetaTagInjectedHTML(html:string,tagContent:string){ function serveHTML(ctx: Koa.Context, file: string) {
return html.replace("<!--MetaTag-Outlet-->",tagContent); ctx.type = "html";
} ctx.body = file;
function serveHTML(ctx: Koa.Context, file: string){
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');
} }
} }
function createMetaTagContent(key: string, value:string){ function createMetaTagContent(key: string, value: string) {
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:type","website"), createMetaTagContent("og:title", title),
createMetaTagContent("og:description",description), createMetaTagContent("og:type", "website"),
createMetaTagContent("og:image",image), createMetaTagContent("og:description", description),
//createMetaTagContent("og:image:width","480"), createMetaTagContent("og:image", image),
//createMetaTagContent("og:image","480"), // createMetaTagContent("og:image:width","480"),
//createMetaTagContent("og:image:type","image/png"), // createMetaTagContent("og:image","480"),
createMetaTagContent("twitter:card","summary_large_image"), // createMetaTagContent("og:image:type","image/png"),
createMetaTagContent("twitter:title",title), createMetaTagContent("twitter:card", "summary_large_image"),
createMetaTagContent("twitter:description",description), createMetaTagContent("twitter:title", title),
createMetaTagContent("twitter:image",image), createMetaTagContent("twitter:description", description),
].join("\n"); createMetaTagContent("twitter:image", image),
].join("\n");
} }
} }
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);
if(setting.mode === "development"){ ctx.set("x-content-type-options", "no-sniff");
ctx.set('cache-control','no-cache'); if (setting.mode === "development") {
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() {
let setting = get_setting(); let setting = get_setting();
console.log("start server"); // todo : support https
//todo : support https console.log(`listen on http://${setting.localmode ? "localhost" : "0.0.0.0"}:${setting.port}`);
return this.app.listen(setting.port,setting.localmode ? "127.0.0.1" : "0.0.0.0"); return this.app.listen(setting.port, setting.localmode ? "127.0.0.1" : "0.0.0.0");
} }
static async createServer(){ static async createServer() {
const setting = get_setting(); const setting = get_setting();
let db = await connectDB(); let db = await connectDB();
const app = new ServerApplication({ const app = new ServerApplication({
userController:createKnexUserController(db), userController: createKnexUserController(db),
documentController: createKnexDocumentAccessor(db), documentController: createKnexDocumentAccessor(db),
tagController: createKnexTagController(db), tagController: createKnexTagController(db),
}); });
@ -221,10 +229,9 @@ class ServerApplication{
return app; return app;
} }
} }
//let Koa = require("koa");
export async function create_server(){ export async function create_server() {
return await ServerApplication.createServer(); return await ServerApplication.createServer();
} }
export default {create_server}; export default { create_server };

62
src/types/db.d.ts vendored
View File

@ -1,34 +1,34 @@
import {Knex} from "knex"; import { Knex } from "knex";
declare module "knex" { declare module "knex" {
interface Tables { interface Tables {
tags: { tags: {
name: string; name: string;
description?: string; description?: string;
}; };
users: { users: {
username: string; username: string;
password_hash: string; password_hash: string;
password_salt: string; password_salt: string;
}; };
document: { document: {
id: number; id: number;
title: string; title: string;
content_type: string; content_type: string;
basepath: string; basepath: string;
filename: string; filename: string;
created_at: number; created_at: number;
deleted_at: number|null; deleted_at: number | null;
content_hash: string; content_hash: string;
additional: string|null; additional: string | null;
}; };
doc_tag_relation: { doc_tag_relation: {
doc_id: number; doc_id: number;
tag_name: string; tag_name: string;
}; };
permissions: { permissions: {
username: string; username: string;
name: string; name: string;
}; };
} }
} }

View File

@ -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;

View File

@ -1,26 +1,26 @@
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;
default_config: T; default_config: T;
config: T| null; config: T | null;
schema:object; schema: object;
constructor(path:string,default_config:T,schema:object){ constructor(path: string, default_config: T, schema: object) {
this.path = path; this.path = path;
this.default_config = default_config; this.default_config = default_config;
this.config = null; this.config = null;
this.schema = schema; this.schema = schema;
} }
get_config_file(): T{ get_config_file(): T {
if(this.config !== null) return this.config; if (this.config !== null) return this.config;
this.config = {...this.read_config_file()}; this.config = { ...this.read_config_file() };
return this.config; return this.config;
} }
private emptyToDefault(target:T){ private emptyToDefault(target: T) {
let occur = false; let occur = false;
for(const key in this.default_config){ for (const key in this.default_config) {
if(key === undefined || key in target){ if (key === undefined || key in target) {
continue; continue;
} }
target[key] = this.default_config[key]; target[key] = this.default_config[key];
@ -28,24 +28,24 @@ export class ConfigManager<T>{
} }
return occur; return occur;
} }
read_config_file():T{ read_config_file(): T {
if(!existsSync(this.path)){ if (!existsSync(this.path)) {
writeFileSync(this.path,JSON.stringify(this.default_config)); writeFileSync(this.path, JSON.stringify(this.default_config));
return this.default_config; return this.default_config;
} }
const ret = JSON.parse(readFileSync(this.path,{encoding:"utf8"})); const ret = JSON.parse(readFileSync(this.path, { encoding: "utf8" }));
if(this.emptyToDefault(ret)){ if (this.emptyToDefault(ret)) {
writeFileSync(this.path,JSON.stringify(ret)); writeFileSync(this.path, JSON.stringify(ret));
} }
const result = validate(ret,this.schema); const result = validate(ret, this.schema);
if(!result.valid){ if (!result.valid) {
throw new Error(result.toString()); throw new Error(result.toString());
} }
return ret; return ret;
} }
async write_config_file(new_config:T){ async write_config_file(new_config: T) {
this.config = new_config; this.config = new_config;
await fs.writeFile(`${this.path}.temp`,JSON.stringify(new_config)); await fs.writeFile(`${this.path}.temp`, JSON.stringify(new_config));
await fs.rename(`${this.path}.temp`,this.path); await fs.rename(`${this.path}.temp`, this.path);
} }
} }

View File

@ -1,16 +1,15 @@
export function check_type<T>(obj: any,check_proto:Record<string,string|undefined>):obj is T{ export function check_type<T>(obj: any, check_proto: Record<string, string | undefined>): obj is T {
for (const it in check_proto) { for (const it in check_proto) {
let defined = check_proto[it]; let defined = check_proto[it];
if(defined === undefined) return false; if (defined === undefined) return false;
defined = defined.trim(); defined = defined.trim();
if(defined.endsWith("[]")){ if (defined.endsWith("[]")) {
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;
}; }

View File

@ -1,31 +1,33 @@
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) {
const entries = await zip.entries(); const entries = await zip.entries();
const ret = orderBy(Object.values(entries),v=>v.name); const ret = orderBy(Object.values(entries), v => v.name);
return ret; return ret;
} }
export async function createReadableStreamFromZip(zip:ZipAsync,entry: ZipEntry):Promise<NodeJS.ReadableStream>{ export async function createReadableStreamFromZip(zip: ZipAsync, entry: ZipEntry): Promise<NodeJS.ReadableStream> {
return await zip.stream(entry); return await zip.stream(entry);
} }
export async function readAllFromZip(zip:ZipAsync,entry: ZipEntry):Promise<Buffer>{ export async function readAllFromZip(zip: ZipAsync, entry: ZipEntry): Promise<Buffer> {
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)));
}); });
} }

View File

@ -4,17 +4,17 @@
/* Basic Options */ /* Basic Options */
// "incremental": true, /* Enable incremental compilation */ // "incremental": true, /* Enable incremental compilation */
"target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
"lib": ["DOM","ES6"], /* Specify library files to be included in the compilation. */ "lib": ["DOM", "ES6"], /* Specify library files to be included in the compilation. */
//"allowJs": true, /* Allow javascript files to be compiled. */ // "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */ // "checkJs": true, /* Report errors in .js files. */
"jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */ // "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */ // "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */ // "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./build", /* Redirect output structure to the directory. */ "outDir": "./build", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */ // "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
@ -25,13 +25,13 @@
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */ /* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */ "strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
"strictNullChecks": true, /* Enable strict null checks. */ "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */ // "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
"noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */ /* Additional Checks */
@ -41,14 +41,14 @@
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */ /* Module Resolution Options */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */ // "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */ // "types": [], /* Type declaration files to be included in compilation. */
"allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"resolveJsonModule": true, "resolveJsonModule": true,
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
@ -64,9 +64,9 @@
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* 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"]
} }