import Koa from "koa"; import Router from "koa-router"; import { connectDB } from "./database"; import { createDiffRouter, DiffManager } from "./diff/mod"; import { get_setting, SettingConfig } from "./SettingConfig"; import { createReadStream, readFileSync } from "fs"; import bodyparser from "koa-bodyparser"; import { createKnexDocumentAccessor, createKnexTagController, createKnexUserController } from "./db/mod"; import { createLoginRouter, createUserMiddleWare, getAdmin, isAdminFirst } from "./login"; import getContentRouter from "./route/contents"; import { error_handler } from "./route/error_handler"; import { createInterface as createReadlineInterface } from "readline"; import { createComicWatcher } from "./diff/watcher/comic_watcher"; import { DocumentAccessor, TagAccessor, UserAccessor } from "./model/mod"; import { getTagRounter } from "./route/tags"; class ServerApplication { readonly userController: UserAccessor; readonly documentController: DocumentAccessor; readonly tagController: TagAccessor; readonly diffManger: DiffManager; readonly app: Koa; private index_html: string; private constructor(controller: { userController: UserAccessor; documentController: DocumentAccessor; tagController: TagAccessor; }) { this.userController = controller.userController; this.documentController = controller.documentController; this.tagController = controller.tagController; this.diffManger = new DiffManager(this.documentController); this.app = new Koa(); this.index_html = readFileSync("index.html", "utf-8"); } private async setup() { const setting = get_setting(); const app = this.app; if (setting.cli) { const userAdmin = await getAdmin(this.userController); if (await isAdminFirst(userAdmin)) { const rl = createReadlineInterface({ input: process.stdin, output: process.stdout, }); const pw = await new Promise((res: (data: string) => void, err) => { rl.question("put admin password :", (data) => { res(data); }); }); rl.close(); userAdmin.reset_password(pw); } } app.use(bodyparser()); app.use(error_handler); app.use(createUserMiddleWare(this.userController)); let diff_router = createDiffRouter(this.diffManger); this.diffManger.register("comic", createComicWatcher()); console.log("setup router"); let router = new Router(); router.use("/api/(.*)", async (ctx, next) => { // For CORS ctx.res.setHeader("access-control-allow-origin", "*"); await next(); }); router.use("/api/diff", diff_router.routes()); 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_index(router); this.serve_static_file(router); const login_router = createLoginRouter(this.userController); router.use("/user", login_router.routes()); router.use("/user", login_router.allowedMethods()); if (setting.mode == "development") { let mm_count = 0; app.use(async (ctx, next) => { console.log(`==========================${mm_count++}`); const ip = ctx.get("X-Real-IP") ?? ctx.ip; const fromClient = ctx.state["user"].username === "" ? ip : ctx.state["user"].username; console.log(`${fromClient} : ${ctx.method} ${ctx.url}`); await next(); // console.log(`404`); }); } app.use(router.routes()); app.use(router.allowedMethods()); console.log("setup done"); } private serve_index(router: Router) { const serveindex = (url: string) => { router.get(url, (ctx) => { ctx.type = "html"; ctx.body = this.index_html; const setting = get_setting(); ctx.set("x-content-type-options", "no-sniff"); if (setting.mode === "development") { ctx.set("cache-control", "no-cache"); } else { ctx.set("cache-control", "public, max-age=3600"); } }); }; serveindex("/"); serveindex("/doc/:rest(.*)"); serveindex("/search"); serveindex("/login"); serveindex("/profile"); serveindex("/difference"); serveindex("/setting"); serveindex("/tags"); } private serve_with_meta_index(router: Router) { const DocMiddleware = async (ctx: Koa.ParameterizedContext) => { const docId = Number.parseInt(ctx.params["id"]); const doc = await this.documentController.findById(docId, true); let meta; if (doc === undefined) { ctx.status = 404; meta = NotFoundContent(); } else { ctx.status = 200; meta = createOgTagContent( doc.title, doc.tags.join(", "), `https://aeolian.prelude.duckdns.org/api/doc/${docId}/comic/thumbnail`, ); } const html = makeMetaTagInjectedHTML(this.index_html, meta); serveHTML(ctx, html); }; router.get("/doc/:id(\\d+)", DocMiddleware); function NotFoundContent() { return createOgTagContent("Not Found Doc", "Not Found", ""); } function makeMetaTagInjectedHTML(html: string, tagContent: string) { return html.replace("", tagContent); } function serveHTML(ctx: Koa.Context, file: string) { ctx.type = "html"; ctx.body = file; const setting = get_setting(); ctx.set("x-content-type-options", "no-sniff"); if (setting.mode === "development") { ctx.set("cache-control", "no-cache"); } else { ctx.set("cache-control", "public, max-age=3600"); } } function createMetaTagContent(key: string, value: string) { return ``; } function createOgTagContent(title: string, description: string, image: string) { return [ createMetaTagContent("og:title", title), createMetaTagContent("og:type", "website"), createMetaTagContent("og:description", description), createMetaTagContent("og:image", image), // createMetaTagContent("og:image:width","480"), // createMetaTagContent("og:image","480"), // createMetaTagContent("og:image:type","image/png"), createMetaTagContent("twitter:card", "summary_large_image"), createMetaTagContent("twitter:title", title), createMetaTagContent("twitter:description", description), createMetaTagContent("twitter:image", image), ].join("\n"); } } private serve_static_file(router: Router) { const static_file_server = (path: string, type: string) => { router.get("/" + path, async (ctx, next) => { const setting = get_setting(); ctx.type = type; ctx.body = createReadStream(path); ctx.set("x-content-type-options", "no-sniff"); if (setting.mode === "development") { ctx.set("cache-control", "no-cache"); } else { ctx.set("cache-control", "public, max-age=3600"); } }); }; const setting = get_setting(); static_file_server("dist/bundle.css", "css"); static_file_server("dist/bundle.js", "js"); if (setting.mode === "development") { static_file_server("dist/bundle.js.map", "text"); static_file_server("dist/bundle.css.map", "text"); } } start_server() { let setting = get_setting(); // 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"); } static async createServer() { const setting = get_setting(); let db = await connectDB(); const app = new ServerApplication({ userController: createKnexUserController(db), documentController: createKnexDocumentAccessor(db), tagController: createKnexTagController(db), }); await app.setup(); return app; } } export async function create_server() { return await ServerApplication.createServer(); } export default { create_server };