Browse Source

FINISH manage video page

features/create-channel
Astri4-4 5 months ago
parent
commit
7311882e38
  1. 9
      backend/app/controllers/channel.controller.js
  2. 6
      backend/app/controllers/comment.controller.js
  3. 18
      backend/app/controllers/playlist.controller.js
  4. 2
      backend/app/controllers/recommendation.controller.js
  5. 2
      backend/app/controllers/search.controller.js
  6. 21
      backend/app/controllers/user.controller.js
  7. 20
      backend/app/controllers/video.controller.js
  8. 999
      backend/logs/access.log
  9. 6
      backend/requests/video.http
  10. 6
      backend/server.js
  11. 22
      frontend/src/components/Tag.jsx
  12. 256
      frontend/src/pages/ManageVideo.jsx
  13. 8
      frontend/src/pages/Video.jsx
  14. 6
      nginx/default.conf

9
backend/app/controllers/channel.controller.js

@ -18,6 +18,7 @@ export async function create(req, res) {
logger.action("try to create new channel with owner " + channel.owner + " and name " + channel.name); logger.action("try to create new channel with owner " + channel.owner + " and name " + channel.name);
logger.write("Successfully created new channel with name " + channel.name, 200); logger.write("Successfully created new channel with name " + channel.name, 200);
client.end();
res.status(200).json(channel); res.status(200).json(channel);
} }
@ -50,6 +51,7 @@ export async function getById(req, res) {
result.rows[0].videos = videoResult.rows; result.rows[0].videos = videoResult.rows;
logger.write("Successfully get channel with id " + id, 200); logger.write("Successfully get channel with id " + id, 200);
client.end();
res.status(200).json(result.rows[0]); res.status(200).json(result.rows[0]);
} }
@ -85,6 +87,7 @@ export async function getAll(req, res) {
}) })
logger.write("Successfully get all channels", 200); logger.write("Successfully get all channels", 200);
client.end();
res.status(200).json(result); res.status(200).json(result);
} }
@ -108,6 +111,7 @@ export async function update(req, res) {
const nameResult = await client.query(nameQuery, [channel.name]); const nameResult = await client.query(nameQuery, [channel.name]);
if (nameResult.rows.length > 0) { if (nameResult.rows.length > 0) {
logger.write("failed to update channel because name already taken", 400); logger.write("failed to update channel because name already taken", 400);
client.end();
res.status(400).json({error: 'Name already used'}); res.status(400).json({error: 'Name already used'});
return return
} }
@ -116,6 +120,7 @@ export async function update(req, res) {
const updateQuery = `UPDATE channels SET name = $1, description = $2 WHERE id = $3`; const updateQuery = `UPDATE channels SET name = $1, description = $2 WHERE id = $3`;
await client.query(updateQuery, [channel.name, channel.description, id]); await client.query(updateQuery, [channel.name, channel.description, id]);
logger.write("Successfully updated channel", 200); logger.write("Successfully updated channel", 200);
client.end();
res.status(200).json(channel); res.status(200).json(channel);
} }
@ -128,6 +133,7 @@ export async function del(req, res) {
const query = `DELETE FROM channels WHERE id = $1`; const query = `DELETE FROM channels WHERE id = $1`;
await client.query(query, [id]); await client.query(query, [id]);
logger.write("Successfully deleted channel", 200); logger.write("Successfully deleted channel", 200);
client.end();
res.status(200).json({message: 'Successfully deleted'}); res.status(200).json({message: 'Successfully deleted'});
} }
@ -152,6 +158,7 @@ export async function toggleSubscription(req, res) {
const remainingSubscriptions = countResult.rows[0].count; const remainingSubscriptions = countResult.rows[0].count;
logger.write("Successfully unsubscribed from channel", 200); logger.write("Successfully unsubscribed from channel", 200);
client.end();
res.status(200).json({message: 'Unsubscribed successfully', subscriptions: remainingSubscriptions}); res.status(200).json({message: 'Unsubscribed successfully', subscriptions: remainingSubscriptions});
} else { } else {
// Subscribe // Subscribe
@ -164,6 +171,7 @@ export async function toggleSubscription(req, res) {
const totalSubscriptions = countResult.rows[0].count; const totalSubscriptions = countResult.rows[0].count;
logger.write("Successfully subscribed to channel", 200); logger.write("Successfully subscribed to channel", 200);
client.end();
res.status(200).json({message: 'Subscribed successfully', subscriptions: totalSubscriptions}); res.status(200).json({message: 'Subscribed successfully', subscriptions: totalSubscriptions});
} }
} }
@ -189,6 +197,7 @@ export async function getStats(req, res) {
const client = await getClient(); const client = await getClient();
const result = await client.query(request, [id]); const result = await client.query(request, [id]);
logger.write("Successfully get stats", 200); logger.write("Successfully get stats", 200);
client.end();
res.status(200).json(result.rows[0]); res.status(200).json(result.rows[0]);
} catch (error) { } catch (error) {
console.log(error); console.log(error);

6
backend/app/controllers/comment.controller.js

@ -39,7 +39,7 @@ export async function upload(req, res) {
createdAt: createdAt createdAt: createdAt
} }
client.end();
res.status(200).json(responseComment); res.status(200).json(responseComment);
} }
@ -52,6 +52,7 @@ export async function getByVideo(req, res) {
const query = `SELECT * FROM comments WHERE video = $1`; const query = `SELECT * FROM comments WHERE video = $1`;
const result = await client.query(query, [videoId]); const result = await client.query(query, [videoId]);
logger.write("successfully get comment", 200); logger.write("successfully get comment", 200);
client.end()
res.status(200).json(result.rows); res.status(200).json(result.rows);
} }
@ -63,6 +64,7 @@ export async function getById(req, res) {
const query = `SELECT * FROM comments WHERE id = $1`; const query = `SELECT * FROM comments WHERE id = $1`;
const result = await client.query(query, [id]); const result = await client.query(query, [id]);
logger.write("successfully get comment", 200); logger.write("successfully get comment", 200);
client.end();
res.status(200).json(result.rows[0]); res.status(200).json(result.rows[0]);
} }
@ -74,6 +76,7 @@ export async function update(req, res) {
const query = `UPDATE comments SET content = $1 WHERE id = $2`; const query = `UPDATE comments SET content = $1 WHERE id = $2`;
const result = await client.query(query, [req.body.content, id]); const result = await client.query(query, [req.body.content, id]);
logger.write("successfully update comment", 200); logger.write("successfully update comment", 200);
client.end();
res.status(200).json(result.rows[0]); res.status(200).json(result.rows[0]);
} }
@ -85,5 +88,6 @@ export async function del(req, res) {
const query = `DELETE FROM comments WHERE id = $1`; const query = `DELETE FROM comments WHERE id = $1`;
const result = await client.query(query, [id]); const result = await client.query(query, [id]);
logger.write("successfully deleted comment", 200); logger.write("successfully deleted comment", 200);
client.end();
res.status(200).json(result.rows[0]); res.status(200).json(result.rows[0]);
} }

18
backend/app/controllers/playlist.controller.js

@ -14,9 +14,11 @@ export async function create(req, res) {
try { try {
const result = await client.query(query, [name, userId]); const result = await client.query(query, [name, userId]);
logger.write("Playlist created with id " + result.rows[0].id, 200); logger.write("Playlist created with id " + result.rows[0].id, 200);
client.end()
res.status(200).json({ id: result.rows[0].id }); res.status(200).json({ id: result.rows[0].id });
} catch (error) { } catch (error) {
logger.write("Error creating playlist: " + error.message, 500); logger.write("Error creating playlist: " + error.message, 500);
client.end();
res.status(500).json({ error: "Internal server error" }); res.status(500).json({ error: "Internal server error" });
} }
} }
@ -31,9 +33,11 @@ export async function addVideo(req, res) {
try { try {
const result = await client.query(query, [video, id]); const result = await client.query(query, [video, id]);
logger.write("Video added to playlist with id " + id, 200); logger.write("Video added to playlist with id " + id, 200);
client.end();
res.status(200).json({id: result.rows[0].id}); res.status(200).json({id: result.rows[0].id});
} catch (error) { } catch (error) {
logger.write("Error adding video to playlist: " + error.message, 500); logger.write("Error adding video to playlist: " + error.message, 500);
client.end();
res.status(500).json({error: "Internal server error"}); res.status(500).json({error: "Internal server error"});
} }
} }
@ -60,13 +64,16 @@ export async function getByUser(req, res) {
const result = await client.query(query, [id]); const result = await client.query(query, [id]);
if (result.rows.length === 0) { if (result.rows.length === 0) {
logger.write("No playlists found for user with id " + id, 404); logger.write("No playlists found for user with id " + id, 404);
client.end();
res.status(404).json({ error: "No playlists found" }); res.status(404).json({ error: "No playlists found" });
return; return;
} }
logger.write("Playlists retrieved for user with id " + id, 200); logger.write("Playlists retrieved for user with id " + id, 200);
client.end();
res.status(200).json(result.rows); res.status(200).json(result.rows);
} catch (error) { } catch (error) {
logger.write("Error retrieving playlists: " + error.message, 500); logger.write("Error retrieving playlists: " + error.message, 500);
client.end();
res.status(500).json({ error: "Internal server error" }); res.status(500).json({ error: "Internal server error" });
} }
} }
@ -82,13 +89,16 @@ export async function getById(req, res) {
const result = await client.query(query, [id]); const result = await client.query(query, [id]);
if (result.rows.length === 0) { if (result.rows.length === 0) {
logger.write("No playlist found with id " + id, 404); logger.write("No playlist found with id " + id, 404);
client.end();
res.status(404).json({ error: "Playlist not found" }); res.status(404).json({ error: "Playlist not found" });
return; return;
} }
logger.write("Playlist retrieved with id " + id, 200); logger.write("Playlist retrieved with id " + id, 200);
client.end();
res.status(200).json(result.rows[0]); res.status(200).json(result.rows[0]);
} catch (error) { } catch (error) {
logger.write("Error retrieving playlist: " + error.message, 500); logger.write("Error retrieving playlist: " + error.message, 500);
client.end();
res.status(500).json({ error: "Internal server error" }); res.status(500).json({ error: "Internal server error" });
} }
} }
@ -105,13 +115,16 @@ export async function update(req, res) {
const result = await client.query(query, [name, id]); const result = await client.query(query, [name, id]);
if (result.rows.length === 0) { if (result.rows.length === 0) {
logger.write("No playlist found with id " + id, 404); logger.write("No playlist found with id " + id, 404);
client.end();
res.status(404).json({ error: "Playlist not found", result: result.rows, query: query }); res.status(404).json({ error: "Playlist not found", result: result.rows, query: query });
return; return;
} }
logger.write("Playlist updated with id " + result.rows[0].id, 200); logger.write("Playlist updated with id " + result.rows[0].id, 200);
client.end();
res.status(200).json({ id: result.rows[0].id }); res.status(200).json({ id: result.rows[0].id });
} catch (error) { } catch (error) {
logger.write("Error updating playlist: " + error.message, 500); logger.write("Error updating playlist: " + error.message, 500);
client.end();
res.status(500).json({ error: "Internal server error" }); res.status(500).json({ error: "Internal server error" });
} }
} }
@ -127,13 +140,16 @@ export async function deleteVideo(req, res) {
const result = await client.query(query, [videoId, id]); const result = await client.query(query, [videoId, id]);
if (result.rows.length === 0) { if (result.rows.length === 0) {
logger.write("No video found in playlist with id " + id, 404); logger.write("No video found in playlist with id " + id, 404);
client.end();
res.status(404).json({ error: "Video not found in playlist" }); res.status(404).json({ error: "Video not found in playlist" });
return; return;
} }
logger.write("Video deleted from playlist with id " + id, 200); logger.write("Video deleted from playlist with id " + id, 200);
client.end();
res.status(200).json({ id: result.rows[0].id }); res.status(200).json({ id: result.rows[0].id });
} catch (error) { } catch (error) {
logger.write("Error deleting video from playlist: " + error.message, 500); logger.write("Error deleting video from playlist: " + error.message, 500);
client.end();
res.status(500).json({ error: "Internal server error" }); res.status(500).json({ error: "Internal server error" });
} }
} }
@ -148,9 +164,11 @@ export async function del(req, res) {
try { try {
const result = await client.query(query, [id]); const result = await client.query(query, [id]);
logger.write("Playlist deleted", 200); logger.write("Playlist deleted", 200);
client.end()
res.status(200).json({ "message": "playlist deleted" }); res.status(200).json({ "message": "playlist deleted" });
} catch (error) { } catch (error) {
logger.write("Error deleting playlist: " + error.message, 500); logger.write("Error deleting playlist: " + error.message, 500);
client.end();
res.status(500).json({ error: "Internal server error" }); res.status(500).json({ error: "Internal server error" });
} }
} }

2
backend/app/controllers/recommendation.controller.js

@ -30,6 +30,7 @@ export async function getRecommendations(req, res) {
// Recuperer 10 videos avec les 3 tags ayant le plus d'interaction avec l'utilisateur // Recuperer 10 videos avec les 3 tags ayant le plus d'interaction avec l'utilisateur
client.end()
res.status(200).json({ res.status(200).json({
message: "Recommendations based on user history and interactions are not yet implemented." message: "Recommendations based on user history and interactions are not yet implemented."
}); });
@ -80,6 +81,7 @@ export async function getTrendingVideos(req, res) {
} }
client.end();
res.status(200).json(trendingVideos); res.status(200).json(trendingVideos);
} catch (error) { } catch (error) {
console.error("Error fetching trending videos:", error); console.error("Error fetching trending videos:", error);

2
backend/app/controllers/search.controller.js

@ -76,7 +76,7 @@ export async function search(req, res) {
} }
client.end()
return res.status(200).json(videos); return res.status(200).json(videos);

21
backend/app/controllers/user.controller.js

@ -99,6 +99,7 @@ export async function login(req, res) {
} }
logger.write("Successfully logged in", 200); logger.write("Successfully logged in", 200);
client.end();
res.status(200).json({token: token, user: userData}); res.status(200).json({token: token, user: userData});
} }
@ -108,15 +109,20 @@ export async function getById(req, res) {
const logger = req.body.logger; const logger = req.body.logger;
logger.action("try to retrieve user " + id); logger.action("try to retrieve user " + id);
const client = await getClient(); const client = await getClient();
const query = `SELECT id, email, username, picture FROM users WHERE id = $1`; const query = `SELECT id, email, username, picture
FROM users
WHERE id = $1`;
const result = await client.query(query, [id]); const result = await client.query(query, [id]);
if (!result.rows[0]) { if (!result.rows[0]) {
logger.write("failed to retrieve user " + id + " because it doesn't exist", 404); logger.write("failed to retrieve user " + id + " because it doesn't exist", 404);
client.end()
res.status(404).json({error: "Not Found"}); res.status(404).json({error: "Not Found"});
return return
} }
logger.write("successfully retrieved user " + id, 200); logger.write("successfully retrieved user " + id, 200);
return res.status(200).json({user: result.rows[0]}); if (result.rows[0].picture) {
return res.status(200).json({user: result.rows[0]});
}
} }
export async function getByUsername(req, res) { export async function getByUsername(req, res) {
@ -128,10 +134,12 @@ export async function getByUsername(req, res) {
const result = await client.query(query, [username]); const result = await client.query(query, [username]);
if (!result.rows[0]) { if (!result.rows[0]) {
logger.write("failed to retrieve user " + username + " because it doesn't exist", 404); logger.write("failed to retrieve user " + username + " because it doesn't exist", 404);
client.end()
res.status(404).json({error: "Not Found"}); res.status(404).json({error: "Not Found"});
return return
} }
logger.write("successfully retrieved user " + username, 200); logger.write("successfully retrieved user " + username, 200);
client.end();
return res.status(200).json({user: result.rows[0]}); return res.status(200).json({user: result.rows[0]});
} }
@ -159,6 +167,7 @@ export async function update(req, res) {
const emailResult = await client.query(emailQuery, [user.email]); const emailResult = await client.query(emailQuery, [user.email]);
if (emailResult.rows[0]) { if (emailResult.rows[0]) {
logger.write("failed to update because email is already used", 400) logger.write("failed to update because email is already used", 400)
client.end();
res.status(400).json({error: "Email already exists"}); res.status(400).json({error: "Email already exists"});
} }
} }
@ -168,6 +177,7 @@ export async function update(req, res) {
const usernameResult = await client.query(usernameQuery, [user.username]); const usernameResult = await client.query(usernameQuery, [user.username]);
if (usernameResult.rows[0]) { if (usernameResult.rows[0]) {
logger.write("failed to update because username is already used", 400) logger.write("failed to update because username is already used", 400)
client.end();
res.status(400).json({error: "Username already exists"}); res.status(400).json({error: "Username already exists"});
} }
} }
@ -204,9 +214,11 @@ export async function update(req, res) {
const updateQuery = `UPDATE users SET email = $1, username = $2, password = $3, picture = $4 WHERE id = $5 RETURNING id, email, username, picture`; const updateQuery = `UPDATE users SET email = $1, username = $2, password = $3, picture = $4 WHERE id = $5 RETURNING id, email, username, picture`;
const result = await client.query(updateQuery, [user.email, user.username, user.password, profilePicture, id]); const result = await client.query(updateQuery, [user.email, user.username, user.password, profilePicture, id]);
logger.write("successfully updated user " + id, 200); logger.write("successfully updated user " + id, 200);
client.end();
res.status(200).json(result.rows[0]); res.status(200).json(result.rows[0]);
} catch (err) { } catch (err) {
console.log(err); console.log(err);
client.end()
res.status(500).json({error: err}); res.status(500).json({error: err});
} }
@ -220,6 +232,7 @@ export async function deleteUser(req, res) {
const query = `DELETE FROM users WHERE id = $1`; const query = `DELETE FROM users WHERE id = $1`;
await client.query(query, [id]); await client.query(query, [id]);
logger.write("successfully deleted user " + id); logger.write("successfully deleted user " + id);
client.end();
res.status(200).json({message: 'User deleted'}); res.status(200).json({message: 'User deleted'});
} }
@ -234,11 +247,13 @@ export async function getChannel(req, res) {
if (!result.rows[0]) { if (!result.rows[0]) {
logger.write("failed to retrieve channel of user " + id + " because it doesn't exist", 404); logger.write("failed to retrieve channel of user " + id + " because it doesn't exist", 404);
client.end();
res.status(404).json({error: "Channel Not Found"}); res.status(404).json({error: "Channel Not Found"});
return; return;
} }
logger.write("successfully retrieved channel of user " + id, 200); logger.write("successfully retrieved channel of user " + id, 200);
client.end();
res.status(200).json({channel: result.rows[0]}); res.status(200).json({channel: result.rows[0]});
} }
@ -285,9 +300,11 @@ export async function getHistory(req, res) {
if (!result.rows[0]) { if (!result.rows[0]) {
logger.write("failed to retrieve history of user " + id + " because it doesn't exist", 404); logger.write("failed to retrieve history of user " + id + " because it doesn't exist", 404);
res.status(404).json({error: "History Not Found"}); res.status(404).json({error: "History Not Found"});
client.end();
return; return;
} }
logger.write("successfully retrieved history of user " + id, 200); logger.write("successfully retrieved history of user " + id, 200);
client.end();
res.status(200).json(videos); res.status(200).json(videos);
} }

20
backend/app/controllers/video.controller.js

@ -52,6 +52,7 @@ export async function upload(req, res) {
const query = `INSERT INTO videos (title, thumbnail, description, channel, visibility, file, slug, format, release_date) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`; const query = `INSERT INTO videos (title, thumbnail, description, channel, visibility, file, slug, format, release_date) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`;
await client.query(query, [video.title, 'null', video.description, video.channel, video.visibility, video.file, video.slug, video.format, releaseDate]); await client.query(query, [video.title, 'null', video.description, video.channel, video.visibility, video.file, video.slug, video.format, releaseDate]);
logger.write("successfully uploaded video", 200); logger.write("successfully uploaded video", 200);
client.end()
res.status(200).json({"message": "Successfully uploaded video"}); res.status(200).json({"message": "Successfully uploaded video"});
} }
@ -75,6 +76,7 @@ export async function uploadThumbnail(req, res) {
const updateQuery = `UPDATE videos SET thumbnail = $1 WHERE id = $2`; const updateQuery = `UPDATE videos SET thumbnail = $1 WHERE id = $2`;
await client.query(updateQuery, [file, req.body.video]); await client.query(updateQuery, [file, req.body.video]);
logger.write("successfully uploaded thumbnail", 200); logger.write("successfully uploaded thumbnail", 200);
client.end()
res.status(200).json({"message": "Successfully uploaded thumbnail"}); res.status(200).json({"message": "Successfully uploaded thumbnail"});
} }
@ -122,6 +124,7 @@ export async function getById(req, res) {
video.tags = tagsResult.rows.map(tag => tag.name); video.tags = tagsResult.rows.map(tag => tag.name);
logger.write("successfully get video " + id, 200); logger.write("successfully get video " + id, 200);
client.end()
res.status(200).json(video); res.status(200).json(video);
} }
@ -133,6 +136,7 @@ export async function getByChannel(req, res) {
const query = `SELECT * FROM videos WHERE channel = $1`; const query = `SELECT * FROM videos WHERE channel = $1`;
const result = await client.query(query, [id]); const result = await client.query(query, [id]);
logger.write("successfully get video from channel " + id, 200); logger.write("successfully get video from channel " + id, 200);
client.end()
res.status(200).json(result.rows); res.status(200).json(result.rows);
} }
@ -144,6 +148,7 @@ export async function update(req, res) {
const query = `UPDATE videos SET title = $1, description = $2, visibility = $3 WHERE id = $4`; const query = `UPDATE videos SET title = $1, description = $2, visibility = $3 WHERE id = $4`;
await client.query(query, [req.body.title, req.body.description, req.body.visibility, id]); await client.query(query, [req.body.title, req.body.description, req.body.visibility, id]);
logger.write("successfully updated video", 200); logger.write("successfully updated video", 200);
client.end()
res.status(200).json({"message": "Successfully updated video"}); res.status(200).json({"message": "Successfully updated video"});
} }
@ -161,6 +166,7 @@ export async function updateVideo(req, res) {
fs.unlink(pathToDelete, (error) => { fs.unlink(pathToDelete, (error) => {
if (error) { if (error) {
logger.write(error, 500); logger.write(error, 500);
client.end()
res.status(500).json({"message": "Failed to delete video"}); res.status(500).json({"message": "Failed to delete video"});
return return
} }
@ -172,6 +178,7 @@ export async function updateVideo(req, res) {
fs.writeFileSync(destinationPath, fileBuffer); fs.writeFileSync(destinationPath, fileBuffer);
logger.write("successfully updated video", 200); logger.write("successfully updated video", 200);
client.end()
res.status(200).json({"message": "Successfully updated video"}); res.status(200).json({"message": "Successfully updated video"});
}) })
@ -193,6 +200,7 @@ export async function del(req, res) {
fs.unlink(pathToDelete, (error) => { fs.unlink(pathToDelete, (error) => {
if (error) { if (error) {
logger.write(error, 500); logger.write(error, 500);
client.end()
res.status(500).json({"message": "Failed to delete video"}); res.status(500).json({"message": "Failed to delete video"});
return return
} }
@ -207,6 +215,7 @@ export async function del(req, res) {
const query = `DELETE FROM videos WHERE id = $1`; const query = `DELETE FROM videos WHERE id = $1`;
await client.query(query, [id]); await client.query(query, [id]);
logger.write("successfully deleted video", 200); logger.write("successfully deleted video", 200);
client.end()
res.status(200).json({"message": "Successfully deleted video"}); res.status(200).json({"message": "Successfully deleted video"});
}) })
}) })
@ -237,6 +246,7 @@ export async function toggleLike(req, res) {
const likesCount = likesCountResult.rows[0].like_count; const likesCount = likesCountResult.rows[0].like_count;
logger.write("no likes found adding likes for video " + id, 200); logger.write("no likes found adding likes for video " + id, 200);
client.end();
res.status(200).json({"message": "Successfully added like", "likes": likesCount}); res.status(200).json({"message": "Successfully added like", "likes": likesCount});
} else { } else {
const query = `DELETE FROM likes WHERE owner = $1 AND video = $2`; const query = `DELETE FROM likes WHERE owner = $1 AND video = $2`;
@ -248,6 +258,7 @@ export async function toggleLike(req, res) {
const likesCount = likesCountResult.rows[0].like_count; const likesCount = likesCountResult.rows[0].like_count;
logger.write("likes found, removing like for video " + id, 200); logger.write("likes found, removing like for video " + id, 200);
client.end();
res.status(200).json({"message": "Successfully removed like", "likes": likesCount}); res.status(200).json({"message": "Successfully removed like", "likes": likesCount});
} }
@ -295,9 +306,15 @@ export async function addTags(req, res) {
const insertVideoTagQuery = `INSERT INTO video_tags (tag, video) VALUES ($1, $2)`; const insertVideoTagQuery = `INSERT INTO video_tags (tag, video) VALUES ($1, $2)`;
await client.query(insertVideoTagQuery, [id, videoId]); await client.query(insertVideoTagQuery, [id, videoId]);
} }
// GET UPDATED TAGS FOR VIDEO
const updatedTagsQuery = `SELECT t.name FROM tags t JOIN video_tags vt ON t.id = vt.tag WHERE vt.video = $1`;
const updatedTagsResult = await client.query(updatedTagsQuery, [videoId]);
const updatedTags = updatedTagsResult.rows;
logger.write("successfully added tags to video " + videoId, 200); logger.write("successfully added tags to video " + videoId, 200);
await client.end(); await client.end();
res.status(200).json({"message": "Successfully added tags to video"}); res.status(200).json({"message": "Successfully added tags to video", "tags" : updatedTags.map(tag => tag.name)});
} }
@ -430,5 +447,6 @@ export async function addViews(req, res) {
} }
logger.write("successfully added views for video " + id, 200); logger.write("successfully added views for video " + id, 200);
await client.end();
res.status(200).json({"message": "Successfully added views"}); res.status(200).json({"message": "Successfully added views"});
} }

999
backend/logs/access.log

File diff suppressed because it is too large

6
backend/requests/video.http

@ -1,4 +1,4 @@
@token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidXNlcm5hbWUiOiJhc3RyaWEiLCJpYXQiOjE3NTI5NDQxMDF9.dbGCL8qqqLR3e7Ngns-xPfZAvp0WQzAbjaEHjDVg1HI @token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhc3RyaWEiLCJpYXQiOjE3NTMzODAyNjB9._rUcieo3acJp6tjQao7V3UQz0_ngHuB2z36_fG_fIX8
### UPDATE VIDEO ### UPDATE VIDEO
PUT http://127.0.0.1:8000/api/videos/3 PUT http://127.0.0.1:8000/api/videos/3
@ -16,7 +16,7 @@ GET http://127.0.0.1:8000/api/videos/14/like
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
### ADD TAGS ### ADD TAGS
PUT http://127.0.0.1:8000/api/videos/2/tags PUT http://127.0.0.1:8000/api/videos/3/tags
Content-Type: application/json Content-Type: application/json
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
@ -26,7 +26,7 @@ Authorization: Bearer {{token}}
"Create Mod", "Create Mod",
"Redstone" "Redstone"
], ],
"channel": 2 "channel": 1
} }
### ###

6
backend/server.js

@ -20,9 +20,9 @@ const app = express();
// INITIALIZE DATABASE // INITIALIZE DATABASE
// Increase body size limits for file uploads
app.use(express.urlencoded({extended: true})); app.use(express.urlencoded({extended: true, limit: '500mb'}));
app.use(express.json()); app.use(express.json({limit: '500mb'}));
app.use(cors()) app.use(cors())
// ROUTES // ROUTES

22
frontend/src/components/Tag.jsx

@ -0,0 +1,22 @@
export default function Tag({ tag, onSuppress, doShowControls=true }) {
return (
<div className="glassmorphism px-2 py-1 w-max flex flex-row items-center gap-2">
<span className="font-inter text-white">#{tag}</span>
{doShowControls && (
<span className="tag-controls cursor-pointer" onClick={onSuppress}>
<svg
className="w-6 h-6 fill-white"
stroke="#FFF"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</span>
)}
</div>
)
}

256
frontend/src/pages/ManageVideo.jsx

@ -4,6 +4,7 @@ import {useAuth} from "../contexts/AuthContext.jsx";
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import LinearGraph from "../components/LinearGraph.jsx"; import LinearGraph from "../components/LinearGraph.jsx";
import Comment from "../components/Comment.jsx"; import Comment from "../components/Comment.jsx";
import Tag from "../components/Tag.jsx";
export default function ManageVideo() { export default function ManageVideo() {
@ -17,9 +18,12 @@ export default function ManageVideo() {
const [viewsPerDay, setViewsPerDay] = useState([]); const [viewsPerDay, setViewsPerDay] = useState([]);
const [videoTitle, setVideoTitle] = useState(null); const [videoTitle, setVideoTitle] = useState(null);
const [description, setDescription] = useState(null); const [description, setDescription] = useState(null);
const [visibility, setVisibility] = useState("private");
const [editMode, setEditMode] = useState(false); const [editMode, setEditMode] = useState(false);
const [thumbnailPreview, setThumbnailPreview] = useState(null);
const [videoFile, setVideoFile] = useState(null);
const nonEditModeClasses = "text-2xl font-semibold text-white p-2 focus:text-white focus:outline-none w-full font-montserrat resizable-none"; const nonEditModeClasses = "text-md font-normal text-white p-2 focus:text-white focus:outline-none w-full font-montserrat resizable-none";
const editModeClasses = nonEditModeClasses + " glassmorphism"; const editModeClasses = nonEditModeClasses + " glassmorphism";
const nonEditModeClassesTextArea = "text-md font-normal text-white p-2 focus:text-white focus:outline-none w-full font-montserrat resizable-none w-full" const nonEditModeClassesTextArea = "text-md font-normal text-white p-2 focus:text-white focus:outline-none w-full font-montserrat resizable-none w-full"
@ -38,6 +42,9 @@ export default function ManageVideo() {
} }
const data = await request.json(); const data = await request.json();
setVideo(data); setVideo(data);
setVisibility(data.visibility)
setVideoTitle(data.title);
setDescription(data.description);
} }
const fetchLikesPerDay = async () => { const fetchLikesPerDay = async () => {
const request = await fetch(`/api/videos/${id}/likes/day`, { const request = await fetch(`/api/videos/${id}/likes/day`, {
@ -62,6 +69,142 @@ export default function ManageVideo() {
} }
}, [user, id, token]); }, [user, id, token]);
const onSubmit = async (e) => {
e.preventDefault();
if (!editMode) return;
const request = await fetch(`/api/videos/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
title: videoTitle,
description: description,
visibility: visibility,
channel: video.channel
})
});
if (!request.ok) {
console.error("Failed to update video");
return;
}
const form = new FormData();
if (videoFile) {
form.append('file', videoFile);
form.append('video', id);
form.append('channel', video.channel);
const videoRequest = await fetch(`/api/videos/${id}/video`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`
},
body: form
});
if (!videoRequest.ok) {
console.error("Failed to update video file");
return;
}
}
const data = await request.json();
setVideo(data);
setEditMode(false);
}
const handleThumbnailChange = async (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
setThumbnailPreview(e.target.result);
};
reader.readAsDataURL(file);
}
const formData = new FormData();
formData.append('file', file);
formData.append('video', id);
formData.append('channel', video.channel);
console.log(formData);
const request = await fetch(`/api/videos/thumbnail`, {
"method": 'POST',
"headers": {
"Authorization": `Bearer ${token}`
},
body: formData
})
if (!request.ok) {
console.error("Failed to upload thumbnail");
return;
}
const data = await request.json();
console.log(data);
};
const onAddTag = async (e) => {
if (e.key !== 'Enter' || e.target.value.trim() === "") return;
const newTag = e.target.value.trim();
e.target.value = "";
const request = await fetch(`/api/videos/${id}/tags`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
tags: [...video.tags, newTag],
channel: video.channel
})
});
if (!request.ok) {
console.error("Failed to add tag");
return;
}
const data = await request.json();
console.log(data);
setVideo({
...video,
tags: [...video.tags, newTag]
});
}
const onSuppressTag = async (tag) => {
const request = await fetch(`/api/videos/${id}/tags`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
tags: video.tags.filter(t => t !== tag),
channel: video.channel
})
});
if (!request.ok) {
console.error("Failed to suppress tag");
return;
}
const data = await request.json();
console.log(data);
const newTags = video.tags.filter(t => t !== tag);
setVideo({
...video,
tags: newTags
});
}
return ( return (
<div className="min-w-screen min-h-screen bg-linear-to-br from-left-gradient to-right-gradient"> <div className="min-w-screen min-h-screen bg-linear-to-br from-left-gradient to-right-gradient">
@ -90,8 +233,8 @@ export default function ManageVideo() {
{ /* LEFT SIDE */ } { /* LEFT SIDE */ }
<div className="flex-1" > <div className="flex-1" >
{ /* THUMBNAIL */ } { /* THUMBNAIL */ }
<div className="glassmorphism flex justify-center items-center py-5 relative overflow-hidden "> <label htmlFor="thumbnail" className="glassmorphism flex justify-center items-center py-5 relative overflow-hidden ">
<img src={video ? video.thumbnail : ""} alt="" className=" rounded-sm"/> <img src={thumbnailPreview || (video ? video.thumbnail : "")} alt="" className=" rounded-sm"/>
<div className="absolute top-0 left-0 bg-[#00000080] w-full h-full flex justify-center items-center opacity-0 hover:opacity-100 transition" > <div className="absolute top-0 left-0 bg-[#00000080] w-full h-full flex justify-center items-center opacity-0 hover:opacity-100 transition" >
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" className="fill-white" viewBox="0 0 24 24"> <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" className="fill-white" viewBox="0 0 24 24">
@ -99,7 +242,8 @@ export default function ManageVideo() {
</svg> </svg>
</div> </div>
</div> </label>
<input type="file" name="thumbnail" id="thumbnail" className="opacity-0 w-0 h-0 hidden" onChange={handleThumbnailChange} accept="image/*"/>
{ /* VIDEO INFOS */ } { /* VIDEO INFOS */ }
@ -111,8 +255,8 @@ export default function ManageVideo() {
type="text" type="text"
id="name" id="name"
value={videoTitle || videoTitle === "" ? videoTitle : video ? video.title : "Chargement"} value={videoTitle || videoTitle === "" ? videoTitle : video ? video.title : "Chargement"}
className={(editMode ? editModeClasses : nonEditModeClasses)} onChange={(e) => setVideoTitle(e.target.value)}
onChange={(e) => setChannelName(e.target.value)} className={(editMode ? editModeClasses : nonEditModeClasses) + " text-xl"}
placeholder="Nom d'utilisateur" placeholder="Nom d'utilisateur"
disabled={!editMode} disabled={!editMode}
/> />
@ -130,12 +274,61 @@ export default function ManageVideo() {
disabled={!editMode} disabled={!editMode}
></textarea> ></textarea>
<button <label htmlFor="visibility" className="text-2xl text-white mb-1 block font-montserrat">
type='button' Visibilité
className="bg-primary p-3 rounded-sm text-white font-montserrat text-2xl font-black cursor-pointer mt-4" </label>
<select
className={(editMode ? editModeClasses : nonEditModeClasses)}
value={visibility}
name="visibility"
onChange={(e) => setVisibility(e.target.value)}
disabled={!editMode}
> >
Modifier <option value="public">Publique</option>
</button> <option value="private">Privée</option>
</select>
<label htmlFor="video" className={`flex gap-2 glassmorphism p-2 items-center mt-4 cursor-pointer ${editMode ? "block" : "hidden"}`}>
<video src={video ? video.file : ""} className="w-1/8 aspect-video rounded-sm" ></video>
<p className="text-2xl text-white mb-1 block font-montserrat">Fichier vidéo</p>
</label>
<input
type="file"
name="video"
id="video"
className="hidden"
accept="video/*"
onChange={(e) => setVideoFile(e.target.files[0])}
/>
{
editMode ? (
<div className="mt-4">
<button
type="button"
className="bg-primary p-3 rounded-sm text-white font-montserrat text-2xl font-black cursor-pointer"
onClick={(e) => {onSubmit(e)}}
>
Enregistrer
</button>
<button
type="button"
className="bg-red-500 p-3 rounded-sm text-white font-montserrat text-2xl font-black cursor-pointer ml-3"
onClick={() => setEditMode(!editMode)}
>
Annuler
</button>
</div>
) : (
<button
type="button"
className="bg-primary p-3 rounded-sm text-white font-montserrat text-2xl font-black cursor-pointer mt-4"
onClick={() => setEditMode(!editMode)}
>
Modifier
</button>
)
}
</form> </form>
@ -172,6 +365,47 @@ export default function ManageVideo() {
</div> </div>
{ /* TAGS */ }
<div className="glassmorphism p-4 mt-4">
<h2 className="text-2xl font-bold text-white mb-4">Tags</h2>
<div className="flex flex-wrap gap-2">
{ video && video.tags && video.tags.length > 0 ? (
video.tags.map((tag) => (
<Tag tag={tag} key={tag} onSuppress={() => onSuppressTag(tag)} />
))
) : (
<p className="text-gray-500">Aucun tag</p>
)}
</div>
<input
type="text"
className="glassmorphism text-md font-normal text-white p-2 focus:text-white focus:outline-none w-full font-montserrat resizable-none mt-4"
placeholder="Ajouter un tag"
onKeyPress={(e) => onAddTag(e)}
/>
</div>
{ /* LINK */ }
<div className="glassmorphism p-4 mt-4">
<h2 className="text-2xl font-bold text-white mb-4">Lien de la vidéo</h2>
<p className="text-md font-normal text-white">
<a href={`/video/${id}`} target="_blank" rel="noopener noreferrer" className="text-blue-500 hover:underline">
{window.location.origin}/video/{id}
</a>
<button
className="ml-2 bg-primary text-white p-2 rounded-sm cursor-pointer"
onClick={() => {
navigator.clipboard.writeText(`${window.location.origin}/video/${id}`);
}}
>
Copier
</button>
</p>
</div>
</div> </div>
</div> </div>

8
frontend/src/pages/Video.jsx

@ -4,6 +4,7 @@ import Navbar from "../components/Navbar.jsx";
import { useAuth } from "../contexts/AuthContext.jsx"; import { useAuth } from "../contexts/AuthContext.jsx";
import Comment from "../components/Comment.jsx"; import Comment from "../components/Comment.jsx";
import VideoCard from "../components/VideoCard.jsx"; import VideoCard from "../components/VideoCard.jsx";
import Tag from "../components/Tag.jsx";
export default function Video() { export default function Video() {
@ -396,12 +397,7 @@ export default function Video() {
<div className="mb-3"> <div className="mb-3">
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{video.tags.map((tag, index) => ( {video.tags.map((tag, index) => (
<span <Tag tag={tag} key={index} doShowControls={false} />
key={index}
className="bg-gray-700 text-white px-3 py-1 rounded-full text-sm font-montserrat"
>
#{tag}
</span>
))} ))}
</div> </div>
</div> </div>

6
nginx/default.conf

@ -12,6 +12,9 @@ server {
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html index.htm; index index.html index.htm;
# Allow large file uploads for videos (up to 500MB)
client_max_body_size 500M;
ssl_certificate /etc/nginx/ssl/nginx-selfsigned.crt; ssl_certificate /etc/nginx/ssl/nginx-selfsigned.crt;
ssl_certificate_key /etc/nginx/ssl/nginx-selfsigned.key; ssl_certificate_key /etc/nginx/ssl/nginx-selfsigned.key;
@ -26,6 +29,9 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off; proxy_buffering off;
# Also set timeout for large uploads
proxy_read_timeout 300s;
proxy_send_timeout 300s;
} }
# Static assets - NO CACHING for development # Static assets - NO CACHING for development

Loading…
Cancel
Save