Ver código fonte

fix: refactor password change endpoint with proper authentication middleware

Adam Jafarov 1 semana atrás
pai
commit
7967ad3c60
2 arquivos alterados com 90 adições e 82 exclusões
  1. 87 80
      backend/server.js
  2. 3 2
      backend/users.json

+ 87 - 80
backend/server.js

@@ -60,7 +60,10 @@ const storage = multer.diskStorage({
     },
     filename: function (req, file, cb) {
         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 newFilename = `${namePart}_${timestamp}${ext}`;
         cb(null, newFilename);
@@ -69,7 +72,6 @@ const storage = multer.diskStorage({
 
 const upload = multer({ storage: storage });
 
-
 // Middleware
 app.use(
     cors({
@@ -191,7 +193,6 @@ function getSlugFromTitle(title) {
     return `${dateStr}-${slugText}`;
 }
 
-
 // Authentication Routes
 
 // POST /api/auth/login - Login
@@ -271,38 +272,52 @@ app.get("/api/auth/me", isAuthenticated, (req, res) => {
 });
 
 // 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
 
@@ -384,7 +399,9 @@ app.get("/api/media", requireAuth, async (req, res) => {
             }
 
             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
                     ? `/media-files/${slugPart}/images/${file}`
                     : `/media-files/uploads/${file}`;
@@ -394,7 +411,7 @@ app.get("/api/media", requireAuth, async (req, res) => {
                     url: url,
                     size: stats.size,
                     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) => {
     try {
         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
         // relativePath typically starts with /media-files/
@@ -431,7 +449,6 @@ app.delete("/api/media", requireAuth, async (req, res) => {
         } else {
             res.status(404).json({ error: "File not found" });
         }
-
     } catch (err) {
         console.error("Delete media error:", err);
         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
-        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);
     } catch (error) {
@@ -495,7 +515,10 @@ app.get("/api/posts/:slug", async (req, res) => {
         const stats = await fs.stat(filePath);
 
         // 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) {
             // Return 404 to hide existence, or 403 if we want to be explicit.
             // 404 is safer for "hidden" content.
@@ -619,7 +642,7 @@ app.put("/api/posts/:slug", requireAuth, async (req, res) => {
         if (oldFilename !== newFilename) {
             await fs.remove(oldFilePath);
             // 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.
             // 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
 app.delete("/api/posts/:slug", requireAuth, async (req, res) => {
     try {
@@ -702,8 +697,8 @@ app.delete("/api/posts/:slug", requireAuth, async (req, res) => {
         if (await fs.pathExists(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
         const slugDir = path.join(POSTS_DIR, slug);
         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
 app.get("/api/config", async (req, res) => {
@@ -777,9 +772,9 @@ app.put("/api/config", requireAuth, async (req, res) => {
     try {
         const updates = req.body;
         // Whitelist allowed config keys to prevent abuse
-        const allowedKeys = ['postWidth', 'activeTheme'];
+        const allowedKeys = ["postWidth", "activeTheme"];
         const filteredUpdates = Object.keys(updates)
-            .filter(key => allowedKeys.includes(key))
+            .filter((key) => allowedKeys.includes(key))
             .reduce((obj, key) => {
                 obj[key] = updates[key];
                 return obj;
@@ -866,7 +861,10 @@ app.use(
         maxAge: "1y", // 1 year
         immutable: true,
         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
     const title = metadata.title || "Gooneral Wheelchair";
     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
-    injected = injected.replace(/<title>.*<\/title>/, `<title>${title}</title>`);
+    injected = injected.replace(
+        /<title>.*<\/title>/,
+        `<title>${title}</title>`,
+    );
 
     // Meta Tags to Inject
     const metaTags = `
@@ -903,7 +905,6 @@ const injectMetaTags = async (html, metadata, url) => {
     return injected.replace("</head>", `${metaTags}</head>`);
 };
 
-
 // Handle Post Routes for SSR
 app.get("/posts/:slug", async (req, res) => {
     try {
@@ -932,9 +933,12 @@ app.get("/posts/:slug", async (req, res) => {
             // Check access (Hidden posts)
             // 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.
-            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) {
-                // Determine behavior: 
+                // Determine behavior:
                 // 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?
                 // Let's send default tags to avoid leaking info.
@@ -945,10 +949,14 @@ app.get("/posts/:slug", async (req, res) => {
             const pageMetadata = {
                 title: metadata.title,
                 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);
         } else {
             // Post not found - serve SPA to handle 404
@@ -971,7 +979,6 @@ app.get("*", async (req, res) => {
         } else {
             res.status(404).send("Frontend build not found");
         }
-
     } catch (err) {
         res.status(500).send("Server Error");
     }

+ 3 - 2
backend/users.json

@@ -1,8 +1,9 @@
 {
   "admin": {
     "username": "admin",
-    "passwordHash": "$2b$10$SmhFBzqW/RLvld2h1a/./OdKZhJL4FZpiBiFan.kzvT.vP1dp1g4C",
+    "passwordHash": "$2b$10$gzwnBlnxxv6PsAnPtszoZOeu9TONHQXW9Uvc8jgyZ6UWtVGN.0N9.",
     "role": "admin",
-    "createdAt": "2025-09-29T08:54:36.509Z"
+    "createdAt": "2025-09-29T08:54:36.509Z",
+    "updatedAt": "2026-02-02T17:18:47.138Z"
   }
 }