server.js 14 KB

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