ionian/packages/server/src/server.ts

242 lines
7.8 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";
2024-03-29 00:19:36 +09:00
import { createReadStream, readFileSync } from "node:fs";
2024-03-26 23:58:26 +09:00
import bodyparser from "koa-bodyparser";
2024-03-29 00:19:36 +09:00
import { createSqliteDocumentAccessor, createSqliteTagController, createSqliteUserController } from "./db/mod";
2024-03-26 23:58:26 +09:00
import { createLoginRouter, createUserMiddleWare, getAdmin, isAdminFirst } from "./login";
import getContentRouter from "./route/contents";
import { error_handler } from "./route/error_handler";
2024-03-29 00:19:36 +09:00
import { createInterface as createReadlineInterface } from "node:readline";
2024-03-26 23:58:26 +09:00
import { createComicWatcher } from "./diff/watcher/comic_watcher";
2024-03-29 00:19:36 +09:00
import type { DocumentAccessor, TagAccessor, UserAccessor } from "./model/mod";
2024-03-26 23:58:26 +09:00
import { getTagRounter } from "./route/tags";
2024-03-29 00:19:36 +09:00
import { config } from "dotenv";
config();
2024-03-26 23:58:26 +09:00
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));
2024-03-29 00:19:36 +09:00
const diff_router = createDiffRouter(this.diffManger);
2024-03-26 23:58:26 +09:00
this.diffManger.register("comic", createComicWatcher());
console.log("setup router");
2024-03-29 00:19:36 +09:00
const router = new Router();
2024-03-26 23:58:26 +09:00
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());
2024-03-29 00:19:36 +09:00
if (setting.mode === "development") {
2024-03-26 23:58:26 +09:00
let mm_count = 0;
app.use(async (ctx, next) => {
console.log(`==========================${mm_count++}`);
const ip = ctx.get("X-Real-IP") ?? ctx.ip;
2024-03-29 00:19:36 +09:00
const fromClient = ctx.state.user.username === "" ? ip : ctx.state.user.username;
2024-03-26 23:58:26 +09:00
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) => {
2024-03-29 00:19:36 +09:00
const docId = Number.parseInt(ctx.params.id);
2024-03-26 23:58:26 +09:00
const doc = await this.documentController.findById(docId, true);
2024-03-29 00:19:36 +09:00
// biome-ignore lint/suspicious/noImplicitAnyLet: <explanation>
2024-03-26 23:58:26 +09:00
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) => {
2024-03-29 00:19:36 +09:00
router.get(`/${path}`, async (ctx, next) => {
2024-03-26 23:58:26 +09:00
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() {
2024-03-29 00:19:36 +09:00
const setting = get_setting();
2024-03-26 23:58:26 +09:00
// 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();
2024-03-29 00:19:36 +09:00
const db = await connectDB();
2024-03-26 23:58:26 +09:00
const app = new ServerApplication({
2024-03-29 00:19:36 +09:00
userController: createSqliteUserController(db),
documentController: createSqliteDocumentAccessor(db),
tagController: createSqliteTagController(db),
2024-03-26 23:58:26 +09:00
});
await app.setup();
return app;
}
}
export async function create_server() {
return await ServerApplication.createServer();
}
export default { create_server };