| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516 |
- import express from 'express';
- import cors from 'cors';
- import session from 'express-session';
- import FileStore from 'session-file-store';
- import fs from 'fs-extra';
- import path from 'path';
- import { fileURLToPath } from 'url';
- import { v4 as uuidv4 } from 'uuid';
- 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);
- // Initialize FileStore for sessions
- const FileStoreSession = FileStore(session);
- const app = express();
- const PORT = process.env.PORT || 3001;
- // Paths
- const POSTS_DIR = path.join(__dirname, '../public/posts');
- const INDEX_FILE = path.join(POSTS_DIR, 'index.json');
- // Middleware
- app.use(cors({
- origin: 'http://localhost:5173', // Frontend URL
- credentials: true // Enable cookies
- }));
- app.use(express.json());
- app.use(express.urlencoded({ extended: true }));
- // Session configuration
- app.use(session({
- store: new FileStoreSession({
- path: path.join(__dirname, 'sessions'),
- ttl: 86400, // 24 hours in seconds
- retries: 5,
- factor: 1,
- minTimeout: 50,
- maxTimeout: 100
- }),
- secret: process.env.SESSION_SECRET || 'gooneral-wheelchair-secret-key-change-in-production',
- resave: false,
- saveUninitialized: false,
- name: 'gooneral-session',
- cookie: {
- secure: false, // Set to true in production with HTTPS
- httpOnly: true,
- maxAge: 24 * 60 * 60 * 1000, // 24 hours
- sameSite: 'lax' // Help with cross-origin cookies
- }
- }));
- // 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;
- 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' });
- });
- });
- // 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 });
- }
- });
- // 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}`);
- });
|