diff --git a/public/js/app_control.js b/public/js/app_control.js
index 62722d4..60a1377 100644
--- a/public/js/app_control.js
+++ b/public/js/app_control.js
@@ -212,23 +212,79 @@ window.onload = async function () {
}
}
- const userResetResponseEl = document.getElementById('user-reset-response');
- const userResetForm = document.getElementById('reset-user-password-form');
- const userResetEmailEl = document.getElementById('reset_user_email');
-
// Admin users list elements
const adminUsersBody = document.getElementById('admin-users-body');
const refreshUsersBtn = document.getElementById('refresh-users');
const adminUsersCountEl = document.getElementById('admin-users-count');
const adminUsersTabBtn = document.getElementById('admin-users-tab');
- let usersLoadedOnce = false;
+ // Admin user modals
+ const resetUserPasswordModalEl = document.getElementById('resetUserPasswordModal');
+ const resetUserPasswordModal = resetUserPasswordModalEl ? new bootstrap.Modal(resetUserPasswordModalEl) : null;
+ const resetUserLabelEl = document.getElementById('reset_user_label');
+ const resetUserEmailLabelEl = document.getElementById('reset_user_email_label');
+ const resetUserModalResponseEl = document.getElementById('reset-user-modal-response');
+ const resetUserModalResultEl = document.getElementById('reset-user-modal-result');
+ const resetUserTempPasswordEl = document.getElementById('reset-user-temp-password');
+ const resetUserExpiresAtEl = document.getElementById('reset-user-expires-at');
+ const confirmResetUserPasswordBtn = document.getElementById('confirm-reset-user-password');
+ const copyTempPasswordBtn = document.getElementById('copy-temp-password');
- function showUserResetResponse(kind, message) {
- if (!userResetResponseEl) return;
- userResetResponseEl.classList.remove('d-none', 'alert-success', 'alert-danger', 'alert-warning');
- userResetResponseEl.classList.add(kind === 'success' ? 'alert-success' : kind === 'warning' ? 'alert-warning' : 'alert-danger');
- userResetResponseEl.textContent = message;
+ const deleteUserModalEl = document.getElementById('deleteUserModal');
+ const deleteUserModal = deleteUserModalEl ? new bootstrap.Modal(deleteUserModalEl) : null;
+ const deleteUserLabelEl = document.getElementById('delete_user_label');
+ const deleteUserEmailLabelEl = document.getElementById('delete_user_email_label');
+ const deleteUserVerifyInputEl = document.getElementById('delete_user_verify_input');
+ const deleteUserModalResponseEl = document.getElementById('delete-user-modal-response');
+ const confirmDeleteUserBtn = document.getElementById('confirm-delete-user');
+
+ let usersLoadedOnce = false;
+ let currentResetTarget = null;
+ let currentDeleteTarget = null;
+
+ function setAlert(el, kind, message) {
+ if (!el) return;
+ el.classList.remove('d-none', 'alert-success', 'alert-danger', 'alert-warning');
+ el.classList.add(kind === 'success' ? 'alert-success' : kind === 'warning' ? 'alert-warning' : 'alert-danger');
+ el.textContent = message;
+ }
+
+ function hideAlert(el) {
+ if (!el) return;
+ el.classList.add('d-none');
+ el.textContent = '';
+ }
+
+ function resetResetUserModalUi() {
+ hideAlert(resetUserModalResponseEl);
+ if (resetUserModalResultEl) resetUserModalResultEl.classList.add('d-none');
+ if (resetUserTempPasswordEl) resetUserTempPasswordEl.textContent = '—';
+ if (resetUserExpiresAtEl) resetUserExpiresAtEl.textContent = '';
+ if (confirmResetUserPasswordBtn) confirmResetUserPasswordBtn.disabled = false;
+ }
+
+ function openResetUserModal(user) {
+ currentResetTarget = user || null;
+ if (!resetUserPasswordModal) return;
+ resetResetUserModalUi();
+ if (resetUserLabelEl) resetUserLabelEl.textContent = user?.name ? user.name : '(sans nom)';
+ if (resetUserEmailLabelEl) resetUserEmailLabelEl.textContent = user?.email || '';
+ resetUserPasswordModal.show();
+ }
+
+ function resetDeleteUserModalUi() {
+ hideAlert(deleteUserModalResponseEl);
+ if (deleteUserVerifyInputEl) deleteUserVerifyInputEl.value = '';
+ if (confirmDeleteUserBtn) confirmDeleteUserBtn.disabled = true;
+ }
+
+ function openDeleteUserModal(user) {
+ currentDeleteTarget = user || null;
+ if (!deleteUserModal) return;
+ resetDeleteUserModalUi();
+ if (deleteUserLabelEl) deleteUserLabelEl.textContent = user?.name ? user.name : '(sans nom)';
+ if (deleteUserEmailLabelEl) deleteUserEmailLabelEl.textContent = user?.email || '';
+ deleteUserModal.show();
}
function setUsersCount(count) {
@@ -242,7 +298,7 @@ window.onload = async function () {
setUsersCount(list.length);
if (!list.length) {
- adminUsersBody.innerHTML = '
| Aucun utilisateur. |
';
+ adminUsersBody.innerHTML = '| Aucun utilisateur. |
';
return;
}
@@ -281,9 +337,34 @@ window.onload = async function () {
});
}
+ const tdActions = document.createElement('td');
+ tdActions.className = 'text-nowrap';
+
+ const resetBtn = document.createElement('button');
+ resetBtn.type = 'button';
+ resetBtn.className = 'btn btn-outline-danger btn-sm me-1';
+ resetBtn.textContent = 'Reset MDP';
+ resetBtn.addEventListener('click', () => openResetUserModal(u));
+
+ const deleteBtn = document.createElement('button');
+ deleteBtn.type = 'button';
+ deleteBtn.className = 'btn btn-outline-danger btn-sm';
+ deleteBtn.textContent = 'Supprimer';
+
+ const isSelf = String(u?.id || '') && String(u.id) === String(decoded?.id || '');
+ if (isSelf || u?.admin) {
+ deleteBtn.disabled = true;
+ deleteBtn.title = isSelf ? 'Impossible de supprimer ton propre compte' : 'Suppression des admins désactivée';
+ }
+
+ deleteBtn.addEventListener('click', () => openDeleteUserModal(u));
+ tdActions.appendChild(resetBtn);
+ tdActions.appendChild(deleteBtn);
+
tr.appendChild(tdName);
tr.appendChild(tdEmail);
tr.appendChild(tdStatus);
+ tr.appendChild(tdActions);
adminUsersBody.appendChild(tr);
});
}
@@ -309,7 +390,7 @@ window.onload = async function () {
if (!adminUsersBody) return;
if (usersLoadedOnce && !force) return;
- adminUsersBody.innerHTML = '| Chargement… |
';
+ adminUsersBody.innerHTML = '| Chargement… |
';
setUsersCount(null);
try {
@@ -318,7 +399,7 @@ window.onload = async function () {
users.sort((a, b) => (a?.name || '').localeCompare((b?.name || ''), 'fr', { sensitivity: 'base' }));
renderAdminUsers(users);
} catch (err) {
- adminUsersBody.innerHTML = `| ${err.message || 'Erreur réseau'} |
`;
+ adminUsersBody.innerHTML = `| ${err.message || 'Erreur réseau'} |
`;
setUsersCount(null);
}
}
@@ -427,37 +508,108 @@ window.onload = async function () {
}
}
- if (userResetForm && userResetEmailEl) {
- userResetForm.addEventListener('submit', async function (e) {
- e.preventDefault();
- const email = (userResetEmailEl.value || '').trim();
- if (!email) {
- showUserResetResponse('warning', 'Email invalide.');
- return;
+ if (deleteUserVerifyInputEl && confirmDeleteUserBtn) {
+ deleteUserVerifyInputEl.addEventListener('input', () => {
+ const expected = String(currentDeleteTarget?.email || '').trim();
+ const typed = String(deleteUserVerifyInputEl.value || '').trim();
+ confirmDeleteUserBtn.disabled = !(expected && typed && expected === typed);
+ });
+ }
+
+ if (copyTempPasswordBtn) {
+ copyTempPasswordBtn.addEventListener('click', async () => {
+ const text = String(resetUserTempPasswordEl?.textContent || '').trim();
+ if (!text || text === '—') return;
+ try {
+ await navigator.clipboard.writeText(text);
+ setAlert(resetUserModalResponseEl, 'success', 'Copié.');
+ setTimeout(() => hideAlert(resetUserModalResponseEl), 1200);
+ } catch (_) {
+ // Fallback: select text for manual copy
+ setAlert(resetUserModalResponseEl, 'warning', 'Impossible de copier automatiquement. Copie manuellement.');
}
+ });
+ }
+
+ if (confirmResetUserPasswordBtn) {
+ confirmResetUserPasswordBtn.addEventListener('click', async () => {
+ const target = currentResetTarget;
+ const userId = String(target?.id || '').trim();
+ if (!userId) return;
+
+ confirmResetUserPasswordBtn.disabled = true;
+ resetResetUserModalUi();
+ setAlert(resetUserModalResponseEl, 'warning', 'Génération du mot de passe temporaire...');
try {
- showUserResetResponse('warning', 'Génération du mot de passe temporaire...');
const res = await fetch('/api/users/admin/reset-temp-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
- body: JSON.stringify({ email }),
+ body: JSON.stringify({ userId }),
});
-
const data = await res.json().catch(() => ({}));
if (!res.ok) {
- showUserResetResponse('danger', data.message || data.error || `Erreur (${res.status})`);
+ setAlert(resetUserModalResponseEl, 'danger', data.message || data.error || `Erreur (${res.status})`);
+ confirmResetUserPasswordBtn.disabled = false;
return;
}
- const label = data?.user?.name ? `${data.user.name} (${data.user.email})` : email;
- const expiresAt = data?.expiresAt ? ` Expire: ${data.expiresAt}` : '';
- showUserResetResponse('success', `Temp password pour ${label}: ${data.tempPassword}.${expiresAt}`);
+ hideAlert(resetUserModalResponseEl);
+ if (resetUserTempPasswordEl) resetUserTempPasswordEl.textContent = data?.tempPassword || '—';
+ if (resetUserExpiresAtEl) {
+ resetUserExpiresAtEl.textContent = data?.expiresAt ? `Expire: ${data.expiresAt}` : '';
+ }
+ if (resetUserModalResultEl) resetUserModalResultEl.classList.remove('d-none');
+ setAlert(resetUserModalResponseEl, 'success', 'Mot de passe temporaire généré. Copie-le et envoie-le à l’utilisateur.');
+ confirmResetUserPasswordBtn.disabled = false;
+ await loadAdminUsers({ force: true });
} catch (err) {
- showUserResetResponse('danger', err.message || 'Erreur réseau');
+ setAlert(resetUserModalResponseEl, 'danger', err.message || 'Erreur réseau');
+ confirmResetUserPasswordBtn.disabled = false;
+ }
+ });
+ }
+
+ if (confirmDeleteUserBtn) {
+ confirmDeleteUserBtn.addEventListener('click', async () => {
+ const target = currentDeleteTarget;
+ const userId = String(target?.id || '').trim();
+ if (!userId) return;
+
+ confirmDeleteUserBtn.disabled = true;
+ hideAlert(deleteUserModalResponseEl);
+ setAlert(deleteUserModalResponseEl, 'warning', 'Suppression...');
+
+ try {
+ const res = await fetch('/api/users/admin/delete', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${token}`,
+ },
+ body: JSON.stringify({ userId }),
+ });
+ const data = await res.json().catch(() => ({}));
+ if (!res.ok) {
+ setAlert(deleteUserModalResponseEl, 'danger', data.message || data.error || `Erreur (${res.status})`);
+ // Keep disabled until the input matches again.
+ const expected = String(target?.email || '').trim();
+ const typed = String(deleteUserVerifyInputEl?.value || '').trim();
+ confirmDeleteUserBtn.disabled = !(expected && typed && expected === typed);
+ return;
+ }
+
+ setAlert(deleteUserModalResponseEl, 'success', data.message || 'Utilisateur supprimé.');
+ await loadAdminUsers({ force: true });
+ // Close shortly after success
+ setTimeout(() => {
+ try { deleteUserModal?.hide(); } catch (_) {}
+ }, 600);
+ } catch (err) {
+ setAlert(deleteUserModalResponseEl, 'danger', err.message || 'Erreur réseau');
}
});
}
diff --git a/routes/movies.js b/routes/movies.js
index e814175..bfcd595 100644
--- a/routes/movies.js
+++ b/routes/movies.js
@@ -45,7 +45,12 @@ function hasAnySource({ vod_link, video_src, embed_src }) {
// Fetch movie details from OMDb API
async function fetchMovieDetailsFromOmdb(imdb_id) {
- const apiKey = process.env.OMDB_API; // Replace with your actual OMDb API key
+ const apiKey = typeof process.env.OMDB_API === "string" ? process.env.OMDB_API.trim() : "";
+ if (!apiKey) {
+ const err = new Error("OMDb n'est pas configuré (env OMDB_API manquante).");
+ err.code = "OMDB_NOT_CONFIGURED";
+ throw err;
+ }
const omdbUrl = `https://www.omdbapi.com/?i=${encodeURIComponent(imdb_id)}&apikey=${apiKey}`;
try {
@@ -204,7 +209,22 @@ router.post("/add", async (req, res) => {
}
// Fetch movie details from OMDb API
- const movieDetails = await fetchMovieDetailsFromOmdb(imdb_id);
+ let movieDetails;
+ try {
+ movieDetails = await fetchMovieDetailsFromOmdb(imdb_id);
+ } catch (err) {
+ // If OMDb isn't configured, keep the feature usable: add with placeholders so admin can fill manually later.
+ if (err?.code === "OMDB_NOT_CONFIGURED") {
+ movieDetails = {
+ title: String(imdb_id || "").trim() || "Untitled",
+ description: "",
+ rating: "",
+ poster: ""
+ };
+ } else {
+ throw err;
+ }
+ }
// Proceed with adding the movie
const movie = new Movie({
@@ -363,11 +383,18 @@ router.put("/:id", async (req, res) => {
// If refreshOmdb is true, overwrite OMDb-derived fields from the imdb_id
if (refreshOmdb) {
- const details = await fetchMovieDetailsFromOmdb(movie.imdb_id);
- movie.title = details.title;
- movie.description = details.description;
- movie.rating = details.rating;
- movie.poster = details.poster;
+ try {
+ const details = await fetchMovieDetailsFromOmdb(movie.imdb_id);
+ movie.title = details.title;
+ movie.description = details.description;
+ movie.rating = details.rating;
+ movie.poster = details.poster;
+ } catch (err) {
+ if (err?.code === "OMDB_NOT_CONFIGURED") {
+ return res.status(503).json({ message: "OMDb n'est pas configuré (env OMDB_API manquante)." });
+ }
+ throw err;
+ }
} else {
if (title !== undefined) movie.title = title;
if (description !== undefined) movie.description = description;
diff --git a/routes/users.js b/routes/users.js
index b99d7a4..cb0fd99 100644
--- a/routes/users.js
+++ b/routes/users.js
@@ -5,19 +5,28 @@ const crypto = require("crypto");
const User = require("../models/User");
const Movie = require("../models/Movie");
const Setting = require("../models/Setting");
+const PlaybackProgress = require("../models/PlaybackProgress");
const axios = require("axios");
const router = express.Router();
// Set password for verification
-const woof = process.env.DOG_NAMES ? process.env.DOG_NAMES.split(",") : [];
+const woof =
+ typeof process.env.DOG_NAMES === "string"
+ ? process.env.DOG_NAMES.split(",").map((s) => s.trim()).filter(Boolean)
+ : [];
const ACTIVE_YEAR_KEY = "active_oscar_year";
// Fetch movie details from OMDb API
async function fetchMovieDetailsFromOmdb(imdb_id) {
- const apiKey = process.env.OMDB_API; // Replace with your actual OMDb API key
+ const apiKey = typeof process.env.OMDB_API === "string" ? process.env.OMDB_API.trim() : "";
+ if (!apiKey) {
+ const err = new Error("OMDb n'est pas configuré (env OMDB_API manquante).");
+ err.code = "OMDB_NOT_CONFIGURED";
+ throw err;
+ }
const omdbUrl = `https://www.omdbapi.com/?i=${encodeURIComponent(imdb_id)}&apikey=${apiKey}`;
try {
@@ -161,7 +170,11 @@ router.get("/stats", verifyToken, async (req, res) => {
// Register User
router.post("/register", async (req, res) => {
const { name, email, password, role, verifoof } = req.body;
+ const dogCheckEnabled = woof.length > 0;
+
function registerVerification() {
+ // If DOG_NAMES isn't configured, skip the "dog name" verification entirely.
+ if (!dogCheckEnabled) return true;
return woof.includes(verifoof);
}
@@ -169,16 +182,16 @@ router.post("/register", async (req, res) => {
if (!password) {
throw new Error("Le mot de passe est obligatoire et ne peut être indéfini");
}
- if (!verifoof) {
- throw new Error("Le nom du chien doit être défini" );
+ if (dogCheckEnabled && !verifoof) {
+ throw new Error("Le nom du chien doit être défini");
}
if (!registerVerification()) {
- return res.status(201).json({ message: "Mauvais nom de chien" });
+ return res.status(400).json({ message: "Mauvais nom de chien" });
}
const existingUser = await User.findOne({ email });
if (existingUser) {
- return res.status(201).json({ message: "L'email existe déjà, veuillez en choisir un autre." });
+ return res.status(409).json({ message: "L'email existe déjà, veuillez en choisir un autre." });
}
// Hash password and create user
@@ -188,8 +201,7 @@ router.post("/register", async (req, res) => {
res.status(201).json({ message: "L'utilisateur s'est enregistré avec succès !" });
} catch (err) {
- res.status(400).json({ error: err.message });
- res.status(403).json({ message: "Erreur interne lmao" });
+ return res.status(400).json({ message: err?.message || "Erreur interne" });
}
});
@@ -286,6 +298,46 @@ router.get("/admin/list", verifyToken, async (req, res) => {
}
});
+// Admin: delete a user (and related playback progress)
+router.post("/admin/delete", verifyToken, async (req, res) => {
+ if (!isAdminFromDecoded(req.user)) {
+ return res.status(403).json({ message: "You do not have admin privileges" });
+ }
+
+ const { email, userId } = req.body || {};
+ const emailNorm = typeof email === "string" ? email.trim() : "";
+ const idNorm = typeof userId === "string" ? userId.trim() : "";
+
+ if (!emailNorm && !idNorm) {
+ return res.status(400).json({ message: "Email ou userId requis." });
+ }
+
+ try {
+ const user = emailNorm ? await User.findOne({ email: emailNorm }) : await User.findById(idNorm);
+ if (!user) return res.status(404).json({ message: "User not found" });
+
+ // Safety: prevent deleting yourself from the admin UI.
+ if (String(user._id) === String(req.user?.id || "")) {
+ return res.status(400).json({ message: "Impossible de supprimer ton propre compte." });
+ }
+
+ // Safety: prevent deleting admin accounts via the UI endpoint.
+ if (user.role === 69) {
+ return res.status(400).json({ message: "Impossible de supprimer un compte admin via cette interface." });
+ }
+
+ await PlaybackProgress.deleteMany({ userId: user._id });
+ await User.deleteOne({ _id: user._id });
+
+ return res.status(200).json({
+ message: "Utilisateur supprimé.",
+ user: { id: String(user._id), name: user.name, email: user.email },
+ });
+ } catch (err) {
+ return res.status(500).json({ error: err.message });
+ }
+});
+
// User: change password (used after temp-password login). Returns a fresh token.
router.post("/change-password", verifyToken, async (req, res) => {
const { newPassword } = req.body || {};
diff --git a/routes/version.js b/routes/version.js
index 21ee502..cee2fd2 100644
--- a/routes/version.js
+++ b/routes/version.js
@@ -4,16 +4,65 @@ const moment = require("moment-timezone");
const router = express.Router();
-let latestCommitCache = null;
-let lastFetchedTime = 0;
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes in milliseconds
+const TIMEZONE = "America/New_York";
+
+// Cache is stored in the exact response shape expected by the client.
+let latestCommitCache = null;
+
+// Avoid noisy logs when GitHub isn't configured or is temporarily unavailable.
+let lastLogTime = 0;
+const LOG_THROTTLE_MS = 60 * 60 * 1000; // 1h
+
+function nowFormatted() {
+ return moment(Date.now()).tz(TIMEZONE).format("YYYY-MM-DD HH:mm:ss");
+}
+
+function getGitHubConfig() {
+ const owner = typeof process.env.GITHUB_OWNER === "string" ? process.env.GITHUB_OWNER.trim() : "";
+ const repo = typeof process.env.GITHUB_REPO === "string" ? process.env.GITHUB_REPO.trim() : "";
+ return { owner, repo, configured: !!owner && !!repo };
+}
+
+function getPackageVersion() {
+ try {
+ // routes/version.js -> ../package.json
+ const pkg = require("../package.json");
+ return typeof pkg?.version === "string" && pkg.version.trim() ? pkg.version.trim() : "dev";
+ } catch (_) {
+ return "dev";
+ }
+}
+
+function setFallbackCache(message, { configured } = { configured: false }) {
+ latestCommitCache = {
+ version: getPackageVersion(),
+ message: message || "Version info not available.",
+ author: "",
+ date: nowFormatted(),
+ configured: !!configured,
+ source: "fallback",
+ };
+}
+
+function logThrottled(...args) {
+ const t = Date.now();
+ if (t - lastLogTime < LOG_THROTTLE_MS) return;
+ lastLogTime = t;
+ // eslint-disable-next-line no-console
+ console.error(...args);
+}
async function fetchLatestCommitFromGitHub() {
- const githubOwner = process.env.GITHUB_OWNER;
- const githubRepo = process.env.GITHUB_REPO;
+ const { owner, repo, configured } = getGitHubConfig();
+ if (!configured) {
+ // Never attempt GitHub calls if not configured.
+ setFallbackCache("Version endpoint is not configured.", { configured: false });
+ return;
+ }
try {
- const url = `https://api.github.com/repos/${githubOwner}/${githubRepo}/commits`;
+ const url = `https://api.github.com/repos/${owner}/${repo}/commits`;
const response = await axios.get(url, {
headers: {
"Content-Type": "application/json",
@@ -23,40 +72,44 @@ async function fetchLatestCommitFromGitHub() {
if (response.data && response.data.length > 0) {
const latestCommit = response.data[0];
latestCommitCache = {
- sha: latestCommit.sha,
- message: latestCommit.commit.message,
- author: latestCommit.commit.author.name,
- date: latestCommit.commit.author.date,
+ version: String(latestCommit.sha || "").substring(0, 5),
+ message: latestCommit?.commit?.message || "",
+ author: latestCommit?.commit?.author?.name || "",
+ date: moment(latestCommit?.commit?.author?.date || Date.now())
+ .tz(TIMEZONE)
+ .format("YYYY-MM-DD HH:mm:ss"),
+ configured: true,
+ source: "github",
};
- lastFetchedTime = Date.now();
} else {
throw new Error("No commits found.");
}
} catch (error) {
- console.error("Error fetching commit:", error.message);
+ // Keep serving a non-500 response, and don't spam logs.
+ logThrottled("Error fetching commit:", error?.message || error);
+ setFallbackCache("GitHub commit data is not available.", { configured: true });
}
}
-// Initial fetch to populate cache
-fetchLatestCommitFromGitHub();
-
-// Refresh cache every 5 minutes
-setInterval(fetchLatestCommitFromGitHub, CACHE_DURATION);
+// Initialize cache (without crashing / spamming when not configured).
+const initialCfg = getGitHubConfig();
+if (!initialCfg.configured) {
+ setFallbackCache("Version endpoint is not configured.", { configured: false });
+} else {
+ fetchLatestCommitFromGitHub();
+ const timer = setInterval(fetchLatestCommitFromGitHub, CACHE_DURATION);
+ // Don't keep the process alive just for this polling.
+ if (typeof timer.unref === "function") timer.unref();
+}
// Route to get the latest commit (cached)
router.get("/", async (req, res) => {
+ // Always return something (client clears footer on non-2xx).
if (!latestCommitCache) {
- return res.status(500).json({ message: "Commit data is not available yet." });
+ const cfg = getGitHubConfig();
+ setFallbackCache("Version info not available.", { configured: cfg.configured });
}
-
- res.json({
- version: latestCommitCache.sha.substring(0, 5),
- message: latestCommitCache.message,
- author: latestCommitCache.author,
- date: moment(latestCommitCache.date)
- .tz("America/New_York")
- .format("YYYY-MM-DD HH:mm:ss"),
- });
+ return res.status(200).json(latestCommitCache);
});
module.exports = router;
\ No newline at end of file