Compare commits
2
Commits
cf16e1f60d
...
417ec30bfd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
417ec30bfd | ||
|
|
f30d636bba |
@@ -1,6 +1,7 @@
|
||||
import http from "node:http";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { exec } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const MIME_TYPES = {
|
||||
@@ -205,12 +206,43 @@ export function createServer(customSiteDir, customPostsDir, customAssetsDir) {
|
||||
}
|
||||
|
||||
const cleanSlug = querySlug.toLowerCase().replace(/[^a-z0-9_-]/g, "-").replace(/^-+|-+$/g, "");
|
||||
const targetFilePath = path.join(postsDir, `${cleanSlug}.md`);
|
||||
if (!cleanSlug) {
|
||||
res.writeHead(400, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ error: "Invalid slug parameter" }));
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. Remove source markdown post
|
||||
const targetFilePath = path.join(postsDir, `${cleanSlug}.md`);
|
||||
if (fs.existsSync(targetFilePath)) {
|
||||
fs.unlinkSync(targetFilePath);
|
||||
}
|
||||
|
||||
// 2. Remove compiled static site directory and file
|
||||
const compiledDir = path.join(siteDir, "posts", cleanSlug);
|
||||
if (fs.existsSync(compiledDir)) {
|
||||
fs.rmSync(compiledDir, { recursive: true, force: true });
|
||||
}
|
||||
const compiledHtml = path.join(siteDir, "posts", `${cleanSlug}.html`);
|
||||
if (fs.existsSync(compiledHtml)) {
|
||||
fs.unlinkSync(compiledHtml);
|
||||
}
|
||||
|
||||
// 3. Rebuild static collections (index.html, dispatches, tags) if in full Eleventy environment
|
||||
try {
|
||||
if (fs.existsSync(path.join(process.cwd(), ".eleventy.js"))) {
|
||||
exec("npx @11ty/eleventy", (err) => {
|
||||
if (err) {
|
||||
console.error("[Ingestion API] Error rebuilding site after delete:", err);
|
||||
} else {
|
||||
console.log(`[Ingestion API] Site rebuild complete after unpublishing '${cleanSlug}'.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (rebuildErr) {
|
||||
console.error("[Ingestion API] Failed to trigger Eleventy rebuild:", rebuildErr);
|
||||
}
|
||||
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ status: "deleted", slug: cleanSlug }));
|
||||
return;
|
||||
|
||||
+3
-3
@@ -105,19 +105,19 @@ export default {
|
||||
{
|
||||
title: "Systems Engineering",
|
||||
slug: "systems",
|
||||
desc: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
desc: "Notes from my Journey builing out interconnected systems.",
|
||||
badge: "TRACK: SYSTEMS"
|
||||
},
|
||||
{
|
||||
title: "Security Research",
|
||||
slug: "security",
|
||||
desc: "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||
desc: "Notes from my CTF training and security research.",
|
||||
badge: "TRACK: SECURITY"
|
||||
},
|
||||
{
|
||||
title: "Homelab Infrastructure",
|
||||
slug: "homelab",
|
||||
desc: "Ut enim ad minim veniam, quis nostrud exercitation ullamco.",
|
||||
desc: "Notes from builing out my homelab.",
|
||||
badge: "TRACK: INFRA"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="hero-pretitle">TECHNICAL PUBLICATION REPOSITORY • ~/NOTES/INDEX.MD</div>
|
||||
<h1 class="hero-main-title">{{ heroTitle or site.description }}</h1>
|
||||
<p class="hero-landing-text">
|
||||
{{ heroText or "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation." }}
|
||||
{{ heroText or "Notes from my Journey builing out interconnected systems." }}
|
||||
</p>
|
||||
|
||||
<div class="filter-pills-row" aria-label="Topic Filters">
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ title: "River Rooks | Homelab Gateway"
|
||||
|
||||
<a href="/dispatches/" class="blog-feature-title">> Technical Notes & Engineering Dispatches</a>
|
||||
<p class="blog-feature-desc">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam.
|
||||
Notes from my Journey builing out interconnected systems.
|
||||
</p>
|
||||
|
||||
<div class="blog-sublinks">
|
||||
|
||||
@@ -173,6 +173,17 @@ Here is an image: ![[camera-board.png]]
|
||||
const assetPath = path.join(tmpAssets, "camera-board.png");
|
||||
assert.ok(fs.existsSync(assetPath), "Asset file must be saved");
|
||||
|
||||
// Create simulated compiled static post files in siteDir
|
||||
const compiledPostDir = path.join(tmpDir, "posts", "flock-firmware");
|
||||
fs.mkdirSync(compiledPostDir, { recursive: true });
|
||||
const compiledPostHtml = path.join(compiledPostDir, "index.html");
|
||||
fs.writeFileSync(compiledPostHtml, "<html><body>Simulated compiled post</body></html>");
|
||||
const standalonePostHtml = path.join(tmpDir, "posts", "flock-firmware.html");
|
||||
fs.writeFileSync(standalonePostHtml, "<html><body>Simulated standalone post</body></html>");
|
||||
|
||||
assert.ok(fs.existsSync(compiledPostHtml), "Simulated compiled post must exist before delete");
|
||||
assert.ok(fs.existsSync(standalonePostHtml), "Simulated standalone post must exist before delete");
|
||||
|
||||
// Delete post
|
||||
const resDelete = await fetch(`http://127.0.0.1:${port}/api/posts?slug=flock-firmware`, {
|
||||
method: "DELETE",
|
||||
@@ -182,6 +193,8 @@ Here is an image: ![[camera-board.png]]
|
||||
});
|
||||
assert.strictEqual(resDelete.status, 200, "Delete must return 200");
|
||||
assert.ok(!fs.existsSync(writtenPath), "Post markdown file must be deleted");
|
||||
assert.ok(!fs.existsSync(compiledPostDir), "Compiled post directory must be deleted from _site");
|
||||
assert.ok(!fs.existsSync(standalonePostHtml), "Compiled standalone HTML must be deleted from _site");
|
||||
} finally {
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
@@ -190,7 +203,7 @@ Here is an image: ![[camera-board.png]]
|
||||
}
|
||||
});
|
||||
|
||||
test("Ingestion API: adversarial protection against path traversal in slug", async () => {
|
||||
test("Ingestion API: adversarial protection against path traversal in slug and DELETE validation", async () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-site-"));
|
||||
const tmpPosts = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-posts-"));
|
||||
process.env.API_SECRET = TEST_SECRET;
|
||||
@@ -200,6 +213,7 @@ test("Ingestion API: adversarial protection against path traversal in slug", asy
|
||||
const port = server.address().port;
|
||||
|
||||
try {
|
||||
// 1. Path traversal in POST
|
||||
const resTraversal = await fetch(`http://127.0.0.1:${port}/api/posts`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -218,6 +232,42 @@ test("Ingestion API: adversarial protection against path traversal in slug", asy
|
||||
} else {
|
||||
assert.strictEqual(resTraversal.status, 400, "Invalid slug must return 400");
|
||||
}
|
||||
|
||||
// 2. DELETE without auth token
|
||||
const resDeleteNoAuth = await fetch(`http://127.0.0.1:${port}/api/posts?slug=any-post`, {
|
||||
method: "DELETE"
|
||||
});
|
||||
assert.strictEqual(resDeleteNoAuth.status, 401, "DELETE without Bearer token must return 401");
|
||||
|
||||
// 3. DELETE with missing slug parameter
|
||||
const resDeleteNoSlug = await fetch(`http://127.0.0.1:${port}/api/posts`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: `Bearer ${TEST_SECRET}`
|
||||
}
|
||||
});
|
||||
assert.strictEqual(resDeleteNoSlug.status, 400, "DELETE without slug parameter must return 400");
|
||||
|
||||
// 4. DELETE with invalid/empty slug
|
||||
const resDeleteBadSlug = await fetch(`http://127.0.0.1:${port}/api/posts?slug=///`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: `Bearer ${TEST_SECRET}`
|
||||
}
|
||||
});
|
||||
assert.strictEqual(resDeleteBadSlug.status, 400, "DELETE with invalid slug must return 400");
|
||||
|
||||
// 5. DELETE non-existent post succeeds gracefully (idempotent)
|
||||
const resDeleteNonExistent = await fetch(`http://127.0.0.1:${port}/api/posts?slug=non-existent-post`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: `Bearer ${TEST_SECRET}`
|
||||
}
|
||||
});
|
||||
assert.strictEqual(resDeleteNonExistent.status, 200, "DELETE non-existent post must return 200 idempotently");
|
||||
const jsonNonExistent = await resDeleteNonExistent.json();
|
||||
assert.strictEqual(jsonNonExistent.status, "deleted");
|
||||
assert.strictEqual(jsonNonExistent.slug, "non-existent-post");
|
||||
} finally {
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
|
||||
Reference in New Issue
Block a user