server.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. import express from 'express';
  2. import cors from 'cors';
  3. import session from 'express-session';
  4. import FileStore from 'session-file-store';
  5. import fs from 'fs-extra';
  6. import path from 'path';
  7. import { fileURLToPath } from 'url';
  8. import { v4 as uuidv4 } from 'uuid';
  9. import { authenticateUser, getUserByUsername, changeUserPassword } from './auth.js';
  10. import {
  11. getAllThemes,
  12. getActiveTheme,
  13. setActiveTheme,
  14. createCustomTheme,
  15. updateCustomTheme,
  16. deleteCustomTheme,
  17. exportTheme
  18. } from './themes.js';
  19. const __filename = fileURLToPath(import.meta.url);
  20. const __dirname = path.dirname(__filename);
  21. // Initialize FileStore for sessions
  22. const FileStoreSession = FileStore(session);
  23. const app = express();
  24. const PORT = process.env.PORT || 3001;
  25. // Paths
  26. const POSTS_DIR = path.join(__dirname, '../public/posts');
  27. const INDEX_FILE = path.join(POSTS_DIR, 'index.json');
  28. // Middleware
  29. app.use(cors({
  30. origin: 'http://localhost:5173', // Frontend URL
  31. credentials: true // Enable cookies
  32. }));
  33. app.use(express.json());
  34. app.use(express.urlencoded({ extended: true }));
  35. // Session configuration
  36. app.use(session({
  37. store: new FileStoreSession({
  38. path: path.join(__dirname, 'sessions'),
  39. ttl: 86400, // 24 hours in seconds
  40. retries: 5,
  41. factor: 1,
  42. minTimeout: 50,
  43. maxTimeout: 100
  44. }),
  45. secret: process.env.SESSION_SECRET || 'gooneral-wheelchair-secret-key-change-in-production',
  46. resave: false,
  47. saveUninitialized: false,
  48. name: 'gooneral-session',
  49. cookie: {
  50. secure: false, // Set to true in production with HTTPS
  51. httpOnly: true,
  52. maxAge: 24 * 60 * 60 * 1000, // 24 hours
  53. sameSite: 'lax' // Help with cross-origin cookies
  54. }
  55. }));
  56. // Ensure posts directory exists
  57. await fs.ensureDir(POSTS_DIR);
  58. // Authentication middleware
  59. function requireAuth(req, res, next) {
  60. if (req.session && req.session.user && req.session.user.role === 'admin') {
  61. return next();
  62. }
  63. return res.status(401).json({ error: 'Authentication required' });
  64. }
  65. // Check if user is authenticated
  66. function isAuthenticated(req, res, next) {
  67. req.isAuthenticated = !!(req.session && req.session.user);
  68. req.user = req.session?.user || null;
  69. next();
  70. }
  71. // Helper function to generate index.json
  72. async function generateIndex() {
  73. try {
  74. const files = await fs.readdir(POSTS_DIR);
  75. const mdFiles = files.filter(f => f.endsWith('.md'));
  76. await fs.writeJSON(INDEX_FILE, mdFiles, { spaces: 2 });
  77. console.log(`Index updated: ${mdFiles.length} posts`);
  78. return mdFiles;
  79. } catch (error) {
  80. console.error('Error generating index:', error);
  81. throw error;
  82. }
  83. }
  84. // Helper function to parse post metadata
  85. function parsePostMetadata(content) {
  86. const titleMatch = content.match(/title:\s*(.*)/);
  87. const descMatch = content.match(/desc:\s*(.*)/);
  88. const tagsMatch = content.match(/tags:\s*(.*)/);
  89. return {
  90. title: titleMatch ? titleMatch[1].trim() : 'Untitled',
  91. description: descMatch ? descMatch[1].trim() : '',
  92. tags: tagsMatch ? tagsMatch[1].split(',').map(tag => tag.trim()) : []
  93. };
  94. }
  95. // Helper function to generate filename from title
  96. function generateFilename(title) {
  97. // Create date-based filename similar to existing pattern
  98. const date = new Date();
  99. const dateStr = date.toISOString().slice(0, 10).replace(/-/g, '');
  100. const slug = title.toLowerCase()
  101. .replace(/[^a-z0-9]+/g, '-')
  102. .replace(/^-|-$/g, '')
  103. .slice(0, 30);
  104. return slug ? `${dateStr}-${slug}.md` : `${dateStr}.md`;
  105. }
  106. // Authentication Routes
  107. // POST /api/auth/login - Login
  108. app.post('/api/auth/login', async (req, res) => {
  109. try {
  110. const { username, password } = req.body;
  111. if (!username || !password) {
  112. return res.status(400).json({ error: 'Username and password are required' });
  113. }
  114. const user = await authenticateUser(username, password);
  115. if (!user) {
  116. return res.status(401).json({ error: 'Invalid username or password' });
  117. }
  118. // Store user in session
  119. req.session.user = user;
  120. console.log('Login successful - Session ID:', req.sessionID);
  121. console.log('Login successful - Stored user:', req.session.user);
  122. // Manually save the session to ensure it's persisted
  123. req.session.save((err) => {
  124. if (err) {
  125. console.error('Session save error:', err);
  126. return res.status(500).json({ error: 'Failed to save session' });
  127. }
  128. console.log('Session saved successfully');
  129. res.json({
  130. success: true,
  131. user: {
  132. username: user.username,
  133. role: user.role
  134. }
  135. });
  136. });
  137. } catch (error) {
  138. console.error('Login error:', error);
  139. res.status(500).json({ error: 'Login failed' });
  140. }
  141. });
  142. // POST /api/auth/logout - Logout
  143. app.post('/api/auth/logout', (req, res) => {
  144. req.session.destroy((err) => {
  145. if (err) {
  146. return res.status(500).json({ error: 'Logout failed' });
  147. }
  148. res.clearCookie('gooneral-session'); // Use the same name as configured
  149. res.json({ success: true, message: 'Logged out successfully' });
  150. });
  151. });
  152. // GET /api/auth/me - Get current user
  153. app.get('/api/auth/me', isAuthenticated, (req, res) => {
  154. console.log('Auth check - Session ID:', req.sessionID);
  155. console.log('Auth check - Session user:', req.session?.user);
  156. console.log('Auth check - Is authenticated:', req.isAuthenticated);
  157. if (req.isAuthenticated) {
  158. res.json({
  159. user: {
  160. username: req.user.username,
  161. role: req.user.role
  162. }
  163. });
  164. } else {
  165. res.json({ user: null });
  166. }
  167. });
  168. // POST /api/auth/change-password - Change password
  169. app.post('/api/auth/change-password', requireAuth, async (req, res) => {
  170. try {
  171. const { currentPassword, newPassword } = req.body;
  172. if (!currentPassword || !newPassword) {
  173. return res.status(400).json({ error: 'Current password and new password are required' });
  174. }
  175. if (newPassword.length < 6) {
  176. return res.status(400).json({ error: 'New password must be at least 6 characters long' });
  177. }
  178. const result = await changeUserPassword(req.user.username, currentPassword, newPassword);
  179. if (result.success) {
  180. res.json({ success: true, message: result.message });
  181. } else {
  182. res.status(400).json({ error: result.message });
  183. }
  184. } catch (error) {
  185. console.error('Change password error:', error);
  186. res.status(500).json({ error: 'Failed to change password' });
  187. }
  188. });
  189. // API Routes
  190. // GET /api/posts - Get all posts with metadata
  191. app.get('/api/posts', async (req, res) => {
  192. try {
  193. const files = await generateIndex();
  194. const posts = [];
  195. for (const filename of files) {
  196. const filePath = path.join(POSTS_DIR, filename);
  197. const content = await fs.readFile(filePath, 'utf8');
  198. const metadata = parsePostMetadata(content);
  199. const slug = filename.replace('.md', '');
  200. posts.push({
  201. slug,
  202. filename,
  203. ...metadata,
  204. content,
  205. createdAt: (await fs.stat(filePath)).birthtime,
  206. updatedAt: (await fs.stat(filePath)).mtime
  207. });
  208. }
  209. // Sort by creation date, newest first
  210. posts.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
  211. res.json(posts);
  212. } catch (error) {
  213. console.error('Error fetching posts:', error);
  214. res.status(500).json({ error: 'Failed to fetch posts' });
  215. }
  216. });
  217. // GET /api/posts/:slug - Get specific post
  218. app.get('/api/posts/:slug', async (req, res) => {
  219. try {
  220. const { slug } = req.params;
  221. const filename = `${slug}.md`;
  222. const filePath = path.join(POSTS_DIR, filename);
  223. if (!(await fs.pathExists(filePath))) {
  224. return res.status(404).json({ error: 'Post not found' });
  225. }
  226. const content = await fs.readFile(filePath, 'utf8');
  227. const metadata = parsePostMetadata(content);
  228. const stats = await fs.stat(filePath);
  229. res.json({
  230. slug,
  231. filename,
  232. ...metadata,
  233. content,
  234. createdAt: stats.birthtime,
  235. updatedAt: stats.mtime
  236. });
  237. } catch (error) {
  238. console.error('Error fetching post:', error);
  239. res.status(500).json({ error: 'Failed to fetch post' });
  240. }
  241. });
  242. // POST /api/posts - Create new post
  243. app.post('/api/posts', requireAuth, async (req, res) => {
  244. try {
  245. const { title, description, content, tags } = req.body;
  246. if (!title || !content) {
  247. return res.status(400).json({ error: 'Title and content are required' });
  248. }
  249. // Generate filename
  250. const filename = generateFilename(title);
  251. const filePath = path.join(POSTS_DIR, filename);
  252. // Check if file already exists
  253. if (await fs.pathExists(filePath)) {
  254. return res.status(409).json({ error: 'Post with similar title already exists' });
  255. }
  256. // Format the post content
  257. let postContent = '';
  258. postContent += `title: ${title}\n`;
  259. if (description) postContent += `desc: ${description}\n`;
  260. if (tags && tags.length > 0) postContent += `tags: ${tags.join(', ')}\n`;
  261. postContent += '\n' + content;
  262. // Write the file
  263. await fs.writeFile(filePath, postContent, 'utf8');
  264. // Update index
  265. await generateIndex();
  266. const slug = filename.replace('.md', '');
  267. const stats = await fs.stat(filePath);
  268. res.status(201).json({
  269. slug,
  270. filename,
  271. title,
  272. description: description || '',
  273. tags: tags || [],
  274. content: postContent,
  275. createdAt: stats.birthtime,
  276. updatedAt: stats.mtime
  277. });
  278. } catch (error) {
  279. console.error('Error creating post:', error);
  280. res.status(500).json({ error: 'Failed to create post' });
  281. }
  282. });
  283. // PUT /api/posts/:slug - Update existing post
  284. app.put('/api/posts/:slug', requireAuth, async (req, res) => {
  285. try {
  286. const { slug } = req.params;
  287. const { title, description, content, tags } = req.body;
  288. const oldFilename = `${slug}.md`;
  289. const oldFilePath = path.join(POSTS_DIR, oldFilename);
  290. if (!(await fs.pathExists(oldFilePath))) {
  291. return res.status(404).json({ error: 'Post not found' });
  292. }
  293. if (!title || !content) {
  294. return res.status(400).json({ error: 'Title and content are required' });
  295. }
  296. // Generate new filename if title changed
  297. const newFilename = generateFilename(title);
  298. const newFilePath = path.join(POSTS_DIR, newFilename);
  299. // Format the post content
  300. let postContent = '';
  301. postContent += `title: ${title}\n`;
  302. if (description) postContent += `desc: ${description}\n`;
  303. if (tags && tags.length > 0) postContent += `tags: ${tags.join(', ')}\n`;
  304. postContent += '\n' + content;
  305. // Write to new file
  306. await fs.writeFile(newFilePath, postContent, 'utf8');
  307. // If filename changed, remove old file
  308. if (oldFilename !== newFilename) {
  309. await fs.remove(oldFilePath);
  310. }
  311. // Update index
  312. await generateIndex();
  313. const newSlug = newFilename.replace('.md', '');
  314. const stats = await fs.stat(newFilePath);
  315. res.json({
  316. slug: newSlug,
  317. filename: newFilename,
  318. title,
  319. description: description || '',
  320. tags: tags || [],
  321. content: postContent,
  322. createdAt: stats.birthtime,
  323. updatedAt: stats.mtime
  324. });
  325. } catch (error) {
  326. console.error('Error updating post:', error);
  327. res.status(500).json({ error: 'Failed to update post' });
  328. }
  329. });
  330. // DELETE /api/posts/:slug - Delete post
  331. app.delete('/api/posts/:slug', requireAuth, async (req, res) => {
  332. try {
  333. const { slug } = req.params;
  334. const filename = `${slug}.md`;
  335. const filePath = path.join(POSTS_DIR, filename);
  336. if (!(await fs.pathExists(filePath))) {
  337. return res.status(404).json({ error: 'Post not found' });
  338. }
  339. await fs.remove(filePath);
  340. await generateIndex();
  341. res.json({ message: 'Post deleted successfully' });
  342. } catch (error) {
  343. console.error('Error deleting post:', error);
  344. res.status(500).json({ error: 'Failed to delete post' });
  345. }
  346. });
  347. // Theme API Routes
  348. // GET /api/themes - Get all themes
  349. app.get('/api/themes', async (req, res) => {
  350. try {
  351. const themesData = await getAllThemes();
  352. res.json(themesData);
  353. } catch (error) {
  354. console.error('Error fetching themes:', error);
  355. res.status(500).json({ error: 'Failed to fetch themes' });
  356. }
  357. });
  358. // GET /api/themes/active - Get active theme
  359. app.get('/api/themes/active', async (req, res) => {
  360. try {
  361. const activeTheme = await getActiveTheme();
  362. res.json(activeTheme);
  363. } catch (error) {
  364. console.error('Error fetching active theme:', error);
  365. res.status(500).json({ error: 'Failed to fetch active theme' });
  366. }
  367. });
  368. // PUT /api/themes/active - Set active theme
  369. app.put('/api/themes/active', requireAuth, async (req, res) => {
  370. try {
  371. const { themeId } = req.body;
  372. if (!themeId) {
  373. return res.status(400).json({ error: 'Theme ID is required' });
  374. }
  375. const activeTheme = await setActiveTheme(themeId);
  376. res.json({ success: true, theme: activeTheme });
  377. } catch (error) {
  378. console.error('Error setting active theme:', error);
  379. res.status(400).json({ error: error.message });
  380. }
  381. });
  382. // POST /api/themes - Create custom theme
  383. app.post('/api/themes', requireAuth, async (req, res) => {
  384. try {
  385. const themeData = req.body;
  386. const newTheme = await createCustomTheme(themeData);
  387. res.status(201).json(newTheme);
  388. } catch (error) {
  389. console.error('Error creating theme:', error);
  390. res.status(400).json({ error: error.message });
  391. }
  392. });
  393. // PUT /api/themes/:themeId - Update custom theme
  394. app.put('/api/themes/:themeId', requireAuth, async (req, res) => {
  395. try {
  396. const { themeId } = req.params;
  397. const themeData = req.body;
  398. const updatedTheme = await updateCustomTheme(themeId, themeData);
  399. res.json(updatedTheme);
  400. } catch (error) {
  401. console.error('Error updating theme:', error);
  402. res.status(400).json({ error: error.message });
  403. }
  404. });
  405. // DELETE /api/themes/:themeId - Delete custom theme
  406. app.delete('/api/themes/:themeId', requireAuth, async (req, res) => {
  407. try {
  408. const { themeId } = req.params;
  409. await deleteCustomTheme(themeId);
  410. res.json({ success: true, message: 'Theme deleted successfully' });
  411. } catch (error) {
  412. console.error('Error deleting theme:', error);
  413. res.status(400).json({ error: error.message });
  414. }
  415. });
  416. // GET /api/themes/:themeId/export - Export theme
  417. app.get('/api/themes/:themeId/export', requireAuth, async (req, res) => {
  418. try {
  419. const { themeId } = req.params;
  420. const themeData = await exportTheme(themeId);
  421. res.setHeader('Content-Type', 'application/json');
  422. res.setHeader('Content-Disposition', `attachment; filename="theme-${themeId}.json"`);
  423. res.json(themeData);
  424. } catch (error) {
  425. console.error('Error exporting theme:', error);
  426. res.status(400).json({ error: error.message });
  427. }
  428. });
  429. // Health check endpoint
  430. app.get('/api/health', (req, res) => {
  431. res.json({ status: 'OK', timestamp: new Date().toISOString() });
  432. });
  433. // Generate initial index on startup
  434. await generateIndex();
  435. app.listen(PORT, () => {
  436. console.log(`🚀 Backend server running on http://localhost:${PORT}`);
  437. console.log(`📁 Posts directory: ${POSTS_DIR}`);
  438. });