|
|
@@ -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}`);
|
|
|
+});
|