Add movie runtime, bulk OMDb refresh, and first-time setup
- Add runtime field to Movie model and display formatted duration (e.g., 2h 22min) on movie cards - Add bulk "Refresh OMDb" button in admin panel for selected movies - Add runtime field to movie edit modal in admin panel - Implement first-time setup flow: show admin creation form on login page when no admin exists - Block registration page until initial setup is complete Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
087f4f4023
commit
6186f3a051
+7
-1
@@ -242,6 +242,7 @@ async function fetchMovieDetailsFromOmdb(imdb_id) {
|
||||
title: response.data.Title,
|
||||
description: response.data.Plot,
|
||||
rating: response.data.imdbRating,
|
||||
runtime: response.data.Runtime,
|
||||
poster: response.data.Poster
|
||||
};
|
||||
} else {
|
||||
@@ -289,7 +290,7 @@ router.get("/", async (req, res) => {
|
||||
|
||||
const projection = isChecklist
|
||||
? "imdb_id title"
|
||||
: "imdb_id title description rating poster cam year category vod_link player_mode video_src embed_src video_file video_file_low subtitles subtitle_file subtitle_lang subtitle_label subtitle_default updatedAt createdAt";
|
||||
: "imdb_id title description rating runtime poster cam year category vod_link player_mode video_src embed_src video_file video_file_low subtitles subtitle_file subtitle_lang subtitle_label subtitle_default updatedAt createdAt";
|
||||
|
||||
const cacheKey = `movies:list:${year || "all"}:${isChecklist ? "checklist" : "films"}`;
|
||||
const cached = cacheGet(cacheKey);
|
||||
@@ -583,6 +584,7 @@ router.post("/add", async (req, res) => {
|
||||
title: String(imdb_id || "").trim() || "Untitled",
|
||||
description: "",
|
||||
rating: "",
|
||||
runtime: "",
|
||||
poster: ""
|
||||
};
|
||||
} else {
|
||||
@@ -596,6 +598,7 @@ router.post("/add", async (req, res) => {
|
||||
title: movieDetails.title,
|
||||
description: movieDetails.description,
|
||||
rating: movieDetails.rating,
|
||||
runtime: movieDetails.runtime,
|
||||
poster: movieDetails.poster,
|
||||
cam: !!cam,
|
||||
year: parsedYear,
|
||||
@@ -852,6 +855,7 @@ router.put("/:id", async (req, res) => {
|
||||
title,
|
||||
description,
|
||||
rating,
|
||||
runtime,
|
||||
poster,
|
||||
year,
|
||||
category,
|
||||
@@ -1008,6 +1012,7 @@ router.put("/:id", async (req, res) => {
|
||||
movie.title = details.title;
|
||||
movie.description = details.description;
|
||||
movie.rating = details.rating;
|
||||
movie.runtime = details.runtime;
|
||||
movie.poster = details.poster;
|
||||
} catch (err) {
|
||||
if (err?.code === "OMDB_NOT_CONFIGURED") {
|
||||
@@ -1019,6 +1024,7 @@ router.put("/:id", async (req, res) => {
|
||||
if (title !== undefined) movie.title = title;
|
||||
if (description !== undefined) movie.description = description;
|
||||
if (rating !== undefined) movie.rating = rating;
|
||||
if (runtime !== undefined) movie.runtime = runtime;
|
||||
if (poster !== undefined) movie.poster = poster;
|
||||
}
|
||||
|
||||
|
||||
@@ -128,6 +128,90 @@ async function getOrInitActiveYear() {
|
||||
return year;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// FIRST-TIME SETUP ENDPOINTS
|
||||
// These allow creating the first admin user when the database is empty/new
|
||||
// ============================================================================
|
||||
|
||||
// Check if the app needs initial setup (no admin users exist)
|
||||
router.get("/setup/status", async (req, res) => {
|
||||
try {
|
||||
const totalUsers = await User.countDocuments({});
|
||||
const adminCount = await User.countDocuments({ role: 69 });
|
||||
|
||||
res.set("Cache-Control", "no-store");
|
||||
return res.status(200).json({
|
||||
needsSetup: adminCount === 0,
|
||||
hasUsers: totalUsers > 0,
|
||||
hasAdmin: adminCount > 0,
|
||||
});
|
||||
} catch (err) {
|
||||
return res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Create the first admin user (only works if no admin exists)
|
||||
router.post("/setup/admin", async (req, res) => {
|
||||
try {
|
||||
// Check if an admin already exists
|
||||
const adminCount = await User.countDocuments({ role: 69 });
|
||||
if (adminCount > 0) {
|
||||
return res.status(403).json({
|
||||
message: "Un administrateur existe déjà. Cette fonctionnalité est désactivée.",
|
||||
});
|
||||
}
|
||||
|
||||
const { name, email, password } = req.body;
|
||||
|
||||
// Validate input
|
||||
const nameNorm = typeof name === "string" ? name.trim() : "";
|
||||
const emailNorm = typeof email === "string" ? email.trim().toLowerCase() : "";
|
||||
|
||||
if (!nameNorm) {
|
||||
return res.status(400).json({ message: "Le nom est obligatoire" });
|
||||
}
|
||||
if (!emailNorm) {
|
||||
return res.status(400).json({ message: "L'email est obligatoire" });
|
||||
}
|
||||
if (!/^\S+@\S+\.\S+$/.test(emailNorm)) {
|
||||
return res.status(400).json({ message: "Email invalide" });
|
||||
}
|
||||
if (!password || password.length < 6) {
|
||||
return res.status(400).json({ message: "Le mot de passe doit contenir au moins 6 caractères" });
|
||||
}
|
||||
|
||||
// Check if email is already taken
|
||||
const existing = await User.findOne({ email: emailNorm });
|
||||
if (existing) {
|
||||
return res.status(409).json({ message: "Cet email est déjà utilisé" });
|
||||
}
|
||||
|
||||
// Create the admin user
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
const adminUser = new User({
|
||||
name: nameNorm,
|
||||
email: emailNorm,
|
||||
password: hashedPassword,
|
||||
role: 69, // Admin role
|
||||
mustChangePassword: false,
|
||||
});
|
||||
|
||||
await adminUser.save();
|
||||
|
||||
console.log(`[setup] First admin user created: ${emailNorm}`);
|
||||
|
||||
// Return a token so the admin is logged in immediately
|
||||
const token = signUserToken(adminUser);
|
||||
|
||||
return res.status(201).json({
|
||||
message: "Administrateur créé avec succès",
|
||||
token,
|
||||
});
|
||||
} catch (err) {
|
||||
return res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get user stats with watched movie titles and details from the movies collection
|
||||
router.get("/stats", verifyToken, async (req, res) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user