@lexho111/plainblog 0.6.14 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Article.js +1 -0
- package/AssetManager.js +215 -0
- package/Auth.js +97 -0
- package/Blog.js +103 -770
- package/Formatter.js +12 -13
- package/blog_test_empty +0 -0
- package/blog_test_load +0 -0
- package/build-styles.js +6 -13
- package/cluster-server.js +4 -4
- package/debug-loader.js +15 -1
- package/index.js +2 -2
- package/model/DataModel.js +6 -6
- package/model/DatabaseModel.js +13 -1
- package/model/FileAdapter.js +59 -7
- package/model/FileModel.js +1 -1
- package/model/PostgresAdapter.js +30 -20
- package/model/SequelizeAdapter.js +33 -18
- package/model/SqliteAdapter.js +194 -43
- package/model/WorkerAdapter.js +137 -0
- package/model/datastructures/BinarySearchTree.js +18 -8
- package/model/datastructures/BinarySearchTreeHashMap.js +1 -1
- package/package.json +4 -1
- package/postinstall.js +6 -6
- package/public/scripts.min.js +8 -2
- package/public/styles.min.css +2 -2
- package/router.js +14 -3
- package/server.js +712 -0
- package/src/fetchData.js +75 -36
- package/src/styles.css +0 -1
- package/temp_style_test_articles.txt +1 -0
- package/workers/compiler-worker.js +15 -0
- package/workers/database-worker.js +110 -0
- package/dependency-graph.html +0 -0
- package/dependency-graph.svg +0 -0
- package/graph.svg +0 -0
- package/model/datastructures/ArrayListHashMap.js.bk +0 -90
package/Article.js
CHANGED
|
@@ -89,6 +89,7 @@ export default class Article {
|
|
|
89
89
|
<h2>${this.title}</h2>
|
|
90
90
|
<span class="datetime">${this.getFormattedDate()}</span>
|
|
91
91
|
<p>${this.getContentShort()}</p>
|
|
92
|
+
<div class="full-content" style="display: none;">${this.content}</div>
|
|
92
93
|
${moreButton}
|
|
93
94
|
</article>`;
|
|
94
95
|
}
|
package/AssetManager.js
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { Worker } from "node:worker_threads";
|
|
6
|
+
import { createDebug } from "./debug-loader.js";
|
|
7
|
+
|
|
8
|
+
const debug = createDebug("plainblog:AssetManager");
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
|
|
12
|
+
export default class AssetManager {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.publicDir = path.join(process.cwd(), "public");
|
|
15
|
+
this.reloadStylesOnGET = false;
|
|
16
|
+
this.stylesheetPath = null;
|
|
17
|
+
this.compilestyle = false;
|
|
18
|
+
this.styles = "body { font-family: Arial; }";
|
|
19
|
+
this.compiledStyles = "";
|
|
20
|
+
this.stylesHash = "";
|
|
21
|
+
this.scriptsHash = "";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
setStyle(style) {
|
|
25
|
+
this.styles += style;
|
|
26
|
+
this.compilestyle = true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async init() {
|
|
30
|
+
// if there is a stylesheet path provided, process it
|
|
31
|
+
if (this.stylesheetPath != null && this.compilestyle) {
|
|
32
|
+
await this.processStylesheets(this.stylesheetPath);
|
|
33
|
+
}
|
|
34
|
+
if (!this.stylesheetPath) {
|
|
35
|
+
// compile and merge hardcoded styles in "this.styles" with "src/styles.css"
|
|
36
|
+
const srcStylePath = path.join(__dirname, "src", "styles.css");
|
|
37
|
+
const publicStylePath = path.join(this.publicDir, "styles.min.css");
|
|
38
|
+
|
|
39
|
+
let publicHash = null;
|
|
40
|
+
let srcStyles = "";
|
|
41
|
+
|
|
42
|
+
await Promise.all([
|
|
43
|
+
fs.promises
|
|
44
|
+
.readFile(publicStylePath, "utf8")
|
|
45
|
+
.then((publicCSS) => {
|
|
46
|
+
const match = publicCSS.match(
|
|
47
|
+
/\/\* source-hash: ([a-f0-9]{64}) \*\//,
|
|
48
|
+
);
|
|
49
|
+
if (match) publicHash = match[1];
|
|
50
|
+
})
|
|
51
|
+
.catch((err) => {
|
|
52
|
+
if (err.code !== "ENOENT") console.error(err);
|
|
53
|
+
}),
|
|
54
|
+
fs.promises
|
|
55
|
+
.readFile(srcStylePath, "utf8")
|
|
56
|
+
.then((content) => {
|
|
57
|
+
srcStyles = content;
|
|
58
|
+
})
|
|
59
|
+
.catch((err) => {
|
|
60
|
+
if (err.code !== "ENOENT") console.error(err);
|
|
61
|
+
}),
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
const combinedStyles = this.styles + srcStyles;
|
|
65
|
+
const srcHash = crypto
|
|
66
|
+
.createHash("sha256")
|
|
67
|
+
.update(combinedStyles)
|
|
68
|
+
.digest("hex");
|
|
69
|
+
|
|
70
|
+
if (srcHash !== publicHash && this.compilestyle) {
|
|
71
|
+
debug("Styles have changed. Recompiling in worker...");
|
|
72
|
+
const finalStyles = await this.runWorker("mergeStyles", [
|
|
73
|
+
this.styles,
|
|
74
|
+
srcStyles,
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
await fs.promises.mkdir(path.dirname(publicStylePath), {
|
|
79
|
+
recursive: true,
|
|
80
|
+
});
|
|
81
|
+
await fs.promises.writeFile(
|
|
82
|
+
publicStylePath,
|
|
83
|
+
finalStyles + `\n/* source-hash: ${srcHash} */`,
|
|
84
|
+
);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.error("Failed to write styles to public folder:", err);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Process Scripts
|
|
92
|
+
const srcScriptPath = path.join(__dirname, "src", "fetchData.js");
|
|
93
|
+
await this.processScripts(srcScriptPath);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async reload() {
|
|
97
|
+
if (this.stylesheetPath != null && this.compilestyle) {
|
|
98
|
+
await this.processStylesheets(this.stylesheetPath);
|
|
99
|
+
}
|
|
100
|
+
const srcScriptPath = path.join(__dirname, "src", "fetchData.js");
|
|
101
|
+
await this.processScripts(srcScriptPath);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async processStylesheets(files) {
|
|
105
|
+
debug("process stylesheets");
|
|
106
|
+
const fileList = Array.isArray(files) ? files : [files];
|
|
107
|
+
const styleFiles = fileList.filter(
|
|
108
|
+
(f) =>
|
|
109
|
+
typeof f === "string" &&
|
|
110
|
+
(f.endsWith(".scss") || f.endsWith(".css")) &&
|
|
111
|
+
!f.endsWith(".min.css"),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
if (styleFiles.length > 0) {
|
|
115
|
+
const fileData = await Promise.all(
|
|
116
|
+
styleFiles.sort().map(async (f) => {
|
|
117
|
+
const content = await fs.promises.readFile(f, "utf-8");
|
|
118
|
+
if (content == "") throw new Error("Invalid Filepath or empty file!");
|
|
119
|
+
return { path: f, content };
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const hash = crypto.createHash("sha256");
|
|
124
|
+
for (const file of fileData) hash.update(file.content);
|
|
125
|
+
const currentHash = hash.digest("hex");
|
|
126
|
+
|
|
127
|
+
if (currentHash !== this.stylesHash && this.compilestyle) {
|
|
128
|
+
console.log("style assets have changed. recompiling in worker...");
|
|
129
|
+
this.stylesHash = currentHash;
|
|
130
|
+
this.compiledStyles = await this.runWorker("styles", fileData);
|
|
131
|
+
|
|
132
|
+
await fs.promises.mkdir(this.publicDir, { recursive: true });
|
|
133
|
+
await fs.promises.writeFile(
|
|
134
|
+
path.join(this.publicDir, "styles.min.css"),
|
|
135
|
+
this.compiledStyles + `\n/* source-hash: ${currentHash} */`,
|
|
136
|
+
);
|
|
137
|
+
} else {
|
|
138
|
+
console.log("styles are up-to-date");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async processScripts(files) {
|
|
144
|
+
const fileList = Array.isArray(files) ? files : [files];
|
|
145
|
+
const scriptFiles = fileList.filter(
|
|
146
|
+
(f) =>
|
|
147
|
+
typeof f === "string" && f.endsWith(".js") && !f.endsWith(".min.js"),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
if (scriptFiles.length > 0) {
|
|
151
|
+
const fileData = await Promise.all(
|
|
152
|
+
scriptFiles.map(async (f) => {
|
|
153
|
+
const content = await fs.promises.readFile(f, "utf-8");
|
|
154
|
+
if (content == "") throw new Error("Invalid Filepath or empty file!");
|
|
155
|
+
return { path: f, content };
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const hash = crypto.createHash("sha256");
|
|
160
|
+
for (const file of fileData) hash.update(file.content);
|
|
161
|
+
const currentHash = hash.digest("hex");
|
|
162
|
+
|
|
163
|
+
if (!this.scriptsHash) {
|
|
164
|
+
try {
|
|
165
|
+
const existing = await fs.promises.readFile(
|
|
166
|
+
path.join(this.publicDir, "scripts.min.js"),
|
|
167
|
+
"utf-8",
|
|
168
|
+
);
|
|
169
|
+
const match = existing.match(/\/\* source-hash: ([a-f0-9]{64}) \*\//);
|
|
170
|
+
if (match) this.scriptsHash = match[1];
|
|
171
|
+
} catch {
|
|
172
|
+
// ignore
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (currentHash !== this.scriptsHash) {
|
|
177
|
+
console.log("script assets have changed. recompiling in worker...");
|
|
178
|
+
this.scriptsHash = currentHash;
|
|
179
|
+
const compiledScripts = await this.runWorker("scripts", fileData);
|
|
180
|
+
|
|
181
|
+
await fs.promises.mkdir(this.publicDir, { recursive: true });
|
|
182
|
+
await fs.promises.writeFile(
|
|
183
|
+
path.join(this.publicDir, "scripts.min.js"),
|
|
184
|
+
compiledScripts + `\n/* source-hash: ${currentHash} */`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
runWorker(type, data) {
|
|
191
|
+
return new Promise((resolve, reject) => {
|
|
192
|
+
const workerData =
|
|
193
|
+
type === "mergeStyles"
|
|
194
|
+
? { type: "mergeStyles", cssContents: data }
|
|
195
|
+
: { type, fileData: data };
|
|
196
|
+
|
|
197
|
+
const worker = new Worker(
|
|
198
|
+
path.resolve(__dirname, "workers", "compiler-worker.js"),
|
|
199
|
+
{ workerData },
|
|
200
|
+
);
|
|
201
|
+
worker.on("message", (msg) => {
|
|
202
|
+
if (msg.status === "success") {
|
|
203
|
+
resolve(msg.result);
|
|
204
|
+
} else {
|
|
205
|
+
reject(new Error(msg.error));
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
worker.on("error", reject);
|
|
209
|
+
worker.on("exit", (code) => {
|
|
210
|
+
if (code !== 0)
|
|
211
|
+
reject(new Error(`Worker stopped with exit code ${code}`));
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
package/Auth.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { URLSearchParams } from "url";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import { createDebug } from "./debug-loader.js";
|
|
4
|
+
import { debug_perf } from "./debug-loader.js";
|
|
5
|
+
|
|
6
|
+
const debug = createDebug("plainblog:Auth");
|
|
7
|
+
|
|
8
|
+
export default class Auth {
|
|
9
|
+
constructor(sessions, password) {
|
|
10
|
+
this.sessions = sessions;
|
|
11
|
+
this.password = password;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
isAuthenticated(req) {
|
|
15
|
+
const time_start = performance.now();
|
|
16
|
+
if (!req.headers.cookie) {
|
|
17
|
+
const time_end = performance.now();
|
|
18
|
+
const duration = time_end - time_start;
|
|
19
|
+
debug_perf("isAuthenticated took " + duration + "ms", duration);
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const match = req.headers.cookie.match(/(?:^|;\s*)session=([^;]*)/);
|
|
23
|
+
const result = match ? this.sessions.has(match[1]) : false;
|
|
24
|
+
const time_end = performance.now();
|
|
25
|
+
const duration = time_end - time_start;
|
|
26
|
+
debug_perf("isAuthenticated took " + duration + "ms", duration);
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async handleLogin(req, res, readBody, timeout, maxSize) {
|
|
31
|
+
debug("handle login request");
|
|
32
|
+
try {
|
|
33
|
+
const body = await readBody(req, timeout, maxSize);
|
|
34
|
+
const params = new URLSearchParams(body);
|
|
35
|
+
const receivedPassword = params.get("password");
|
|
36
|
+
|
|
37
|
+
if (receivedPassword === this.password) {
|
|
38
|
+
const id = crypto.randomUUID();
|
|
39
|
+
this.sessions.add(id);
|
|
40
|
+
// Limit session count to prevent memory exhaustion
|
|
41
|
+
if (this.sessions.size > 10000) {
|
|
42
|
+
debug("Warning: too many sessions, clearing oldest");
|
|
43
|
+
const sessionsArray = Array.from(this.sessions);
|
|
44
|
+
for (let i = 0; i < 1000; i++) {
|
|
45
|
+
this.sessions.delete(sessionsArray[i]);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (!res.headersSent) {
|
|
49
|
+
res.writeHead(303, {
|
|
50
|
+
"Set-Cookie": `session=${id}; Path=/; SameSite=Lax`,
|
|
51
|
+
"X-Session-ID": id,
|
|
52
|
+
Location: "/",
|
|
53
|
+
});
|
|
54
|
+
res.end();
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
if (!res.headersSent) {
|
|
58
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
59
|
+
res.end(
|
|
60
|
+
JSON.stringify({
|
|
61
|
+
error: "unauthorized",
|
|
62
|
+
message: "Please enter the correct password.",
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch (loginErr) {
|
|
68
|
+
debug("Login error: %s", loginErr.message);
|
|
69
|
+
if (!res.headersSent) {
|
|
70
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
71
|
+
res.end(
|
|
72
|
+
JSON.stringify({
|
|
73
|
+
error: "Bad Request",
|
|
74
|
+
message: "Invalid request body",
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
handleLogout(req, res) {
|
|
82
|
+
if (req.headers.cookie) {
|
|
83
|
+
const params = new URLSearchParams(
|
|
84
|
+
req.headers.cookie.replace(/; /g, "&"),
|
|
85
|
+
);
|
|
86
|
+
const sessionId = params.get("session");
|
|
87
|
+
if (this.sessions.has(sessionId)) {
|
|
88
|
+
this.sessions.delete(sessionId);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
res.writeHead(303, {
|
|
92
|
+
"Set-Cookie": "session=; HttpOnly; Path=/; Max-Age=0",
|
|
93
|
+
Location: "/",
|
|
94
|
+
});
|
|
95
|
+
res.end();
|
|
96
|
+
}
|
|
97
|
+
}
|