Statistics page redesign (#60)
* feat: Add modal to display watched movies with details Co-authored-by: mathieumoran1 <[email protected]> * Refactor movie modal: remove search, improve category display Co-authored-by: mathieumoran1 <[email protected]> * Refactor movie category display to use details element Co-authored-by: mathieumoran1 <[email protected]> * Refactor movie playback and add clickability to stats Co-authored-by: mathieumoran1 <[email protected]> * Refactor: Improve movie linking and player logic Co-authored-by: mathieumoran1 <[email protected]> * Prevent caching of sensitive and dynamic API data Co-authored-by: mathieumoran1 <[email protected]> * feat: Prevent caching of sensitive admin data Co-authored-by: mathieumoran1 <[email protected]> * feat: Add no-cache headers to API requests Co-authored-by: mathieumoran1 <[email protected]> * Remove unused movie sorting functionality The movie sorting dropdown and its associated logic were removed as they are no longer needed. Co-authored-by: mathieumoran1 <[email protected]> * Refactor admin settings to use tabs instead of accordion Co-authored-by: mathieumoran1 <[email protected]> * Refactor: Adjust button layout for active year selection Co-authored-by: mathieumoran1 <[email protected]> * Refactor active year selection UI Co-authored-by: mathieumoran1 <[email protected]> * Refactor: Improve admin settings layout for better responsiveness Co-authored-by: mathieumoran1 <[email protected]> * Remove redundant completion explanation from user-stat.html Co-authored-by: mathieumoran1 <[email protected]> * Refactor user stats page to display year in ranking tab badge Co-authored-by: mathieumoran1 <[email protected]> * Refactor: Rename 'Finisseur(s)' column to 'Nombre' Co-authored-by: mathieumoran1 <[email protected]> * Refactor: Remove redundant completer count column Co-authored-by: mathieumoran1 <[email protected]> --------- Co-authored-by: Cursor Agent <[email protected]>
This commit was merged in pull request #60.
This commit is contained in:
co-authored by
Cursor Agent
parent
82f9462dda
commit
79b9f4a5a3
@@ -152,6 +152,9 @@ async function listBackupFiles() {
|
||||
router.get("/db/backups", async (req, res) => {
|
||||
try {
|
||||
requireAdmin(req);
|
||||
// Admin + auth-scoped data: never allow caching.
|
||||
res.setHeader("Cache-Control", "private, no-store, must-revalidate");
|
||||
res.setHeader("Vary", "Authorization");
|
||||
const backups = await listBackupFiles();
|
||||
return res.status(200).json({ backups });
|
||||
} catch (err) {
|
||||
@@ -227,6 +230,8 @@ router.post("/db/backup", async (req, res) => {
|
||||
router.get("/db/backups/:name", async (req, res) => {
|
||||
try {
|
||||
requireAdmin(req);
|
||||
res.setHeader("Cache-Control", "private, no-store, must-revalidate");
|
||||
res.setHeader("Vary", "Authorization");
|
||||
const safe = safeBackupFileName(req.params?.name);
|
||||
if (!safe) return res.status(400).json({ message: "Invalid backup name" });
|
||||
|
||||
|
||||
+3
-21
@@ -291,6 +291,9 @@ router.get("/watchedMovies", async (req, res) => {
|
||||
const user = await User.findById(decoded.id).select("watchedMovies").lean(); // Get user by ID
|
||||
if (!user) return res.status(404).json({ message: "User not found" });
|
||||
|
||||
// User-specific: never allow shared/proxy caching.
|
||||
res.set("Cache-Control", "private, no-store, must-revalidate");
|
||||
res.set("Vary", "Authorization");
|
||||
res.status(200).json(user.watchedMovies); // Return the list of watched movies
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
@@ -361,16 +364,6 @@ router.post("/add", async (req, res) => {
|
||||
if (normalizedMode === null) {
|
||||
return res.status(400).json({ message: "player_mode invalide (auto|video|embed)" });
|
||||
}
|
||||
if (
|
||||
!hasAnySourceIncludingFile({
|
||||
vod_link: normalizedVod || "",
|
||||
video_src: normalizedVideo || "",
|
||||
embed_src: normalizedEmbed || "",
|
||||
video_file: normalizedFile || "",
|
||||
})
|
||||
) {
|
||||
return res.status(400).json({ message: "Ajoute au moins une source (VOD / video_src / embed_src)." });
|
||||
}
|
||||
|
||||
// Fetch movie details from OMDb API
|
||||
let movieDetails;
|
||||
@@ -584,17 +577,6 @@ router.put("/:id", async (req, res) => {
|
||||
if (poster !== undefined) movie.poster = poster;
|
||||
}
|
||||
|
||||
if (
|
||||
!hasAnySourceIncludingFile({
|
||||
vod_link: movie.vod_link || "",
|
||||
video_src: movie.video_src || "",
|
||||
embed_src: movie.embed_src || "",
|
||||
video_file: movie.video_file || "",
|
||||
})
|
||||
) {
|
||||
return res.status(400).json({ message: "Ajoute au moins une source (VOD / video_src / embed_src / video_file)." });
|
||||
}
|
||||
|
||||
await movie.save();
|
||||
cacheClear();
|
||||
return res.status(200).json(movie);
|
||||
|
||||
+43
-13
@@ -122,14 +122,20 @@ async function getOrInitActiveYear() {
|
||||
// Get user stats with watched movie titles and details from the movies collection
|
||||
router.get("/stats", verifyToken, async (req, res) => {
|
||||
try {
|
||||
// Fetch all users
|
||||
const users = await User.find();
|
||||
// Auth-scoped response: never allow caching.
|
||||
res.set("Cache-Control", "private, no-store, must-revalidate");
|
||||
res.set("Vary", "Authorization");
|
||||
|
||||
// Fetch all users (only needed fields)
|
||||
const users = await User.find().select("name watchedMovies").lean();
|
||||
|
||||
// If a year is explicitly provided, use it; otherwise default to the global active year.
|
||||
const year = parseOscarYear(req.query.year) || await getOrInitActiveYear();
|
||||
|
||||
// Fetch movies (optionally filtered by year)
|
||||
const allMovies = await Movie.find({ year });
|
||||
const allMovies = await Movie.find({ year })
|
||||
.select("imdb_id title poster rating category year vod_link player_mode video_src embed_src video_file")
|
||||
.lean();
|
||||
const totalMoviesCount = allMovies.length;
|
||||
|
||||
if (totalMoviesCount === 0) {
|
||||
@@ -140,19 +146,35 @@ router.get("/stats", verifyToken, async (req, res) => {
|
||||
const userStats = users.map((user) => {
|
||||
const movieByImdbId = new Map(allMovies.map((m) => [m.imdb_id, m]));
|
||||
|
||||
// Get watched movies with details from the database
|
||||
const watchedMovies = user.watchedMovies
|
||||
.map((wm) => movieByImdbId.get(wm.imdb_id)) // Match movie details
|
||||
.filter(Boolean); // Remove any `undefined` values (if movie is not found)
|
||||
// Join watched movies with movie details for the selected year (keep watchedDate)
|
||||
const watchedRaw = Array.isArray(user?.watchedMovies) ? user.watchedMovies : [];
|
||||
const watchedMoviesDetails = watchedRaw
|
||||
.map((wm) => {
|
||||
const imdbId = typeof wm?.imdb_id === "string" ? wm.imdb_id : "";
|
||||
if (!imdbId) return null;
|
||||
const movie = movieByImdbId.get(imdbId);
|
||||
if (!movie) return null;
|
||||
return {
|
||||
movieId: movie?._id ? String(movie._id) : null,
|
||||
imdb_id: movie.imdb_id,
|
||||
title: movie.title || "",
|
||||
poster: movie.poster || "",
|
||||
rating: movie.rating || "",
|
||||
category: movie.category || "",
|
||||
year: Number.isInteger(movie.year) ? movie.year : year,
|
||||
vod_link: movie.vod_link || "",
|
||||
player_mode: movie.player_mode || "auto",
|
||||
video_src: movie.video_src || "",
|
||||
embed_src: movie.embed_src || "",
|
||||
video_file: movie.video_file || "",
|
||||
watchedDate: wm?.watchedDate ? new Date(wm.watchedDate).toISOString() : null,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
const watchedCount = watchedMovies.length;
|
||||
const watchedCount = watchedMoviesDetails.length;
|
||||
const watchedRatio = ((watchedCount / totalMoviesCount) * 100).toFixed(1);
|
||||
|
||||
const watchedMoviesDetails = watchedMovies.map((movie) => ({
|
||||
imdb_id: movie.imdb_id,
|
||||
title: movie.title
|
||||
}));
|
||||
|
||||
return {
|
||||
name: user.name,
|
||||
year,
|
||||
@@ -174,6 +196,10 @@ router.get("/stats", verifyToken, async (req, res) => {
|
||||
// Returns: { years: number[], totals: { [year]: number }, completersByYear: { [year]: {name, watchedCount, totalMoviesCount}[] } }
|
||||
router.get("/completions", verifyToken, async (req, res) => {
|
||||
try {
|
||||
// Auth-scoped response: never allow caching.
|
||||
res.set("Cache-Control", "private, no-store, must-revalidate");
|
||||
res.set("Vary", "Authorization");
|
||||
|
||||
const yearsRaw = await Movie.distinct("year");
|
||||
const years = (Array.isArray(yearsRaw) ? yearsRaw : [])
|
||||
.map((y) => Number(y))
|
||||
@@ -376,6 +402,10 @@ router.get("/admin/list", verifyToken, async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Admin + auth-scoped data: never allow caching.
|
||||
res.set("Cache-Control", "private, no-store, must-revalidate");
|
||||
res.set("Vary", "Authorization");
|
||||
|
||||
const users = await User.find()
|
||||
.select("name email role mustChangePassword")
|
||||
.sort({ name: 1, email: 1 });
|
||||
|
||||
Reference in New Issue
Block a user