238 lines
7.7 KiB
TypeScript
238 lines
7.7 KiB
TypeScript
|
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("<!--MetaTag-Outlet-->", 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 `<meta property="${key}" content="${value}">`;
|
||
|
}
|
||
|
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 };
|