ionian/packages/server/src/server.ts

238 lines
7.7 KiB
TypeScript
Raw Normal View History

2024-03-26 23:58:26 +09:00
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 };