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'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const app = express(); 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'); // Middleware 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 - Simple as fuck app.use(session({ secret: 'your-secret-key-here-change-this', resave: true, saveUninitialized: true, cookie: { secure: false, // Disable secure for now to test maxAge: 24 * 60 * 60 * 1000 } })); // 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' }); } // Check if user is authenticated function isAuthenticated(req, res, 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; } } // 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()) : [] }; } // 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`; } // 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; 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('connect.sid'); // Default session cookie name res.json({ success: true, message: 'Logged out successfully' }); }); }); // GET /api/auth/me - Get current user app.get('/api/auth/me', isAuthenticated, (req, res) => { 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 }); } } 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 }); } // 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' }); } 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' }); } // 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); } // 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' }); } 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' }); } }); // 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' }); } }); // 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' }); } 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 }); } }); // 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 }); } }); // 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 }); } }); // 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 }); } }); // Health check endpoint 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}`); });