Spaces:
Running
Running
| 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 }; | |