tinfo-web-server / controllers /achievementController.js
Faridaqurr
initial backend
e8c33fa
Raw
History Blame Contribute Delete
5.42 kB
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 };