@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.
- package/Article.js +91 -85
- package/AssetManager.js +12 -4
- package/Auth.js +93 -36
- package/Blog.js +167 -53
- package/Formatter.js +92 -39
- package/README.md +21 -6
- package/cluster-server.js +1 -1
- package/dist/Article.js +92 -0
- package/dist/FileAdapter.js +1 -0
- package/dist/model/Article.interface.js +1 -0
- package/dist/model/DatabaseAdapter.js +1 -0
- package/dist/model/FileAdapter.js +148 -0
- package/dist/model/FileModel.js +102 -0
- package/dist/model/SqliteAdapter.js +216 -0
- package/dist/model/datastructures/BinarySearchTreeWithInvertedIndex.js +351 -0
- package/dist/model/datastructures/InvertedIndexSearch.js +96 -0
- package/index.js +5 -1
- package/jsconfig.json +8 -0
- package/knip.json +6 -0
- package/localhost.cert +19 -0
- package/localhost.key +28 -0
- package/model/Article.interface.js +1 -0
- package/model/DataModel.js +10 -3
- package/model/FileAdapter.js +159 -156
- package/model/FileModel.js +94 -103
- package/model/SequelizeAdapter.js +23 -4
- package/model/SqliteAdapter.js +209 -206
- package/model/datastructures/BinarySearchTree.js +15 -18
- package/model/datastructures/BinarySearchTreeHashMap.js +1 -19
- package/model/datastructures/BinarySearchTreeWithInvertedIndex.js +315 -0
- package/model/datastructures/InvertedIndexSearch.js +99 -0
- package/model/datastructures/searchWorker.js +0 -0
- package/modules/jscompiler/jscompiler.js +1 -1
- package/package.json +3 -7
- package/public/index.html +2 -2
- package/public/main-M7EVVOPM.js +7 -0
- package/public/scripts.min.js +557 -8
- package/public/{styles-CRQIYMR5.css → styles-MSHVQ6J2.css} +1 -1
- package/public/styles.min.css +1 -1
- package/render.js +24 -0
- package/server.js +394 -294
- package/src/compress.js +73 -0
- package/src/fetchData.js +56 -10
- package/src/styles.css +65 -0
- package/src/timeline.js +110 -0
- package/src1/FileAdapter.ts +0 -0
- package/src1/model/Article.interface.ts +7 -0
- package/src1/model/DatabaseAdapter.ts +15 -0
- package/src1/model/FileAdapter.ts +187 -0
- package/src1/model/FileModel.ts +108 -0
- package/src1/model/SqliteAdapter.ts +240 -0
- package/src1/model/datastructures/BinarySearchTreeWithInvertedIndex.ts +363 -0
- package/src1/model/datastructures/InvertedIndexSearch.ts +118 -0
- package/styles.hash +1 -1
- package/templates/article.html +9 -0
- package/templates/articleButtons.html +1 -0
- package/templates/header.html +10 -0
- package/templates/layout.html +21 -0
- package/templates/loginTemplate.html +60 -0
- package/templates/moreButton.html +1 -0
- package/templates/newArticleForm.html +10 -0
- package/templates/timelineSelector.html +10 -0
- package/tsconfig.json +13 -5
- package/utilities.js +101 -59
- package/public/favicon.ico +0 -0
- package/public/main-LAI5FAZI.js +0 -7
- package/public/main-SKL6R4NB.js +0 -7
- package/public/main-V4OTOWYB.js +0 -7
- package/public/styles-I55BTQOK.css +0 -1
- package/router.js +0 -121
- /package/{Article.ts → src1/Article.ts} +0 -0
package/Article.js
CHANGED
|
@@ -1,88 +1,94 @@
|
|
|
1
1
|
export default class Article {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
10
|
-
this.
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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: "/",
|