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 };