Prechádzať zdrojové kódy

refactor: reformat backend server and remove old post

Adam 3 mesiacov pred
rodič
commit
7ede690377

+ 441 - 399
backend/server.js

@@ -1,509 +1,551 @@
-import dotenv from 'dotenv';
-import express from 'express';
-import cors from 'cors';
-import session from 'express-session';
-import fs from 'fs-extra';
-import path from 'path';
-import { fileURLToPath } from 'url';
-import { v4 as uuidv4 } from 'uuid';
+import dotenv from "dotenv";
+import express from "express";
+import cors from "cors";
+import session from "express-session";
+import fs from "fs-extra";
+import path from "path";
+import { fileURLToPath } from "url";
+import { v4 as uuidv4 } from "uuid";
 
 // Load environment variables
 dotenv.config();
-import { authenticateUser, getUserByUsername, changeUserPassword } from './auth.js';
-import { 
-  getAllThemes, 
-  getActiveTheme, 
-  setActiveTheme, 
-  createCustomTheme, 
-  updateCustomTheme, 
-  deleteCustomTheme, 
-  exportTheme 
-} from './themes.js';
+import {
+    authenticateUser,
+    getUserByUsername,
+    changeUserPassword,
+} from "./auth.js";
+import {
+    getAllThemes,
+    getActiveTheme,
+    setActiveTheme,
+    createCustomTheme,
+    updateCustomTheme,
+    deleteCustomTheme,
+    exportTheme,
+} from "./themes.js";
 
 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);
 
-
 const app = express();
+app.set("trust proxy", 1); // Trust the first proxy (Caddy) so secure cookies work behind HTTPS termination
 const PORT = process.env.PORT || 3001;
 
 // Paths
-const POSTS_DIR = path.resolve(__dirname, process.env.POSTS_DIR || '../public/posts');
-const INDEX_FILE = path.join(POSTS_DIR, 'index.json');
+const POSTS_DIR = path.resolve(
+    __dirname,
+    process.env.POSTS_DIR || "../public/posts",
+);
+const INDEX_FILE = path.join(POSTS_DIR, "index.json");
 
 // Middleware
-app.use(cors({
-  origin: ['http://localhost:5173', 'https://goonblog.thevakhovske.eu.org'],
-  credentials: true // Enable cookies
-}));
+app.use(
+    cors({
+        origin: [
+            "http://localhost:5173",
+            "https://goonblog.thevakhovske.eu.org",
+        ],
+        credentials: true, // Enable cookies
+    }),
+);
 app.use(express.json());
 app.use(express.urlencoded({ extended: true }));
 
 // Session configuration
-app.use(session({
-  secret: process.env.SESSION_SECRET || 'gooneral-wheelchair-secret-key-change-in-production',
-  resave: false,
-  saveUninitialized: false,
-  name: 'gooneral-session',
-  cookie: {
-    secure: true,  // HTTPS required
-    httpOnly: true,
-    maxAge: 24 * 60 * 60 * 1000, // 24 hours
-    sameSite: 'lax'  // Changed from 'strict' to 'lax'
-  }
-}));
+app.use(
+    session({
+        secret:
+            process.env.SESSION_SECRET ||
+            "gooneral-wheelchair-secret-key-change-in-production",
+        resave: false,
+        saveUninitialized: false,
+        name: "gooneral-session",
+        cookie: {
+            secure: true, // HTTPS required
+            httpOnly: true,
+            maxAge: 24 * 60 * 60 * 1000, // 24 hours
+            sameSite: "lax", // Changed from 'strict' to 'lax'
+        },
+    }),
+);
 
 // Ensure posts directory exists
 await fs.ensureDir(POSTS_DIR);
 
 // Authentication middleware
 function requireAuth(req, res, next) {
-  if (req.session && req.session.user && req.session.user.role === 'admin') {
-    return next();
-  }
-  return res.status(401).json({ error: 'Authentication required' });
+    if (req.session && req.session.user && req.session.user.role === "admin") {
+        return next();
+    }
+    return res.status(401).json({ error: "Authentication required" });
 }
 
 // Check if user is authenticated
 function isAuthenticated(req, res, next) {
-  req.isAuthenticated = !!(req.session && req.session.user);
-  req.user = req.session?.user || null;
-  next();
+    req.isAuthenticated = !!(req.session && req.session.user);
+    req.user = req.session?.user || null;
+    next();
 }
 
 // Helper function to generate index.json
 async function generateIndex() {
-  try {
-    const files = await fs.readdir(POSTS_DIR);
-    const mdFiles = files.filter(f => f.endsWith('.md'));
-    await fs.writeJSON(INDEX_FILE, mdFiles, { spaces: 2 });
-    console.log(`Index updated: ${mdFiles.length} posts`);
-    return mdFiles;
-  } catch (error) {
-    console.error('Error generating index:', error);
-    throw error;
-  }
+    try {
+        const files = await fs.readdir(POSTS_DIR);
+        const mdFiles = files.filter((f) => f.endsWith(".md"));
+        await fs.writeJSON(INDEX_FILE, mdFiles, { spaces: 2 });
+        console.log(`Index updated: ${mdFiles.length} posts`);
+        return mdFiles;
+    } catch (error) {
+        console.error("Error generating index:", error);
+        throw error;
+    }
 }
 
 // Helper function to parse post metadata
 function parsePostMetadata(content) {
-  const titleMatch = content.match(/title:\s*(.*)/);
-  const descMatch = content.match(/desc:\s*(.*)/);
-  const tagsMatch = content.match(/tags:\s*(.*)/);
-  
-  return {
-    title: titleMatch ? titleMatch[1].trim() : 'Untitled',
-    description: descMatch ? descMatch[1].trim() : '',
-    tags: tagsMatch ? tagsMatch[1].split(',').map(tag => tag.trim()) : []
-  };
+    const titleMatch = content.match(/title:\s*(.*)/);
+    const descMatch = content.match(/desc:\s*(.*)/);
+    const tagsMatch = content.match(/tags:\s*(.*)/);
+
+    return {
+        title: titleMatch ? titleMatch[1].trim() : "Untitled",
+        description: descMatch ? descMatch[1].trim() : "",
+        tags: tagsMatch ? tagsMatch[1].split(",").map((tag) => tag.trim()) : [],
+    };
 }
 
 // Helper function to generate filename from title
 function generateFilename(title) {
-  // Create date-based filename similar to existing pattern
-  const date = new Date();
-  const dateStr = date.toISOString().slice(0, 10).replace(/-/g, '');
-  const slug = title.toLowerCase()
-    .replace(/[^a-z0-9]+/g, '-')
-    .replace(/^-|-$/g, '')
-    .slice(0, 30);
-  
-  return slug ? `${dateStr}-${slug}.md` : `${dateStr}.md`;
+    // Create date-based filename similar to existing pattern
+    const date = new Date();
+    const dateStr = date.toISOString().slice(0, 10).replace(/-/g, "");
+    const slug = title
+        .toLowerCase()
+        .replace(/[^a-z0-9]+/g, "-")
+        .replace(/^-|-$/g, "")
+        .slice(0, 30);
+
+    return slug ? `${dateStr}-${slug}.md` : `${dateStr}.md`;
 }
 
 // Authentication Routes
 
 // POST /api/auth/login - Login
-app.post('/api/auth/login', async (req, res) => {
-  try {
-    const { username, password } = req.body;
-    
-    if (!username || !password) {
-      return res.status(400).json({ error: 'Username and password are required' });
-    }
-    
-    const user = await authenticateUser(username, password);
-    if (!user) {
-      return res.status(401).json({ error: 'Invalid username or password' });
-    }
-    
-    // Store user in session
-    req.session.user = user;
-    console.log('Login successful - Session ID:', req.sessionID);
-    console.log('Login successful - Stored user:', req.session.user);
-    
-    // Manually save the session to ensure it's persisted
-    req.session.save((err) => {
-      if (err) {
-        console.error('Session save error:', err);
-        return res.status(500).json({ error: 'Failed to save session' });
-      }
-      
-      console.log('Session saved successfully');
-      res.json({ 
-        success: true, 
-        user: {
-          username: user.username,
-          role: user.role
+app.post("/api/auth/login", async (req, res) => {
+    try {
+        const { username, password } = req.body;
+
+        if (!username || !password) {
+            return res
+                .status(400)
+                .json({ error: "Username and password are required" });
         }
-      });
-    });
-  } catch (error) {
-    console.error('Login error:', error);
-    res.status(500).json({ error: 'Login failed' });
-  }
+
+        const user = await authenticateUser(username, password);
+        if (!user) {
+            return res
+                .status(401)
+                .json({ error: "Invalid username or password" });
+        }
+
+        // Store user in session
+        req.session.user = user;
+        console.log("Login successful - Session ID:", req.sessionID);
+        console.log("Login successful - Stored user:", req.session.user);
+
+        // Manually save the session to ensure it's persisted
+        req.session.save((err) => {
+            if (err) {
+                console.error("Session save error:", err);
+                return res
+                    .status(500)
+                    .json({ error: "Failed to save session" });
+            }
+
+            console.log("Session saved successfully");
+            res.json({
+                success: true,
+                user: {
+                    username: user.username,
+                    role: user.role,
+                },
+            });
+        });
+    } catch (error) {
+        console.error("Login error:", error);
+        res.status(500).json({ error: "Login failed" });
+    }
 });
 
 // POST /api/auth/logout - Logout
-app.post('/api/auth/logout', (req, res) => {
-  req.session.destroy((err) => {
-    if (err) {
-      return res.status(500).json({ error: 'Logout failed' });
-    }
-    res.clearCookie('gooneral-session'); // Use the same name as configured
-    res.json({ success: true, message: 'Logged out successfully' });
-  });
+app.post("/api/auth/logout", (req, res) => {
+    req.session.destroy((err) => {
+        if (err) {
+            return res.status(500).json({ error: "Logout failed" });
+        }
+        res.clearCookie("gooneral-session"); // Use the same name as configured
+        res.json({ success: true, message: "Logged out successfully" });
+    });
 });
 
 // GET /api/auth/me - Get current user
-app.get('/api/auth/me', isAuthenticated, (req, res) => {
-  console.log('Auth check - Session ID:', req.sessionID);
-  console.log('Auth check - Session user:', req.session?.user);
-  console.log('Auth check - Is authenticated:', req.isAuthenticated);
-  
-  if (req.isAuthenticated) {
-    res.json({ 
-      user: {
-        username: req.user.username,
-        role: req.user.role
-      }
-    });
-  } else {
-    res.json({ user: null });
-  }
+app.get("/api/auth/me", isAuthenticated, (req, res) => {
+    console.log("Auth check - Session ID:", req.sessionID);
+    console.log("Auth check - Session user:", req.session?.user);
+    console.log("Auth check - Is authenticated:", req.isAuthenticated);
+
+    if (req.isAuthenticated) {
+        res.json({
+            user: {
+                username: req.user.username,
+                role: req.user.role,
+            },
+        });
+    } else {
+        res.json({ user: null });
+    }
 });
 
 // POST /api/auth/change-password - Change password
-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 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' });
-    }
-    
-    const result = await changeUserPassword(req.user.username, currentPassword, newPassword);
-    
-    if (result.success) {
-      res.json({ success: true, message: result.message });
-    } else {
-      res.status(400).json({ error: result.message });
+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 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",
+            });
+        }
+
+        const result = await changeUserPassword(
+            req.user.username,
+            currentPassword,
+            newPassword,
+        );
+
+        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' });
-  }
 });
 
 // API Routes
 
 // GET /api/posts - Get all posts with metadata
-app.get('/api/posts', async (req, res) => {
-  try {
-    const files = await generateIndex();
-    const posts = [];
-    
-    for (const filename of files) {
-      const filePath = path.join(POSTS_DIR, filename);
-      const content = await fs.readFile(filePath, 'utf8');
-      const metadata = parsePostMetadata(content);
-      const slug = filename.replace('.md', '');
-      
-      posts.push({
-        slug,
-        filename,
-        ...metadata,
-        content,
-        createdAt: (await fs.stat(filePath)).birthtime,
-        updatedAt: (await fs.stat(filePath)).mtime
-      });
+app.get("/api/posts", async (req, res) => {
+    try {
+        const files = await generateIndex();
+        const posts = [];
+
+        for (const filename of files) {
+            const filePath = path.join(POSTS_DIR, filename);
+            const content = await fs.readFile(filePath, "utf8");
+            const metadata = parsePostMetadata(content);
+            const slug = filename.replace(".md", "");
+
+            posts.push({
+                slug,
+                filename,
+                ...metadata,
+                content,
+                createdAt: (await fs.stat(filePath)).birthtime,
+                updatedAt: (await fs.stat(filePath)).mtime,
+            });
+        }
+
+        // Sort by creation date, newest first
+        posts.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
+
+        res.json(posts);
+    } catch (error) {
+        console.error("Error fetching posts:", error);
+        res.status(500).json({ error: "Failed to fetch posts" });
     }
-    
-    // Sort by creation date, newest first
-    posts.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
-    
-    res.json(posts);
-  } catch (error) {
-    console.error('Error fetching posts:', error);
-    res.status(500).json({ error: 'Failed to fetch posts' });
-  }
 });
 
 // GET /api/posts/:slug - Get specific post
-app.get('/api/posts/:slug', async (req, res) => {
-  try {
-    const { slug } = req.params;
-    const filename = `${slug}.md`;
-    const filePath = path.join(POSTS_DIR, filename);
-    
-    if (!(await fs.pathExists(filePath))) {
-      return res.status(404).json({ error: 'Post not found' });
+app.get("/api/posts/:slug", async (req, res) => {
+    try {
+        const { slug } = req.params;
+        const filename = `${slug}.md`;
+        const filePath = path.join(POSTS_DIR, filename);
+
+        if (!(await fs.pathExists(filePath))) {
+            return res.status(404).json({ error: "Post not found" });
+        }
+
+        const content = await fs.readFile(filePath, "utf8");
+        const metadata = parsePostMetadata(content);
+        const stats = await fs.stat(filePath);
+
+        res.json({
+            slug,
+            filename,
+            ...metadata,
+            content,
+            createdAt: stats.birthtime,
+            updatedAt: stats.mtime,
+        });
+    } catch (error) {
+        console.error("Error fetching post:", error);
+        res.status(500).json({ error: "Failed to fetch post" });
     }
-    
-    const content = await fs.readFile(filePath, 'utf8');
-    const metadata = parsePostMetadata(content);
-    const stats = await fs.stat(filePath);
-    
-    res.json({
-      slug,
-      filename,
-      ...metadata,
-      content,
-      createdAt: stats.birthtime,
-      updatedAt: stats.mtime
-    });
-  } catch (error) {
-    console.error('Error fetching post:', error);
-    res.status(500).json({ error: 'Failed to fetch post' });
-  }
 });
 
 // POST /api/posts - Create new post
-app.post('/api/posts', requireAuth, async (req, res) => {
-  try {
-    const { title, description, content, tags } = req.body;
-    
-    if (!title || !content) {
-      return res.status(400).json({ error: 'Title and content are required' });
-    }
-    
-    // Generate filename
-    const filename = generateFilename(title);
-    const filePath = path.join(POSTS_DIR, filename);
-    
-    // Check if file already exists
-    if (await fs.pathExists(filePath)) {
-      return res.status(409).json({ error: 'Post with similar title already exists' });
+app.post("/api/posts", requireAuth, async (req, res) => {
+    try {
+        const { title, description, content, tags } = req.body;
+
+        if (!title || !content) {
+            return res
+                .status(400)
+                .json({ error: "Title and content are required" });
+        }
+
+        // Generate filename
+        const filename = generateFilename(title);
+        const filePath = path.join(POSTS_DIR, filename);
+
+        // Check if file already exists
+        if (await fs.pathExists(filePath)) {
+            return res
+                .status(409)
+                .json({ error: "Post with similar title already exists" });
+        }
+
+        // Format the post content
+        let postContent = "";
+        postContent += `title: ${title}\n`;
+        if (description) postContent += `desc: ${description}\n`;
+        if (tags && tags.length > 0)
+            postContent += `tags: ${tags.join(", ")}\n`;
+        postContent += "\n" + content;
+
+        // Write the file
+        await fs.writeFile(filePath, postContent, "utf8");
+
+        // Update index
+        await generateIndex();
+
+        const slug = filename.replace(".md", "");
+        const stats = await fs.stat(filePath);
+
+        res.status(201).json({
+            slug,
+            filename,
+            title,
+            description: description || "",
+            tags: tags || [],
+            content: postContent,
+            createdAt: stats.birthtime,
+            updatedAt: stats.mtime,
+        });
+    } catch (error) {
+        console.error("Error creating post:", error);
+        res.status(500).json({ error: "Failed to create post" });
     }
-    
-    // Format the post content
-    let postContent = '';
-    postContent += `title: ${title}\n`;
-    if (description) postContent += `desc: ${description}\n`;
-    if (tags && tags.length > 0) postContent += `tags: ${tags.join(', ')}\n`;
-    postContent += '\n' + content;
-    
-    // Write the file
-    await fs.writeFile(filePath, postContent, 'utf8');
-    
-    // Update index
-    await generateIndex();
-    
-    const slug = filename.replace('.md', '');
-    const stats = await fs.stat(filePath);
-    
-    res.status(201).json({
-      slug,
-      filename,
-      title,
-      description: description || '',
-      tags: tags || [],
-      content: postContent,
-      createdAt: stats.birthtime,
-      updatedAt: stats.mtime
-    });
-  } catch (error) {
-    console.error('Error creating post:', error);
-    res.status(500).json({ error: 'Failed to create post' });
-  }
 });
 
 // PUT /api/posts/:slug - Update existing post
-app.put('/api/posts/:slug', requireAuth, async (req, res) => {
-  try {
-    const { slug } = req.params;
-    const { title, description, content, tags } = req.body;
-    
-    const oldFilename = `${slug}.md`;
-    const oldFilePath = path.join(POSTS_DIR, oldFilename);
-    
-    if (!(await fs.pathExists(oldFilePath))) {
-      return res.status(404).json({ error: 'Post not found' });
-    }
-    
-    if (!title || !content) {
-      return res.status(400).json({ error: 'Title and content are required' });
-    }
-    
-    // Generate new filename if title changed
-    const newFilename = generateFilename(title);
-    const newFilePath = path.join(POSTS_DIR, newFilename);
-    
-    // Format the post content
-    let postContent = '';
-    postContent += `title: ${title}\n`;
-    if (description) postContent += `desc: ${description}\n`;
-    if (tags && tags.length > 0) postContent += `tags: ${tags.join(', ')}\n`;
-    postContent += '\n' + content;
-    
-    // Write to new file
-    await fs.writeFile(newFilePath, postContent, 'utf8');
-    
-    // If filename changed, remove old file
-    if (oldFilename !== newFilename) {
-      await fs.remove(oldFilePath);
+app.put("/api/posts/:slug", requireAuth, async (req, res) => {
+    try {
+        const { slug } = req.params;
+        const { title, description, content, tags } = req.body;
+
+        const oldFilename = `${slug}.md`;
+        const oldFilePath = path.join(POSTS_DIR, oldFilename);
+
+        if (!(await fs.pathExists(oldFilePath))) {
+            return res.status(404).json({ error: "Post not found" });
+        }
+
+        if (!title || !content) {
+            return res
+                .status(400)
+                .json({ error: "Title and content are required" });
+        }
+
+        // Generate new filename if title changed
+        const newFilename = generateFilename(title);
+        const newFilePath = path.join(POSTS_DIR, newFilename);
+
+        // Format the post content
+        let postContent = "";
+        postContent += `title: ${title}\n`;
+        if (description) postContent += `desc: ${description}\n`;
+        if (tags && tags.length > 0)
+            postContent += `tags: ${tags.join(", ")}\n`;
+        postContent += "\n" + content;
+
+        // Write to new file
+        await fs.writeFile(newFilePath, postContent, "utf8");
+
+        // If filename changed, remove old file
+        if (oldFilename !== newFilename) {
+            await fs.remove(oldFilePath);
+        }
+
+        // Update index
+        await generateIndex();
+
+        const newSlug = newFilename.replace(".md", "");
+        const stats = await fs.stat(newFilePath);
+
+        res.json({
+            slug: newSlug,
+            filename: newFilename,
+            title,
+            description: description || "",
+            tags: tags || [],
+            content: postContent,
+            createdAt: stats.birthtime,
+            updatedAt: stats.mtime,
+        });
+    } catch (error) {
+        console.error("Error updating post:", error);
+        res.status(500).json({ error: "Failed to update post" });
     }
-    
-    // Update index
-    await generateIndex();
-    
-    const newSlug = newFilename.replace('.md', '');
-    const stats = await fs.stat(newFilePath);
-    
-    res.json({
-      slug: newSlug,
-      filename: newFilename,
-      title,
-      description: description || '',
-      tags: tags || [],
-      content: postContent,
-      createdAt: stats.birthtime,
-      updatedAt: stats.mtime
-    });
-  } catch (error) {
-    console.error('Error updating post:', error);
-    res.status(500).json({ error: 'Failed to update post' });
-  }
 });
 
 // DELETE /api/posts/:slug - Delete post
-app.delete('/api/posts/:slug', requireAuth, async (req, res) => {
-  try {
-    const { slug } = req.params;
-    const filename = `${slug}.md`;
-    const filePath = path.join(POSTS_DIR, filename);
-    
-    if (!(await fs.pathExists(filePath))) {
-      return res.status(404).json({ error: 'Post not found' });
+app.delete("/api/posts/:slug", requireAuth, async (req, res) => {
+    try {
+        const { slug } = req.params;
+        const filename = `${slug}.md`;
+        const filePath = path.join(POSTS_DIR, filename);
+
+        if (!(await fs.pathExists(filePath))) {
+            return res.status(404).json({ error: "Post not found" });
+        }
+
+        await fs.remove(filePath);
+        await generateIndex();
+
+        res.json({ message: "Post deleted successfully" });
+    } catch (error) {
+        console.error("Error deleting post:", error);
+        res.status(500).json({ error: "Failed to delete post" });
     }
-    
-    await fs.remove(filePath);
-    await generateIndex();
-    
-    res.json({ message: 'Post deleted successfully' });
-  } catch (error) {
-    console.error('Error deleting post:', error);
-    res.status(500).json({ error: 'Failed to delete post' });
-  }
 });
 
 // Theme API Routes
 
 // GET /api/themes - Get all themes
-app.get('/api/themes', async (req, res) => {
-  try {
-    const themesData = await getAllThemes();
-    res.json(themesData);
-  } catch (error) {
-    console.error('Error fetching themes:', error);
-    res.status(500).json({ error: 'Failed to fetch themes' });
-  }
+app.get("/api/themes", async (req, res) => {
+    try {
+        const themesData = await getAllThemes();
+        res.json(themesData);
+    } catch (error) {
+        console.error("Error fetching themes:", error);
+        res.status(500).json({ error: "Failed to fetch themes" });
+    }
 });
 
 // GET /api/themes/active - Get active theme
-app.get('/api/themes/active', async (req, res) => {
-  try {
-    const activeTheme = await getActiveTheme();
-    res.json(activeTheme);
-  } catch (error) {
-    console.error('Error fetching active theme:', error);
-    res.status(500).json({ error: 'Failed to fetch active theme' });
-  }
+app.get("/api/themes/active", async (req, res) => {
+    try {
+        const activeTheme = await getActiveTheme();
+        res.json(activeTheme);
+    } catch (error) {
+        console.error("Error fetching active theme:", error);
+        res.status(500).json({ error: "Failed to fetch active theme" });
+    }
 });
 
 // PUT /api/themes/active - Set active theme
-app.put('/api/themes/active', requireAuth, async (req, res) => {
-  try {
-    const { themeId } = req.body;
-    
-    if (!themeId) {
-      return res.status(400).json({ error: 'Theme ID is required' });
+app.put("/api/themes/active", requireAuth, async (req, res) => {
+    try {
+        const { themeId } = req.body;
+
+        if (!themeId) {
+            return res.status(400).json({ error: "Theme ID is required" });
+        }
+
+        const activeTheme = await setActiveTheme(themeId);
+        res.json({ success: true, theme: activeTheme });
+    } catch (error) {
+        console.error("Error setting active theme:", error);
+        res.status(400).json({ error: error.message });
     }
-    
-    const activeTheme = await setActiveTheme(themeId);
-    res.json({ success: true, theme: activeTheme });
-  } catch (error) {
-    console.error('Error setting active theme:', error);
-    res.status(400).json({ error: error.message });
-  }
 });
 
 // POST /api/themes - Create custom theme
-app.post('/api/themes', requireAuth, async (req, res) => {
-  try {
-    const themeData = req.body;
-    const newTheme = await createCustomTheme(themeData);
-    res.status(201).json(newTheme);
-  } catch (error) {
-    console.error('Error creating theme:', error);
-    res.status(400).json({ error: error.message });
-  }
+app.post("/api/themes", requireAuth, async (req, res) => {
+    try {
+        const themeData = req.body;
+        const newTheme = await createCustomTheme(themeData);
+        res.status(201).json(newTheme);
+    } catch (error) {
+        console.error("Error creating theme:", error);
+        res.status(400).json({ error: error.message });
+    }
 });
 
 // PUT /api/themes/:themeId - Update custom theme
-app.put('/api/themes/:themeId', requireAuth, async (req, res) => {
-  try {
-    const { themeId } = req.params;
-    const themeData = req.body;
-    
-    const updatedTheme = await updateCustomTheme(themeId, themeData);
-    res.json(updatedTheme);
-  } catch (error) {
-    console.error('Error updating theme:', error);
-    res.status(400).json({ error: error.message });
-  }
+app.put("/api/themes/:themeId", requireAuth, async (req, res) => {
+    try {
+        const { themeId } = req.params;
+        const themeData = req.body;
+
+        const updatedTheme = await updateCustomTheme(themeId, themeData);
+        res.json(updatedTheme);
+    } catch (error) {
+        console.error("Error updating theme:", error);
+        res.status(400).json({ error: error.message });
+    }
 });
 
 // DELETE /api/themes/:themeId - Delete custom theme
-app.delete('/api/themes/:themeId', requireAuth, async (req, res) => {
-  try {
-    const { themeId } = req.params;
-    await deleteCustomTheme(themeId);
-    res.json({ success: true, message: 'Theme deleted successfully' });
-  } catch (error) {
-    console.error('Error deleting theme:', error);
-    res.status(400).json({ error: error.message });
-  }
+app.delete("/api/themes/:themeId", requireAuth, async (req, res) => {
+    try {
+        const { themeId } = req.params;
+        await deleteCustomTheme(themeId);
+        res.json({ success: true, message: "Theme deleted successfully" });
+    } catch (error) {
+        console.error("Error deleting theme:", error);
+        res.status(400).json({ error: error.message });
+    }
 });
 
 // GET /api/themes/:themeId/export - Export theme
-app.get('/api/themes/:themeId/export', requireAuth, async (req, res) => {
-  try {
-    const { themeId } = req.params;
-    const themeData = await exportTheme(themeId);
-    
-    res.setHeader('Content-Type', 'application/json');
-    res.setHeader('Content-Disposition', `attachment; filename="theme-${themeId}.json"`);
-    res.json(themeData);
-  } catch (error) {
-    console.error('Error exporting theme:', error);
-    res.status(400).json({ error: error.message });
-  }
+app.get("/api/themes/:themeId/export", requireAuth, async (req, res) => {
+    try {
+        const { themeId } = req.params;
+        const themeData = await exportTheme(themeId);
+
+        res.setHeader("Content-Type", "application/json");
+        res.setHeader(
+            "Content-Disposition",
+            `attachment; filename="theme-${themeId}.json"`,
+        );
+        res.json(themeData);
+    } catch (error) {
+        console.error("Error exporting theme:", error);
+        res.status(400).json({ error: error.message });
+    }
 });
 
 // Health check endpoint
-app.get('/api/health', (req, res) => {
-  res.json({ status: 'OK', timestamp: new Date().toISOString() });
+app.get("/api/health", (req, res) => {
+    res.json({ status: "OK", timestamp: new Date().toISOString() });
 });
 
 // Generate initial index on startup
 await generateIndex();
 
 app.listen(PORT, () => {
-  console.log(`🚀 Backend server running on http://localhost:${PORT}`);
-  console.log(`📁 Posts directory: ${POSTS_DIR}`);
-});
+    console.log(`🚀 Backend server running on http://localhost:${PORT}`);
+    console.log(`📁 Posts directory: ${POSTS_DIR}`);
+});

+ 0 - 47
public/posts/20250929-170-pixels-sharpening-included.md

@@ -1,47 +0,0 @@
-title: 170 pixels (sharpening included)
-desc: Eradicate all edges. Or how OEMs reiterate their meaning of portraits.
-
-For years, OEMs have been keen on working with portrait modes on phones, each with their own varieties and quirks. But in the past four years, portrait mode has been redefined from the ground up, prioritizing hardware over software to better separate the subject from the background and maintain compression ratios [^first].
-
-[^first]: _The difference in how flattened subject is, related to background, and directly correlating to the final bokeh look._
-
-This shift marks a significant evolution from the early days of portrait photography on phones. Initially, portrait modes heavily relied on software algorithms to create a blurred background effect, but now, the mode relies on both the provided hardware background blur and the supplementary effect from mode. 
-
-These methods often _actually, mostly_ produced inconsistent results, with jagged edges around the subject and an unnatural, sometimes "painted" look to the bokeh.
-
-
-
-With all of that being taken in account, the main source of improvements is now, _**wow**_, hardware!
-
-## Test subjects
-
-| | HONOR Magic 7 Pro | OPPO Find X7 Ultra | vivo X200 (CN, Base model) |
-| :--- | :--- | :--- | :--- |
-| **Processor** | Qualcomm Snapdragon 8 Elite | Qualcomm Snapdragon 8 Gen 3 | MediaTek Dimensity 9400 |
-| **Telephoto Camera (3x, primary portrait camera)**| 200MP 1/1.4" 0.56µm (2.24µm @ Hexa-deca-bayer) Samsung S5KHP3 based telephoto | 50MP 1/1.56" 1.0µm (2.0µm @ Quad-bayer) Sony IMX890 based telephoto | 50MP 1/1.95" 0.8µm (1.6µm @ Quad-bayer) Sony IMX882/LYT-600 based telephoto |
-| **EFL & Aperture** | 72mm at ƒ2.6 | 62mm (cropped to 70mm) at ƒ2.6 | 70mm at ƒ2.6 |
-| **Portrait Profiles** | Harcourt-tuned | No portrait profiles provided by Hasselblad | Zeiss-tuned |
-| **Software version**| MagicOS meafnaf[^second] 9.0.0.123 (C185E1R3P2)|ColorOS CN 15.0.0.731(CN01)|OriginOS CN 15.0.31.15|
-
-[^second]: _Middle East, North Africa, Africa. Also common region for Hong Kong units._
-
-## _Why these three?_
-Because it would be fun to see three different types of hardware stacked against each other, wouldn't it? And to make people speculate whether each of these phones would be above either just based on raw specs.
-
-# Chapter 1: Loss of detail (LoD)
-Loss of detail is a common consequence of lower-resolution shots, that portrait shots usually are, since they're being shot with a "fusion" technique by combining the monochromatic information from auxiliary camera to separate foreground subject from background.
-
-In this case, the auxiliary cameras are not needed, as we have enough data to split FG from BG:
-- Natural telephoto DoF provides an understandable canvas for image processing
-- Laser AF provides information for distance between the foreground and camera, to provide accurate measurements to be used later
-
-Our test devices work exactly in accordance to these principles, and do not require monochrome segmentation source to acquire differential information.
-
-However, some devices without hardware capabilities to precisely differ S/FG use software depth map, like Google's Pixel devices do:
-
-<details>
-<summary>Click to view image comparison</summary>
-
-![Comparison of portrait modes](image.png)
-
-</details>

+ 1 - 1
public/posts/index.json

@@ -1,3 +1,3 @@
 [
-  "20250929-170-pixels-sharpening-included.md"
+  "20251022-hey-there.md"
 ]

+ 1 - 1
src/App.jsx

@@ -41,7 +41,7 @@ const md = new MarkdownIt({
 .use(emoji)                   // GitHub-style emoji :emoji_name:
 .use(footnote);               // Standard footnotes [^1]
 
-const API_BASE = import.meta.env.PROD ? '/api' : 'http://localhost:3001/api';
+const API_BASE = 'https://goonblog.thevakhovske.eu.org/api';
 
 // Navigation Header Component
 function NavHeader() {

+ 1 - 1
src/components/AdminDashboard.jsx

@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
 import { Link } from 'react-router-dom';
 import { useTheme } from '../contexts/ThemeContext';
 
-const API_BASE = 'http://localhost:3001/api';
+const API_BASE = 'https://goonblog.thevakhovske.eu.org/api';
 
 function AdminDashboard() {
   const [posts, setPosts] = useState([]);

+ 1 - 1
src/components/PostEditor.jsx

@@ -3,7 +3,7 @@ import { useNavigate, useParams, Link } from 'react-router-dom';
 import MDEditor from '@uiw/react-md-editor';
 import '@uiw/react-md-editor/markdown-editor.css';
 import '@uiw/react-markdown-preview/markdown.css';
-const API_BASE = 'http://localhost:3001/api';
+const API_BASE = 'https://goonblog.thevakhovske.eu.org/api';
 
 // Note: MDEditor handles its own markdown processing for the editor interface
 // The final blog rendering uses the MarkdownIt instance in App.jsx

+ 1 - 1
src/contexts/AuthContext.jsx

@@ -1,6 +1,6 @@
 import React, { createContext, useContext, useState, useEffect } from 'react';
 
-const API_BASE = import.meta.env.PROD ? '/api' : 'http://localhost:3001/api';
+const API_BASE = 'https://goonblog.thevakhovske.eu.org/api';
 
 const AuthContext = createContext();
 

+ 1 - 1
src/contexts/ThemeContext.jsx

@@ -1,6 +1,6 @@
 import React, { createContext, useContext, useState, useEffect } from 'react';
 
-const API_BASE = import.meta.env.PROD ? '/api' : 'http://localhost:3001/api';
+const API_BASE = 'https://goonblog.thevakhovske.eu.org/api';
 
 const ThemeContext = createContext();