@lexho111/plainblog 0.6.4 → 0.6.6
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 -1
- package/Blog.js +249 -138
- package/Formatter.js +5 -7
- package/model/DataModel.js +6 -2
- package/model/datastructures/BinarySearchTree.js +27 -0
- package/model/datastructures/BinarySearchTreeHashMap.js +14 -5
- package/package.json +1 -1
- package/public/favicon.ico +0 -0
- package/public/index.html +17 -0
- package/public/main-LAI5FAZI.js +7 -0
- package/public/main-SKL6R4NB.js +7 -0
- package/public/scripts.min.js +2 -2
- package/public/styles-I55BTQOK.css +1 -0
- package/public/styles.min.css +2 -2
- package/src/fetchData.js +53 -29
- package/src/styles.css +152 -42
- package/.vscode/settings.json +0 -2
- package/blog_test_empty.db +0 -0
- package/blog_test_load.db +0 -0
- package/coverage/clover.xml +0 -1051
- package/coverage/coverage-final.json +0 -20
- package/coverage/lcov-report/base.css +0 -224
- package/coverage/lcov-report/block-navigation.js +0 -87
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +0 -161
- package/coverage/lcov-report/package/Article.js.html +0 -406
- package/coverage/lcov-report/package/Blog.js.html +0 -2674
- package/coverage/lcov-report/package/Formatter.js.html +0 -379
- package/coverage/lcov-report/package/build-scripts.js.html +0 -247
- package/coverage/lcov-report/package/build-styles.js.html +0 -367
- package/coverage/lcov-report/package/index.html +0 -191
- package/coverage/lcov-report/package/model/APIModel.js.html +0 -190
- package/coverage/lcov-report/package/model/ArrayList.js.html +0 -382
- package/coverage/lcov-report/package/model/ArrayListHashMap.js.html +0 -379
- package/coverage/lcov-report/package/model/BinarySearchTree.js.html +0 -856
- package/coverage/lcov-report/package/model/BinarySearchTreeHashMap.js.html +0 -346
- package/coverage/lcov-report/package/model/DataModel.js.html +0 -325
- package/coverage/lcov-report/package/model/DatabaseModel.js.html +0 -235
- package/coverage/lcov-report/package/model/FileAdapter.js.html +0 -397
- package/coverage/lcov-report/package/model/FileList.js.html +0 -244
- package/coverage/lcov-report/package/model/FileModel.js.html +0 -361
- package/coverage/lcov-report/package/model/SequelizeAdapter.js.html +0 -538
- package/coverage/lcov-report/package/model/SqliteAdapter.js.html +0 -247
- package/coverage/lcov-report/package/model/datastructures/ArrayList.js.html +0 -439
- package/coverage/lcov-report/package/model/datastructures/ArrayListHashMap.js.html +0 -196
- package/coverage/lcov-report/package/model/datastructures/BinarySearchTree.js.html +0 -916
- package/coverage/lcov-report/package/model/datastructures/BinarySearchTreeHashMap.js.html +0 -355
- package/coverage/lcov-report/package/model/datastructures/FileList.js.html +0 -244
- package/coverage/lcov-report/package/model/datastructures/index.html +0 -176
- package/coverage/lcov-report/package/model/index.html +0 -206
- package/coverage/lcov-report/package/utilities.js.html +0 -511
- package/coverage/lcov-report/prettify.css +0 -1
- package/coverage/lcov-report/prettify.js +0 -2
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +0 -210
- package/coverage/lcov.info +0 -2078
- package/eslint.config.js +0 -27
- package/plainblog - Verkn/303/274pfung.lnk +0 -0
package/Article.js
CHANGED
|
@@ -52,7 +52,7 @@ export default class Article {
|
|
|
52
52
|
if (this.content == null) return "";
|
|
53
53
|
const moreButton = this.content.length > 400 ? `<a href="#">more</a>` : "";
|
|
54
54
|
const buttons = loggedin
|
|
55
|
-
? `<div class="buttons"><div id="editButton${this.id}" class="
|
|
55
|
+
? `<div class="buttons"><div id="editButton${this.id}" class="btn edit">edit</div><div id="deleteButton${this.id}" class="btn delete">delete</div><div id="somethingelseButton${this.id}" class="btn light">something else</div></div>`
|
|
56
56
|
: "";
|
|
57
57
|
const script = loggedin
|
|
58
58
|
? `<script>
|
package/Blog.js
CHANGED
|
@@ -16,6 +16,7 @@ import FileAdapter from "./model/FileAdapter.js";
|
|
|
16
16
|
import DataModel from "./model/DataModel.js";
|
|
17
17
|
import { BinarySearchTreeHashMap } from "./model/datastructures/BinarySearchTreeHashMap.js";
|
|
18
18
|
import createDebug from "./debug-loader.js";
|
|
19
|
+
import { readFile } from "node:fs/promises";
|
|
19
20
|
|
|
20
21
|
// Initialize the debugger with a specific namespace
|
|
21
22
|
const debug = createDebug("plainblog:Blog");
|
|
@@ -92,6 +93,7 @@ export default class Blog {
|
|
|
92
93
|
#stylesheetPath = "";
|
|
93
94
|
compilestyle = false;
|
|
94
95
|
#initPromise = null;
|
|
96
|
+
#publicDir = path.join(process.cwd(), "public");
|
|
95
97
|
|
|
96
98
|
setTitle(title) {
|
|
97
99
|
this.#title = title;
|
|
@@ -161,8 +163,8 @@ export default class Blog {
|
|
|
161
163
|
|
|
162
164
|
#isAuthenticated(req) {
|
|
163
165
|
if (!req.headers.cookie) return false;
|
|
164
|
-
const
|
|
165
|
-
return this.sessions.has(
|
|
166
|
+
const match = req.headers.cookie.match(/(?:^|;\s*)session=([^;]*)/);
|
|
167
|
+
return match ? this.sessions.has(match[1]) : false;
|
|
166
168
|
}
|
|
167
169
|
|
|
168
170
|
async #handleLogin(req, res) {
|
|
@@ -184,13 +186,17 @@ export default class Blog {
|
|
|
184
186
|
});
|
|
185
187
|
res.end();
|
|
186
188
|
} else {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
189
|
+
debug("login failed");
|
|
190
|
+
debug("password did not match %s", params.get("password"));
|
|
191
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
192
|
+
|
|
193
|
+
// Send the JSON string
|
|
194
|
+
res.end(
|
|
195
|
+
JSON.stringify({
|
|
196
|
+
error: "unauthorized",
|
|
197
|
+
message: "Please enter the correct password.",
|
|
198
|
+
}),
|
|
199
|
+
);
|
|
194
200
|
}
|
|
195
201
|
}
|
|
196
202
|
|
|
@@ -232,11 +238,7 @@ export default class Blog {
|
|
|
232
238
|
const __filename = fileURLToPath(import.meta.url);
|
|
233
239
|
const __dirname = path.dirname(__filename);
|
|
234
240
|
const srcStylePath = path.join(__dirname, "src", "styles.css");
|
|
235
|
-
const publicStylePath = path.join(
|
|
236
|
-
process.cwd(),
|
|
237
|
-
"public",
|
|
238
|
-
"styles.min.css",
|
|
239
|
-
);
|
|
241
|
+
const publicStylePath = path.join(this.#publicDir, "styles.min.css");
|
|
240
242
|
|
|
241
243
|
let publicHash = null;
|
|
242
244
|
let srcStyles = "";
|
|
@@ -348,43 +350,25 @@ export default class Blog {
|
|
|
348
350
|
return this.#initPromise;
|
|
349
351
|
}
|
|
350
352
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const promises = [];
|
|
370
|
-
if (this.#databaseModel)
|
|
371
|
-
promises.push(this.#databaseModel.save(newArticleData));
|
|
372
|
-
if (this.#isExternalAPI)
|
|
373
|
-
promises.push(postData(this.#apiUrl, newArticleData));
|
|
374
|
-
await Promise.all(promises);
|
|
375
|
-
// Add the article to the local list for immediate display
|
|
376
|
-
this.#articles.insert(Article.createNew(title, content));
|
|
377
|
-
// remove sample entries
|
|
378
|
-
this.#articles.remove(1); // "Sample Entry #1"
|
|
379
|
-
this.#articles.remove(2); // "Sample Entry #2"
|
|
380
|
-
} catch (error) {
|
|
381
|
-
console.error("Failed to post new article to API:", error);
|
|
382
|
-
}
|
|
353
|
+
async postArticle(newArticle) {
|
|
354
|
+
try {
|
|
355
|
+
// Save the new article to the database via the ApiServer
|
|
356
|
+
const promises = [];
|
|
357
|
+
//if (this.#databaseModel)
|
|
358
|
+
// promises.push(this.#databaseModel.save(newArticle));
|
|
359
|
+
if (this.#isExternalAPI)
|
|
360
|
+
promises.push(postData(this.#apiUrl, newArticle));
|
|
361
|
+
await Promise.all(promises);
|
|
362
|
+
const title = newArticle.title;
|
|
363
|
+
const content = newArticle.content;
|
|
364
|
+
// Add the article to the local list for immediate display
|
|
365
|
+
this.#articles.insert(Article.createNew(title, content));
|
|
366
|
+
// remove sample entries
|
|
367
|
+
this.#articles.remove(1); // "Sample Entry #1"
|
|
368
|
+
this.#articles.remove(2); // "Sample Entry #2"
|
|
369
|
+
} catch (error) {
|
|
370
|
+
console.error("Failed to post new article to API:", error);
|
|
383
371
|
}
|
|
384
|
-
|
|
385
|
-
res.writeHead(303, { Location: "/" });
|
|
386
|
-
res.end();
|
|
387
|
-
//});
|
|
388
372
|
}
|
|
389
373
|
|
|
390
374
|
/** start a http server with default port 8080 */
|
|
@@ -393,7 +377,18 @@ export default class Blog {
|
|
|
393
377
|
|
|
394
378
|
const server = http.createServer(async (req, res) => {
|
|
395
379
|
//debug("query %s", req.url);
|
|
380
|
+
|
|
396
381
|
// API routes
|
|
382
|
+
// workaround for angular frontend
|
|
383
|
+
if (req.url === "/api/login" && req.method === "POST") {
|
|
384
|
+
await this.#handleLogin(req, res);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (req.url === "/api/logout") {
|
|
388
|
+
this.#handleLogout(req, res);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
// ---------------------------------------------
|
|
397
392
|
if (req.url.startsWith("/api")) {
|
|
398
393
|
await this.#jsonAPI(req, res);
|
|
399
394
|
return;
|
|
@@ -403,13 +398,61 @@ export default class Blog {
|
|
|
403
398
|
if (req.url === "/login") {
|
|
404
399
|
if (req.method === "GET") {
|
|
405
400
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
406
|
-
res.end(`${header("My Blog")}
|
|
407
|
-
<
|
|
408
|
-
<h1>
|
|
409
|
-
|
|
410
|
-
<
|
|
401
|
+
res.end(`${header("My Blog")}<body>
|
|
402
|
+
<form class="loginform" id="loginForm">
|
|
403
|
+
<h1>Blog</h1>
|
|
404
|
+
<!-- Message container -->
|
|
405
|
+
<div id="statusMessage"></div>
|
|
406
|
+
|
|
407
|
+
<input type="password" class="form_element" name="password" placeholder="Password" required />
|
|
408
|
+
<button class="btn" type="submit">Login</button>
|
|
409
|
+
</form>
|
|
410
|
+
|
|
411
|
+
<script>
|
|
412
|
+
const loginForm = document.getElementById('loginForm');
|
|
413
|
+
const statusDiv = document.getElementById('statusMessage');
|
|
414
|
+
|
|
415
|
+
loginForm.addEventListener('submit', async (e) => {
|
|
416
|
+
e.preventDefault(); // Prevent page reload
|
|
417
|
+
|
|
418
|
+
// Clear previous messages
|
|
419
|
+
statusDiv.innerHTML = '';
|
|
420
|
+
|
|
421
|
+
const formData = new FormData(loginForm);
|
|
422
|
+
const body = new URLSearchParams(formData); // Replicates form-urlencoded
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
const response = await fetch('/login', {
|
|
426
|
+
method: 'POST',
|
|
427
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
428
|
+
body: body.toString()
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
if (response.ok) {
|
|
432
|
+
window.location.href = '/'; // Redirect on success
|
|
433
|
+
} else if (response.status === 401) {
|
|
434
|
+
// Handle Unauthorized status
|
|
435
|
+
statusDiv.innerHTML = '<h2>Unauthorized</h2><p>Please enter the correct password.</p>';
|
|
436
|
+
} else {
|
|
437
|
+
statusDiv.innerHTML = '<p>Something went wrong. Please try again.</p>';
|
|
438
|
+
}
|
|
439
|
+
} catch (error) {
|
|
440
|
+
console.error('Network Error:', error);
|
|
441
|
+
statusDiv.innerHTML = '<p>Unable to connect to server.</p>';
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
</script>
|
|
411
445
|
</body></html>`);
|
|
412
446
|
return;
|
|
447
|
+
/*res.end(`${header("My Blog")}
|
|
448
|
+
<body>
|
|
449
|
+
<form class="loginform" method="POST">
|
|
450
|
+
<h1>Blog</h1>
|
|
451
|
+
<h2>Login</h2>
|
|
452
|
+
<input type="password" class="form_element password" name="password" placeholder="Password" />
|
|
453
|
+
<button class="btn">Login</button></form>
|
|
454
|
+
</body></html>`);
|
|
455
|
+
return;*/
|
|
413
456
|
} else if (req.method === "POST") {
|
|
414
457
|
await this.#handleLogin(req, res);
|
|
415
458
|
return;
|
|
@@ -420,18 +463,9 @@ export default class Blog {
|
|
|
420
463
|
this.#handleLogout(req, res);
|
|
421
464
|
return;
|
|
422
465
|
}
|
|
423
|
-
//
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
427
|
-
res.end("Forbidden");
|
|
428
|
-
return;
|
|
429
|
-
}
|
|
430
|
-
await this.postArticle(req, res);
|
|
431
|
-
// GET artciles
|
|
432
|
-
} else if (req.method === "GET" && req.url === "/") {
|
|
433
|
-
// load articles
|
|
434
|
-
|
|
466
|
+
// load articles
|
|
467
|
+
// GET artciles
|
|
468
|
+
if (req.method === "GET" && req.url === "/") {
|
|
435
469
|
// reload styles and scripts on (every) request
|
|
436
470
|
if (this.reloadStylesOnGET) {
|
|
437
471
|
if (this.#stylesheetPath != null && this.compilestyle) {
|
|
@@ -452,6 +486,7 @@ export default class Blog {
|
|
|
452
486
|
loggedin = true;
|
|
453
487
|
}
|
|
454
488
|
|
|
489
|
+
// use built in view engine
|
|
455
490
|
try {
|
|
456
491
|
const html = await this.toHTML(loggedin); // render this blog to HTML
|
|
457
492
|
res.writeHead(200, { "Content-Type": "text/html; charset=UTF-8" });
|
|
@@ -461,48 +496,72 @@ export default class Blog {
|
|
|
461
496
|
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
462
497
|
res.end("Internal Server Error");
|
|
463
498
|
}
|
|
499
|
+
|
|
500
|
+
// use angular frontend
|
|
501
|
+
/*const filePath = path.join(this.#publicDir, "index.html");
|
|
502
|
+
|
|
503
|
+
debug("%s", filePath);
|
|
504
|
+
try {
|
|
505
|
+
const data = await readFile(filePath);
|
|
506
|
+
// Manual MIME type detection (simplified)
|
|
507
|
+
const ext = path.extname(filePath);
|
|
508
|
+
const mimeTypes = {
|
|
509
|
+
".html": "text/html",
|
|
510
|
+
".css": "text/css",
|
|
511
|
+
".js": "text/javascript",
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
res.writeHead(200, {
|
|
515
|
+
"Content-Type": mimeTypes[ext] || "application/octet-stream",
|
|
516
|
+
});
|
|
517
|
+
res.end(data);
|
|
518
|
+
} catch (err) {
|
|
519
|
+
if (err) {
|
|
520
|
+
res.writeHead(404);
|
|
521
|
+
return res.end("Index-File Not Found");
|
|
522
|
+
}
|
|
523
|
+
}*/
|
|
524
|
+
|
|
525
|
+
try {
|
|
526
|
+
//const html = await this.toHTML(loggedin); // render this blog to HTML
|
|
527
|
+
//res.writeHead(200, { "Content-Type": "text/html; charset=UTF-8" });
|
|
528
|
+
//res.end(html);
|
|
529
|
+
} catch (err) {
|
|
530
|
+
console.error(err);
|
|
531
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
532
|
+
res.end("Internal Server Error");
|
|
533
|
+
}
|
|
464
534
|
} else {
|
|
465
535
|
// Try to serve static files from public folder
|
|
536
|
+
// Normalize path to prevent directory traversal attacks
|
|
537
|
+
const safePath = path.normalize(req.url).replace(/^(\.\.[\/\\])+/, "");
|
|
538
|
+
const filePath = path.join(
|
|
539
|
+
this.#publicDir,
|
|
540
|
+
safePath === "/" ? "index.html" : safePath,
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
debug("%s", filePath);
|
|
466
544
|
try {
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
".js": "text/javascript",
|
|
481
|
-
".css": "text/css",
|
|
482
|
-
".json": "application/json",
|
|
483
|
-
".png": "image/png",
|
|
484
|
-
".jpg": "image/jpeg",
|
|
485
|
-
".gif": "image/gif",
|
|
486
|
-
".svg": "image/svg+xml",
|
|
487
|
-
".ico": "image/x-icon",
|
|
488
|
-
};
|
|
489
|
-
const contentType = mimeTypes[ext] || "application/octet-stream";
|
|
490
|
-
res.writeHead(200, { "Content-Type": contentType });
|
|
491
|
-
fs.createReadStream(filePath).pipe(res);
|
|
492
|
-
return;
|
|
493
|
-
}
|
|
494
|
-
}
|
|
545
|
+
const data = await readFile(filePath);
|
|
546
|
+
// Manual MIME type detection (simplified)
|
|
547
|
+
const ext = path.extname(filePath);
|
|
548
|
+
const mimeTypes = {
|
|
549
|
+
".html": "text/html",
|
|
550
|
+
".css": "text/css",
|
|
551
|
+
".js": "text/javascript",
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
res.writeHead(200, {
|
|
555
|
+
"Content-Type": mimeTypes[ext] || "application/octet-stream",
|
|
556
|
+
});
|
|
557
|
+
res.end(data);
|
|
495
558
|
} catch (err) {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
console.error(err);
|
|
559
|
+
if (err) {
|
|
560
|
+
res.writeHead(404);
|
|
561
|
+
return res.end("File Not Found");
|
|
500
562
|
}
|
|
501
563
|
}
|
|
502
|
-
|
|
503
|
-
// Error 404
|
|
504
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
505
|
-
res.end(JSON.stringify({ message: "Not Found" }));
|
|
564
|
+
return;
|
|
506
565
|
}
|
|
507
566
|
});
|
|
508
567
|
|
|
@@ -588,11 +647,27 @@ export default class Blog {
|
|
|
588
647
|
// controller
|
|
589
648
|
/** everything that happens in /api */
|
|
590
649
|
async #jsonAPI(req, res) {
|
|
650
|
+
const origin = req.headers.origin;
|
|
651
|
+
if (origin) {
|
|
652
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
653
|
+
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
654
|
+
res.setHeader(
|
|
655
|
+
"Access-Control-Allow-Methods",
|
|
656
|
+
"GET, POST, PUT, DELETE, OPTIONS",
|
|
657
|
+
);
|
|
658
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
659
|
+
}
|
|
660
|
+
if (req.method === "OPTIONS") {
|
|
661
|
+
res.writeHead(204);
|
|
662
|
+
res.end();
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
591
665
|
const url = new URL(req.url, `http://${req.headers.host || "localhost"}`);
|
|
592
666
|
const pathname = url.pathname;
|
|
593
667
|
|
|
594
668
|
if (req.method === "GET") {
|
|
595
669
|
if (pathname === "/api" || pathname === "/api/") {
|
|
670
|
+
debug("GET /api");
|
|
596
671
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
597
672
|
const data = {
|
|
598
673
|
title: this.title,
|
|
@@ -601,20 +676,46 @@ export default class Blog {
|
|
|
601
676
|
}
|
|
602
677
|
// Search
|
|
603
678
|
if (url.searchParams.has("q")) {
|
|
679
|
+
debug("GET search article by query");
|
|
604
680
|
const query = url.searchParams.get("q");
|
|
681
|
+
const pLimit = parseInt(url.searchParams.get("limit"));
|
|
682
|
+
const limit = !isNaN(pLimit) ? pLimit : null;
|
|
683
|
+
debug("GET search article by query %s with limit %s", query, limit);
|
|
605
684
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
606
|
-
const results = this.#articles.search(query);
|
|
685
|
+
const results = this.#articles.search(query, limit);
|
|
607
686
|
res.end(JSON.stringify({ articles: results }));
|
|
608
687
|
return;
|
|
609
688
|
}
|
|
689
|
+
// GET article by ID
|
|
690
|
+
// /api/articles/1
|
|
691
|
+
const match = pathname.match(/^\/api\/articles\/(\d+)$/);
|
|
692
|
+
if (match) {
|
|
693
|
+
debug("GET article by id");
|
|
694
|
+
const id = parseInt(match[1]);
|
|
695
|
+
debug("GET article by id %d", id);
|
|
696
|
+
//console.log(this.#articles.getAllArticles());
|
|
697
|
+
const article = this.#articles.get(id);
|
|
698
|
+
if (article) {
|
|
699
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
700
|
+
res.end(JSON.stringify(article));
|
|
701
|
+
} else {
|
|
702
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
703
|
+
res.end(JSON.stringify({ error: "Not Found" }));
|
|
704
|
+
}
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
610
707
|
// GET all blog data
|
|
611
708
|
if (pathname === "/api/articles") {
|
|
709
|
+
debug("GET all articles");
|
|
612
710
|
// Use 'offset' param as startId (filter) to get items starting at ID
|
|
613
711
|
const pLimit = parseInt(url.searchParams.get("limit"));
|
|
614
712
|
const limit = !isNaN(pLimit) ? pLimit : null;
|
|
615
713
|
|
|
616
714
|
const start = url.searchParams.get("startdate");
|
|
617
715
|
const end = url.searchParams.get("enddate");
|
|
716
|
+
|
|
717
|
+
debug("startdate: %d, enddate: %d, limit: %d", start, end, limit);
|
|
718
|
+
|
|
618
719
|
//const parsedStart = parseDateParam(qStartdate);
|
|
619
720
|
//const parsedEnd = parseDateParam(qEnddate, true);
|
|
620
721
|
|
|
@@ -633,7 +734,9 @@ export default class Blog {
|
|
|
633
734
|
|
|
634
735
|
// POST a new article
|
|
635
736
|
} else if (req.method === "POST" && pathname === "/api/articles") {
|
|
737
|
+
debug("POST an article");
|
|
636
738
|
if (!this.#isAuthenticated(req)) {
|
|
739
|
+
debug("not authenticated");
|
|
637
740
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
638
741
|
res.end(JSON.stringify({ error: "Forbidden" }));
|
|
639
742
|
return;
|
|
@@ -645,45 +748,57 @@ export default class Blog {
|
|
|
645
748
|
req.on("error", reject);
|
|
646
749
|
});
|
|
647
750
|
const newArticle = JSON.parse(body);
|
|
751
|
+
debug("new article: %s", newArticle.title);
|
|
648
752
|
// local
|
|
649
753
|
await this.#databaseModel.save(newArticle); // --> to api server
|
|
754
|
+
this.postArticle(newArticle);
|
|
650
755
|
// extern
|
|
651
756
|
res.writeHead(201, { "Content-Type": "application/json" });
|
|
652
757
|
res.end(JSON.stringify(newArticle));
|
|
653
|
-
} else if (req.method === "DELETE"
|
|
758
|
+
} else if (req.method === "DELETE") {
|
|
759
|
+
debug("DELETE an article");
|
|
760
|
+
const match = pathname.match(/^\/api\/articles\/(\d+)$/);
|
|
761
|
+
if (pathname === "/api/articles" || match) {
|
|
762
|
+
if (!this.#isAuthenticated(req)) {
|
|
763
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
764
|
+
res.end(JSON.stringify({ error: "Forbidden" }));
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
const id = match ? match[1] : url.searchParams.get("id");
|
|
768
|
+
debug("delete an article by id $d", id);
|
|
769
|
+
if (id) {
|
|
770
|
+
this.#articles.remove(parseInt(id));
|
|
771
|
+
if (this.#databaseModel) {
|
|
772
|
+
await this.#databaseModel.remove(parseInt(id));
|
|
773
|
+
}
|
|
774
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
775
|
+
res.end(JSON.stringify({ status: "deleted", id }));
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
} else if (req.method === "PUT") {
|
|
779
|
+
debug("PUT an article");
|
|
654
780
|
if (!this.#isAuthenticated(req)) {
|
|
655
781
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
656
782
|
res.end(JSON.stringify({ error: "Forbidden" }));
|
|
657
783
|
return;
|
|
658
784
|
}
|
|
659
|
-
const
|
|
660
|
-
if (
|
|
661
|
-
|
|
785
|
+
const match = pathname.match(/^\/api\/articles\/(\d+)$/);
|
|
786
|
+
if (pathname === "/api/articles" || match) {
|
|
787
|
+
const id = match ? match[1] : url.searchParams.get("id");
|
|
788
|
+
debug("PUT article id: %d", id);
|
|
789
|
+
const body = await new Promise((resolve) => {
|
|
790
|
+
let data = "";
|
|
791
|
+
req.on("data", (chunk) => (data += chunk));
|
|
792
|
+
req.on("end", () => resolve(data));
|
|
793
|
+
});
|
|
794
|
+
const { title, content } = JSON.parse(body);
|
|
795
|
+
this.#articles.update(parseInt(id), title, content);
|
|
662
796
|
if (this.#databaseModel) {
|
|
663
|
-
await this.#databaseModel.
|
|
797
|
+
await this.#databaseModel.update(parseInt(id), { title, content });
|
|
664
798
|
}
|
|
665
799
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
666
|
-
res.end(JSON.stringify({ status: "
|
|
800
|
+
res.end(JSON.stringify({ status: "updated", id }));
|
|
667
801
|
}
|
|
668
|
-
} else if (req.method === "PUT" && pathname === "/api/articles") {
|
|
669
|
-
if (!this.#isAuthenticated(req)) {
|
|
670
|
-
res.writeHead(403, { "Content-Type": "application/json" });
|
|
671
|
-
res.end(JSON.stringify({ error: "Forbidden" }));
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
const id = url.searchParams.get("id");
|
|
675
|
-
const body = await new Promise((resolve) => {
|
|
676
|
-
let data = "";
|
|
677
|
-
req.on("data", (chunk) => (data += chunk));
|
|
678
|
-
req.on("end", () => resolve(data));
|
|
679
|
-
});
|
|
680
|
-
const { title, content } = JSON.parse(body);
|
|
681
|
-
this.#articles.update(parseInt(id), title, content);
|
|
682
|
-
if (this.#databaseModel) {
|
|
683
|
-
await this.#databaseModel.update(parseInt(id), { title, content });
|
|
684
|
-
}
|
|
685
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
686
|
-
res.end(JSON.stringify({ status: "updated", id }));
|
|
687
802
|
}
|
|
688
803
|
}
|
|
689
804
|
|
|
@@ -781,11 +896,9 @@ export default class Blog {
|
|
|
781
896
|
this.compiledStyles = await compileStyles(fileData);
|
|
782
897
|
|
|
783
898
|
// generate a file
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
await fs.promises.mkdir(publicDir, { recursive: true });
|
|
899
|
+
await fs.promises.mkdir(this.#publicDir, { recursive: true });
|
|
787
900
|
await fs.promises.writeFile(
|
|
788
|
-
path.join(publicDir, "styles.min.css"),
|
|
901
|
+
path.join(this.#publicDir, "styles.min.css"),
|
|
789
902
|
this.compiledStyles + `\n/* source-hash: ${currentHash} */`,
|
|
790
903
|
);
|
|
791
904
|
} else {
|
|
@@ -828,9 +941,8 @@ export default class Blog {
|
|
|
828
941
|
|
|
829
942
|
if (!this.#scriptsHash) {
|
|
830
943
|
try {
|
|
831
|
-
const publicDir = path.join(process.cwd(), "public");
|
|
832
944
|
const existing = await fs.promises.readFile(
|
|
833
|
-
path.join(publicDir, "scripts.min.js"),
|
|
945
|
+
path.join(this.#publicDir, "scripts.min.js"),
|
|
834
946
|
"utf-8",
|
|
835
947
|
);
|
|
836
948
|
const match = existing.match(/\/\* source-hash: ([a-f0-9]{64}) \*\//);
|
|
@@ -846,10 +958,9 @@ export default class Blog {
|
|
|
846
958
|
|
|
847
959
|
const compiledScripts = await compileScripts(fileData);
|
|
848
960
|
|
|
849
|
-
|
|
850
|
-
await fs.promises.mkdir(publicDir, { recursive: true });
|
|
961
|
+
await fs.promises.mkdir(this.#publicDir, { recursive: true });
|
|
851
962
|
await fs.promises.writeFile(
|
|
852
|
-
path.join(publicDir, "scripts.min.js"),
|
|
963
|
+
path.join(this.#publicDir, "scripts.min.js"),
|
|
853
964
|
compiledScripts + `\n/* source-hash: ${currentHash} */`,
|
|
854
965
|
);
|
|
855
966
|
}
|
package/Formatter.js
CHANGED
|
@@ -27,20 +27,18 @@ export function formatHTML(data) {
|
|
|
27
27
|
let form1 = "";
|
|
28
28
|
if (data.loggedin) {
|
|
29
29
|
form1 = `<form action="/" method="POST">
|
|
30
|
-
<div class="box">
|
|
31
30
|
<h3>Add a New Article</h3>
|
|
32
|
-
<input type="text" id="title" name="title" placeholder="Article Title" required
|
|
33
|
-
<textarea id="content" name="content" placeholder="Article Content" required
|
|
31
|
+
<input type="text" id="title" class="form_element new_title wide" name="title" placeholder="Article Title" required>
|
|
32
|
+
<textarea id="content" class="form_element new_content wide" name="content" placeholder="Article Content" required></textarea>
|
|
34
33
|
<button type="submit">Add Article</button>${button}
|
|
35
34
|
</form>
|
|
36
|
-
</div>
|
|
37
35
|
<hr>`;
|
|
38
36
|
}
|
|
39
|
-
const form = form1;
|
|
37
|
+
const form = ""; //form1;
|
|
40
38
|
return `${header(data.title)}
|
|
41
39
|
<body>
|
|
42
40
|
<nav>
|
|
43
|
-
<input type="text" id="search" placeholder="Search..."
|
|
41
|
+
<input type="text" id="search" placeholder="Search...">
|
|
44
42
|
${data.login}
|
|
45
43
|
</nav>
|
|
46
44
|
<div id="header">
|
|
@@ -49,7 +47,7 @@ export function formatHTML(data) {
|
|
|
49
47
|
</div>
|
|
50
48
|
<div id="wrapper">
|
|
51
49
|
${form}
|
|
52
|
-
<section id="articles" class="
|
|
50
|
+
<section id="articles" class="articles">
|
|
53
51
|
${data.articles.map((article) => article.toHTML(data.loggedin)).join("")}
|
|
54
52
|
</section>
|
|
55
53
|
</div>
|
package/model/DataModel.js
CHANGED
|
@@ -34,10 +34,10 @@ export default class DataModel {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
search(query) {
|
|
37
|
+
search(query, limit) {
|
|
38
38
|
debug("search");
|
|
39
39
|
if (this.storage.search) {
|
|
40
|
-
return this.storage.search(query);
|
|
40
|
+
return this.storage.search(query, limit);
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -46,6 +46,10 @@ export default class DataModel {
|
|
|
46
46
|
this.storage.remove(article);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
get(id) {
|
|
50
|
+
return this.storage.get(id);
|
|
51
|
+
}
|
|
52
|
+
|
|
49
53
|
getAllArticles() {
|
|
50
54
|
debug("get all articles");
|
|
51
55
|
const order = "newest";
|
|
@@ -122,6 +122,33 @@ export class BinarySearchTree {
|
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
*traverse(order = "newest") {
|
|
126
|
+
const stack = [];
|
|
127
|
+
let current = this.root;
|
|
128
|
+
|
|
129
|
+
if (order === "newest") {
|
|
130
|
+
while (current || stack.length > 0) {
|
|
131
|
+
while (current) {
|
|
132
|
+
stack.push(current);
|
|
133
|
+
current = current.right;
|
|
134
|
+
}
|
|
135
|
+
current = stack.pop();
|
|
136
|
+
yield current.article;
|
|
137
|
+
current = current.left;
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
while (current || stack.length > 0) {
|
|
141
|
+
while (current) {
|
|
142
|
+
stack.push(current);
|
|
143
|
+
current = current.left;
|
|
144
|
+
}
|
|
145
|
+
current = stack.pop();
|
|
146
|
+
yield current.article;
|
|
147
|
+
current = current.right;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
125
152
|
getRange(startdate, enddate, limit) {
|
|
126
153
|
const results = [];
|
|
127
154
|
// Optimization: Convert to timestamps once to avoid Date object creation/coercion in recursion
|