|
@@ -60,7 +60,10 @@ const storage = multer.diskStorage({
|
|
|
},
|
|
},
|
|
|
filename: function (req, file, cb) {
|
|
filename: function (req, file, cb) {
|
|
|
const ext = path.extname(file.originalname).toLowerCase();
|
|
const ext = path.extname(file.originalname).toLowerCase();
|
|
|
- const namePart = path.basename(file.originalname, ext).replace(/[^a-z0-9]/gi, "-").toLowerCase();
|
|
|
|
|
|
|
+ const namePart = path
|
|
|
|
|
+ .basename(file.originalname, ext)
|
|
|
|
|
+ .replace(/[^a-z0-9]/gi, "-")
|
|
|
|
|
+ .toLowerCase();
|
|
|
const timestamp = Date.now();
|
|
const timestamp = Date.now();
|
|
|
const newFilename = `${namePart}_${timestamp}${ext}`;
|
|
const newFilename = `${namePart}_${timestamp}${ext}`;
|
|
|
cb(null, newFilename);
|
|
cb(null, newFilename);
|
|
@@ -69,7 +72,6 @@ const storage = multer.diskStorage({
|
|
|
|
|
|
|
|
const upload = multer({ storage: storage });
|
|
const upload = multer({ storage: storage });
|
|
|
|
|
|
|
|
-
|
|
|
|
|
// Middleware
|
|
// Middleware
|
|
|
app.use(
|
|
app.use(
|
|
|
cors({
|
|
cors({
|
|
@@ -191,7 +193,6 @@ function getSlugFromTitle(title) {
|
|
|
return `${dateStr}-${slugText}`;
|
|
return `${dateStr}-${slugText}`;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-
|
|
|
|
|
// Authentication Routes
|
|
// Authentication Routes
|
|
|
|
|
|
|
|
// POST /api/auth/login - Login
|
|
// POST /api/auth/login - Login
|
|
@@ -271,38 +272,52 @@ app.get("/api/auth/me", isAuthenticated, (req, res) => {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// POST /api/auth/change-password - Change password
|
|
// POST /api/auth/change-password - Change password
|
|
|
-app.post("/api/auth/change-password", requireAuth, async (req, res) => {
|
|
|
|
|
- try {
|
|
|
|
|
- const { currentPassword, newPassword } = req.body;
|
|
|
|
|
|
|
+app.post(
|
|
|
|
|
+ "/api/auth/change-password",
|
|
|
|
|
+ requireAuth,
|
|
|
|
|
+ isAuthenticated,
|
|
|
|
|
+ async (req, res) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const { currentPassword, newPassword } = req.body;
|
|
|
|
|
+
|
|
|
|
|
+ console.log("Change password request for user:", req.user.username);
|
|
|
|
|
+ console.log(
|
|
|
|
|
+ "Current password received:",
|
|
|
|
|
+ currentPassword ? "Yes" : "No",
|
|
|
|
|
+ );
|
|
|
|
|
+ console.log("New password received:", newPassword ? "Yes" : "No");
|
|
|
|
|
+
|
|
|
|
|
+ if (!currentPassword || !newPassword) {
|
|
|
|
|
+ return res.status(400).json({
|
|
|
|
|
+ error: "Current password and new password are required",
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- if (!currentPassword || !newPassword) {
|
|
|
|
|
- return res.status(400).json({
|
|
|
|
|
- error: "Current password and new password are required",
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (newPassword.length < 6) {
|
|
|
|
|
+ return res.status(400).json({
|
|
|
|
|
+ error: "New password must be at least 6 characters long",
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- if (newPassword.length < 6) {
|
|
|
|
|
- return res.status(400).json({
|
|
|
|
|
- error: "New password must be at least 6 characters long",
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const result = await changeUserPassword(
|
|
|
|
|
+ req.user.username,
|
|
|
|
|
+ currentPassword,
|
|
|
|
|
+ newPassword,
|
|
|
|
|
+ );
|
|
|
|
|
|
|
|
- const result = await changeUserPassword(
|
|
|
|
|
- req.user.username,
|
|
|
|
|
- currentPassword,
|
|
|
|
|
- newPassword,
|
|
|
|
|
- );
|
|
|
|
|
|
|
+ console.log("Result from changeUserPassword:", result);
|
|
|
|
|
|
|
|
- if (result.success) {
|
|
|
|
|
- res.json({ success: true, message: result.message });
|
|
|
|
|
- } else {
|
|
|
|
|
- res.status(400).json({ error: result.message });
|
|
|
|
|
|
|
+ if (result.success) {
|
|
|
|
|
+ res.json({ success: true, message: result.message });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ res.status(400).json({ error: result.message });
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error("Change password error:", error);
|
|
|
|
|
+ res.status(500).json({ error: "Failed to change password" });
|
|
|
}
|
|
}
|
|
|
- } catch (error) {
|
|
|
|
|
- console.error("Change password error:", error);
|
|
|
|
|
- res.status(500).json({ error: "Failed to change password" });
|
|
|
|
|
- }
|
|
|
|
|
-});
|
|
|
|
|
|
|
+ },
|
|
|
|
|
+);
|
|
|
|
|
|
|
|
// MEDIA ROUTES
|
|
// MEDIA ROUTES
|
|
|
|
|
|
|
@@ -384,7 +399,9 @@ app.get("/api/media", requireAuth, async (req, res) => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (shouldInclude) {
|
|
if (shouldInclude) {
|
|
|
- const slugPart = slug ? slug.replace(/[^a-z0-9-]/gi, "") : "uploads";
|
|
|
|
|
|
|
+ const slugPart = slug
|
|
|
|
|
+ ? slug.replace(/[^a-z0-9-]/gi, "")
|
|
|
|
|
+ : "uploads";
|
|
|
const url = slug
|
|
const url = slug
|
|
|
? `/media-files/${slugPart}/images/${file}`
|
|
? `/media-files/${slugPart}/images/${file}`
|
|
|
: `/media-files/uploads/${file}`;
|
|
: `/media-files/uploads/${file}`;
|
|
@@ -394,7 +411,7 @@ app.get("/api/media", requireAuth, async (req, res) => {
|
|
|
url: url,
|
|
url: url,
|
|
|
size: stats.size,
|
|
size: stats.size,
|
|
|
date: stats.mtime,
|
|
date: stats.mtime,
|
|
|
- type: isImage ? "image" : (isCsv ? "csv" : "file")
|
|
|
|
|
|
|
+ type: isImage ? "image" : isCsv ? "csv" : "file",
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -413,7 +430,8 @@ app.get("/api/media", requireAuth, async (req, res) => {
|
|
|
app.delete("/api/media", requireAuth, async (req, res) => {
|
|
app.delete("/api/media", requireAuth, async (req, res) => {
|
|
|
try {
|
|
try {
|
|
|
const { path: relativePath } = req.body; // e.g. "/media-files/slug/images/file.jpg"
|
|
const { path: relativePath } = req.body; // e.g. "/media-files/slug/images/file.jpg"
|
|
|
- if (!relativePath) return res.status(400).json({ error: "Path is required" });
|
|
|
|
|
|
|
+ if (!relativePath)
|
|
|
|
|
+ return res.status(400).json({ error: "Path is required" });
|
|
|
|
|
|
|
|
// Security check: ensure path is within POSTS_DIR
|
|
// Security check: ensure path is within POSTS_DIR
|
|
|
// relativePath typically starts with /media-files/
|
|
// relativePath typically starts with /media-files/
|
|
@@ -431,7 +449,6 @@ app.delete("/api/media", requireAuth, async (req, res) => {
|
|
|
} else {
|
|
} else {
|
|
|
res.status(404).json({ error: "File not found" });
|
|
res.status(404).json({ error: "File not found" });
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
console.error("Delete media error:", err);
|
|
console.error("Delete media error:", err);
|
|
|
res.status(500).json({ error: "Failed to delete media" });
|
|
res.status(500).json({ error: "Failed to delete media" });
|
|
@@ -469,8 +486,11 @@ app.get("/api/posts", async (req, res) => {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// Filter out hidden posts for non-admins
|
|
// Filter out hidden posts for non-admins
|
|
|
- const isAdmin = req.session && req.session.user && req.session.user.role === "admin";
|
|
|
|
|
- const visiblePosts = posts.filter(post => isAdmin || !post.hidden);
|
|
|
|
|
|
|
+ const isAdmin =
|
|
|
|
|
+ req.session &&
|
|
|
|
|
+ req.session.user &&
|
|
|
|
|
+ req.session.user.role === "admin";
|
|
|
|
|
+ const visiblePosts = posts.filter((post) => isAdmin || !post.hidden);
|
|
|
|
|
|
|
|
res.json(visiblePosts);
|
|
res.json(visiblePosts);
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
@@ -495,7 +515,10 @@ app.get("/api/posts/:slug", async (req, res) => {
|
|
|
const stats = await fs.stat(filePath);
|
|
const stats = await fs.stat(filePath);
|
|
|
|
|
|
|
|
// Access control for hidden posts
|
|
// Access control for hidden posts
|
|
|
- const isAdmin = req.session && req.session.user && req.session.user.role === "admin";
|
|
|
|
|
|
|
+ const isAdmin =
|
|
|
|
|
+ req.session &&
|
|
|
|
|
+ req.session.user &&
|
|
|
|
|
+ req.session.user.role === "admin";
|
|
|
if (metadata.hidden && !isAdmin) {
|
|
if (metadata.hidden && !isAdmin) {
|
|
|
// Return 404 to hide existence, or 403 if we want to be explicit.
|
|
// Return 404 to hide existence, or 403 if we want to be explicit.
|
|
|
// 404 is safer for "hidden" content.
|
|
// 404 is safer for "hidden" content.
|
|
@@ -619,7 +642,7 @@ app.put("/api/posts/:slug", requireAuth, async (req, res) => {
|
|
|
if (oldFilename !== newFilename) {
|
|
if (oldFilename !== newFilename) {
|
|
|
await fs.remove(oldFilePath);
|
|
await fs.remove(oldFilePath);
|
|
|
// IMPORTANT: If we rename the post, should we verify if images folder usage needs update?
|
|
// IMPORTANT: If we rename the post, should we verify if images folder usage needs update?
|
|
|
- // Currently images are stored in /slug/images/.
|
|
|
|
|
|
|
+ // Currently images are stored in /slug/images/.
|
|
|
// If the slug (filename) depends on the title, changing title changes slug.
|
|
// If the slug (filename) depends on the title, changing title changes slug.
|
|
|
// We should rename the image folder too!
|
|
// We should rename the image folder too!
|
|
|
|
|
|
|
@@ -656,34 +679,6 @@ app.put("/api/posts/:slug", requireAuth, async (req, res) => {
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
-// Change password endpoint
|
|
|
|
|
-app.post('/api/auth/change-password', requireAuth, async (req, res) => {
|
|
|
|
|
- try {
|
|
|
|
|
- const { currentPassword, newPassword } = req.body;
|
|
|
|
|
-
|
|
|
|
|
- if (!currentPassword || !newPassword) {
|
|
|
|
|
- return res.status(400).json({ error: 'Current and new password are required' });
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (newPassword.length < 6) {
|
|
|
|
|
- return res.status(400).json({ error: 'New password must be at least 6 characters' });
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const result = await changeUserPassword(req.user.username, currentPassword, newPassword);
|
|
|
|
|
-
|
|
|
|
|
- if (!result.success) {
|
|
|
|
|
- return res.status(400).json({ error: result.message });
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- console.log(`Password changed for user: ${req.user.username}`);
|
|
|
|
|
- res.json({ success: true, message: 'Password changed successfully' });
|
|
|
|
|
-
|
|
|
|
|
- } catch (err) {
|
|
|
|
|
- console.error('Change password error:', err);
|
|
|
|
|
- res.status(500).json({ error: 'Internal server error' });
|
|
|
|
|
- }
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
// DELETE /api/posts/:slug - Delete post
|
|
// DELETE /api/posts/:slug - Delete post
|
|
|
app.delete("/api/posts/:slug", requireAuth, async (req, res) => {
|
|
app.delete("/api/posts/:slug", requireAuth, async (req, res) => {
|
|
|
try {
|
|
try {
|
|
@@ -702,8 +697,8 @@ app.delete("/api/posts/:slug", requireAuth, async (req, res) => {
|
|
|
if (await fs.pathExists(imgDir)) {
|
|
if (await fs.pathExists(imgDir)) {
|
|
|
await fs.remove(imgDir);
|
|
await fs.remove(imgDir);
|
|
|
}
|
|
}
|
|
|
- // Also remove parent folder if it was created just for this?
|
|
|
|
|
- // Structure is POSTS_DIR/slug/images.
|
|
|
|
|
|
|
+ // Also remove parent folder if it was created just for this?
|
|
|
|
|
+ // Structure is POSTS_DIR/slug/images.
|
|
|
// We should remove POSTS_DIR/slug
|
|
// We should remove POSTS_DIR/slug
|
|
|
const slugDir = path.join(POSTS_DIR, slug);
|
|
const slugDir = path.join(POSTS_DIR, slug);
|
|
|
if (await fs.pathExists(slugDir)) {
|
|
if (await fs.pathExists(slugDir)) {
|
|
@@ -759,7 +754,7 @@ app.put("/api/themes/active", requireAuth, async (req, res) => {
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
-import { getConfig, updateConfig } from './config.js';
|
|
|
|
|
|
|
+import { getConfig, updateConfig } from "./config.js";
|
|
|
|
|
|
|
|
// GET /api/config - Get app configuration
|
|
// GET /api/config - Get app configuration
|
|
|
app.get("/api/config", async (req, res) => {
|
|
app.get("/api/config", async (req, res) => {
|
|
@@ -777,9 +772,9 @@ app.put("/api/config", requireAuth, async (req, res) => {
|
|
|
try {
|
|
try {
|
|
|
const updates = req.body;
|
|
const updates = req.body;
|
|
|
// Whitelist allowed config keys to prevent abuse
|
|
// Whitelist allowed config keys to prevent abuse
|
|
|
- const allowedKeys = ['postWidth', 'activeTheme'];
|
|
|
|
|
|
|
+ const allowedKeys = ["postWidth", "activeTheme"];
|
|
|
const filteredUpdates = Object.keys(updates)
|
|
const filteredUpdates = Object.keys(updates)
|
|
|
- .filter(key => allowedKeys.includes(key))
|
|
|
|
|
|
|
+ .filter((key) => allowedKeys.includes(key))
|
|
|
.reduce((obj, key) => {
|
|
.reduce((obj, key) => {
|
|
|
obj[key] = updates[key];
|
|
obj[key] = updates[key];
|
|
|
return obj;
|
|
return obj;
|
|
@@ -866,7 +861,10 @@ app.use(
|
|
|
maxAge: "1y", // 1 year
|
|
maxAge: "1y", // 1 year
|
|
|
immutable: true,
|
|
immutable: true,
|
|
|
setHeaders: (res, path) => {
|
|
setHeaders: (res, path) => {
|
|
|
- res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
|
|
|
|
|
|
+ res.setHeader(
|
|
|
|
|
+ "Cache-Control",
|
|
|
|
|
+ "public, max-age=31536000, immutable",
|
|
|
|
|
+ );
|
|
|
},
|
|
},
|
|
|
}),
|
|
}),
|
|
|
);
|
|
);
|
|
@@ -881,10 +879,14 @@ const injectMetaTags = async (html, metadata, url) => {
|
|
|
// Default values
|
|
// Default values
|
|
|
const title = metadata.title || "Gooneral Wheelchair";
|
|
const title = metadata.title || "Gooneral Wheelchair";
|
|
|
const description = metadata.description || "A blog about stuff.";
|
|
const description = metadata.description || "A blog about stuff.";
|
|
|
- const image = metadata.image || "https://goonblog.thevakhovske.eu.org/og-image.jpg"; // Fallback image
|
|
|
|
|
|
|
+ const image =
|
|
|
|
|
+ metadata.image || "https://goonblog.thevakhovske.eu.org/og-image.jpg"; // Fallback image
|
|
|
|
|
|
|
|
// Replace Title
|
|
// Replace Title
|
|
|
- injected = injected.replace(/<title>.*<\/title>/, `<title>${title}</title>`);
|
|
|
|
|
|
|
+ injected = injected.replace(
|
|
|
|
|
+ /<title>.*<\/title>/,
|
|
|
|
|
+ `<title>${title}</title>`,
|
|
|
|
|
+ );
|
|
|
|
|
|
|
|
// Meta Tags to Inject
|
|
// Meta Tags to Inject
|
|
|
const metaTags = `
|
|
const metaTags = `
|
|
@@ -903,7 +905,6 @@ const injectMetaTags = async (html, metadata, url) => {
|
|
|
return injected.replace("</head>", `${metaTags}</head>`);
|
|
return injected.replace("</head>", `${metaTags}</head>`);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-
|
|
|
|
|
// Handle Post Routes for SSR
|
|
// Handle Post Routes for SSR
|
|
|
app.get("/posts/:slug", async (req, res) => {
|
|
app.get("/posts/:slug", async (req, res) => {
|
|
|
try {
|
|
try {
|
|
@@ -932,9 +933,12 @@ app.get("/posts/:slug", async (req, res) => {
|
|
|
// Check access (Hidden posts)
|
|
// Check access (Hidden posts)
|
|
|
// If hidden and not admin, serve standard index.html (SPA will handle 404/auth check client-side)
|
|
// If hidden and not admin, serve standard index.html (SPA will handle 404/auth check client-side)
|
|
|
// OR we can pretend it doesn't exist to bots.
|
|
// OR we can pretend it doesn't exist to bots.
|
|
|
- const isAdmin = req.session && req.session.user && req.session.user.role === "admin";
|
|
|
|
|
|
|
+ const isAdmin =
|
|
|
|
|
+ req.session &&
|
|
|
|
|
+ req.session.user &&
|
|
|
|
|
+ req.session.user.role === "admin";
|
|
|
if (metadata.hidden && !isAdmin) {
|
|
if (metadata.hidden && !isAdmin) {
|
|
|
- // Determine behavior:
|
|
|
|
|
|
|
+ // Determine behavior:
|
|
|
// If we send raw index.html, client app loads and shows "Not Found" or "Login".
|
|
// If we send raw index.html, client app loads and shows "Not Found" or "Login".
|
|
|
// We'll just send raw index.html without generic meta tags? Or default tags?
|
|
// We'll just send raw index.html without generic meta tags? Or default tags?
|
|
|
// Let's send default tags to avoid leaking info.
|
|
// Let's send default tags to avoid leaking info.
|
|
@@ -945,10 +949,14 @@ app.get("/posts/:slug", async (req, res) => {
|
|
|
const pageMetadata = {
|
|
const pageMetadata = {
|
|
|
title: metadata.title,
|
|
title: metadata.title,
|
|
|
description: metadata.description,
|
|
description: metadata.description,
|
|
|
- image: imageUrl
|
|
|
|
|
|
|
+ image: imageUrl,
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- const finalHtml = await injectMetaTags(html, pageMetadata, `${req.protocol}://${req.get("host")}${req.originalUrl}`);
|
|
|
|
|
|
|
+ const finalHtml = await injectMetaTags(
|
|
|
|
|
+ html,
|
|
|
|
|
+ pageMetadata,
|
|
|
|
|
+ `${req.protocol}://${req.get("host")}${req.originalUrl}`,
|
|
|
|
|
+ );
|
|
|
res.send(finalHtml);
|
|
res.send(finalHtml);
|
|
|
} else {
|
|
} else {
|
|
|
// Post not found - serve SPA to handle 404
|
|
// Post not found - serve SPA to handle 404
|
|
@@ -971,7 +979,6 @@ app.get("*", async (req, res) => {
|
|
|
} else {
|
|
} else {
|
|
|
res.status(404).send("Frontend build not found");
|
|
res.status(404).send("Frontend build not found");
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
res.status(500).send("Server Error");
|
|
res.status(500).send("Server Error");
|
|
|
}
|
|
}
|