@lexho111/plainblog 0.7.5 → 0.8.1

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.
Files changed (71) hide show
  1. package/Article.js +91 -85
  2. package/AssetManager.js +12 -4
  3. package/Auth.js +93 -36
  4. package/Blog.js +167 -53
  5. package/Formatter.js +92 -39
  6. package/README.md +21 -6
  7. package/cluster-server.js +1 -1
  8. package/dist/Article.js +92 -0
  9. package/dist/FileAdapter.js +1 -0
  10. package/dist/model/Article.interface.js +1 -0
  11. package/dist/model/DatabaseAdapter.js +1 -0
  12. package/dist/model/FileAdapter.js +148 -0
  13. package/dist/model/FileModel.js +102 -0
  14. package/dist/model/SqliteAdapter.js +216 -0
  15. package/dist/model/datastructures/BinarySearchTreeWithInvertedIndex.js +351 -0
  16. package/dist/model/datastructures/InvertedIndexSearch.js +96 -0
  17. package/index.js +5 -1
  18. package/jsconfig.json +8 -0
  19. package/knip.json +6 -0
  20. package/localhost.cert +19 -0
  21. package/localhost.key +28 -0
  22. package/model/Article.interface.js +1 -0
  23. package/model/DataModel.js +10 -3
  24. package/model/FileAdapter.js +159 -156
  25. package/model/FileModel.js +94 -103
  26. package/model/SequelizeAdapter.js +23 -4
  27. package/model/SqliteAdapter.js +209 -206
  28. package/model/datastructures/BinarySearchTree.js +15 -18
  29. package/model/datastructures/BinarySearchTreeHashMap.js +1 -19
  30. package/model/datastructures/BinarySearchTreeWithInvertedIndex.js +315 -0
  31. package/model/datastructures/InvertedIndexSearch.js +99 -0
  32. package/model/datastructures/searchWorker.js +0 -0
  33. package/modules/jscompiler/jscompiler.js +1 -1
  34. package/package.json +3 -7
  35. package/public/index.html +2 -2
  36. package/public/main-M7EVVOPM.js +7 -0
  37. package/public/scripts.min.js +557 -8
  38. package/public/{styles-CRQIYMR5.css → styles-MSHVQ6J2.css} +1 -1
  39. package/public/styles.min.css +1 -1
  40. package/render.js +24 -0
  41. package/server.js +394 -294
  42. package/src/compress.js +73 -0
  43. package/src/fetchData.js +56 -10
  44. package/src/styles.css +65 -0
  45. package/src/timeline.js +110 -0
  46. package/src1/FileAdapter.ts +0 -0
  47. package/src1/model/Article.interface.ts +7 -0
  48. package/src1/model/DatabaseAdapter.ts +15 -0
  49. package/src1/model/FileAdapter.ts +187 -0
  50. package/src1/model/FileModel.ts +108 -0
  51. package/src1/model/SqliteAdapter.ts +240 -0
  52. package/src1/model/datastructures/BinarySearchTreeWithInvertedIndex.ts +363 -0
  53. package/src1/model/datastructures/InvertedIndexSearch.ts +118 -0
  54. package/styles.hash +1 -1
  55. package/templates/article.html +9 -0
  56. package/templates/articleButtons.html +1 -0
  57. package/templates/header.html +10 -0
  58. package/templates/layout.html +21 -0
  59. package/templates/loginTemplate.html +60 -0
  60. package/templates/moreButton.html +1 -0
  61. package/templates/newArticleForm.html +10 -0
  62. package/templates/timelineSelector.html +10 -0
  63. package/tsconfig.json +13 -5
  64. package/utilities.js +101 -59
  65. package/public/favicon.ico +0 -0
  66. package/public/main-LAI5FAZI.js +0 -7
  67. package/public/main-SKL6R4NB.js +0 -7
  68. package/public/main-V4OTOWYB.js +0 -7
  69. package/public/styles-I55BTQOK.css +0 -1
  70. package/router.js +0 -121
  71. /package/{Article.ts → src1/Article.ts} +0 -0
package/Article.js CHANGED
@@ -1,88 +1,94 @@
1
1
  export default class Article {
2
- constructor(id, title, content, createdAt) {
3
- this.id = id;
4
- this.title = title;
5
- this.content = content;
6
- this.createdAt = new Date(createdAt).getTime();
7
- }
8
- static createNew(title, content, createdAt = new Date()) {
9
- const id = this.getNextID(); // Your existing ID generator
10
- //const createdAt = new Date();
11
- return new Article(id, title, content, createdAt);
12
- }
13
- static getNextID() {
14
- // Generiert eine kollisionssichere ID basierend auf Zeitstempel + Zufall
15
- // Passt in JS Safe Integer und SQLite Integer (64-bit)
16
- return Date.now() * 1000 + Math.floor(Math.random() * 1000);
17
- }
18
- getContent() {
19
- return this.content;
20
- }
21
- getContentVeryShort() {
22
- if (!this.content)
23
- return "";
24
- if (this.content.length < 50)
25
- return this.content;
26
- let contentShort = this.content.substring(0, 50);
27
- contentShort += "...";
28
- return contentShort;
29
- }
30
- getContentShort() {
31
- if (!this.content)
32
- return "";
33
- if (this.content.length < 400)
34
- return this.content;
35
- let contentShort = this.content.substring(0, 400);
36
- contentShort += "...";
37
- return contentShort;
38
- }
39
- /**
40
- * Returns a JSON-serializable representation of the article.
41
- * This method is automatically called by JSON.stringify().
42
- */
43
- toJSON() {
44
- // Return a plain object with the desired properties
45
- const date = !isNaN(this.createdAt)
46
- ? new Date(this.createdAt).toISOString()
47
- : new Date().toISOString();
48
- return {
49
- id: this.id,
50
- title: this.title,
51
- content: this.content,
52
- createdAt: date,
53
- };
54
- }
55
- /**
56
- * Returns a JSON-serializable representation of the article.
57
- * This method is automatically called by JSON.stringify().
58
- */
59
- json() {
60
- return this.toJSON();
61
- }
62
- getFormattedDate() {
63
- const date = new Date(this.createdAt);
64
- const year = date.getFullYear();
65
- const month = String(date.getMonth() + 1).padStart(2, "0");
66
- const day = String(date.getDate()).padStart(2, "0");
67
- const hours = String(date.getHours()).padStart(2, "0");
68
- const minutes = String(date.getMinutes()).padStart(2, "0");
69
- return `${year}/${month}/${day} ${hours}:${minutes}`;
70
- }
71
- toHTML(loggedin = false) {
72
- if (this.content == null)
73
- return "";
74
- const moreButton = this.content.length > 400 ? `<a href="#">more</a>` : "";
75
- const buttons = loggedin // prettier-ignore
76
- ? `<div class="buttons"><div id="editButton${this.id}" class="btn edit" role="button" tabindex="0" data-action="edit" data-id="${this.id}">edit</div><div id="deleteButton${this.id}" class="btn delete" role="button" tabindex="0" data-action="delete" data-id="${this.id}">delete</div><div id="somethingelseButton${this.id}" class="btn light">something else</div></div>`
77
- : "";
78
- // prettier-ignore
79
- return `<article data-id="${this.id}" data-date="${this.createdAt}">
80
- ${buttons}
81
- <h2>${this.title}</h2>
82
- <span class="datetime">${this.getFormattedDate()}</span>
83
- <p>${this.getContentShort()}</p>
84
- <div class="full-content" style="display: none;">${this.content}</div>
85
- ${moreButton}
2
+ id;
3
+ title;
4
+ content;
5
+ createdAt;
6
+ image;
7
+ constructor(id, title, content, createdAt, image = null) {
8
+ this.id = id;
9
+ this.title = title;
10
+ this.content = content;
11
+ this.createdAt = new Date(createdAt).getTime();
12
+ this.image = image;
13
+ }
14
+ static createNew(title, content, createdAt = new Date(), image = null) {
15
+ const id = this.getNextID(); // Your existing ID generator
16
+ //const createdAt = new Date();
17
+ return new Article(id, title, content, createdAt, image);
18
+ }
19
+ static getNextID() {
20
+ // Generiert eine kollisionssichere ID basierend auf Zeitstempel + Zufall
21
+ // Passt in JS Safe Integer und SQLite Integer (64-bit)
22
+ return Date.now() * 1000 + Math.floor(Math.random() * 1000);
23
+ }
24
+ getContent() {
25
+ return this.content;
26
+ }
27
+ getContentVeryShort() {
28
+ if (!this.content) return "";
29
+ if (this.content.length < 50) return this.content;
30
+ let contentShort = this.content.substring(0, 50);
31
+ contentShort += "...";
32
+ return contentShort;
33
+ }
34
+ getContentShort() {
35
+ if (!this.content) return "";
36
+ if (this.content.length < 400) return this.content;
37
+ let contentShort = this.content.substring(0, 400);
38
+ contentShort += "...";
39
+ return contentShort;
40
+ }
41
+ /**
42
+ * Returns a JSON-serializable representation of the article.
43
+ * This method is automatically called by JSON.stringify().
44
+ */
45
+ toJSON() {
46
+ // Return a plain object with the desired properties
47
+ const date = !isNaN(this.createdAt)
48
+ ? new Date(this.createdAt).toISOString()
49
+ : new Date().toISOString();
50
+ return {
51
+ id: this.id,
52
+ title: this.title,
53
+ content: this.content,
54
+ createdAt: date,
55
+ image: this.image,
56
+ };
57
+ }
58
+ /**
59
+ * Returns a JSON-serializable representation of the article.
60
+ * This method is automatically called by JSON.stringify().
61
+ */
62
+ json() {
63
+ return this.toJSON();
64
+ }
65
+ getFormattedDate() {
66
+ const date = new Date(this.createdAt);
67
+ const year = date.getFullYear();
68
+ const month = String(date.getMonth() + 1).padStart(2, "0");
69
+ const day = String(date.getDate()).padStart(2, "0");
70
+ const hours = String(date.getHours()).padStart(2, "0");
71
+ const minutes = String(date.getMinutes()).padStart(2, "0");
72
+ return `${year}/${month}/${day} ${hours}:${minutes}`;
73
+ }
74
+ toHTML(loggedin = false) {
75
+ if (this.content == null) return "";
76
+ const moreButton = this.content.length > 400 ? `<a href="#">more</a>` : "";
77
+ const buttons = loggedin // prettier-ignore
78
+ ? `<div class="buttons"><div id="editButton${this.id}" class="btn edit" role="button" tabindex="0" data-action="edit" data-id="${this.id}">edit</div><div id="deleteButton${this.id}" class="btn delete" role="button" tabindex="0" data-action="delete" data-id="${this.id}">delete</div><div id="somethingelseButton${this.id}" class="btn light invisible">something else</div></div>`
79
+ : "";
80
+ const imageHtml = this.image
81
+ ? `<img src="${this.image}" alt="${this.title}" style="max-width: 100%;">`
82
+ : "";
83
+ // prettier-ignore
84
+ return `<article data-id="${this.id}" data-date="${this.createdAt}">
85
+ ${buttons}
86
+ <h2>${this.title}</h2>
87
+ <span class="datetime">${this.getFormattedDate()}</span>
88
+ ${imageHtml}
89
+ <p>${this.getContentShort()}</p>
90
+ <div class="full-content" style="display: none;">${this.content}</div>
91
+ ${moreButton}
86
92
  </article>`;
87
- }
93
+ }
88
94
  }
package/AssetManager.js CHANGED
@@ -34,20 +34,28 @@ export default class AssetManager {
34
34
 
35
35
  async init() {
36
36
  debug("init");
37
- const srcScriptPath = path.join(__dirname, "src", "fetchData.js");
37
+ const srcScriptPaths = [
38
+ path.join(__dirname, "src", "fetchData.js"),
39
+ path.join(__dirname, "src", "timeline.js"),
40
+ path.join(__dirname, "src", "compress.js"),
41
+ ];
38
42
  await Promise.all([
39
43
  this.processStyles(),
40
- this.processScripts(srcScriptPath),
44
+ this.processScripts(srcScriptPaths),
41
45
  ]);
42
46
  debug("init finished");
43
47
  }
44
48
 
45
49
  async reload() {
46
50
  debug("reload");
47
- const srcScriptPath = path.join(__dirname, "src", "fetchData.js");
51
+ const srcScriptPaths = [
52
+ path.join(__dirname, "src", "fetchData.js"),
53
+ path.join(__dirname, "src", "timeline.js"),
54
+ path.join(__dirname, "src", "compress.js"),
55
+ ];
48
56
  await Promise.all([
49
57
  this.processStyles(),
50
- this.processScripts(srcScriptPath),
58
+ this.processScripts(srcScriptPaths),
51
59
  ]);
52
60
  }
53
61
 
package/Auth.js CHANGED
@@ -1,26 +1,76 @@
1
1
  import { URLSearchParams } from "url";
2
2
  import crypto from "node:crypto";
3
+ import { Buffer } from "node:buffer";
3
4
  import { createDebug } from "./debug-loader.js";
4
5
  import { debug_perf } from "./debug-loader.js";
5
6
 
6
7
  const debug = createDebug("plainblog:Auth");
7
8
 
8
9
  export default class Auth {
9
- constructor(sessions, password) {
10
- this.sessions = sessions;
10
+ constructor(secret, password) {
11
+ this.secret = secret;
11
12
  this.password = password;
12
13
  }
13
14
 
15
+ #sign(payload) {
16
+ const data = Buffer.from(JSON.stringify(payload)).toString("base64url");
17
+ const signature = crypto
18
+ .createHmac("sha256", this.secret)
19
+ .update(data)
20
+ .digest("base64url");
21
+ return `${data}.${signature}`;
22
+ }
23
+
24
+ #verify(token) {
25
+ if (!token) return false;
26
+ const parts = token.split(".");
27
+ if (parts.length !== 2) return false;
28
+ const [data, signature] = parts;
29
+
30
+ const expectedSignature = crypto
31
+ .createHmac("sha256", this.secret)
32
+ .update(data)
33
+ .digest("base64url");
34
+
35
+ if (signature.length !== expectedSignature.length) return false;
36
+ if (
37
+ !crypto.timingSafeEqual(
38
+ Buffer.from(signature),
39
+ Buffer.from(expectedSignature),
40
+ )
41
+ )
42
+ return false;
43
+
44
+ try {
45
+ const payload = JSON.parse(Buffer.from(data, "base64url").toString());
46
+ if (payload.exp < Date.now()) return false;
47
+ return true;
48
+ } catch (e) {
49
+ return false;
50
+ }
51
+ }
52
+
14
53
  isAuthenticated(req) {
15
54
  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", duration);
20
- return false;
55
+ let token = null;
56
+
57
+ // 1. Check Authorization Header (Bearer) for REST clients
58
+ if (
59
+ req.headers.authorization &&
60
+ req.headers.authorization.startsWith("Bearer ")
61
+ ) {
62
+ token = req.headers.authorization.substring(7);
21
63
  }
22
- const match = req.headers.cookie.match(/(?:^|;\s*)session=([^;]*)/);
23
- const result = match ? this.sessions.has(match[1]) : false;
64
+ // 2. Check Cookie for Browser
65
+ else if (req.headers.cookie) {
66
+ const match = req.headers.cookie.match(/(?:^|;\s*)session=([^;]*)/);
67
+ if (match) {
68
+ token = match[1];
69
+ }
70
+ }
71
+
72
+ const result = this.#verify(token);
73
+
24
74
  const time_end = performance.now();
25
75
  const duration = time_end - time_start;
26
76
  debug_perf("isAuthenticated", duration);
@@ -31,27 +81,43 @@ export default class Auth {
31
81
  debug("handle login request");
32
82
  try {
33
83
  const body = await readBody(req, timeout, maxSize);
34
- const params = new URLSearchParams(body);
35
- const receivedPassword = params.get("password");
84
+ let receivedPassword = null;
85
+
86
+ // 1. Support JSON body for API clients (Angular)
87
+ if (req.headers["content-type"] === "application/json") {
88
+ try {
89
+ const json = JSON.parse(body);
90
+ receivedPassword = json.password;
91
+ } catch (e) {}
92
+ } else {
93
+ const params = new URLSearchParams(body);
94
+ receivedPassword = params.get("password");
95
+ }
36
96
 
37
97
  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
- }
98
+ const payload = {
99
+ role: "admin",
100
+ exp: Date.now() + 24 * 60 * 60 * 1000, // 24 hours
101
+ };
102
+ const token = this.#sign(payload);
103
+
48
104
  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();
105
+ // 2. If client accepts JSON, return token in body (RESTful)
106
+ if (req.headers["accept"] === "application/json") {
107
+ res.writeHead(200, {
108
+ "Content-Type": "application/json",
109
+ "Set-Cookie": `session=${token}; Path=/; SameSite=Lax; HttpOnly`,
110
+ });
111
+ res.end(JSON.stringify({ token }));
112
+ } else {
113
+ // 3. Default Browser Redirect
114
+ res.writeHead(303, {
115
+ "Set-Cookie": `session=${token}; Path=/; SameSite=Lax; HttpOnly`,
116
+ "X-Session-ID": token,
117
+ Location: "/",
118
+ });
119
+ res.end();
120
+ }
55
121
  }
56
122
  } else {
57
123
  if (!res.headersSent) {
@@ -79,15 +145,6 @@ export default class Auth {
79
145
  }
80
146
 
81
147
  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
148
  res.writeHead(303, {
92
149
  "Set-Cookie": "session=; HttpOnly; Path=/; Max-Age=0",
93
150
  Location: "/",