Spaces:
Sleeping
Sleeping
File size: 5,416 Bytes
e8c33fa | 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 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | const Achievement = require('../models/Achievement');
const {
normalizeAchievementCreateInput,
normalizeAchievementUpdateInput,
} = require('../validation/achievementValidation');
const { ensureValidIdOrRespond, findByIdOrRespondNotFound } = require('../utils/controllerResponses');
const { asyncController } = require('../utils/asyncController');
const { saveOptimizedImage, removeStoredFile } = require('../services/mediaService');
const parseBoundedInt = (value, { min, max, fallback }) => {
const parsed = Number.parseInt(String(value ?? ''), 10);
if (!Number.isInteger(parsed)) return fallback;
return Math.min(Math.max(parsed, min), max);
};
// GET /api/achievements
const getAchievements = asyncController(async (req, res) => {
const limit = req.query.limit !== undefined
? parseBoundedInt(req.query.limit, { min: 1, max: 5000, fallback: 1000 })
: undefined;
const q = Achievement.find().sort({ tahun: -1 });
if (limit !== undefined) q.limit(limit);
const achievements = await q.lean();
res.json(achievements);
});
// GET /api/achievements/:id
const getAchievementById = asyncController(async (req, res) => {
if (!ensureValidIdOrRespond(res, req.params.id, 'achievement')) return;
const achievement = await findByIdOrRespondNotFound({
Model: Achievement,
id: req.params.id,
res,
notFoundMessage: 'Achievement not found',
lean: true,
});
if (!achievement) return;
res.json(achievement);
});
const ALLOWED_MIME = new Set(['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/x-png']);
const stripHtml = (value) => String(value || '').replace(/<[^>]*>/g, '');
// POST /api/achievements
const createAchievement = asyncController(async (req, res) => {
const payload = normalizeAchievementCreateInput(req.body);
// Strip HTML from free-text fields
payload.nama = stripHtml(payload.nama);
payload.namaKejuaraan = stripHtml(payload.namaKejuaraan);
payload.penyelenggara = stripHtml(payload.penyelenggara);
payload.juara = stripHtml(payload.juara);
const errors = {};
if (!payload.nama) errors.nama = 'Nama is required';
if (!payload.nrp) errors.nrp = 'NRP is required';
if (!payload.namaKejuaraan) errors.namaKejuaraan = 'Nama Kejuaraan is required';
if (!payload.penyelenggara) errors.penyelenggara = 'Penyelenggara is required';
if (!payload.juara) errors.juara = 'Juara is required';
if (!Number.isFinite(payload.tahun)) errors.tahun = 'Tahun is required and must be a number';
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) {
const photoPath = await saveOptimizedImage({
file: req.file,
subdir: 'achievements',
width: 800,
height: 800,
quality: 80,
});
if (photoPath) payload.photo = photoPath;
}
const achievement = await Achievement.create(payload);
res.status(201).json(achievement);
}, { defaultStatus: 400 });
// PUT /api/achievements/:id
const updateAchievement = asyncController(async (req, res) => {
if (!ensureValidIdOrRespond(res, req.params.id, 'achievement')) return;
const achievement = await findByIdOrRespondNotFound({
Model: Achievement,
id: req.params.id,
res,
notFoundMessage: 'Achievement not found',
});
if (!achievement) return;
const payload = normalizeAchievementUpdateInput(req.body);
// Strip HTML from free-text fields when present
if (payload.nama !== undefined) payload.nama = stripHtml(payload.nama);
if (payload.namaKejuaraan !== undefined) payload.namaKejuaraan = stripHtml(payload.namaKejuaraan);
if (payload.penyelenggara !== undefined) payload.penyelenggara = stripHtml(payload.penyelenggara);
if (payload.juara !== undefined) payload.juara = stripHtml(payload.juara);
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) {
const oldPhoto = achievement.photo;
const photoPath = await saveOptimizedImage({
file: req.file,
subdir: 'achievements',
width: 800,
height: 800,
quality: 80,
});
if (photoPath) {
payload.photo = photoPath;
await removeStoredFile(oldPhoto);
}
} else if (req.body.removePhoto === 'true') {
await removeStoredFile(achievement.photo);
payload.photo = '';
}
if (!Object.keys(payload).length) {
return res.status(400).json({ message: 'No fields provided to update' });
}
Object.assign(achievement, payload);
const updated = await achievement.save();
res.json(updated);
}, { defaultStatus: 400 });
// DELETE /api/achievements/:id
const deleteAchievement = asyncController(async (req, res) => {
if (!ensureValidIdOrRespond(res, req.params.id, 'achievement')) return;
const achievement = await findByIdOrRespondNotFound({
Model: Achievement,
id: req.params.id,
res,
notFoundMessage: 'Achievement not found',
});
if (!achievement) return;
await removeStoredFile(achievement.photo);
await achievement.deleteOne();
res.json({ message: 'Achievement deleted' });
});
module.exports = { getAchievements, getAchievementById, createAchievement, updateAchievement, deleteAchievement };
|