source: Klonkt/src/routes/comments.js@ 7bc636b

main
Last change on this file since 7bc636b was 7bc636b, checked in by Robin <robin@…>, 4 months ago

Initial commit — PrutFolio v1 source (pulled from Hetzner /srv/prutfolio)

  • Property mode set to 100644
File size: 4.4 KB
Line 
1/**
2 * Comments — phase G v1.
3 *
4 * POST /comments -> create a comment on a post (auth required)
5 * POST /comments/:id/delete -> delete (own, or god/site-admin)
6 *
7 * Threading: 1 level deep (top-level + replies). Replies-of-replies fold up
8 * into the same parent (UI keeps it shallow).
9 *
10 * Status: auto-approved for logged-in users (trust mode). The schema's
11 * `status` column stays so we can switch to moderation later without changing
12 * shape. Anonymous comments (require_login_to_comment = 0 + no user) come
13 * later — for now we always require login.
14 */
15
16import express from 'express';
17import { v4 as uuid } from 'uuid';
18import db from '../config/database.js';
19import { requireAuth } from '../middleware/auth.js';
20import PermissionsService from '../services/PermissionsService.js';
21
22const router = express.Router();
23
24// Limits
25const MAX_LEN = 4000;
26const MIN_LEN = 1;
27
28router.post('/', requireAuth, (req, res) => {
29 const site = res.locals.site;
30 if (!site) return res.status(404).send('Site required');
31
32 const postSlug = (req.body.post_slug || '').trim();
33 const rawContent = (req.body.content || '').trim();
34 const parentId = (req.body.parent_comment_id || '').trim() || null;
35
36 if (!postSlug) return res.status(400).send('post_slug required');
37 if (rawContent.length < MIN_LEN) return res.status(400).send('Comment cannot be empty');
38 if (rawContent.length > MAX_LEN) return res.status(413).send(`Comment too long (max ${MAX_LEN} chars)`);
39
40 const post = db.prepare(
41 'SELECT id, slug FROM posts WHERE site_id = ? AND slug = ? AND status = ?'
42 ).get(site.id, postSlug, 'published');
43 if (!post) return res.status(404).send('Post not found');
44
45 if (!PermissionsService.canComment(req.session.user, site, post)) {
46 return res.status(403).send('Comments not allowed');
47 }
48
49 // Validate parent (must belong to this post; collapses replies-of-replies
50 // up to the top-level parent so we never go deeper than 1)
51 let resolvedParent = null;
52 if (parentId) {
53 const parent = db.prepare(
54 'SELECT id, parent_comment_id FROM comments WHERE id = ? AND post_id = ?'
55 ).get(parentId, post.id);
56 if (!parent) return res.status(400).send('Invalid parent comment');
57 resolvedParent = parent.parent_comment_id || parent.id;
58 }
59
60 // Status depends on the site's moderation mode.
61 // 'trust' = auto-approve immediately (default).
62 // 'moderate' = pending until an admin reviews in /admin/comments.
63 // Author is the post author or god → always trusted (no point gatekeeping yourself).
64 const isTrustedAuthor = req.session.user.role === 'god'
65 || req.session.user.id === post.author_id;
66 const status = (site.comments_moderation_mode === 'moderate' && !isTrustedAuthor)
67 ? 'pending'
68 : 'approved';
69
70 const commentId = uuid();
71 db.prepare(`
72 INSERT INTO comments (id, post_id, author_id, parent_comment_id, content, status)
73 VALUES (?, ?, ?, ?, ?, ?)
74 `).run(commentId, post.id, req.session.user.id, resolvedParent, rawContent, status);
75
76 // Where to land after submit:
77 // approved → scroll to the new comment
78 // pending → comments anchor + ?pending=1 query so post page can flash a notice
79 const target = status === 'approved'
80 ? `${res.locals.siteUrlBase || ''}/${post.slug}#comment-${commentId}`
81 : `${res.locals.siteUrlBase || ''}/${post.slug}?pending=1#comments`;
82 if (req.headers['hx-request']) {
83 res.setHeader('HX-Redirect', target);
84 return res.send('OK');
85 }
86 res.redirect(target);
87});
88
89router.post('/:id/delete', requireAuth, (req, res) => {
90 const site = res.locals.site;
91 if (!site) return res.status(404).send('Site required');
92
93 const comment = db.prepare(`
94 SELECT c.id, c.author_id, c.post_id, p.slug AS post_slug
95 FROM comments c JOIN posts p ON p.id = c.post_id
96 WHERE c.id = ? AND p.site_id = ?
97 `).get(req.params.id, site.id);
98
99 if (!comment) return res.status(404).send('Not found');
100 if (!PermissionsService.canDeleteComment(req.session.user, comment, site)) {
101 return res.status(403).send('No permission');
102 }
103
104 // Delete the comment plus any replies that hung off it
105 db.prepare('DELETE FROM comments WHERE id = ? OR parent_comment_id = ?')
106 .run(req.params.id, req.params.id);
107
108 if (req.headers['hx-request']) {
109 res.setHeader('HX-Redirect', `${res.locals.siteUrlBase || ''}/${comment.post_slug}#comments`);
110 return res.send('OK');
111 }
112 res.redirect(`${res.locals.siteUrlBase || ''}/${comment.post_slug}#comments`);
113});
114
115export default router;
Note: See TracBrowser for help on using the repository browser.