@lexho111/plainblog 0.5.10 → 0.5.12
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/Blog.js +242 -157
- package/Formatter.js +20 -3
- package/build-styles.js +24 -17
- package/eslint.config.js +27 -45
- package/knip.json +10 -0
- package/model/FileAdapter.js +5 -5
- package/model/FileModel.js +4 -0
- package/model/PostgresAdapter.js +2 -2
- package/model/SequelizeAdapter.js +25 -20
- package/model/SqliteAdapter.js +2 -2
- package/package.json +10 -14
- package/public/styles.min.css +2 -2
- package/test/blog.test.js +120 -6
- package/test/simpleServer.js +0 -2
- package/.eslintignore +0 -0
- package/.eslintrc.json +0 -0
- package/articles.txt +0 -11
- package/blog.db +0 -0
- package/blog.json +0 -110
- package/bloginfo.json +0 -3
- package/lexho111-plainblog-0.4.1.tgz +0 -0
- package/lexho111-plainblog-0.5.7.tgz +0 -0
package/Blog.js
CHANGED
|
@@ -5,16 +5,11 @@ import { URLSearchParams } from "url";
|
|
|
5
5
|
import Article from "./Article.js";
|
|
6
6
|
import DatabaseModel from "./model/DatabaseModel.js";
|
|
7
7
|
import { fetchData, postData } from "./model/APIModel.js";
|
|
8
|
-
import { formatHTML, header, formatMarkdown, validate } from "./Formatter.js";
|
|
9
|
-
import pkg from "./package.json" with { type: "json" };
|
|
8
|
+
import { formatHTML, header, formatMarkdown, validate } from "./Formatter.js"; // import pkg from "./package.json" with { type: "json" };
|
|
10
9
|
import path from "path";
|
|
11
10
|
import { fileURLToPath } from "url";
|
|
12
|
-
import { exec } from "child_process";
|
|
13
|
-
import { promisify } from "util";
|
|
14
11
|
import { compileStyles, mergeStyles } from "./build-styles.js";
|
|
15
12
|
|
|
16
|
-
const execPromise = promisify(exec);
|
|
17
|
-
|
|
18
13
|
export default class Blog {
|
|
19
14
|
constructor() {
|
|
20
15
|
this.database = {
|
|
@@ -22,33 +17,60 @@ export default class Blog {
|
|
|
22
17
|
username: "user",
|
|
23
18
|
password: "password",
|
|
24
19
|
host: "localhost",
|
|
25
|
-
dbname: "
|
|
20
|
+
dbname: "articles.txt", // x
|
|
26
21
|
};
|
|
27
22
|
this.#title = "";
|
|
28
|
-
this
|
|
29
|
-
this.filename = null;
|
|
23
|
+
this.#articles = [];
|
|
30
24
|
this.#server = null;
|
|
31
25
|
this.#password = "admin";
|
|
32
|
-
this
|
|
33
|
-
this.scripts = "";
|
|
26
|
+
this.#styles = "body { font-family: Arial; }";
|
|
27
|
+
//this.scripts = "";
|
|
34
28
|
this.compiledStyles = "";
|
|
35
|
-
this.compiledScripts = "";
|
|
29
|
+
//this.compiledScripts = "";
|
|
36
30
|
this.reloadStylesOnGET = false;
|
|
37
31
|
this.sessions = new Set();
|
|
38
32
|
|
|
39
|
-
|
|
40
|
-
console.log(`version: ${version}`);
|
|
33
|
+
this.#version = "0.0.1"; //pkg.version;
|
|
34
|
+
console.log(`version: ${this.#version}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** @returns a json representation of the blog */
|
|
38
|
+
json() {
|
|
39
|
+
const serverInfo = this.#server
|
|
40
|
+
? {
|
|
41
|
+
listening: this.#server.listening,
|
|
42
|
+
address: this.#server.address(),
|
|
43
|
+
}
|
|
44
|
+
: null;
|
|
45
|
+
|
|
46
|
+
const json = {
|
|
47
|
+
version: this.#version,
|
|
48
|
+
title: this.#title,
|
|
49
|
+
articles: this.#articles,
|
|
50
|
+
server: serverInfo,
|
|
51
|
+
compiledStyles: this.compiledStyles,
|
|
52
|
+
sessions: this.sessions,
|
|
53
|
+
database: this.database,
|
|
54
|
+
password: this.#password,
|
|
55
|
+
styles: this.#styles,
|
|
56
|
+
reloadStylesOnGET: this.reloadStylesOnGET,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return JSON.parse(JSON.stringify(json));
|
|
41
60
|
}
|
|
42
61
|
|
|
43
62
|
// Private fields
|
|
63
|
+
#version = null;
|
|
44
64
|
#server = null;
|
|
45
65
|
#password = null;
|
|
46
66
|
#databaseModel;
|
|
47
67
|
#isExternalAPI = false;
|
|
48
68
|
#apiUrl = "";
|
|
49
69
|
#title = "";
|
|
70
|
+
#articles = [];
|
|
71
|
+
#styles = "";
|
|
50
72
|
#stylesHash = "";
|
|
51
|
-
|
|
73
|
+
//#scriptsHash = "";
|
|
52
74
|
#stylesheetPath = "";
|
|
53
75
|
|
|
54
76
|
setTitle(title) {
|
|
@@ -60,7 +82,7 @@ export default class Blog {
|
|
|
60
82
|
}
|
|
61
83
|
|
|
62
84
|
setStyle(style) {
|
|
63
|
-
this
|
|
85
|
+
this.#styles += style;
|
|
64
86
|
}
|
|
65
87
|
|
|
66
88
|
set title(t) {
|
|
@@ -69,7 +91,7 @@ export default class Blog {
|
|
|
69
91
|
this.#databaseModel = new DatabaseModel(this.database);
|
|
70
92
|
}
|
|
71
93
|
console.log(`connected to database`);
|
|
72
|
-
if(t != this.#title && t.length == 0)
|
|
94
|
+
if (t != this.#title && t.length == 0)
|
|
73
95
|
this.#databaseModel.updateBlogTitle(t);
|
|
74
96
|
}
|
|
75
97
|
|
|
@@ -81,6 +103,10 @@ export default class Blog {
|
|
|
81
103
|
this.#password = x;
|
|
82
104
|
}
|
|
83
105
|
|
|
106
|
+
/**
|
|
107
|
+
* allows you to inject a specific database implementation
|
|
108
|
+
* @param {*} adapter a database adapter like PostgresAdapter or SqliteAdapter
|
|
109
|
+
*/
|
|
84
110
|
setDatabaseAdapter(adapter) {
|
|
85
111
|
if (!this.#databaseModel) {
|
|
86
112
|
this.#databaseModel = new DatabaseModel(this.database);
|
|
@@ -88,112 +114,134 @@ export default class Blog {
|
|
|
88
114
|
this.#databaseModel.setDatabaseAdapter(adapter);
|
|
89
115
|
}
|
|
90
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Appends CSS rules to the \<style\>-tag.
|
|
119
|
+
* @param {string} style - A string containing CSS rules.
|
|
120
|
+
*/
|
|
91
121
|
set style(style) {
|
|
92
|
-
this
|
|
122
|
+
this.#styles += style;
|
|
93
123
|
}
|
|
94
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Sets the path(s) to custom CSS or SCSS files to be compiled and used by the blog.
|
|
127
|
+
* @param {string|string[]} files - A single file path or an array of file paths.
|
|
128
|
+
*/
|
|
95
129
|
set stylesheetPath(files) {
|
|
96
130
|
this.#stylesheetPath = files;
|
|
97
|
-
console.log(`this.#stylesheetPath: ${this.#stylesheetPath}`)
|
|
131
|
+
console.log(`this.#stylesheetPath: ${this.#stylesheetPath}`);
|
|
98
132
|
}
|
|
99
133
|
|
|
100
134
|
addArticle(article) {
|
|
101
|
-
this
|
|
135
|
+
this.#articles.push(article);
|
|
102
136
|
}
|
|
103
137
|
|
|
104
|
-
isAuthenticated(req) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
138
|
+
#isAuthenticated(req) {
|
|
139
|
+
if (!req.headers.cookie) return false;
|
|
140
|
+
const params = new URLSearchParams(req.headers.cookie.replace(/; /g, "&"));
|
|
141
|
+
return this.sessions.has(params.get("session"));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async #handleLogin(req, res) {
|
|
145
|
+
const body = await new Promise((resolve, reject) => {
|
|
146
|
+
let data = "";
|
|
147
|
+
req.on("data", (chunk) => (data += chunk.toString()));
|
|
148
|
+
req.on("end", () => resolve(data));
|
|
149
|
+
req.on("error", reject);
|
|
150
|
+
});
|
|
151
|
+
const params = new URLSearchParams(body);
|
|
152
|
+
|
|
153
|
+
if (params.get("password") === this.#password) {
|
|
154
|
+
const id = crypto.randomUUID();
|
|
155
|
+
this.sessions.add(id);
|
|
156
|
+
res.writeHead(303, {
|
|
157
|
+
"Set-Cookie": `session=${id}; HttpOnly; Path=/`,
|
|
158
|
+
Location: "/",
|
|
116
159
|
});
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
this.sessions.add(id);
|
|
122
|
-
res.writeHead(303, {
|
|
123
|
-
"Set-Cookie": `session=${id}; HttpOnly; Path=/`,
|
|
124
|
-
Location: "/",
|
|
125
|
-
});
|
|
126
|
-
res.end();
|
|
127
|
-
} else {
|
|
128
|
-
res.writeHead(401, { "Content-Type": "text/html" });
|
|
129
|
-
res.end(`${header("My Blog")}
|
|
160
|
+
res.end();
|
|
161
|
+
} else {
|
|
162
|
+
res.writeHead(401, { "Content-Type": "text/html" });
|
|
163
|
+
res.end(`${header("My Blog")}
|
|
130
164
|
<body>
|
|
131
165
|
<h1>Unauthorized</h1><p>Please enter the password.<form method="POST">
|
|
132
166
|
<input type="password" name="password" placeholder="Password" />
|
|
133
167
|
<button style="margin: 2px;">Login</button></form>
|
|
134
168
|
</body></html>`);
|
|
135
|
-
}
|
|
136
169
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
#handleLogout(req, res) {
|
|
173
|
+
if (req.headers.cookie) {
|
|
174
|
+
const params = new URLSearchParams(
|
|
175
|
+
req.headers.cookie.replace(/; /g, "&")
|
|
176
|
+
);
|
|
177
|
+
const sessionId = params.get("session");
|
|
178
|
+
if (this.sessions.has(sessionId)) {
|
|
179
|
+
this.sessions.delete(sessionId);
|
|
145
180
|
}
|
|
146
|
-
res.writeHead(303, {
|
|
147
|
-
"Set-Cookie": "session=; HttpOnly; Path=/; Max-Age=0",
|
|
148
|
-
Location: "/",
|
|
149
|
-
});
|
|
150
|
-
res.end();
|
|
151
181
|
}
|
|
182
|
+
res.writeHead(303, {
|
|
183
|
+
"Set-Cookie": "session=; HttpOnly; Path=/; Max-Age=0",
|
|
184
|
+
Location: "/",
|
|
185
|
+
});
|
|
186
|
+
res.end();
|
|
187
|
+
}
|
|
152
188
|
|
|
153
189
|
/** initializes database */
|
|
154
190
|
async init() {
|
|
155
|
-
//await this.buildFrontend();
|
|
156
191
|
//this.loadStyles();
|
|
157
192
|
//this.loadScripts();
|
|
158
|
-
if
|
|
159
|
-
|
|
193
|
+
// if there is a stylesheet path provided, process it
|
|
194
|
+
if (this.#stylesheetPath != null) {
|
|
195
|
+
// read file from stylesheet path, compare checksums and write to public/styles.min.css
|
|
196
|
+
await this.#processStylesheets(this.#stylesheetPath);
|
|
160
197
|
}
|
|
161
|
-
if(!this.#stylesheetPath) {
|
|
162
|
-
//
|
|
198
|
+
if (!this.#stylesheetPath) {
|
|
199
|
+
// this.#styles
|
|
200
|
+
// src/styles.css
|
|
201
|
+
// compile and merge hardcoded styles in "this.#styles" with "src/styles.css" and write to file "styles.min.css"
|
|
163
202
|
// which will be imported by webbrowser via '<link rel="stylesheet" href="styles.min.css"...'
|
|
164
|
-
|
|
203
|
+
|
|
165
204
|
const __filename = fileURLToPath(import.meta.url);
|
|
166
205
|
const __dirname = path.dirname(__filename);
|
|
167
206
|
const srcStylePath = path.join(__dirname, "src", "styles.css");
|
|
168
207
|
const publicStylePath = path.join(__dirname, "public", "styles.min.css");
|
|
169
208
|
|
|
170
209
|
let publicHash = null;
|
|
171
|
-
try {
|
|
172
|
-
const publicCSS = await fs.promises.readFile(publicStylePath, "utf8");
|
|
173
|
-
const match = publicCSS.match(/\/\* source-hash: ([a-f0-9]{64}) \*\//);
|
|
174
|
-
if (match) {
|
|
175
|
-
publicHash = match[1];
|
|
176
|
-
}
|
|
177
|
-
} catch (err) {
|
|
178
|
-
// public/styles.min.css doesn't exist, will be created.
|
|
179
|
-
}
|
|
180
|
-
|
|
181
210
|
let srcStyles = "";
|
|
182
|
-
try {
|
|
183
|
-
srcStyles = await fs.promises.readFile(srcStylePath, "utf8");
|
|
184
|
-
} catch (err) {
|
|
185
|
-
// ignore if src/styles.css doesn't exist
|
|
186
|
-
}
|
|
187
211
|
|
|
188
|
-
|
|
189
|
-
|
|
212
|
+
await Promise.all([
|
|
213
|
+
fs.promises
|
|
214
|
+
.readFile(publicStylePath, "utf8")
|
|
215
|
+
.then((publicCSS) => {
|
|
216
|
+
const match = publicCSS.match(
|
|
217
|
+
/\/\* source-hash: ([a-f0-9]{64}) \*\//
|
|
218
|
+
);
|
|
219
|
+
if (match) publicHash = match[1];
|
|
220
|
+
})
|
|
221
|
+
.catch((err) => console.error(err)), // public/styles.min.css doesn't exist, will be created.
|
|
222
|
+
fs.promises
|
|
223
|
+
.readFile(srcStylePath, "utf8")
|
|
224
|
+
.then((content) => {
|
|
225
|
+
srcStyles = content;
|
|
226
|
+
})
|
|
227
|
+
.catch((err) => console.error(err)), // ignore if src/styles.css doesn't exist
|
|
228
|
+
]);
|
|
229
|
+
|
|
230
|
+
const combinedStyles = this.#styles + srcStyles;
|
|
231
|
+
const srcHash = crypto
|
|
232
|
+
.createHash("sha256")
|
|
233
|
+
.update(combinedStyles)
|
|
234
|
+
.digest("hex");
|
|
190
235
|
|
|
191
236
|
if (srcHash !== publicHash) {
|
|
192
237
|
console.log("Styles have changed. Recompiling...");
|
|
193
|
-
const finalStyles = await mergeStyles(this
|
|
238
|
+
const finalStyles = await mergeStyles(this.#styles, srcStyles);
|
|
194
239
|
try {
|
|
195
|
-
await fs.promises.mkdir(path.dirname(publicStylePath), { recursive: true });
|
|
196
|
-
await fs.promises.writeFile(
|
|
240
|
+
//await fs.promises.mkdir(path.dirname(publicStylePath), { recursive: true });
|
|
241
|
+
await fs.promises.writeFile(
|
|
242
|
+
publicStylePath,
|
|
243
|
+
finalStyles + `\n/* source-hash: ${srcHash} */`
|
|
244
|
+
);
|
|
197
245
|
} catch (err) {
|
|
198
246
|
console.error("Failed to write styles to public folder:", err);
|
|
199
247
|
}
|
|
@@ -201,7 +249,7 @@ export default class Blog {
|
|
|
201
249
|
}
|
|
202
250
|
if (this.#isExternalAPI) {
|
|
203
251
|
console.log("external API");
|
|
204
|
-
await this
|
|
252
|
+
await this.#loadFromAPI();
|
|
205
253
|
} else {
|
|
206
254
|
console.log(`database: ${this.database.type}`);
|
|
207
255
|
if (!this.#databaseModel) {
|
|
@@ -209,17 +257,32 @@ export default class Blog {
|
|
|
209
257
|
}
|
|
210
258
|
console.log(`connected to database`);
|
|
211
259
|
await this.#databaseModel.initialize();
|
|
212
|
-
const dbTitle = await
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
dbArticles.push(
|
|
260
|
+
const [dbTitle, dbArticles] = await Promise.all([
|
|
261
|
+
this.#databaseModel.getBlogTitle(),
|
|
262
|
+
this.#databaseModel.findAll(),
|
|
263
|
+
]);
|
|
264
|
+
|
|
265
|
+
if (dbArticles.length == 0) {
|
|
266
|
+
dbArticles.push(
|
|
267
|
+
new Article(
|
|
268
|
+
"Sample Entry #1",
|
|
269
|
+
"Prow scuttle parrel provost Sail ho shrouds spirits boom mizzenmast yardarm. Pinnace holystone mizzenmast quarter crow's nest nipperkin grog yardarm hempen halter furl. Swab barque interloper chantey doubloon starboard grog black jack gangway rutters.",
|
|
270
|
+
new Date()
|
|
271
|
+
)
|
|
272
|
+
);
|
|
273
|
+
dbArticles.push(
|
|
274
|
+
new Article(
|
|
275
|
+
"Sample Entry #2",
|
|
276
|
+
"Deadlights jack lad schooner scallywag dance the hempen jig carouser broadside cable strike colors. Bring a spring upon her cable holystone blow the man down spanker Shiver me timbers to go on account lookout wherry doubloon chase. Belay yo-ho-ho keelhaul squiffy black spot yardarm spyglass sheet transom heave to.",
|
|
277
|
+
new Date()
|
|
278
|
+
)
|
|
279
|
+
);
|
|
219
280
|
}
|
|
220
|
-
if(this.reloadStylesOnGET)
|
|
281
|
+
if (this.reloadStylesOnGET)
|
|
282
|
+
console.log("reload scripts and styles on GET-Request");
|
|
221
283
|
let title = "";
|
|
222
|
-
if (this.#title != null && this.#title.length > 0)
|
|
284
|
+
if (this.#title != null && this.#title.length > 0)
|
|
285
|
+
title = this.#title; // use blog title if set
|
|
223
286
|
else title = dbTitle; // use title from the database
|
|
224
287
|
const responseData = { title: title, articles: dbArticles };
|
|
225
288
|
this.#applyBlogData(responseData);
|
|
@@ -244,12 +307,16 @@ export default class Blog {
|
|
|
244
307
|
const newArticleData = { title, content };
|
|
245
308
|
try {
|
|
246
309
|
// Save the new article to the database via the ApiServer
|
|
247
|
-
|
|
248
|
-
if (this.#
|
|
310
|
+
const promises = [];
|
|
311
|
+
if (this.#databaseModel)
|
|
312
|
+
promises.push(this.#databaseModel.save(newArticleData));
|
|
313
|
+
if (this.#isExternalAPI)
|
|
314
|
+
promises.push(postData(this.#apiUrl, newArticleData));
|
|
315
|
+
await Promise.all(promises);
|
|
249
316
|
// Add the article to the local list for immediate display
|
|
250
|
-
this
|
|
317
|
+
this.#articles.unshift(new Article(title, content, new Date()));
|
|
251
318
|
// remove sample entries
|
|
252
|
-
this
|
|
319
|
+
this.#articles = this.#articles.filter(
|
|
253
320
|
(art) =>
|
|
254
321
|
art.title !== "Sample Entry #1" && art.title !== "Sample Entry #2"
|
|
255
322
|
);
|
|
@@ -286,18 +353,18 @@ export default class Blog {
|
|
|
286
353
|
</body></html>`);
|
|
287
354
|
return;
|
|
288
355
|
} else if (req.method === "POST") {
|
|
289
|
-
await this
|
|
356
|
+
await this.#handleLogin(req, res);
|
|
290
357
|
return;
|
|
291
358
|
}
|
|
292
359
|
}
|
|
293
360
|
|
|
294
361
|
if (req.url === "/logout") {
|
|
295
|
-
this
|
|
362
|
+
this.#handleLogout(req, res);
|
|
296
363
|
return;
|
|
297
364
|
}
|
|
298
365
|
// POST new article
|
|
299
366
|
if (req.method === "POST" && req.url === "/") {
|
|
300
|
-
if (!this
|
|
367
|
+
if (!this.#isAuthenticated(req)) {
|
|
301
368
|
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
302
369
|
res.end("Forbidden");
|
|
303
370
|
return;
|
|
@@ -308,14 +375,14 @@ export default class Blog {
|
|
|
308
375
|
// load articles
|
|
309
376
|
|
|
310
377
|
// reload styles and scripts on (every) request
|
|
311
|
-
if(this.reloadStylesOnGET) {
|
|
378
|
+
if (this.reloadStylesOnGET) {
|
|
312
379
|
if (this.#stylesheetPath) {
|
|
313
|
-
await this
|
|
380
|
+
await this.#processStylesheets(this.#stylesheetPath);
|
|
314
381
|
}
|
|
315
382
|
}
|
|
316
383
|
|
|
317
384
|
let loggedin = false;
|
|
318
|
-
if (!this
|
|
385
|
+
if (!this.#isAuthenticated(req)) {
|
|
319
386
|
// login
|
|
320
387
|
loggedin = false;
|
|
321
388
|
} else {
|
|
@@ -338,7 +405,10 @@ export default class Blog {
|
|
|
338
405
|
const __filename = fileURLToPath(import.meta.url);
|
|
339
406
|
const __dirname = path.dirname(__filename);
|
|
340
407
|
const publicDir = path.join(__dirname, "public");
|
|
341
|
-
const parsedUrl = new URL(
|
|
408
|
+
const parsedUrl = new URL(
|
|
409
|
+
req.url,
|
|
410
|
+
`http://${req.headers.host || "localhost"}`
|
|
411
|
+
);
|
|
342
412
|
const filePath = path.join(publicDir, parsedUrl.pathname);
|
|
343
413
|
|
|
344
414
|
if (filePath.startsWith(publicDir)) {
|
|
@@ -363,6 +433,7 @@ export default class Blog {
|
|
|
363
433
|
}
|
|
364
434
|
}
|
|
365
435
|
} catch (err) {
|
|
436
|
+
console.error(err);
|
|
366
437
|
// Continue to 404
|
|
367
438
|
}
|
|
368
439
|
|
|
@@ -374,8 +445,11 @@ export default class Blog {
|
|
|
374
445
|
|
|
375
446
|
this.#server = server;
|
|
376
447
|
|
|
377
|
-
return new Promise((resolve) => {
|
|
378
|
-
|
|
448
|
+
return new Promise((resolve, reject) => {
|
|
449
|
+
const errorHandler = (err) => reject(err);
|
|
450
|
+
this.#server.once("error", errorHandler);
|
|
451
|
+
this.#server.listen(port, "127.0.0.1", () => {
|
|
452
|
+
this.#server.removeListener("error", errorHandler);
|
|
379
453
|
console.log(`server running at http://localhost:${port}/`);
|
|
380
454
|
resolve(); // Resolve the promise when the server is listening
|
|
381
455
|
});
|
|
@@ -385,12 +459,14 @@ export default class Blog {
|
|
|
385
459
|
async closeServer() {
|
|
386
460
|
return new Promise((resolve, reject) => {
|
|
387
461
|
if (this.#server) {
|
|
462
|
+
// if server is running
|
|
388
463
|
this.#server.close((err) => {
|
|
389
|
-
if (err) return reject(err);
|
|
464
|
+
if (err && err.code !== "ERR_SERVER_NOT_RUNNING") return reject(err);
|
|
390
465
|
console.log("Server closed.");
|
|
391
466
|
resolve();
|
|
392
467
|
});
|
|
393
468
|
} else {
|
|
469
|
+
// server is not running
|
|
394
470
|
resolve(); // Nothing to close
|
|
395
471
|
}
|
|
396
472
|
});
|
|
@@ -398,19 +474,23 @@ export default class Blog {
|
|
|
398
474
|
|
|
399
475
|
/** Populates the blog's title and articles from a data object. */
|
|
400
476
|
#applyBlogData(data) {
|
|
401
|
-
this
|
|
477
|
+
this.#articles = []; // Clear existing articles before loading new ones
|
|
402
478
|
this.#title = data.title;
|
|
403
|
-
// Assuming
|
|
479
|
+
// Assuming data contains a title and an array of articles with title and content
|
|
404
480
|
if (data.articles && Array.isArray(data.articles)) {
|
|
405
481
|
for (const articleData of data.articles) {
|
|
406
|
-
const article = new Article(
|
|
407
|
-
|
|
482
|
+
const article = new Article(
|
|
483
|
+
articleData.title,
|
|
484
|
+
articleData.content,
|
|
485
|
+
articleData.createdAt
|
|
486
|
+
);
|
|
487
|
+
article.id = articleData.id; // TODO x
|
|
408
488
|
this.addArticle(article);
|
|
409
489
|
}
|
|
410
490
|
}
|
|
411
491
|
}
|
|
412
492
|
|
|
413
|
-
async loadFromAPI() {
|
|
493
|
+
async #loadFromAPI() {
|
|
414
494
|
const data = await fetchData(this.#apiUrl);
|
|
415
495
|
if (data) {
|
|
416
496
|
this.#applyBlogData(data);
|
|
@@ -423,35 +503,40 @@ export default class Blog {
|
|
|
423
503
|
const url = new URL(req.url, `http://${req.headers.host || "localhost"}`);
|
|
424
504
|
const pathname = url.pathname;
|
|
425
505
|
|
|
426
|
-
if(req.method === "GET") {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
506
|
+
if (req.method === "GET") {
|
|
507
|
+
if (pathname === "/api" || pathname === "/api/") {
|
|
508
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
509
|
+
const data = {
|
|
510
|
+
title: this.title,
|
|
511
|
+
};
|
|
512
|
+
res.end(JSON.stringify(data));
|
|
513
|
+
}
|
|
514
|
+
// GET all blog data
|
|
515
|
+
if (pathname === "/api/articles") {
|
|
516
|
+
// Use 'offset' param as startId (filter) to get items starting at ID
|
|
517
|
+
const pStartID = parseInt(url.searchParams.get("startID"));
|
|
518
|
+
const startID = !isNaN(pStartID) ? pStartID : null;
|
|
519
|
+
const pEndID = parseInt(url.searchParams.get("endID"));
|
|
520
|
+
const endID = !isNaN(pEndID) ? pEndID : null;
|
|
521
|
+
const limit = parseInt(url.searchParams.get("limit")) || 10;
|
|
522
|
+
// controller
|
|
523
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
524
|
+
const dbArticles = await this.#databaseModel.findAll(
|
|
525
|
+
limit,
|
|
526
|
+
0,
|
|
527
|
+
startID,
|
|
528
|
+
endID
|
|
529
|
+
);
|
|
530
|
+
const responseData = {
|
|
531
|
+
title: this.title, // Keep the title from the original constant
|
|
532
|
+
articles: dbArticles,
|
|
533
|
+
};
|
|
534
|
+
res.end(JSON.stringify(responseData));
|
|
431
535
|
}
|
|
432
|
-
res.end(JSON.stringify(data));
|
|
433
|
-
}
|
|
434
|
-
// GET all blog data
|
|
435
|
-
if (pathname === "/api/articles") {
|
|
436
|
-
// Use 'offset' param as startId (filter) to get items starting at ID
|
|
437
|
-
const pStartID = parseInt(url.searchParams.get("startID"));
|
|
438
|
-
const startID = !isNaN(pStartID) ? pStartID : null;
|
|
439
|
-
const pEndID = parseInt(url.searchParams.get("endID"));
|
|
440
|
-
const endID = !isNaN(pEndID) ? pEndID : null;
|
|
441
|
-
const limit = parseInt(url.searchParams.get("limit")) || 10;
|
|
442
|
-
// controller
|
|
443
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
444
|
-
const dbArticles = await this.#databaseModel.findAll(limit, 0, startID, endID);
|
|
445
|
-
const responseData = {
|
|
446
|
-
title: this.title, // Keep the title from the original constant
|
|
447
|
-
articles: dbArticles,
|
|
448
|
-
};
|
|
449
|
-
res.end(JSON.stringify(responseData));
|
|
450
|
-
}
|
|
451
536
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
if (!this
|
|
537
|
+
// POST a new article
|
|
538
|
+
} else if (req.method === "POST" && pathname === "/api/articles") {
|
|
539
|
+
if (!this.#isAuthenticated(req)) {
|
|
455
540
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
456
541
|
res.end(JSON.stringify({ error: "Forbidden" }));
|
|
457
542
|
return;
|
|
@@ -481,7 +566,7 @@ export default class Blog {
|
|
|
481
566
|
print() {
|
|
482
567
|
const data = {
|
|
483
568
|
title: this.title,
|
|
484
|
-
articles: this
|
|
569
|
+
articles: this.#articles,
|
|
485
570
|
};
|
|
486
571
|
const markdown = formatMarkdown(data);
|
|
487
572
|
console.log(markdown);
|
|
@@ -491,12 +576,12 @@ export default class Blog {
|
|
|
491
576
|
async toHTML(loggedin) {
|
|
492
577
|
const data = {
|
|
493
578
|
title: this.title,
|
|
494
|
-
articles: this
|
|
579
|
+
articles: this.#articles,
|
|
495
580
|
loggedin,
|
|
496
|
-
login: ""
|
|
581
|
+
login: "",
|
|
497
582
|
};
|
|
498
583
|
|
|
499
|
-
if(loggedin) data.login = `<a href="/logout">logout</a>`;
|
|
584
|
+
if (loggedin) data.login = `<a href="/logout">logout</a>`;
|
|
500
585
|
else data.login = `<a href="/login">login</a>`;
|
|
501
586
|
|
|
502
587
|
const html = formatHTML(data);
|
|
@@ -505,21 +590,21 @@ export default class Blog {
|
|
|
505
590
|
}
|
|
506
591
|
|
|
507
592
|
/**
|
|
508
|
-
*
|
|
593
|
+
* read files, compare checksums, compile and write to public/styles.min.css
|
|
509
594
|
* @param {string[]} files - Array of css/scss file paths to process.
|
|
510
595
|
*/
|
|
511
|
-
async processStylesheets(files) {
|
|
512
|
-
console.log("process stylesheets")
|
|
513
|
-
|
|
596
|
+
async #processStylesheets(files) {
|
|
597
|
+
console.log("process stylesheets");
|
|
598
|
+
|
|
514
599
|
// Normalize input to array (handles string or array)
|
|
515
600
|
// "file1.css" --> ["file1.css"]
|
|
516
601
|
// ["file1.css", "file2.css",...]
|
|
517
602
|
const fileList = Array.isArray(files) ? files : [files];
|
|
518
|
-
|
|
519
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
520
|
-
const __dirname = path.dirname(__filename);
|
|
521
603
|
const styleFiles = fileList.filter(
|
|
522
|
-
(f) =>
|
|
604
|
+
(f) =>
|
|
605
|
+
typeof f === "string" &&
|
|
606
|
+
(f.endsWith(".scss") || f.endsWith(".css")) &&
|
|
607
|
+
!f.endsWith(".min.css")
|
|
523
608
|
);
|
|
524
609
|
//const scriptFiles = files.filter((f) => f.endsWith(".js") && !f.endsWith(".min.js"));
|
|
525
610
|
|
|
@@ -529,7 +614,7 @@ export default class Blog {
|
|
|
529
614
|
const fileData = await Promise.all(
|
|
530
615
|
styleFiles.sort().map(async (f) => {
|
|
531
616
|
const content = await fs.promises.readFile(f, "utf-8");
|
|
532
|
-
if(content == "") throw new Error("Invalid Filepath or empty file!");
|
|
617
|
+
if (content == "") throw new Error("Invalid Filepath or empty file!");
|
|
533
618
|
return { path: f, content };
|
|
534
619
|
})
|
|
535
620
|
);
|
|
@@ -550,7 +635,7 @@ export default class Blog {
|
|
|
550
635
|
if (currentHash !== this.#stylesHash) {
|
|
551
636
|
console.log("Style assets have changed. Recompiling...");
|
|
552
637
|
this.#stylesHash = currentHash;
|
|
553
|
-
|
|
638
|
+
|
|
554
639
|
// Compile styles using the standalone script from build-styles.js
|
|
555
640
|
this.compiledStyles = await compileStyles(fileData);
|
|
556
641
|
|
|
@@ -558,13 +643,13 @@ export default class Blog {
|
|
|
558
643
|
const __filename = fileURLToPath(import.meta.url);
|
|
559
644
|
const __dirname = path.dirname(__filename);
|
|
560
645
|
const publicDir = path.join(__dirname, "public");
|
|
561
|
-
|
|
646
|
+
|
|
562
647
|
await fs.promises.writeFile(
|
|
563
648
|
path.join(publicDir, "styles.min.css"),
|
|
564
649
|
this.compiledStyles + `\n/* source-hash: ${currentHash} */`
|
|
565
650
|
);
|
|
566
651
|
} else {
|
|
567
|
-
console.log("styles are up-to-date")
|
|
652
|
+
console.log("styles are up-to-date");
|
|
568
653
|
}
|
|
569
654
|
}
|
|
570
655
|
}
|