server.js 15 KB

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