import { readFile } from "fs";
import * as path from "path";
import Database from "better-sqlite3";
import getSqliteStore from "better-sqlite3-session-store";
import { watch } from "chokidar";
import { load as loadYaml } from "js-yaml";
import passport from "passport";
import * as pug from "pug";
import express from "express";
import session from "express-session";
import { rateLimit, ipKeyGenerator } from "express-rate-limit";
import cookieParser from "cookie-parser";
import helmet from "helmet";
import morgan from "morgan";
import { createStream } from "rotating-file-stream";
import grimm from "./app/grimm.js";
import router from "./app/router.js";
import { rootFolder } from "./app/helpers.js";
import { cookieSecret, sessionSecret } from "./shush/shush.js";
const db = new Database("./db/site.db");
db.pragma("journal_mode = DELETE");
const SqliteStore = getSqliteStore(session);
const sessions = new Database("./db/sessions.db");
let app = express();
app.engine("pug", pug.__express);
app.set("view engine", "pug");
app.set("views", `${rootFolder}/hypertext/public`);
const speedLimiter = rateLimit({
windowMs: 30000,
limit: 10,
skip: (req, res) => req.path.includes("."),
keyGenerator: (req, res) => ipKeyGenerator(req.headers["x-real-ip"])
});
app.use(speedLimiter);
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser(cookieSecret));
app.set("trust proxy", true);
app.use(
session({
secret: sessionSecret,
resave: false,
saveUninitialized: false,
store: new SqliteStore({
client: sessions,
expired: {
clear: true,
intervalMs: 60 * 60 * 1000
}
})
})
);
app.use(passport.initialize());
app.use(passport.session());
app.use(passport.authenticate("remember-me"));
app.use((req, res, next) => {
helmet({
referrerPolicy: {
policy: "no-referrer-when-downgrade"
},
contentSecurityPolicy: {
directives: {
"connect-src": ["'self'", "blob:"],
"script-src": ["'self'", "'unsafe-inline'"],
"script-src-attr": "'unsafe-inline'",
"frame-src": [
"'self'",
"youtube-nocookie.com",
"www.youtube-nocookie.com"
]
}
},
crossOriginResourcePolicy: {
policy: ["/maps", "maps/", "files/", "/files"].some(x =>
req.path.startsWith(x)
)
? "cross-origin"
: "same-site"
}
})(req, res, next);
});
const generator = (time, index) => {
if (!time) return "access.log";
return `access-${time.getFullYear()}-${(time.getMonth() + 1)
.toString()
.padStart(2, "0")}.log`;
};
const accessLogStream = createStream(generator, {
interval: "1M",
path: `${rootFolder}/analytics`
});
const logger = morgan(":date[iso] :url :referrer /// :user-agent", {
skip: (req, res) =>
(req.path.includes(".") && !req.path.includes(".xml")) ||
res.statusCode >= 400,
stream: accessLogStream
});
app.use(logger);
app.use(router);
const publisher = watch(["./hypertext"], {
awaitWriteFinish: {
stabilityThreshold: 250
}
});
function getParsed(parsed) {
return {
editions: {
editionID: parsed?.slug,
lang: parsed?.lang ?? "en",
translates: parsed?.translates ?? parsed?.slug,
title: parsed?.title,
pageAdded: parsed?.pageAdded,
pageTranslated: parsed?.pageTranslated,
pageUpdated: parsed?.pageUpdated,
description: parsed?.description,
thumbnail: parsed?.thumbnail,
thumbnailAlt: parsed?.thumbnailAlt,
subtitle: parsed?.subtitle,
supertitle: parsed?.supertitle
},
pagesets: {
pagesetID: parsed?.translates ?? parsed?.slug,
original: parsed?.slug,
subsite: parsed?.subsite,
category: parsed?.category,
alexandrine: parsed?.alexandrine,
pageCreated: parsed?.pageCreated
},
tags:
typeof parsed?.tags == "object"
? parsed.tags.map(x => {
return { tagID: grimm.slugify(x), defaultName: x };
})
: null
};
}
const insertKeys = db.transaction((row, table) => {
const rowKeys = Object.keys(row);
db.prepare(
`INSERT INTO ${table} (${rowKeys
.map(x => ` ${x}`)
.toString()
.trim()}) VALUES (${rowKeys
.map(x => ` @${x}`)
.toString()
.trim()})`
).run(row);
});
const updateKeys = db.transaction((newRow, oldRow, table) => {
const tableID = table.replace(/s$/, "ID");
const constKeys = ["original"];
for (let key in newRow) {
if (
newRow[key] != oldRow[key] &&
!constKeys.includes(key) &&
newRow[key] !== undefined
) {
db.prepare(
`UPDATE ${table} SET ${key} = ? WHERE ${tableID} = ?`
).run(newRow[key], newRow[tableID]);
}
}
});
const insertTag = db.transaction(tag => {
db.prepare(
`INSERT INTO tags (tagID, defaultName) VALUES (@tagID, @defaultName)`
).run(tag);
});
const insertTaggings = db.transaction((pageset, tags) => {
for (t of tags) {
db.prepare(`INSERT INTO taggings (pageset, tag) VALUES (?, ?)`).run(
pageset,
t
);
}
});
const deleteTaggings = db.transaction((pageset, tags) => {
for (t of tags) {
db.prepare(`DELETE FROM taggings WHERE pageset = ? AND tag = ?`).run(
pageset,
t
);
}
});
publisher.on("change", filePath => {
const fullPath = path.join(rootFolder, filePath);
if (fullPath.endsWith(".pug") && pug.cache[fullPath]) {
delete pug.cache[fullPath];
}
readFile(filePath, "utf-8", (err, input) => {
let rawData = input.match(/meta[\r\n]+((\t.*?[\r\n]+)+)/);
if (typeof rawData != "object" || rawData === null) return;
if (rawData.length < 2) return;
rawData = rawData[1].replace(/[\r\n]+$/, "").replace(/^\t/gm, "");
let parsed;
try {
parsed = getParsed(loadYaml(rawData));
} catch {
return;
}
const records = {
editions: db
.prepare("SELECT * FROM editions WHERE editionID = ?")
.get(parsed.editions.editionID),
pagesets: db
.prepare("SELECT * FROM pagesets WHERE pagesetID = ?")
.get(parsed.pagesets.pagesetID),
tags: db
.prepare("SELECT tag FROM taggings WHERE pageset = ?")
.all(parsed.pagesets.pagesetID)
.map(x => x?.tag)
};
if (records.pagesets === undefined) {
insertKeys(parsed.pagesets, "pagesets");
} else {
updateKeys(parsed.pagesets, records.pagesets, "pagesets");
}
if (records.editions === undefined) {
insertKeys(parsed.editions, "editions");
} else {
updateKeys(parsed.editions, records.editions, "editions");
}
if (parsed.tags !== null) {
const parsedTagIDs = parsed.tags.map(x => x.tagID);
for (const tag of parsed.tags) {
if (
db
.prepare("SELECT * FROM tags WHERE tagID = ?")
.get(tag.tagID) === undefined
) {
insertTag(tag);
}
}
const tagActions = {
base: new Set(records.tags.concat(parsedTagIDs)),
inserted: [],
deleted: []
};
for (const tag of tagActions.base) {
const parsedHas = parsedTagIDs.includes(tag);
const recordsHas = records.tags.includes(tag);
if (parsedHas && !recordsHas) {
tagActions.inserted.push(tag);
}
if (!parsedHas && recordsHas) {
tagActions.deleted.push(tag);
}
}
insertTaggings(parsed.pagesets.pagesetID, tagActions.inserted);
deleteTaggings(parsed.pagesets.pagesetID, tagActions.deleted);
}
});
});
export default app;