tinfo-web-server / controllers /announcementController.js
Faridaqurr
initial backend
e8c33fa
Raw
History Blame Contribute Delete
6.94 kB
const Announcement = require('../models/Announcement');
const { saveOptimizedImage, removeStoredFile } = require('../services/mediaService');
const { sanitizeText, isValidDateInput } = require('../validation/commonValidation');
const { isValidAnnouncementCategory, normalizeAnnouncementInput, parseBooleanInput } = require('../validation/announcementValidation');
const { ensureValidIdOrRespond, findByIdOrRespondNotFound } = require('../utils/controllerResponses');
const { asyncController } = require('../utils/asyncController');
const { emitAnnouncementUpdated } = require('../socket/socketServer');
const MAX_TITLE_LENGTH = 180;
const MAX_DESCRIPTION_LENGTH = 2200;
const ALLOWED_MIME = new Set(['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/x-png']);
const stripHtml = (value) => String(value || '').replace(/<[^>]*>/g, '');
const publishedOrLegacyFilter = {
$or: [{ isPublished: true }, { isPublished: { $exists: false } }],
};
const getAnnouncements = asyncController(async (req, res) => {
const category = sanitizeText(req.query.category);
const parsedLimit = Number.parseInt(String(req.query.limit ?? ''), 10);
const includeUnpublished = req.user
? parseBooleanInput(req.query.includeUnpublished) === true
: false;
const published = parseBooleanInput(req.query.published);
if (category && !isValidAnnouncementCategory(category)) return res.status(400).json({ message: 'Invalid category filter' });
let filter = category ? { category } : {};
if (published === true) {
filter = { ...filter, ...publishedOrLegacyFilter };
} else if (published === false) {
filter = { ...filter, isPublished: false };
} else if (!includeUnpublished) {
filter = { ...filter, ...publishedOrLegacyFilter };
}
const query = Announcement.find(filter).sort({ createdAt: -1 }).lean();
if (Number.isInteger(parsedLimit) && parsedLimit > 0) query.limit(Math.min(parsedLimit, 100));
res.json(await query);
});
const getAnnouncementById = asyncController(async (req, res) => {
if (!ensureValidIdOrRespond(res, req.params.id, 'announcement')) return;
const announcement = await findByIdOrRespondNotFound({
Model: Announcement,
id: req.params.id,
res,
notFoundMessage: 'Announcement not found',
lean: true,
});
if (!announcement) return;
res.json(announcement);
});
const createAnnouncement = asyncController(async (req, res) => {
const payload = normalizeAnnouncementInput(req.body);
// Strip HTML from free-text fields
payload.title = stripHtml(payload.title);
payload.description = stripHtml(payload.description);
const errors = {};
if (!payload.title) errors.title = 'Title is required';
if (!payload.date) errors.date = 'Date is required';
else if (!isValidDateInput(payload.date)) errors.date = 'Date must use a valid YYYY-MM-DD format';
if (payload.title && payload.title.length > MAX_TITLE_LENGTH) errors.title = `Title must be ${MAX_TITLE_LENGTH} characters or less`;
if (payload.description && payload.description.length > MAX_DESCRIPTION_LENGTH) errors.description = `Description must be ${MAX_DESCRIPTION_LENGTH} characters or less`;
if (payload.category && !isValidAnnouncementCategory(payload.category)) errors.category = 'Invalid category';
if (Object.keys(errors).length > 0) {
return res.status(400).json({ error: 'Validation failed', fields: errors });
}
if (req.file && !ALLOWED_MIME.has(req.file.mimetype)) {
return res.status(415).json({ message: 'Unsupported file type. Use JPG, PNG, or WEBP.' });
}
if (req.file) {
payload.photo = await saveOptimizedImage({
file: req.file,
subdir: 'announcements',
width: 1200,
height: 900,
quality: 80,
});
}
const announcement = await Announcement.create(payload);
emitAnnouncementUpdated({ action: 'created', id: String(announcement._id) });
res.status(201).json(announcement);
}, { defaultStatus: 400 });
const updateAnnouncement = asyncController(async (req, res) => {
if (!ensureValidIdOrRespond(res, req.params.id, 'announcement')) return;
const announcement = await findByIdOrRespondNotFound({
Model: Announcement,
id: req.params.id,
res,
notFoundMessage: 'Announcement not found',
});
if (!announcement) return;
const payload = normalizeAnnouncementInput(req.body, { partial: true });
if (!Object.keys(payload).length && !req.file) return res.status(400).json({ message: 'No fields provided to update' });
// Strip HTML from free-text fields when present
if (payload.title !== undefined) payload.title = stripHtml(payload.title);
if (payload.description !== undefined) payload.description = stripHtml(payload.description);
const errors = {};
if (payload.title !== undefined && !payload.title) errors.title = 'Title is required';
if (payload.date !== undefined && !payload.date) errors.date = 'Date is required';
else if (payload.date !== undefined && !isValidDateInput(payload.date)) errors.date = 'Date must use a valid YYYY-MM-DD format';
if (payload.title !== undefined && payload.title && payload.title.length > MAX_TITLE_LENGTH) errors.title = `Title must be ${MAX_TITLE_LENGTH} characters or less`;
if (payload.description !== undefined && payload.description.length > MAX_DESCRIPTION_LENGTH) errors.description = `Description must be ${MAX_DESCRIPTION_LENGTH} characters or less`;
if (payload.category !== undefined && !isValidAnnouncementCategory(payload.category)) errors.category = 'Invalid category';
if (Object.keys(errors).length > 0) {
return res.status(400).json({ error: 'Validation failed', fields: errors });
}
if (req.file && !ALLOWED_MIME.has(req.file.mimetype)) {
return res.status(415).json({ message: 'Unsupported file type. Use JPG, PNG, or WEBP.' });
}
Object.assign(announcement, payload);
if (req.file) {
await removeStoredFile(announcement.photo);
announcement.photo = await saveOptimizedImage({
file: req.file,
subdir: 'announcements',
width: 1200,
height: 900,
quality: 80,
});
}
const updated = await announcement.save();
emitAnnouncementUpdated({ action: 'updated', id: String(updated._id) });
res.json(updated);
}, { defaultStatus: 400 });
const deleteAnnouncement = asyncController(async (req, res) => {
if (!ensureValidIdOrRespond(res, req.params.id, 'announcement')) return;
const announcement = await findByIdOrRespondNotFound({
Model: Announcement,
id: req.params.id,
res,
notFoundMessage: 'Announcement not found',
});
if (!announcement) return;
const deletedId = String(announcement._id);
await removeStoredFile(announcement.photo);
await announcement.deleteOne();
emitAnnouncementUpdated({ action: 'deleted', id: deletedId });
res.json({ message: 'Announcement deleted' });
});
module.exports = { getAnnouncements, getAnnouncementById, createAnnouncement, updateAnnouncement, deleteAnnouncement };