-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathpost.controller.js
More file actions
135 lines (115 loc) · 4.37 KB
/
post.controller.js
File metadata and controls
135 lines (115 loc) · 4.37 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import Post from '../models/post.js'
import cuid from 'cuid'
import slug from 'limax'
import sanitizeHtml from 'sanitize-html'
// ─── Constants ────────────────────────────────────────────────────────────────
/** Fields required to create a new post */
const REQUIRED_POST_FIELDS = ['name', 'title', 'content']
/** Shared sanitize-html options — strips all HTML tags for plain-text safety */
const SANITIZE_OPTIONS = { allowedTags: [], allowedAttributes: {} }
// ─── Helpers ──────────────────────────────────────────────────────────────────
/**
* Sanitizes a string value using the shared sanitize options.
* @param {string} value - Raw input string
* @returns {string} - Sanitized plain-text string
*/
const sanitize = (value) => sanitizeHtml(value, SANITIZE_OPTIONS)
/**
* Sends a consistent JSON error response.
* @param {object} res - Express response object
* @param {number} status - HTTP status code
* @param {string} message - Error message
*/
const sendError = (res, status, message) => res.status(status).json({ error: message })
/**
* Validates that all required fields are present and non-empty on a post body.
* @param {object} post - The post object from req.body
* @returns {string|null} - Returns the missing field name, or null if all present
*/
const getMissingField = (post) =>
REQUIRED_POST_FIELDS.find((field) => !post?.[field]?.trim()) ?? null
// ─── Controllers ──────────────────────────────────────────────────────────────
/**
* @desc Fetch all posts sorted by newest first
* @route GET /api/posts
* @access Public
*/
export async function getPosts(req, res) {
try {
// .lean() returns plain JS objects — faster for read-only responses
const posts = await Post.find().sort({ dateAdded: -1 }).lean()
res.json({ posts })
} catch (err) {
sendError(res, 500, 'Failed to retrieve posts')
}
}
/**
* @desc Create and save a new post
* @route POST /api/posts
* @access Private
*/
export async function addPost(req, res) {
const postData = req.body?.post
// Validate all required fields are present before processing
const missingField = getMissingField(postData)
if (missingField) {
return sendError(res, 400, `Missing required field: ${missingField}`)
}
try {
// Sanitize all user-supplied fields to prevent XSS
const sanitizedTitle = sanitize(postData.title)
const sanitizedName = sanitize(postData.name)
const sanitizedContent = sanitize(postData.content)
const newPost = new Post({
...postData,
title: sanitizedTitle,
name: sanitizedName,
content: sanitizedContent,
// Generate a URL-friendly slug from the sanitized title
slug: slug(sanitizedTitle.toLowerCase(), { lowercase: true }),
// Assign a unique collision-resistant ID
cuid: cuid(),
})
const saved = await newPost.save()
res.status(201).json({ post: saved })
} catch (err) {
// Surface Mongoose validation errors with a helpful message
if (err.name === 'ValidationError') {
return sendError(res, 400, err.message)
}
sendError(res, 500, 'Failed to save post')
}
}
/**
* @desc Fetch a single post by its cuid
* @route GET /api/posts/:cuid
* @access Public
*/
export async function getPost(req, res) {
try {
const post = await Post.findOne({ cuid: req.params.cuid }).lean()
if (!post) {
return sendError(res, 404, 'Post not found')
}
res.json({ post })
} catch (err) {
sendError(res, 500, 'Failed to retrieve post')
}
}
/**
* @desc Delete a post by its cuid
* @route DELETE /api/posts/:cuid
* @access Private
*/
export async function deletePost(req, res) {
try {
// findOneAndDelete is atomic — avoids the extra find + remove round-trip
const deleted = await Post.findOneAndDelete({ cuid: req.params.cuid })
if (!deleted) {
return sendError(res, 404, 'Post not found')
}
res.status(200).json({ message: 'Post deleted successfully' })
} catch (err) {
sendError(res, 500, 'Failed to delete post')
}
}