Compare commits

...

12 Commits

  1. 2
      backend/app/controllers/channel.controller.js
  2. 15
      backend/app/controllers/media.controller.js
  3. 2
      backend/app/controllers/oauth.controller.js
  4. 2
      backend/app/controllers/search.controller.js
  5. 12
      backend/app/controllers/user.controller.js
  6. 8
      backend/app/controllers/video.controller.js
  7. 2
      backend/app/middlewares/error.middleware.js
  8. 0
      backend/app/middlewares/media.middleware.js
  9. 2
      backend/app/middlewares/video.middleware.js
  10. 12
      backend/app/routes/user.route.js
  11. BIN
      backend/app/uploads/profiles/astri6.jpg
  12. BIN
      backend/app/uploads/profiles/astri7.jpg
  13. BIN
      backend/app/uploads/profiles/astria.png
  14. BIN
      backend/app/uploads/profiles/astria2.jpg
  15. BIN
      backend/app/uploads/profiles/astria3.jpg
  16. BIN
      backend/app/uploads/profiles/default.png
  17. 29
      backend/app/utils/database.js
  18. 2
      backend/app/utils/mail.js
  19. 16
      backend/app/utils/wait-for-it.sh
  20. 14
      backend/freetube.sh
  21. 162
      backend/logs/access.log
  22. 1
      backend/package.json
  23. 30
      backend/requests/channel.http
  24. 25
      backend/requests/comment.http
  25. 11
      backend/requests/medias.http
  26. 11
      backend/requests/playlist.http
  27. 5
      backend/requests/recommendation.http
  28. 17
      backend/requests/top-tags-videos.http
  29. 49
      backend/requests/user.http
  30. 33
      backend/requests/video.http
  31. 9
      backend/server.js
  32. 190
      backend/test/channel.test.js
  33. 175
      backend/test/comment.test.js
  34. 231
      backend/test/playlist.test.js
  35. 413
      backend/test/user.test.js
  36. 198
      backend/test/video.test.js
  37. 29
      backend/tools.js
  38. 6
      backend/vitest.config.js
  39. 2
      developpement.yaml
  40. 6
      docker-compose.yaml
  41. 7
      frontend/Docker.Development
  42. 12
      frontend/README.md
  43. 15
      frontend/rebuild.sh
  44. 4
      frontend/src/components/CreatorCard.jsx
  45. 2
      frontend/src/components/Recommendations.jsx
  46. 2
      frontend/src/modals/CreateChannelModal.jsx
  47. 2
      frontend/src/modals/EmailVerificationModal.jsx
  48. 4
      frontend/src/pages/AddVideo.jsx
  49. 35
      frontend/src/pages/Home.jsx
  50. 6
      frontend/src/pages/LoginSuccess.jsx
  51. 4
      frontend/src/pages/ManageVideo.jsx
  52. 2
      frontend/src/pages/Register.jsx
  53. 451
      frontend/src/pages/Video.jsx
  54. 2
      frontend/src/services/channel.service.js
  55. 4
      frontend/src/services/user.service.js
  56. BIN
      uploads/profiles/sacha.jpg

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

@ -236,7 +236,7 @@ export async function getStats(req, res) {
client.release();
res.status(200).json(result.rows[0]);
} catch (error) {
console.log(error);
res.status(500).json({error: error.message});
}

15
backend/app/controllers/media.controller.js

@ -15,12 +15,13 @@ export async function getProfilePicture(req, res) {
path.join('/app/uploads/profiles', file)
];
console.log('Possible paths:', possiblePaths);
console.log('Current working directory:', process.cwd());
console.log('__dirname:', __dirname);
// Try the most likely path first (based on your volume mapping)
const filePath = path.join('/app/app/uploads/profiles', file);
console.log(path.join(__dirname, '../uploads/profiles', file))
const filePath = path.join(__dirname, '../uploads/profiles', file);
try {
res.sendFile(filePath, (err) => {
@ -28,7 +29,7 @@ export async function getProfilePicture(req, res) {
console.error("Error sending profile picture:", err);
res.status(404).json({ error: "Profile picture not found." });
} else {
console.log("Profile picture sent successfully.");
}
});
} catch (error) {
@ -47,7 +48,7 @@ export async function getThumbnail(req, res) {
console.error("Error sending thumbnail:", err);
res.status(404).json({ error: "Thumbnail not found." });
} else {
console.log("Thumbnail sent successfully.");
}
});
} catch (error) {
@ -65,7 +66,7 @@ export async function getVideo(req, res) {
if (err) {
console.error("Error sending video:", err);
} else {
console.log("Video sent successfully.");
}
});
} catch (error) {

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

@ -26,7 +26,7 @@ export async function callback(req, res, next) {
const avatarUrl = user.photos && user.photos[0] ? user.photos[0].value : null;
const displayName = user.displayName || username;
console.log(user);
console.log("GitHub user info:", {
githubId,

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

@ -2,7 +2,7 @@ import {getClient} from "../utils/database.js";
export async function search(req, res) {
try {
console.log(req.query);
const query = req.query.q;
const type = req.query.type || 'all';
const offset = parseInt(req.query.offset) || 0;

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

@ -111,7 +111,7 @@ export async function register(req, res) {
</html>
`;
console.log("Sending email to:", user.email);
await sendEmail(user.email, "🎬 Bienvenue sur Freetube - Vérifiez votre email", textMessage, htmlMessage);
@ -122,11 +122,11 @@ export async function register(req, res) {
const insertQuery = `INSERT INTO email_verification (email, token, expires_at) VALUES ($1, $2, $3)`;
await client.query(insertQuery, [user.email, token, expirationDate]);
console.log("Successfully registered");
logger.write("successfully registered", 200);
res.status(200).send({ user: user });
} catch (err) {
console.log(err);
logger?.write("failed to register user", 500);
res.status(500).json({ error: "Internal server error" });
} finally {
@ -217,7 +217,7 @@ export async function login(req, res) {
logger.write("Successfully logged in", 200);
res.status(200).json({ token: token, user: userData });
} catch (err) {
console.log(err);
logger?.write("failed to login", 500);
res.status(500).json({ error: "Internal server error" });
} finally {
@ -317,7 +317,7 @@ export async function update(req, res) {
let __filename = fileURLToPath(import.meta.url);
let __dirname = dirname(__filename);
console.log(__dirname);
let profilePicture = userInBase.picture.split("/").pop();
fs.rename(
@ -338,7 +338,7 @@ export async function update(req, res) {
client.release();
res.status(200).json(result.rows[0]);
} catch (err) {
console.log(err);
client.release()
res.status(500).json({ error: err });
}

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

@ -74,7 +74,7 @@ export async function upload(req, res) {
if (Array.isArray(authorizedUsers) && authorizedUsers.length > 0) {
for (let i = 0; i < authorizedUsers.length; i++) {
const user = authorizedUsers[i];
console.log("authorized user", user);
const query = `INSERT INTO video_authorized_users (video_id, user_id) VALUES ($1, $2)`;
// SEND EMAIL TO AUTHORIZED USER
@ -565,7 +565,7 @@ export async function getLikesPerDay(req, res) {
const resultLikes = await client.query(likeQuery, [id]);
response.likes = resultLikes.rows;
console.log(response);
logger.write("successfully retrieved likes per day", 200);
res.status(200).json(response);
@ -607,14 +607,14 @@ export async function updateAuthorizedUsers(req, res) {
const { authorizedUsers } = req.body;
console.log(authorizedUsers);
const client = await getClient();
try {
// Remove all existing authorized users
const deleteQuery = `DELETE FROM video_authorized_users WHERE video_id = $1`;
console.log(`DELETE FROM video_authorized_users WHERE video_id = ${id}`);
await client.query(deleteQuery, [id]);
// Add new authorized users

2
backend/app/middlewares/error.middleware.js

@ -6,7 +6,7 @@ export default async function validator(req, res, next) {
if (!errors.isEmpty()) {
const logger = req.body.logger;
logger.write("failed due to invalid values", 400);
console.log(req.body);
return res.status(400).json({ errors: errors.array() });
} else {
next()

0
backend/app/middlewares/media.middleware.js

2
backend/app/middlewares/video.middleware.js

@ -150,7 +150,7 @@ export async function hasAccess(req, res, next) {
const videoResult = await client.query(videoQuery, [videoId]);
const video = videoResult.rows[0];
console.log(video);
if (video.visibility === 'private') {
const token = req.headers.authorization?.split(" ")[1];

12
backend/app/routes/user.route.js

@ -32,25 +32,25 @@ import {Channel} from "../middlewares/channel.middleware.js";
const router = Router();
// REGISTER A USER
// REGISTER A USER+
router.post("/", [profileUpload.single("profile"), addLogger, UserRegister.email, UserRegister.username, UserRegister.password, validator, doEmailExists, doUsernameExists], register);
// LOGIN A USER
// LOGIN A USER+
router.post("/login", [addLogger, UserLogin.username, UserLogin.password, validator], login)
// SEARCH BY USERNAME
router.get("/search", [addLogger, isTokenValid, UserSearch.username, validator], searchByUsername);
// GET USER BY ID
// GET USER BY ID+
router.get("/:id", [addLogger, isTokenValid, User.id, validator], getById)
// GET USER BY USERNAME
// GET USER BY USERNAME+
router.get("/username/:username", [addLogger, isTokenValid, UserRequest.username, validator], getByUsername);
// UPDATE USER
// UPDATE USER+
router.put("/:id", [addLogger, isTokenValid, User.id, UserRegister.email, UserRegister.username, validator, doUserExists, isOwner], update);
// DELETE USER
// DELETE USER+
router.delete("/:id", [addLogger, isTokenValid, User.id, validator, doUserExists, isOwner], deleteUser);
// GET USER CHANNEL

BIN
backend/app/uploads/profiles/astri6.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

BIN
backend/app/uploads/profiles/astri7.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

BIN
backend/app/uploads/profiles/astria.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 MiB

BIN
backend/app/uploads/profiles/astria2.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

BIN
backend/app/uploads/profiles/astria3.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

BIN
backend/app/uploads/profiles/default.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

29
backend/app/utils/database.js

@ -1,19 +1,20 @@
import pg from "pg";
export async function getClient() {
// Create a connection pool instead of individual connections
const pool = new pg.Pool({
user: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD,
host: process.env.POSTGRES_HOST,
database: process.env.POSTGRES_DB,
port: 5432,
max: 30, // Increased maximum number of connections in the pool
idleTimeoutMillis: 30000, // Close idle connections after 30 seconds
connectionTimeoutMillis: 10000, // Increased timeout to 10 seconds
acquireTimeoutMillis: 10000, // Wait up to 10 seconds for a connection
});
const pool = new pg.Pool({
user: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD,
host: process.env.POSTGRES_HOST,
database: process.env.POSTGRES_DB,
port: 5432,
max: 30, // Increased maximum number of connections in the pool
idleTimeoutMillis: 30000, // Close idle connections after 30 seconds
connectionTimeoutMillis: 10000, // Increased timeout to 10 seconds
acquireTimeoutMillis: 10000, // Wait up to 10 seconds for a connection
});
export async function getClient() {
// Use pool.connect() instead of creating new clients
return await pool.connect();
}
@ -21,7 +22,7 @@ export async function getClient() {
// Graceful shutdown
process.on('SIGINT', () => {
pool.end(() => {
console.log('Pool has ended');
process.exit(0);
});
});
@ -150,7 +151,7 @@ export async function initDb() {
await client.query(`ALTER TABLE users ALTER COLUMN email DROP NOT NULL`);
await client.query(`ALTER TABLE users ALTER COLUMN password DROP NOT NULL`);
} catch (e) {
console.log("OAuth columns already exist or error adding them:", e.message);
}
} catch (e) {

2
backend/app/utils/mail.js

@ -7,7 +7,7 @@ function getTransporter() {
secure: false,
auth: {
user: process.env.GMAIL_USER,
pass: "yuuu kvoi ytrf blla",
pass: process.env.GMAIL_PASSWORD,
},
});
};

16
backend/app/utils/wait-for-it.sh

@ -1,16 +0,0 @@
#!/bin/bash
hostport="$1"
shift
cmd="$@"
host="${hostport%%:*}"
port="${hostport##*:}"
while ! nc -z "$host" "$port"; do
echo "Waiting for $host:$port..."
sleep 1
done
echo "$host:$port is available. Running command: $cmd"
exec $cmd

14
backend/freetube.sh

@ -1,14 +0,0 @@
#!/usr/bin/env bash
# Script pour exécuter Node.js avec les paramètres passés en ligne de commande
# Récupérer tous les paramètres passés au script
PARAMS="$@"
# Vérifier si des paramètres ont été fournis
if [ $# -eq 0 ]; then
echo "Aucun paramètre fourni."
else
echo "Exécution de Node.js avec les paramètres: $PARAMS"
node /app/tools.js $PARAMS
fi

162
backend/logs/access.log

@ -12072,3 +12072,165 @@
[2025-09-05 17:57:44.224] [undefined] POST(/:id): user not the owner of the playlist with id 1 with status 403
[2025-09-05 17:58:14.088] [undefined] POST(/:id): Video added to playlist with id 2 with status 200
[2025-09-05 17:58:21.531] [undefined] GET(/:id): Playlist retrieved with id 2 with status 200
[2025-09-06 10:11:19.116] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200
[2025-09-06 10:11:56.023] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200
[2025-09-06 10:12:16.248] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200
[2025-09-06 10:12:23.630] [undefined] GET(/:id): failed due to invalid values with status 400
[2025-09-06 10:12:35.862] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200
[2025-09-06 10:13:32.803] [undefined] GET(/:id): try to get channel with id 1
[2025-09-06 10:13:32.809] [undefined] GET(/:id): Successfully get channel with id 1 with status 200
[2025-09-06 10:13:32.824] [undefined] GET(/:id/channel/subscribed): check if user 1 is subscribed to channel 1
[2025-09-06 10:13:32.829] [undefined] GET(/:id/channel/subscribed): user 1 is not subscribed to channel 1 with status 200
[2025-09-06 10:13:58.268] [undefined] POST(/:id/subscribe): try to toggle subscription for channel with id 1
[2025-09-06 10:13:58.276] [undefined] POST(/:id/subscribe): Successfully subscribed to channel with status 200
[2025-09-06 10:14:41.764] [undefined] GET(/:id): try to get channel with id 1
[2025-09-06 10:14:41.775] [undefined] GET(/:id): Successfully get channel with id 1 with status 200
[2025-09-06 10:14:41.789] [undefined] GET(/:id/channel/subscribed): check if user 1 is subscribed to channel 1
[2025-09-06 10:14:41.794] [undefined] GET(/:id/channel/subscribed): user 1 is subscribed to channel 1 with status 200
[2025-09-06 10:14:42.428] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-06 10:14:42.437] [undefined] GET(/:id): try to get video 1
[2025-09-06 10:14:42.447] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-06 10:14:42.457] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-06 10:14:42.464] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-06 10:14:42.479] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-06 10:14:42.484] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-06 10:14:49.450] [undefined] GET(/:id): try to get video 1
[2025-09-06 10:14:49.454] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-06 10:14:49.462] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-06 10:14:49.473] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-06 10:14:49.480] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-06 10:14:49.529] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-06 10:14:49.536] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-06 10:14:51.048] [undefined] GET(/:id): try to get channel with id 1
[2025-09-06 10:14:51.054] [undefined] GET(/:id): Successfully get channel with id 1 with status 200
[2025-09-06 10:14:51.065] [undefined] GET(/:id/channel/subscribed): check if user 1 is subscribed to channel 1
[2025-09-06 10:14:51.071] [undefined] GET(/:id/channel/subscribed): user 1 is subscribed to channel 1 with status 200
[2025-09-06 10:15:00.735] [undefined] GET(/:id): try to get channel with id 1
[2025-09-06 10:15:00.741] [undefined] GET(/:id): Successfully get channel with id 1 with status 200
[2025-09-06 10:15:00.754] [undefined] GET(/:id/channel/subscribed): check if user 1 is subscribed to channel 1
[2025-09-06 10:15:00.759] [undefined] GET(/:id/channel/subscribed): user 1 is subscribed to channel 1 with status 200
[2025-09-06 10:15:01.676] [undefined] GET(/:id): try to get video 1
[2025-09-06 10:15:01.681] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-06 10:15:01.690] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-06 10:15:01.700] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-06 10:15:01.706] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-06 10:15:01.723] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-06 10:15:01.731] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-06 10:15:54.639] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-06 10:15:54.651] [undefined] GET(/:id): try to get video 1
[2025-09-06 10:15:54.658] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-06 10:15:54.669] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-06 10:15:54.676] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-06 10:15:54.696] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-06 10:15:54.704] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-06 10:15:57.359] [undefined] GET(/:id): try to get channel with id 1
[2025-09-06 10:15:57.365] [undefined] GET(/:id): Successfully get channel with id 1 with status 200
[2025-09-06 10:15:57.377] [undefined] GET(/:id/channel/subscribed): check if user 1 is subscribed to channel 1
[2025-09-06 10:15:57.382] [undefined] GET(/:id/channel/subscribed): user 1 is subscribed to channel 1 with status 200
[2025-09-06 10:16:01.637] [undefined] GET(/:id): try to get video 1
[2025-09-06 10:16:01.642] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-06 10:16:01.649] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-06 10:16:01.658] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-06 10:16:01.666] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-06 10:16:01.710] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-06 10:16:01.716] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-06 10:16:04.193] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200
[2025-09-06 10:16:13.098] [undefined] GET(/:id): try to get channel with id 2
[2025-09-06 10:16:13.154] [undefined] GET(/:id): Successfully get channel with id 2 with status 200
[2025-09-06 10:16:13.165] [undefined] GET(/:id/channel/subscribed): check if user 1 is subscribed to channel 2
[2025-09-06 10:16:13.170] [undefined] GET(/:id/channel/subscribed): user 1 is not subscribed to channel 2 with status 200
[2025-09-06 10:19:06.785] [undefined] GET(/:id): try to get channel with id 2
[2025-09-06 10:19:06.798] [undefined] GET(/:id): Successfully get channel with id 2 with status 200
[2025-09-06 10:19:06.814] [undefined] GET(/:id/channel/subscribed): check if user 1 is subscribed to channel 2
[2025-09-06 10:19:06.819] [undefined] GET(/:id/channel/subscribed): user 1 is not subscribed to channel 2 with status 200
[2025-09-06 10:19:15.009] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200
[2025-09-06 10:23:43.105] [undefined] GET(/:id/channel): try to retrieve channel of user 1
[2025-09-06 10:23:43.111] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200
[2025-09-06 10:23:43.117] [undefined] GET(/:id/history): try to retrieve history of user 1
[2025-09-06 10:23:43.124] [undefined] GET(/:id/history): successfully retrieved history of user 1 with status 200
[2025-09-06 10:23:43.129] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-06 10:23:44.349] [undefined] GET(/:id): try to get channel with id 1
[2025-09-06 10:23:44.354] [undefined] GET(/:id/stats): try to get stats
[2025-09-06 10:23:44.359] [undefined] GET(/:id/stats): Successfully get stats with status 200
[2025-09-06 10:23:44.365] [undefined] GET(/:id): Successfully get channel with id 1 with status 200
[2025-09-06 10:23:47.007] [undefined] GET(/:id/history): try to retrieve history of user 1
[2025-09-06 10:23:47.013] [undefined] GET(/:id/channel): try to retrieve channel of user 1
[2025-09-06 10:23:47.066] [undefined] GET(/:id/history): successfully retrieved history of user 1 with status 200
[2025-09-06 10:23:47.070] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200
[2025-09-06 10:23:47.077] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-06 10:23:54.346] [undefined] GET(/:id): try to get channel with id 2
[2025-09-06 10:23:54.352] [undefined] GET(/:id): Successfully get channel with id 2 with status 200
[2025-09-06 10:23:54.364] [undefined] GET(/:id/channel/subscribed): check if user 1 is subscribed to channel 2
[2025-09-06 10:23:54.370] [undefined] GET(/:id/channel/subscribed): user 1 is not subscribed to channel 2 with status 200
[2025-09-08 16:38:02.990] [undefined] POST(/): failed because email already exists with status 400
[2025-09-08 16:38:11.467] [undefined] POST(/): failed because email already exists with status 400
[2025-09-08 16:38:28.340] [undefined] POST(/): failed because email already exists with status 400
[2025-09-08 16:39:29.271] [undefined] POST(/): try to register a user with username: astria and email: sachaguerin.sg@gmail.com
[2025-09-08 16:39:31.245] [undefined] POST(/): successfully registered with status 200
[2025-09-08 16:39:43.524] [undefined] POST(/verify-email): try to verify email for sachaguerin.sg@gmail.com with token 86155
[2025-09-08 16:39:43.543] [undefined] POST(/verify-email): successfully verified email for sachaguerin.sg@gmail.com with status 200
[2025-09-08 16:39:50.092] [undefined] POST(/login): try to login with username 'astria'
[2025-09-08 16:39:50.164] [undefined] POST(/login): Successfully logged in with status 200
[2025-09-08 16:39:50.339] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200
[2025-09-08 16:39:53.065] [undefined] GET(/:id/history): try to retrieve history of user 1
[2025-09-08 16:39:53.069] [undefined] GET(/:id/channel): try to retrieve channel of user 1
[2025-09-08 16:39:53.077] [undefined] GET(/:id/history): failed to retrieve history of user 1 because it doesn't exist with status 404
[2025-09-08 16:39:53.084] [undefined] GET(/:id/channel): failed to retrieve channel of user 1 because it doesn't exist with status 404
[2025-09-08 16:39:53.095] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-08 16:43:38.736] [undefined] POST(/): try to register a user with username: sacha and email: thelolshow974@gmail.com
[2025-09-08 16:43:40.388] [undefined] POST(/): successfully registered with status 200
[2025-09-08 16:43:55.477] [undefined] POST(/verify-email): try to verify email for thelolshow974@gmail.com with token dd574
[2025-09-08 16:43:55.494] [undefined] POST(/verify-email): successfully verified email for thelolshow974@gmail.com with status 200
[2025-09-08 16:44:02.186] [undefined] POST(/login): try to login with username 'sacha'
[2025-09-08 16:44:02.255] [undefined] POST(/login): Successfully logged in with status 200
[2025-09-08 16:44:02.388] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 2 with status 200
[2025-09-08 16:44:04.504] [undefined] GET(/:id/channel): try to retrieve channel of user 2
[2025-09-08 16:44:04.508] [undefined] GET(/:id/channel): failed to retrieve channel of user 2 because it doesn't exist with status 404
[2025-09-08 16:44:04.517] [undefined] GET(/:id/history): try to retrieve history of user 2
[2025-09-08 16:44:04.523] [undefined] GET(/:id/history): failed to retrieve history of user 2 because it doesn't exist with status 404
[2025-09-08 16:44:04.537] [undefined] GET(/user/:id): Playlists retrieved for user with id 2 with status 200
[2025-09-08 16:45:13.811] [undefined] GET(/:id/channel): try to retrieve channel of user 2
[2025-09-08 16:45:13.816] [undefined] GET(/:id/channel): failed to retrieve channel of user 2 because it doesn't exist with status 404
[2025-09-08 16:45:13.824] [undefined] GET(/:id/history): try to retrieve history of user 2
[2025-09-08 16:45:13.829] [undefined] GET(/:id/history): failed to retrieve history of user 2 because it doesn't exist with status 404
[2025-09-08 16:45:13.838] [undefined] GET(/user/:id): Playlists retrieved for user with id 2 with status 200
[2025-09-08 16:46:03.637] [undefined] GET(/:id/channel): try to retrieve channel of user 2
[2025-09-08 16:46:03.643] [undefined] GET(/:id/channel): failed to retrieve channel of user 2 because it doesn't exist with status 404
[2025-09-08 16:46:03.650] [undefined] GET(/:id/history): try to retrieve history of user 2
[2025-09-08 16:46:03.656] [undefined] GET(/:id/history): failed to retrieve history of user 2 because it doesn't exist with status 404
[2025-09-08 16:46:03.666] [undefined] GET(/user/:id): Playlists retrieved for user with id 2 with status 200
[2025-09-08 16:46:17.413] [undefined] GET(/:id/history): try to retrieve history of user 2
[2025-09-08 16:46:17.417] [undefined] GET(/:id/channel): try to retrieve channel of user 2
[2025-09-08 16:46:17.426] [undefined] GET(/:id/channel): failed to retrieve channel of user 2 because it doesn't exist with status 404
[2025-09-08 16:46:17.431] [undefined] GET(/:id/history): failed to retrieve history of user 2 because it doesn't exist with status 404
[2025-09-08 16:46:17.442] [undefined] GET(/user/:id): Playlists retrieved for user with id 2 with status 200
[2025-09-08 16:46:35.561] [undefined] GET(/:id/channel): try to retrieve channel of user 2
[2025-09-08 16:46:35.567] [undefined] GET(/:id/channel): failed to retrieve channel of user 2 because it doesn't exist with status 404
[2025-09-08 16:46:35.576] [undefined] GET(/:id/history): try to retrieve history of user 2
[2025-09-08 16:46:35.583] [undefined] GET(/:id/history): failed to retrieve history of user 2 because it doesn't exist with status 404
[2025-09-08 16:46:35.595] [undefined] GET(/user/:id): Playlists retrieved for user with id 2 with status 200
[2025-09-08 16:47:27.440] [undefined] GET(/:id/channel): try to retrieve channel of user 2
[2025-09-08 16:47:27.445] [undefined] GET(/:id/channel): failed to retrieve channel of user 2 because it doesn't exist with status 404
[2025-09-08 16:47:27.454] [undefined] GET(/:id/history): try to retrieve history of user 2
[2025-09-08 16:47:27.459] [undefined] GET(/:id/history): failed to retrieve history of user 2 because it doesn't exist with status 404
[2025-09-08 16:47:27.469] [undefined] GET(/user/:id): Playlists retrieved for user with id 2 with status 200
[2025-09-08 16:47:49.574] [undefined] POST(/login): try to login with username 'sacha'
[2025-09-08 16:47:49.634] [undefined] POST(/login): Successfully logged in with status 200
[2025-09-08 16:47:49.727] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 2 with status 200
[2025-09-08 16:47:51.469] [undefined] GET(/:id/channel): try to retrieve channel of user 2
[2025-09-08 16:47:51.474] [undefined] GET(/:id/channel): failed to retrieve channel of user 2 because it doesn't exist with status 404
[2025-09-08 16:47:51.482] [undefined] GET(/:id/history): try to retrieve history of user 2
[2025-09-08 16:47:51.492] [undefined] GET(/:id/history): failed to retrieve history of user 2 because it doesn't exist with status 404
[2025-09-08 16:47:51.503] [undefined] GET(/user/:id): Playlists retrieved for user with id 2 with status 200
[2025-09-08 16:48:31.139] [undefined] GET(/:id/channel): try to retrieve channel of user 2
[2025-09-08 16:48:31.145] [undefined] GET(/:id/channel): failed to retrieve channel of user 2 because it doesn't exist with status 404
[2025-09-08 16:48:31.153] [undefined] GET(/:id/history): try to retrieve history of user 2
[2025-09-08 16:48:31.160] [undefined] GET(/user/:id): failed because user doesn't exists with status 404
[2025-09-08 16:48:31.165] [undefined] GET(/:id/history): failed to retrieve history of user 2 because it doesn't exist with status 404
[2025-09-08 16:49:08.966] [undefined] POST(/): try to register a user with username: astria and email: sachaguerin.sg@gmail.com
[2025-09-08 16:49:10.628] [undefined] POST(/): successfully registered with status 200
[2025-09-08 16:49:20.080] [undefined] POST(/verify-email): try to verify email for sachaguerin.sg@gmail.com with token fe65c
[2025-09-08 16:49:20.097] [undefined] POST(/verify-email): successfully verified email for sachaguerin.sg@gmail.com with status 200
[2025-09-08 16:49:30.515] [undefined] POST(/login): try to login with username 'astria'
[2025-09-08 16:49:30.577] [undefined] POST(/login): Successfully logged in with status 200
[2025-09-08 16:49:30.682] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200

1
backend/package.json

@ -5,7 +5,6 @@
"type": "module",
"scripts": {
"dev": "nodemon .",
"test": "vitest",
"start": "node server.js"
},
"keywords": [],

30
backend/requests/channel.http

@ -1,30 +0,0 @@
@token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhc3RyaWEiLCJpYXQiOjE3NTMwNDAxMTh9.GNGee4BYCL-wO3I8FtXXJIc-xsLMfuOoUlBhHjQJcWo
### CREATE CHANNEL
POST http://localhost:8000/api/channels
Authorization: Bearer {{token}}
Content-Type: application/json
{
"name": "Arcanas",
"description": "kljsdfjklsdfjkl",
"owner": 1
}
### GET CHANNEL BY ID
GET http://localhost:8000/api/channels/20
Authorization: Bearer {{token}}
### GET ALL CHANNEL
GET http://localhost:8000/api/channels/
Authorization: Bearer {{token}}
### UPDATE CHANNEL
PUT http://localhost:8000/api/channels/2
Authorization: Bearer {{token}}
Content-Type: application/json
{
"name": "Salut cest cool",
"description": "hjqdfshjklsdqfhjkl"
}

25
backend/requests/comment.http

@ -1,25 +0,0 @@
@token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidXNlcm5hbWUiOiJhc3RyaWEiLCJpYXQiOjE3NTI5NDQxMDF9.dbGCL8qqqLR3e7Ngns-xPfZAvp0WQzAbjaEHjDVg1HI
### POST COMMENT
POST http://127.0.0.1:8000/api/comments/
Content-Type: application/json
Authorization: Bearer {{token}}
{
"content": "Lorem",
"video": 3
}
### GET COMMENT FROM VIDEO ID
GET http://127.0.0.1:8000/api/comments/video/astria
Authorization: Bearer {{token}}
### POST COMMENT
PUT http://127.0.0.1:8000/api/comments/1
Content-Type: application/json
Authorization: Bearer {{token}}
{
"content": "fsd",
"video": 14
}

11
backend/requests/medias.http

@ -1,11 +0,0 @@
### GET PROFILE PICTURE (through nginx)
GET http://localhost/api/media/profile/sacha.jpg
### GET PROFILE PICTURE (direct backend - for testing)
GET http://localhost:8000/api/media/profile/sacha.jpg
### GET THUMBNAIL (through nginx)
GET http://localhost/api/media/thumbnail/E90B982DE9C5112C.jpg
### GET THUMBNAIL (direct backend - for testing)
GET http://localhost:8000/api/media/thumbnail/E90B982DE9C5112C.jpg

11
backend/requests/playlist.http

@ -1,11 +0,0 @@
@token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhc3RyaWEiLCJpYXQiOjE3NTMwNDAxMTh9.GNGee4BYCL-wO3I8FtXXJIc-xsLMfuOoUlBhHjQJcWo
### ADD PLAYLIST
POST http://localhost/api/playlists
Authorization: Bearer {{token}}
Content-Type: application/json
{
"name": "A regarder plus tard",
"owner": 1
}

5
backend/requests/recommendation.http

@ -1,5 +0,0 @@
### GET NON-AUTHENTICATED RECOMMENDATIONS
GET http://localhost:8000/api/recommendations/
### GET TRENDING VIDS
GET http://localhost:8000/api/recommendations/trending/

17
backend/requests/top-tags-videos.http

@ -1,17 +0,0 @@
### Get all videos from the three most watched tags
GET http://127.0.0.1:8000/api/videos/top-tags/videos
### Alternative localhost URL
GET http://localhost:8000/api/videos/top-tags/videos
### With frontend URL (if using nginx proxy)
GET http://localhost/api/videos/top-tags/videos
###
# This endpoint returns:
# - topTags: The 3 most used tags with their usage count
# - videos: All public videos that have any of these top 3 tags
# - totalVideos: Count of videos returned
#
# Videos are ordered by popularity score (calculated from views, likes, comments)
# and then by release date (newest first)

49
backend/requests/user.http

@ -1,49 +0,0 @@
@token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhc3RyaWEiLCJpYXQiOjE3NTMwNDAxMTh9.GNGee4BYCL-wO3I8FtXXJIc-xsLMfuOoUlBhHjQJcWo
### CREATE USER
POST http://localhost:8000/api/users/
Content-Type: application/json
{
"email": "sacha@gmail.com",
"username": "astria",
"password": "Test_974",
"picture": "null"
}
### LOGIN USER
POST http://localhost:8000/api/users/login
Content-Type: application/json
{
"username": "astria",
"password": "Test1234!"
}
### GET USER BY ID
GET http://localhost:8000/api/users/astria
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MywidXNlcm5hbWUiOiJhc3RyaWEiLCJpYXQiOjE3NTE3NDYxMzJ9.i0MPfB7IJbMGNnaktV0OlxoCRs6ujKtVmSmlxLa2eNY
### GET USER BY USERNAME
GET http://localhost:8000/api/users/username/astria
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MywidXNlcm5hbWUiOiJhc3RyaWEiLCJpYXQiOjE3NTE3NDYxMzJ9.i0MPfB7IJbMGNnaktV0OlxoCRs6ujKtVmSmlxLa2eNY
### UPDATE USER
PUT http://localhost:8000/api/users/5
Authorization: Bearer {{token}}
Content-Type: application/json
{
"email": "test@test.com",
"username": "test",
"password": "Rwqfsfasxc_974",
"picture": "null"
}
### DELETE USER
DELETE http://localhost:8000/api/users/4
Authorization: Bearer {{token}}
### GET HISTORY
GET http://localhost:8000/api/users/1/history/
Authorization: Bearer {{token}}

33
backend/requests/video.http

@ -1,33 +0,0 @@
@token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhc3RyaWEiLCJpYXQiOjE3NTMzODAyNjB9._rUcieo3acJp6tjQao7V3UQz0_ngHuB2z36_fG_fIX8
### UPDATE VIDEO
PUT http://127.0.0.1:8000/api/videos/3
Content-Type: application/json
Authorization: Bearer {{token}}
{
"title": " I built a REDSTONE FACTORY in Minecraft Create Mod! [#28] ",
"description": "I've been struggling for Redstone, so it's time we make a Crete Mod 6 redstone farm! This does mean we need to also make a cobble generator, netherwart farm and blaze farm to reach our goals, all in one new victorian-era-steampunky-type-brick buiilding.",
"visibility": "public"
}
### TOGGLE LIKE
GET http://127.0.0.1:8000/api/videos/14/like
Authorization: Bearer {{token}}
### ADD TAGS
PUT http://127.0.0.1:8000/api/videos/3/tags
Content-Type: application/json
Authorization: Bearer {{token}}
{
"tags": [
"Minecraft",
"Create Mod",
"Redstone"
],
"channel": 1
}
###
GET http://localhost/api/search?q=minecraft&type=videos&offset=0&limit=10

9
backend/server.js

@ -22,7 +22,7 @@ import { Strategy as GitHubStrategy } from "passport-github2";
console.clear();
dotenv.config();
console.log(process.env)
const app = express();
@ -91,15 +91,16 @@ app.use("/api/media", MediaRoutes);
app.use("/api/search", SearchRoute);
app.use("/api/oauth", OAuthRoute);
const port = process.env.PORT;
const port = process.env.BACKEND_PORT;
if (process.env.NODE_ENV !== "test") {
const server = http.createServer(app);
server.listen(port, '0.0.0.0', async () => {
console.log("Server's listening on port " + port);
console.log("Initializing database...");
console.log(`Server started on port ${port}`);
await initDb();
console.log("Database initialized successfully.");
});
}

190
backend/test/channel.test.js

@ -1,190 +0,0 @@
import vitest, {describe} from "vitest";
import app from "../server.js";
import request from "supertest";
const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NSwidXNlcm5hbWUiOiJ0ZXN0IiwiaWF0IjoxNzUxNzk2MjM2fQ.XmaVA_NQcpW7fxRtDWOinMyQXPaFixpp3ib_mzo6M6c"
describe("CREATE CHANNEL", async function() {
it("Should return 401 if token isn't valid", async function() {
const channel = {
"name": "Astria",
"description": "A channel already exists",
"owner": 5
}
const req = await request(app).post("/api/channels/").send(channel);
expect(req.statusCode).toBe(401);
})
it("Should return 400 if owner is not number", async function() {
const channel = {
"name": "Machin",
"description": "kljsdfjklsdfjkl",
"owner": "astria"
}
const req = await request(app).post("/api/channels/").send(channel).set("Authorization", "Bearer " + token);
expect(req.statusCode).toBe(400);
})
it("Should return 404 if owner don't exist", async function() {
const channel = {
"name": "Machin",
"description": "kljsdfjklsdfjkl",
"owner": 55
}
const req = await request(app).post("/api/channels/").send(channel).set("Authorization", "Bearer " + token);
expect(req.statusCode).toBe(404);
})
it("Should return 403 if user isn't owner of owner", async function() {
const channel = {
"name": "Machin",
"description": "kljsdfjklsdfjkl",
"owner": 3
}
const req = await request(app).post("/api/channels/").send(channel).set("Authorization", "Bearer " + token);
expect(req.statusCode).toBe(403);
})
it("Should return 400 if channel name already exists", async function() {
const channel = {
"name": "Sacha",
"description": "A channel already exists",
"owner": 5
}
const req = await request(app).post("/api/channels/").send(channel).set("Authorization", "Bearer " + token);
expect(req.statusCode).toBe(400);
})
it("Should return 400 if channel name is empty", async function() {
const channel = {
"description": "A channel already exists",
"owner": 5
}
const req = await request(app).post("/api/channels/").send(channel).set("Authorization", "Bearer " + token);
expect(req.statusCode).toBe(400);
})
it("Should return 400 if owner is empty", async function() {
const channel = {
"name": "Astria",
"description": "A channel already exists",
}
const req = await request(app).post("/api/channels/").send(channel).set("Authorization", "Bearer " + token);
expect(req.statusCode).toBe(400);
})
})
describe("GET CHANNEL BY ID", async function() {
it("Should return 401 if token is not valid", async function() {
const req = await request(app).get("/api/channels/2").send();
expect(req.statusCode).toBe(401);
})
it("Should return 400 if id is not number", async function() {
const req = await request(app).get("/api/channels/sacha").send().set("Authorization", "Bearer " + token);
expect(req.statusCode).toBe(400);
})
it("Should return 404 if channel does not exist", async function() {
const req = await request(app).get("/api/channels/200").send().set("Authorization", "Bearer " + token);
expect(req.statusCode).toBe(404);
})
it("Should return 200 if OK", async function() {
const req = await request(app).get("/api/channels/2").send().set("Authorization", "Bearer " + token);
expect(req.statusCode).toBe(200);
})
})
describe("GET ALL CHANNELS", async function() {
it("Should be 401 if token is not valid", async function() {
const req = await request(app).get("/api/channels/").send();
expect(req.statusCode).toBe(401);
})
it("Should return 200 if OK", async function() {
const req = await request(app).get("/api/channels/").send().set("Authorization", "Bearer " + token);
expect(req.statusCode).toBe(200);
})
})
describe("UPDATE CHANNEL", async function() {
it("Should be 401 if token is not valid", async function() {
const channel = {
"name": "Machin",
"description": "kljsdfjklsdfjkl",
}
const req = await request(app).put("/api/channels/2").send(channel);
expect(req.statusCode).toBe(401);
})
it("Should return 400 if name is empty", async function() {
const channel = {
"name": null,
"description": "A channel already exists",
}
const req = await request(app).put("/api/channels/2").send(channel).set("Authorization", "Bearer " + token);
expect(req.statusCode).toBe(400);
})
it("Should return 404 if channel does not exist", async function() {
const channel = {
"name": "Machin",
"description": "kljsdfjklsdfjkl",
}
const req = await request(app).put("/api/channels/404").send(channel).set("Authorization", "Bearer " + token);
expect(req.statusCode).toBe(404);
})
it("Should return 403 if user is not the owner of the channel", async function() {
const channel = {
"name": "Machin",
"description": "kljsdfjklsdfjkl",
}
const req = await request(app).put("/api/channels/3").send(channel).set("Authorization", "Bearer " + token);
expect(req.statusCode).toBe(403);
})
it("Should return 400 if channel name is changed but already used", async function() {
const channel = {
"name": "Astria",
"description": "kljsdfjklsdfjkl",
}
const req = await request(app).put("/api/channels/2").send(channel).set("Authorization", "Bearer " + token);
expect(req.statusCode).toBe(400);
})
})
describe("DELETE CHANNEL", async function() {
it("Should be 401 if token is not valid", async function() {
const req = await request(app).del("/api/channels/2").send();
expect(req.statusCode).toBe(401);
})
it("Should return 400 if id is not number", async function() {
const req = await request(app).del("/api/channels/astria").send().set("Authorization", "Bearer " + token);
expect(req.statusCode).toBe(400);
})
it("Should return 404 if channel does not exist", async function() {
const req = await request(app).del("/api/channels/404").send().set("Authorization", "Bearer " + token);
expect(req.statusCode).toBe(404);
})
it("Should return 403 if user is not the owner of the channel", async function() {
const req = await request(app).del("/api/channels/3").send().set("Authorization", "Bearer " + token);
expect(req.statusCode).toBe(403);
})
})
describe("TOGGLE SUBSCRIPTION", async function() {
it("Should return 401 if token is not valid", async function() {
const req = await request(app).post("/api/channels/2/subscribe").send();
expect(req.statusCode).toBe(401);
})
it("Should return 400 if id is not number", async function() {
const req = await request(app).post("/api/channels/sacha/subscribe").send().set("Authorization", "Bearer " + token);
expect(req.statusCode).toBe(400);
})
it("Should return 404 if channel does not exist", async function() {
const req = await request(app).post("/api/channels/404/subscribe").send().set("Authorization", "Bearer " + token);
expect(req.statusCode).toBe(404);
})
it("Should return 200 if OK", async function() {
const body = {
"userId": 5
}
const req = await request(app).post("/api/channels/2/subscribe").send(body).set("Authorization", "Bearer " + token);
expect(req.statusCode).toBe(200);
})
})

175
backend/test/comment.test.js

@ -1,175 +0,0 @@
import app from "../server.js";
import request from "supertest";
const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NSwidXNlcm5hbWUiOiJ0ZXN0IiwiaWF0IjoxNzUxNzk2MjM2fQ.XmaVA_NQcpW7fxRtDWOinMyQXPaFixpp3ib_mzo6M6c"
const videoId = 14
const commentId = 1
describe("POST COMMENT", async function() {
it("Should return 401 if token is missing", async function() {
const comment = {
"content": "Lorem ipsum dolor sit amet",
"video": videoId
}
const req = await request(app).post("/api/comments/").send(comment);
expect(req.status).toBe(401);
})
it("Should return 400 if video is missing", async function() {
const comment = {
"content": "Lorem ipsum dolor sit amet",
}
const req = await request(app).post("/api/comments/").send(comment).set("Authorization", "Bearer " + token);
expect(req.status).toBe(400);
})
it("Should return 400 if content is missing", async function() {
const comment = {
'video': videoId
}
const req = await request(app).post("/api/comments/").send(comment).set("Authorization", "Bearer " + token);
expect(req.status).toBe(400);
})
it("Should return 400 if video is not number", async function() {
const comment = {
"content": "Lorem ipsum dolor sit amet",
"video": "lorem"
}
const req = await request(app).post("/api/comments/").send(comment).set("Authorization", "Bearer " + token);
expect(req.status).toBe(400);
})
it("Should return 404 if video doesn't exist", async function() {
const comment = {
"content": "Lorem ipsum dolor sit amet",
"author": 5,
"video": 404
}
const req = await request(app).post("/api/comments/").send(comment).set("Authorization", "Bearer " + token);
expect(req.status).toBe(404);
})
})
describe("GET COMMENTS FROM VIDEO ID", async function() {
it("Should return 401 if token is missing", async function() {
const req = await request(app).get("/api/comments/video/" + videoId).send();
expect(req.status).toBe(401);
})
it("Should return 404 if video doesn't exist", async function() {
const req = await request(app).get("/api/comments/video/404").send().set("Authorization", "Bearer " + token)
expect(req.status).toBe(404);
})
it("Should return 200 if OK", async function() {
const req = await request(app).get("/api/comments/video/" + videoId).send().set("Authorization", "Bearer " + token)
expect(req.status).toBe(200);
})
})
describe("GET COMMENT BY ID", async function() {
it("Should return 401 if token is missing", async function() {
const req = await request(app).get("/api/comments/1").send()
expect(req.status).toBe(401);
})
it("Should return 400 if id is not number", async function() {
const req = await request(app).get("/api/comments/sacha").send().set("Authorization", "Bearer " + token)
expect(req.status).toBe(400);
})
it("Should return 404 if comment doesn't exist", async function() {
const req = await request(app).get("/api/comments/404").send().set("Authorization", "Bearer " + token)
expect(req.status).toBe(404);
})
it("Should return 200 if OK", async function() {
const req = await request(app).get("/api/comments/1").send().set("Authorization", "Bearer " + token)
expect(req.status).toBe(200);
})
})
describe("UPDATE COMMENT", async function() {
it("Should return 401 if token is missing", async function() {
const comment = {
"content": "Lorem ipsum dolor sit amet",
"video": videoId
}
const req = await request(app).put("/api/comments/" + commentId).send(comment);
expect(req.status).toBe(401);
})
it("Should return 400 if video is missing", async function() {
const comment = {
"content": "Lorem ipsum dolor sit amet",
}
const req = await request(app).put("/api/comments/" + commentId).send(comment).set("Authorization", "Bearer " + token);
expect(req.status).toBe(400);
})
it("Should return 400 if content is missing", async function() {
const comment = {
'video': videoId
}
const req = await request(app).put("/api/comments/" + commentId).send(comment).set("Authorization", "Bearer " + token);
expect(req.status).toBe(400);
})
it("Should return 400 if video is not number", async function() {
const comment = {
"content": "Lorem ipsum dolor sit amet",
"video": "lorem"
}
const req = await request(app).put("/api/comments/" + commentId).send(comment).set("Authorization", "Bearer " + token);
expect(req.status).toBe(400);
})
it("Should return 403 if user is not the author", async function() {
const comment = {
"content": "Lorem ipsum dolor sit amet",
"video": videoId
}
const req = await request(app).put("/api/comments/2").send(comment).set("Authorization", "Bearer " + token);
expect(req.status).toBe(403);
})
it("Should return 404 if comment doesn't exist", async function() {
const comment = {
"content": "Lorem ipsum dolor sit amet",
"video": videoId
}
const req = await request(app).put("/api/comments/12").send(comment).set("Authorization", "Bearer " + token);
expect(req.status).toBe(404);
})
it("Should return 404 if video doesn't exist", async function() {
const comment = {
"content": "Lorem ipsum dolor sit amet",
"author": 5,
"video": 404
}
const req = await request(app).put("/api/comments/" + commentId).send(comment).set("Authorization", "Bearer " + token);
expect(req.status).toBe(404);
})
})
describe("DELETE COMMENT", async function() {
it("Should return 401 if token is missing", async function() {
const req = await request(app).del("/api/comments/" + commentId).send();
expect(req.status).toBe(401);
})
it("Should return 403 if user is not the author", async function() {
const req = await request(app).del("/api/comments/2").send();
expect(req.status).toBe(401);
})
it("Should return 404 if comment doesn't exist", async function() {
const req = await request(app).del("/api/comments/404" + commentId).send().set("Authorization", "Bearer " + token);
expect(req.status).toBe(404);
})
})

231
backend/test/playlist.test.js

@ -1,231 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../server.js';
const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NSwidXNlcm5hbWUiOiJ0ZXN0IiwiaWF0IjoxNzUxODA5OTA3fQ.uCJFysaOiXC8qZKykWLi58WDcItKSkQPUIZ7kH-87Tg";
const playlistId = 1;
const playlistId2 = 3;
const videoId = 14;
const falseVideoId = 404;
const falseplaylistId = 404;
const userId = 5;
describe('CREATE PLAYLIST', () => {
it("Should return 401 if token is missing", async () => {
const playlist = {
"name": "Playlist Test",
}
const req = await request(app).post("/api/playlists").send(playlist);
expect(req.statusCode).toBe(401);
})
it("Should return 400 if name is missing", async () => {
const playlist = {}
const req = await request(app).post("/api/playlists").set("Authorization", `Bearer ${token}`).send(playlist);
expect(req.statusCode).toBe(400);
})
it("Should return 400 if name is not alphanumeric", async () => {
const playlist = {
"name": "Playlist Test !@#"
}
const req = await request(app).post("/api/playlists").set("Authorization", `Bearer ${token}`).send(playlist);
expect(req.statusCode).toBe(400);
})
it("Should return 200 if playlist is created", async () => {
const playlist = {
"name": "Playlist Test",
}
const req = await request(app).post("/api/playlists").set("Authorization", `Bearer ${token}`).send(playlist);
expect(req.statusCode).toBe(200);
})
});
describe('ADD VIDEO TO PLAYLIST', () => {
it("Should return 401 if token is missing", async () => {
const req = await request(app).post(`/api/playlists/${playlistId}`).send({ video: videoId });
expect(req.statusCode).toBe(401);
})
it("Should return 404 if playlist does not exist", async () => {
const req = await request(app).post(`/api/playlists/${falseplaylistId}`).set("Authorization", `Bearer ${token}`).send({ video: videoId });
console.log(req.body);
expect(req.statusCode).toBe(404);
})
it("Should return 403 if user is not the owner of the playlist", async () => {
const req = await request(app).post(`/api/playlists/${playlistId2}`).set("Authorization", `Bearer ${token}`).send({ video: videoId });
expect(req.statusCode).toBe(403);
})
it("Should return 400 if video id is missing", async () => {
const req = await request(app).post(`/api/playlists/${playlistId}`).set("Authorization", `Bearer ${token}`).send({});
expect(req.statusCode).toBe(400);
})
it("Should return 400 if video id is not numeric", async () => {
const req = await request(app).post(`/api/playlists/${playlistId}`).set("Authorization", `Bearer ${token}`).send({ video: "test" });
expect(req.statusCode).toBe(400);
})
it("Should return 404 if video does not exist", async () => {
const req = await request(app).post(`/api/playlists/${playlistId}`).set("Authorization", `Bearer ${token}`).send({ video: falseVideoId });
expect(req.statusCode).toBe(404);
})
it("Should return 200 if video is added to playlist", async () => {
const req = await request(app).post(`/api/playlists/${playlistId}`).set("Authorization", `Bearer ${token}`).send({ video: videoId });
expect(req.statusCode).toBe(200);
})
});
describe('GET PLAYLIST BY USER', () => {
it("Should return 401 if token is missing", async () => {
const req = await request(app).get("/api/playlists/user/" + userId);
expect(req.statusCode).toBe(401);
})
it("Should return 400 if user id is not numeric", async () => {
const req = await request(app).get("/api/playlists/user/test").set("Authorization", `Bearer ${token}`);
expect(req.statusCode).toBe(400);
})
it("Should return 404 if user does not exist", async () => {
const req = await request(app).get("/api/playlists/user/404").set("Authorization", `Bearer ${token}`);
expect(req.statusCode).toBe(404);
})
it("Should return 403 if user is not the owner of the account", async () => {
const req = await request(app).get("/api/playlists/user/" + 6).set("Authorization", `Bearer ${token}`);
expect(req.statusCode).toBe(403);
})
it("Should return 200 if playlists are found for user", async () => {
const req = await request(app).get("/api/playlists/user/" + 5).set("Authorization", `Bearer ${token}`);
expect(req.statusCode).toBe(200);
})
});
describe('GET PLAYLIST BY ID', () => {
it("Should return 401 if token is missing", async ()=> {
const req = await request(app).get(`/api/playlists/${playlistId}`);
expect(req.statusCode).toBe(401);
})
it("Should return 400 if playlist id is not numeric", async () => {
const req = await request(app).get("/api/playlists/test").set("Authorization", `Bearer ${token}`);
expect(req.statusCode).toBe(400);
})
it("Should return 404 if playlist does not exist", async () => {
const req = await request(app).get(`/api/playlists/${falseplaylistId}`).set("Authorization", `Bearer ${token}`);
expect(req.statusCode).toBe(404);
})
it("Should return 403 if user is not the owner of the playlist", async () => {
const req = await request(app).get(`/api/playlists/${playlistId2}`).set("Authorization", `Bearer ${token}`);
expect(req.statusCode).toBe(403);
})
it("Should return 200 if playlist is found", async () => {
const req = await request(app).get(`/api/playlists/${playlistId}`).set("Authorization", `Bearer ${token}`);
expect(req.statusCode).toBe(200);
})
})
describe("UPDATE PLAYLIST", () => {
it("Should return 401 if token is missing", async () => {
const req = await request(app).put(`/api/playlists/${playlistId}`).send({ name: "New Playlist Name" });
expect(req.statusCode).toBe(401);
})
it("Should return 400 if playlist id is not numeric", async () => {
const req = await request(app).put("/api/playlists/test").set("Authorization", `Bearer ${token}`).send({ name: "New Playlist Name" });
expect(req.statusCode).toBe(400);
})
it("Should return 404 if playlist does not exist", async () => {
const req = await request(app).put(`/api/playlists/${falseplaylistId}`).set("Authorization", `Bearer ${token}`).send({ name: "New Playlist Name" });
expect(req.statusCode).toBe(404);
})
it("Should return 403 if user is not the owner of the playlist", async () => {
const req = await request(app).put(`/api/playlists/${playlistId2}`).set("Authorization", `Bearer ${token}`).send({ name: "New Playlist Name" });
expect(req.statusCode).toBe(403);
})
it("Should return 400 if name is missing", async () => {
const req = await request(app).put(`/api/playlists/${playlistId}`).set("Authorization", `Bearer ${token}`).send({});
expect(req.statusCode).toBe(400);
})
it("Should return 400 if name is not alphanumeric", async () => {
const req = await request(app).put(`/api/playlists/${playlistId}`).set("Authorization", `Bearer ${token}`).send({ name: "New Playlist Name !@#" });
expect(req.statusCode).toBe(400);
})
it("Should return 200 if playlist is updated", async () => {
const req = await request(app).put(`/api/playlists/${playlistId}`).set("Authorization", `Bearer ${token}`).send({ name: "Updated Playlist Name" });
expect(req.statusCode).toBe(200);
})
})
describe("DELETE VIDEO FROM PLAYLIST", () => {
it("Should return 401 if token is missing", async () => {
const req = await request(app).delete(`/api/playlists/${playlistId}/video/${videoId}`);
expect(req.statusCode).toBe(401);
})
it("Should return 400 if playlist id is not numeric", async () => {
const req = await request(app).delete("/api/playlists/test/video/14").set("Authorization", `Bearer ${token}`);
expect(req.statusCode).toBe(400);
})
it("Should return 400 if video id is not numeric", async () => {
const req = await request(app).delete(`/api/playlists/${playlistId}/video/test`).set("Authorization", `Bearer ${token}`);
expect(req.statusCode).toBe(400);
})
it("Should return 404 if playlist does not exist", async () => {
const req = await request(app).delete(`/api/playlists/${falseplaylistId}/video/${videoId}`).set("Authorization", `Bearer ${token}`);
expect(req.statusCode).toBe(404);
})
it("Should return 403 if user is not the owner of the playlist", async () => {
const req = await request(app).delete(`/api/playlists/${playlistId2}/video/${videoId}`).set("Authorization", `Bearer ${token}`);
expect(req.statusCode).toBe(403);
})
it("Should return 404 if video is not in the playlist", async () => {
const req = await request(app).delete(`/api/playlists/${playlistId}/video/${falseVideoId}`).set("Authorization", `Bearer ${token}`);
expect(req.statusCode).toBe(404);
})
it("Should return 200 if video is deleted from playlist", async () => {
const req = await request(app).delete(`/api/playlists/${playlistId}/video/${videoId}`).set("Authorization", `Bearer ${token}`);
expect(req.statusCode).toBe(200);
})
})
describe("DELETE PLAYLIST", () => {
it("Should return 401 if token is missing", async () => {
const req = await request(app).delete(`/api/playlists/${playlistId}`);
expect(req.statusCode).toBe(401);
})
it("Should return 400 if playlist id is not numeric", async () => {
const req = await request(app).delete("/api/playlists/test").set("Authorization", `Bearer ${token}`);
expect(req.statusCode).toBe(400);
})
it("Should return 404 if playlist does not exist", async () => {
const req = await request(app).delete(`/api/playlists/${falseplaylistId}`).set("Authorization", `Bearer ${token}`);
expect(req.statusCode).toBe(404);
})
it("Should return 403 if user is not the owner of the playlist", async () => {
const req = await request(app).delete(`/api/playlists/${playlistId2}`).set("Authorization", `Bearer ${token}`);
expect(req.statusCode).toBe(403);
})
})

413
backend/test/user.test.js

@ -1,413 +0,0 @@
import { describe, it, expect } from 'vitest';
import app from '../server.js';
import request from 'supertest';
const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NSwidXNlcm5hbWUiOiJ0ZXN0IiwiaWF0IjoxNzUxNzk2MjM2fQ.XmaVA_NQcpW7fxRtDWOinMyQXPaFixpp3ib_mzo6M6c"
describe('USER REGISTER', () => {
it('should return 400 when password is too short', async () => {
const user = {
email: "sachaguerin@gmail.com",
username: "sachaguerin",
password: "Toor!9",
picture: "null"
};
const res = await request(app)
.post('/api/users')
.send(user);
expect(res.status).toBe(400);
});
it('should return 400 when password is too long', async () => {
const user = {
email: "sachaguerin",
username: "sachaguerin",
password: "Toor!95555555555555555555555555555555555555555555555555555555",
picture: "null"
}
const res = await request(app)
.post("/api/users")
.send(user);
expect(res.status).toBe(400);
});
it('should return 400 when password don\'t have capital letters', async () => {
const user = {
email: "sachaguerin",
username: "sachaguerin",
password: "toooooooooor_974",
picture: "null"
}
const res = await request(app).post('/api/users').send(user);
expect(res.status).toBe(400);
});
it('should return 400 when password don\'t have lowercase letters', async () => {
const user = {
email: "sachaguerin",
username: "sachaguerin",
password: "TOOOOOOOOOOOOOOOOOOOORRR_974",
picture: "null"
}
const res = await request(app).post('/api/users').send(user);
expect(res.status).toBe(400);
});
it('should return 400 when password don\'t have symbols', async () => {
const user = {
email: "sachaguerin",
username: "sachaguerin",
password: "Rwqfsfasxc974",
picture: "null"
}
const res = await request(app).post('/api/users').send(user);
expect(res.status).toBe(400);
});
it("Should return 400 if email is invalid", async () => {
const user = {
email: "sachaguerin",
username: "sachaguerin",
password: "Rwqfsfasxc_974",
picture: "null"
}
const res = await request(app).post('/api/users').send(user);
expect(res.status).toBe(400);
})
it("Should return 400 if username isn't string", async () => {
const user = {
email: "sachaguerin",
username: 48,
password: "Rwqfsfasxc_974",
picture: "null"
}
const res = await request(app).post('/api/users').send(user);
expect(res.status).toBe(400);
})
it("Should return 400 if email is already used", async () => {
const user = {
email: "sachaguerin.sg@gmail.com",
username: "sachaguerin",
password: "Rwqfsfasxc_974",
picture: "null"
}
const res = await request(app).post('/api/users').send(user);
expect(res.status).toBe(400);
})
it("Should return 400 if username is already used", async () => {
const user = {
email: "sachaguerin@gmail.com",
username: "astria",
password: "Rwqfsfasxc_974",
picture: "null"
}
const res = await request(app).post('/api/users').send(user);
expect(res.status).toBe(400);
})
it("Should return 400 if email isn't provided", async () => {
const user = {
username: "sachaguerin",
password: "Rwqfsfasxc_974",
picture: "null"
}
const res = await request(app).post('/api/users').send(user);
expect(res.status).toBe(400);
})
it("Should return 400 if username isn't provided", async () => {
const user = {
email: "sachaguerin@gmail.com",
password: "Rwqfsfasxc_974",
picture: "null"
}
const res = await request(app).post('/api/users').send(user);
expect(res.status).toBe(400);
})
it("Should return 400 if password isn't provided", async () => {
const user = {
email: "sachaguerin@gmail.com",
username: "sachaguerin",
picture: "null"
}
const res = await request(app).post('/api/users').send(user);
expect(res.status).toBe(400);
})
it("Should return 400 if picture isn't provided", async () => {
const user = {
email: "sachaguerin@gmail.com",
username: "sachaguerin",
password: "Rwqfsfasxc_974",
}
const res = await request(app).post('/api/users').send(user);
expect(res.status).toBe(400);
})
});
describe('USER LOGIN', () => {
it('Should return 401 if account doesn\'t exist', async () => {
const user = {
username: "sachaguerin",
password: "Rwqfsfasxc__974",
}
const req = await request(app).post('/api/users/login').send(user);
expect(req.status).toBe(401);
})
it('Should return 401 if password is not valid', async () => {
const user = {
username: "astria",
password: "Rwqfsfasxc__974",
}
const req = await request(app).post('/api/users/login').send(user);
expect(req.status).toBe(401);
})
it("Should return 400 if username isn't provided", async () => {
const user = {
password: "Rwqfsfasxc_974",
}
const res = await request(app).post('/api/users/login').send(user);
expect(res.status).toBe(400);
})
it("Should return 400 if password isn't provided", async () => {
const user = {
username: "astria",
}
const res = await request(app).post('/api/users/login').send(user);
expect(res.status).toBe(400);
})
it("Should return 200 if OK", async () => {
const user = {
username: "astria",
password: "Rwqfsfasxc_974",
}
const res = await request(app).post('/api/users/login').send(user);
expect(res.status).toBe(200);
})
})
describe('GET USER BY ID', async () => {
it("Should return 401 if token is not valid", async () => {
const res = await request(app).get("/api/users/5").send();
expect(res.status).toBe(401);
})
it("Should return 404 if user doesn\'t exist", async () => {
const res = await request(app).get("/api/users/3333").send().set("Authorization", "Bearer " + token);
expect(res.status).toBe(404);
})
it("Should return 400 if id is not integer", async () => {
const res = await request(app).get("/api/users/sacha").send().set("Authorization", "Bearer " + token);
expect(res.status).toBe(400);
})
it("Should return 200 if OK", async () => {
const res = await request(app).get("/api/users/3").send().set("Authorization", "Bearer " + token);
expect(res.status).toBe(200);
})
})
describe('GET USER BY USERNAME', async () => {
it("Should return 401 if token is not valid", async () => {
const req = await request(app).get("/api/users/username/astria").send();
expect(req.status).toBe(401);
})
it("Should return 404 if user doesn\'t exist", async () => {
const req = await request(app).get("/api/users/username/congolexicomatision").send().set("Authorization", "Bearer " + token);
expect(req.status).toBe(404);
})
it("Should return 200 if OK", async () => {
const req = await request(app).get("/api/users/username/astria").send().set("Authorization", "Bearer " + token);
expect(req.status).toBe(200);
})
})
describe('UPDATE USER', () => {
it("Should return 401 if token is not valid", async () => {
const user = {
email: "sachaguerin@gmail.com",
username: "sacha",
password: "Rwqfsfasxc__974",
picture: "Sachaguerin",
}
const res = await request(app).put('/api/users/3').send(user);
expect(res.status).toBe(401);
})
it('should return 400 when password is too short', async () => {
const user = {
email: "test@test.com",
username: "test",
password: "Toor!9",
picture: "null"
};
const res = await request(app)
.put('/api/users/5')
.send(user)
.set("Authorization", "Bearer " + token);
expect(res.status).toBe(400);
});
it('should return 400 when password don\'t have capital letters', async () => {
const user = {
email: "test@test.com",
username: "test",
password: "toooooooooor_974",
picture: "null"
}
const res = await request(app).put('/api/users/5').send(user).set("Authorization", "Bearer " + token);
expect(res.status).toBe(400);
});
it('should return 400 when password don\'t have lowercase letters', async () => {
const user = {
email: "test@test.com",
username: "test",
password: "TOOOOOOOOOOOOOOOOOOOORRR_974",
picture: "null"
}
const res = await request(app).put('/api/users/5').send(user).set("Authorization", "Bearer " + token);
expect(res.status).toBe(400);
});
it('should return 400 when password don\'t have symbols', async () => {
const user = {
email: "test@test.com",
username: "test",
password: "Rwqfsfasxc974",
picture: "null"
}
const res = await request(app).put('/api/users/5').send(user).set("Authorization", "Bearer " + token);
expect(res.status).toBe(400);
});
it("Should return 400 if email is invalid", async () => {
const user = {
email: "sachaguerin",
username: "test",
password: "Rwqfsfasxc_974",
picture: "null"
}
const res = await request(app).put('/api/users/5').send(user).set("Authorization", "Bearer " + token);
expect(res.status).toBe(400);
})
it("Should return 403 if the user is not the owner", async () => {
const user = {
email: "test@test.com",
username: "test",
password: "Rwqfsfasxc_974",
picture: "null"
}
const res = await request(app).put('/api/users/3').send(user).set("Authorization", "Bearer " + token);
expect(res.status).toBe(403);
})
it("Should return 400 if mail change but already taken", async () => {
const user = {
email: "sachaguerin.sg@gmail.com",
username: "test",
picture: "null",
password: "Test_974"
}
const res = await request(app).put('/api/users/5').send(user).set("Authorization", "Bearer " + token);
expect(res.status).toBe(400);
})
it("Should return 400 if username changed but already taken", async () => {
const user = {
email: "test@test.com",
username: "astria",
password: "Test_974",
picture: "null"
}
const res = await request(app).put('/api/users/5').send(user).set("Authorization", "Bearer " + token);
expect(res.status).toBe(400);
})
it("Should return 404 if user doesn't exist", async () => {
const user = {
email: "test@test.com",
username: "test",
password: "Rwqfsfasxc_974",
picture: "null"
}
const res = await request(app).put('/api/users/666').send(user).set("Authorization", "Bearer " + token);
expect(res.status).toBe(404);
})
})
describe ('DELETE USER', async function () {
it("Should return 401 if token is not valid", async () => {
const req = await request(app).del('/api/users/5').send();
expect(req.status).toBe(401);
})
it ("Should return 403 if user isn't the owner", async () => {
const req = await request(app).del('/api/users/4').send().set("Authorization", "Bearer " + token);
expect(req.status).toBe(403);
})
it("Should return 400 if id is empty", async () => {
const req = await request(app).del('/api/users').send().set("Authorization", "Bearer " + token);
expect(req.status).toBe(404);
})
it("Should return 400 if id isn't number", async () => {
const req = await request(app).del('/api/users/sacha').send().set("Authorization", "Bearer " + token);
expect(req.status).toBe(400);
})
it("Should return 404 if user doesn't exist", async () => {
const req = await request(app).del('/api/users/666').send().set("Authorization", "Bearer " + token);
expect(req.status).toBe(404);
})
})

198
backend/test/video.test.js

@ -1,198 +0,0 @@
import vitest from "vitest";
import app from "../server.js";
import request from "supertest";
const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NSwidXNlcm5hbWUiOiJ0ZXN0IiwiaWF0IjoxNzUxNzk2MjM2fQ.XmaVA_NQcpW7fxRtDWOinMyQXPaFixpp3ib_mzo6M6c"
const videoId = 14
describe("GET BY ID", async () => {
it("Should return 401 if token is missing", async () => {
const req = await request(app).get("/api/videos/" + videoId).send()
expect(req.status).toBe(401)
})
it("Should return 400 if id is not number", async () => {
const req = await request(app).get("/api/videos/sacha").send().set("Authorization", `Bearer ${token}`);
expect(req.status).toBe(400)
})
it("Should return 404 if video does not exist", async () => {
const req = await request(app).get("/api/videos/404").send().set("Authorization", `Bearer ${token}`);
expect(req.status).toBe(404)
})
it("Should return 200 if OK", async () => {
const req = await request(app).get("/api/videos/" + videoId).send().set("Authorization", `Bearer ${token}`);
expect(req.status).toBe(200)
})
})
describe("GET BY CHANNEL", async () => {
it("Should return 401 if token is missing", async () => {
const req = await request(app).get("/api/videos/channel/2").send()
expect(req.status).toBe(401)
})
it("Should return 400 if channeld id is not number", async () => {
const req = await request(app).get("/api/videos/channel/astria").send().set("Authorization", `Bearer ${token}`);
expect(req.status).toBe(400)
})
it("Should return 404 if channel does not exist", async () => {
const req = await request(app).get("/api/videos/channel/404").send().set("Authorization", `Bearer ${token}`);
expect(req.status).toBe(404)
})
it("Should return 200 if OK", async () => {
const req = await request(app).get("/api/videos/channel/2").send().set("Authorization", `Bearer ${token}`);
expect(req.status).toBe(200)
})
})
describe("UPDATE VIDEO DATA", async () => {
it("Should return 401 if token is missing", async () => {
const video = {
"title": "video",
"description": "video",
"visibility": "private",
"channel": 2
}
const req = await request(app).put(`/api/videos/` + videoId).send(video)
expect(req.status).toBe(401)
})
it("Should return 400 if id is not number", async () => {
const video = {
"title": "video",
"description": "video",
"visibility": "private",
"channel": 2
}
const req = await request(app).put(`/api/videos/sacha`).send(video).set("Authorization", `Bearer ${token}`);
expect(req.status).toBe(400)
})
it("Should return 400 if title is missing", async () => {
const video = {
"description": "video",
"visibility": "private",
"channel": 2
}
const req = await request(app).put(`/api/videos/` + videoId).send(video).set("Authorization", `Bearer ${token}`);
expect(req.status).toBe(400)
})
it("Should return 400 if visibility is missing", async () => {
const video = {
"title": "video",
"description": "video",
"channel": 2
}
const req = await request(app).put('/api/videos/' + videoId).send(video).set("Authorization", `Bearer ${token}`);
expect(req.status).toBe(400)
})
it("Should return 400 if visibility is not string", async () => {
const video = {
"title": "video",
"description": "video",
"visibility": 400,
"channel": 2
}
const req = await request(app).put("/api/videos/" + videoId).send(video).set("Authorization", `Bearer ${token}`);
expect(req.status).toBe(400)
})
it("Should return 400 if channel is missing", async () => {
const video = {
"title": "video",
"description": "video",
"visibility": "private",
}
const req = await request(app).put("/api/videos/" + videoId).send(video).set("Authorization", `Bearer ${token}`);
expect(req.status).toBe(400)
})
it("Should return 400 if channel is not number", async () => {
const video = {
"title": "video",
"description": "video",
"visibility": "private",
"channel": "astria"
}
const req = await request(app).put("/api/videos/" + videoId).send(video).set("Authorization", `Bearer ${token}`);
expect(req.status).toBe(400)
})
it("Should return 404 if video does not exist", async () => {
const video = {
"title": "video",
"description": "video",
"visibility": "private",
"channel": 2
}
const req = await request(app).put("/api/videos/404").send(video).set("Authorization", `Bearer ${token}`);
expect(req.status).toBe(404);
})
it("Should return 404 if channel does not exist", async () => {
const video = {
"title": "video",
"description": "video",
"visibility": "private",
"channel": 404
}
const req = await request(app).put("/api/videos/" + videoId).send(video).set("Authorization", `Bearer ${token}`);
expect(req.status).toBe(404);
})
it("Should return 403 if user not the owner", async () => {
const video = {
"title": "video",
"description": "video",
"visibility": "private",
"channel": 3
}
const req = await request(app).put("/api/videos/" + videoId).send(video).set("Authorization", `Bearer ${token}`);
expect(req.status).toBe(403);
})
})
describe("UPDATE TAGS", async () => {
it("Should return 401 if token is missing", async () => {
const tags = {
"tags": ["tag1", "tag2"]
}
const req = await request(app).put(`/api/videos/` + videoId + "/tags").send(tags)
expect(req.status).toBe(401)
})
it("Should return 400 if id is not number", async () => {
const tags = {
"tags": ["tag1", "tag2"]
}
const req = await request(app).put(`/api/videos/sacha/tags`).send(tags).set("Authorization", `Bearer ${token}`);
expect(req.status).toBe(400)
})
it("Should return 400 if tags is not array", async () => {
const tags = {
"tags": "tag1, tag2"
}
const req = await request(app).put(`/api/videos/` + videoId + "/tags").send(tags).set("Authorization", `Bearer ${token}`);
expect(req.status).toBe(400)
})
it("Should return 404 if video does not exist", async () => {
const tags = {
"tags": ["tag1", "tag2"]
}
const req = await request(app).put("/api/videos/404/tags").send(tags).set("Authorization", `Bearer ${token}`);
expect(req.status).toBe(404);
})
it("Should return 403 if user not the owner", async () => {
const tags = {
"tags": ["tag1", "tag2"],
"channel": 3
}
const req = await request(app).put("/api/videos/" + videoId + "/tags").send(tags).set("Authorization", `Bearer ${token}`);
expect(req.status).toBe(403);
})
})

29
backend/tools.js

@ -1,29 +0,0 @@
import pg from "pg";
console.log(process.argv[2]);
if (process.argv[2] === "flush") {
flushDatabase();
}
async function flushDatabase() {
const client = new pg.Client({
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
host: process.env.DB_HOST,
database: process.env.DB_NAME,
port: 5432
});
try {
await client.connect();
await client.query('TRUNCATE TABLE users CASCADE');
console.log('Database flushed successfully.');
} catch (err) {
console.error('Error flushing database:', err);
} finally {
await client.end();
}
}

6
backend/vitest.config.js

@ -1,6 +0,0 @@
export default {
test: {
globals: true,
environment: 'node',
},
};

2
developpement.yaml

@ -15,7 +15,7 @@ services:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
JWT_SECRET: ${JWT_SECRET}
LOG_FILE: ${LOG_FILE}
PORT: ${BACKEND_PORT}
BACKEND_PORT: ${BACKEND_PORT}
GMAIL_USER: ${GMAIL_USER}
GMAIL_PASSWORD: ${GMAIL_PASSWORD}
GITHUB_ID: ${GITHUB_ID}

6
docker-compose.yaml

@ -15,7 +15,7 @@ services:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
JWT_SECRET: ${JWT_SECRET}
LOG_FILE: ${LOG_FILE}
PORT: ${BACKEND_PORT}
BACKEND_PORT: ${BACKEND_PORT}
GMAIL_USER: ${GMAIL_USER}
GMAIL_PASSWORD: ${GMAIL_PASSWORD}
GITHUB_ID: ${GITHUB_ID}
@ -23,7 +23,7 @@ services:
FRONTEND_URL: ${FRONTEND_URL}
volumes:
- ./backend/logs:/var/log/freetube
- ./backend:/app
- ./uploads:/app/app/uploads/
depends_on:
db:
condition: service_healthy
@ -61,3 +61,5 @@ services:
volumes:
db_data:
driver: local
upload:
driver: local

7
frontend/Docker.Development

@ -1,7 +0,0 @@
# Build stage
FROM node:22-alpine3.20 as build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .

12
frontend/README.md

@ -1,12 +0,0 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.

15
frontend/rebuild.sh

@ -1,15 +0,0 @@
#!/bin/bash
echo "🧹 Nettoyage des caches..."
rm -rf dist
rm -rf node_modules/.vite
rm -rf node_modules/.cache
echo "📦 Build de l'application..."
npm run build
echo "🔄 Redémarrage du container frontend..."
cd ../
sudo docker-compose restart frontend
echo "✅ Build terminé et container redémarré !"

4
frontend/src/components/CreatorCard.jsx

@ -2,9 +2,11 @@
export default function CreatorCard({ creator }) {
const handleClick = () => {
window.location.href = `/manage-channel/${creator.id}`;
window.location.href = `/channel/${creator.channel_id}`;
};
return (
<div className="flex flex-col glassmorphism w-full p-6 cursor-pointer" onClick={handleClick}>
<img

2
frontend/src/components/Recommendations.jsx

@ -1,7 +1,7 @@
import VideoCard from "./VideoCard";
export default function Recommendations({videos}) {
console.log(videos);
return (
<div className="">
<h2 className="text-3xl font-bold mb-4 text-white">Recommandations</h2>

2
frontend/src/modals/CreateChannelModal.jsx

@ -22,7 +22,7 @@ export default function CreateChannelModal({isOpen, onClose, addAlert}) {
const data = await createChannel(body, token, addAlert);
console.log(data);
onClose();
}

2
frontend/src/modals/EmailVerificationModal.jsx

@ -17,7 +17,7 @@ export default function EmailVerificationModal({ isOpen, onSubmit, onClose }) {
onChange={(e) => setVerificationCode(e.target.value)}
/>
<button className="bg-primary px-3 py-2 rounded-sm text-white font-montserrat text-lg font-semibold cursor-pointer mt-2" onClick={() => {
console.log("Verification code submitted:", verificationCode);
onSubmit(verificationCode)
}}>Vérifier</button>
</div>

4
frontend/src/pages/AddVideo.jsx

@ -35,7 +35,7 @@ export default function AddVideo() {
const fetchChannel = async () => {
const fetchedChannel = await getChannel(user.id, token, addAlert);
setChannel(fetchedChannel.channel);
console.log(fetchedChannel.channel);
}
const handleTagKeyDown = (e) => {
@ -123,7 +123,7 @@ export default function AddVideo() {
// Call the API to search for users
searchByUsername(searchUser, token, addAlert)
.then((results) => {
console.log(results);
setSearchResults(results);
})
.catch((error) => {

35
frontend/src/pages/Home.jsx

@ -77,40 +77,7 @@ export default function Home() {
<div className=" lg:min-w-screen lg:min-h-screen bg-linear-to-br from-left-gradient to-right-gradient">
<Navbar isSearchPage={false} />
<main className=" px-5 lg:px-36">
{/* Hero section */}
<div className="flex flex-col items-center w-full pt-[128px] lg:pt-[304px]">
<img src={HeroImage} alt="" className=" w-1700/1920 lg:w-1046/1920" />
{isAuthenticated ? (
<h1 className="font-montserrat text-4xl lg:text-8xl font-black w-1200/1920 text-center text-white -translate-y-1/2">
Bienvenue {user?.username} !
</h1>
) : (
<>
<h1 className="font-montserrat text-4xl lg:text-8xl font-black w-1200/1920 text-center text-white -translate-y-1/2">
Regarder des vidéos comme jamais auparavant
</h1>
<div className="flex justify-center gap-10 -translate-y-[100px] mt-10">
<button className="bg-white text-black font-montserrat p-3 rounded-sm text-xl lg:text-2xl font-bold">
<a href="/login">
<p>Se connecter</p>
</a>
</button>
<button className="bg-primary p-3 rounded-sm text-white font-montserrat text-xl lg:text-2xl font-bold cursor-pointer">
<a href="/register">
<p>Créer un compte</p>
</a>
</button>
</div>
</>
)}
</div>
<main className="pt-[30px] px-5 lg:px-36">
{/* Recommendations section */}
<Recommendations videos={recommendations} />

6
frontend/src/pages/LoginSuccess.jsx

@ -15,14 +15,14 @@ export default function LoginSuccess() {
const token = searchParams.get('token');
const userParam = searchParams.get('user');
console.log('Processing OAuth callback:', { token: !!token, userParam: !!userParam });
if (token && userParam) {
try {
hasProcessed.current = true;
const userData = JSON.parse(decodeURIComponent(userParam));
console.log('Parsed user data:', userData);
// Use the OAuth login method to update auth context
loginWithOAuth(userData, token);
@ -40,7 +40,7 @@ export default function LoginSuccess() {
}, 100);
}
} else {
console.log('Missing token or user data');
hasProcessed.current = true;
setTimeout(() => {
navigate('/login?error=missing_data', { replace: true });

4
frontend/src/pages/ManageVideo.jsx

@ -173,7 +173,7 @@ export default function ManageVideo() {
channel: video.channel
};
console.log("Sending to API:", body); // Debug log
// Debug log
const request = await updateAuthorizedUsers(id, body, token, addAlert);
@ -188,7 +188,7 @@ export default function ManageVideo() {
}
const onRemoveAuthorizedUser = async (user) => {
console.log("Êtes-vous sûr de vouloir supprimer cet utilisateur autorisé ?", user.id);
const body = {
authorizedUsers: video.authorizedUsers.filter(u => u.id !== user.id),
channel: video.channel

2
frontend/src/pages/Register.jsx

@ -77,7 +77,7 @@ export default function Register() {
const onVerificationSubmit = async (token) => {
console.log("Submitting email verification with token:", token);
const response = await verifyEmail(formData.email, token, addAlert);

451
frontend/src/pages/Video.jsx

@ -1,20 +1,20 @@
import {useNavigate, useParams, useSearchParams} from "react-router-dom";
import {useEffect, useState, useRef, useCallback} from "react";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import { useEffect, useState, useRef, useCallback } from "react";
import Navbar from "../components/Navbar.jsx";
import { useAuth } from "../contexts/AuthContext.jsx";
import Comment from "../components/Comment.jsx";
import VideoCard from "../components/VideoCard.jsx";
import Tag from "../components/Tag.jsx";
import {addView, getSimilarVideos, getVideoById, toggleLike} from "../services/video.service.js";
import {subscribe} from "../services/channel.service.js";
import {addComment} from "../services/comment.service.js";
import { addView, getSimilarVideos, getVideoById, toggleLike } from "../services/video.service.js";
import { subscribe } from "../services/channel.service.js";
import { addComment } from "../services/comment.service.js";
import { getPlaylists } from "../services/user.service.js";
import { addToPlaylist, getPlaylistById } from "../services/playlist.service.js";
import PlaylistVideoCard from "../components/PlaylistVideoCard.jsx";
export default function Video() {
const {id} = useParams();
const { id } = useParams();
const { user, isAuthenticated } = useAuth();
const videoRef = useRef(null);
const controllerRef = useRef(null);
@ -84,32 +84,32 @@ export default function Video() {
const token = localStorage.getItem('token');
if (!token) return;
console.log("Fetching next video");
console.log("currentPlaylist", currentPlaylist);
console.log("current video id from params:", id, "type:", typeof id);
console.log("playlist videos:", currentPlaylist?.videos?.map(v => ({ id: v.id, type: typeof v.id })));
//Find position of current video id in currentPlaylist.videos
const currentIndex = currentPlaylist?.videos.findIndex(video => {
console.log(`Comparing video.id: ${video.id} (${typeof video.id}) with id: ${id} (${typeof id})`);
return video.id.toString() === id.toString();
});
console.log("currentIndex", currentIndex);
if (currentIndex !== -1) {
if (currentPlaylist?.videos[currentIndex + 1]) {
setNextVideo(currentPlaylist.videos[currentIndex + 1]);
console.log("nextVideo", currentPlaylist.videos[currentIndex + 1]);
}
}
}
const passToNextVideo = () => {
if (!nextVideo) {
console.log("No next video available");
return;
}
console.log("Passing to next video:", nextVideo);
// Navigate to the next video with playlist context
if (playlistId) {
@ -209,7 +209,7 @@ export default function Video() {
const handlePlaying = () => {
if (videoRef.current) {
console.log(`Video is playing at ${videoRef.current.currentTime} seconds`);
}
}
@ -249,7 +249,7 @@ export default function Video() {
const response = await subscribe(video.creator.id, addAlert);
console.log('Subscription successful:', response);
const subscriptionCount = response.subscriptions || 0;
setVideo((prevVideo) => {
@ -339,84 +339,83 @@ export default function Video() {
setIsAddToPlaylistOpen(!isAddToPlaylistOpen);
}
return (
<div className="min-w-screen min-h-screen bg-linear-to-br from-left-gradient to-right-gradient">
<Navbar isSearchPage={false} alerts={alerts} onCloseAlert={onCloseAlert} />
<main className="px-5 lg:px-36 w-full lg:flex justify-between pt-[48px] lg:pt-[118px]">
{video ? (
<>
{/* Video player section */}
<div className="lg:w-1280/1920">
<div
className="relative w-full aspect-video mx-auto rounded-lg overflow-hidden"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onMouseMove={handleMouseMove}
>
<video
key={video.id}
id={`video-${video.id}`}
ref={videoRef}
onPlay={handlePlaying}
onTimeUpdate={handleTimeUpdate}
onLoadedMetadata={handleLoadedMetadata}
onEnded={passToNextVideo}
className={`w-full h-full object-cover cursor-pointer ${isFullscreen ? 'fixed top-0 left-0 w-full h-full z-50 bg-black' : ''}`}
controls={window.innerWidth < 1024} // Show native controls on small screens
>
<source src={`${video.file}`} type="video/mp4" />
Your browser does not support the video tag.
</video>
{/* Video controls */}
<div
className={`glassmorphism-rounded-md p-4 hidden lg:flex gap-4 items-center transition-opacity duration-300 ${
showControls ? 'opacity-100' : 'opacity-0'
}
${isFullscreen ? 'fixed bottom-8 left-1/2 transform -translate-x-1/2 opacity-100 z-[60] w-[70%] max-w-3xl' : 'absolute bottom-4 left-4 right-4'}`}
ref={controllerRef}
>
<p
className="text-white"
onClick={() => {
if (videoRef.current) {
videoRef.current.currentTime = Math.max(0, videoRef.current.currentTime - 10);
return (
<div className="min-w-screen min-h-screen bg-linear-to-br from-left-gradient to-right-gradient">
<Navbar isSearchPage={false} alerts={alerts} onCloseAlert={onCloseAlert} />
<main className="px-5 lg:px-36 w-full lg:flex justify-between pt-[48px] lg:pt-[118px]">
{video ? (
<>
{/* Video player section */}
<div className="lg:w-1280/1920">
<div
className="relative w-full aspect-video mx-auto rounded-lg overflow-hidden"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onMouseMove={handleMouseMove}
>
<video
key={video.id}
id={`video-${video.id}`}
ref={videoRef}
onPlay={handlePlaying}
onTimeUpdate={handleTimeUpdate}
onLoadedMetadata={handleLoadedMetadata}
onEnded={passToNextVideo}
className={`w-full h-full object-cover cursor-pointer ${isFullscreen ? 'fixed top-0 left-0 w-full h-full z-50 bg-black' : ''}`}
controls={window.innerWidth < 1024} // Show native controls on small screens
>
<source src={`${video.file}`} type="video/mp4" />
Your browser does not support the video tag.
</video>
{/* Video controls */}
<div
className={`glassmorphism-rounded-md p-4 hidden lg:flex gap-4 items-center transition-opacity duration-300 ${showControls ? 'opacity-100' : 'opacity-0'
}
}}
>-10</p>
<svg width="30" height="34" viewBox="0 0 30 34" fill="none" xmlns="http://www.w3.org/2000/svg" onClick={handlePlayPause}>
<path d="M28.5 14.4019C30.5 15.5566 30.5 18.4434 28.5 19.5981L4.5 33.4545C2.5 34.6092 2.14642e-06 33.1658 2.24736e-06 30.8564L3.45873e-06 3.14359C3.55968e-06 0.834193 2.5 -0.609184 4.5 0.545517L28.5 14.4019Z" fill="white"/>
</svg>
<p
className="text-white"
onClick={() => {
if (videoRef.current) {
videoRef.current.currentTime = Math.min(videoRef.current.currentTime + 10, duration);
}
}}
>+10</p>
<div className="flex-1 mx-4">
{/* Time display */}
<div className="flex justify-between items-center text-white text-sm mt-2">
<span>{formatTime(currentTime)}</span>
<span>{formatTime(duration)}</span>
</div>
{/* Time bar */}
<div
className="w-full h-2 bg-gray-300 rounded-full mt-2 cursor-pointer hover:h-3 transition-all duration-200"
onClick={handleTimeBarClick}
>
<div
className="h-full bg-white rounded-full transition-all duration-100"
style={{ width: `${progress}%` }}
></div>
</div>
</div>
${isFullscreen ? 'fixed bottom-8 left-1/2 transform -translate-x-1/2 opacity-100 z-[60] w-[70%] max-w-3xl' : 'absolute bottom-4 left-4 right-4'}`}
ref={controllerRef}
>
<p
className="text-white"
onClick={() => {
if (videoRef.current) {
videoRef.current.currentTime = Math.max(0, videoRef.current.currentTime - 10);
}
}}
>-10</p>
<svg width="30" height="34" viewBox="0 0 30 34" fill="none" xmlns="http://www.w3.org/2000/svg" onClick={handlePlayPause}>
<path d="M28.5 14.4019C30.5 15.5566 30.5 18.4434 28.5 19.5981L4.5 33.4545C2.5 34.6092 2.14642e-06 33.1658 2.24736e-06 30.8564L3.45873e-06 3.14359C3.55968e-06 0.834193 2.5 -0.609184 4.5 0.545517L28.5 14.4019Z" fill="white" />
</svg>
<p
className="text-white"
onClick={() => {
if (videoRef.current) {
videoRef.current.currentTime = Math.min(videoRef.current.currentTime + 10, duration);
}
}}
>+10</p>
<div className="flex-1 mx-4">
{/* Time display */}
<div className="flex justify-between items-center text-white text-sm mt-2">
<span>{formatTime(currentTime)}</span>
<span>{formatTime(duration)}</span>
</div>
{/* Time bar */}
<div
className="w-full h-2 bg-gray-300 rounded-full mt-2 cursor-pointer hover:h-3 transition-all duration-200"
onClick={handleTimeBarClick}
>
<div
className="h-full bg-white rounded-full transition-all duration-100"
style={{ width: `${progress}%` }}
></div>
</div>
</div>
{
!isFullscreen ? (
<svg
@ -443,144 +442,152 @@ export default function Video() {
)
}
</div>
</div>
<h1 className="mt-3 font-montserrat font-bold text-2xl text-white">{video.title}</h1>
{/* Channel and like */}
<div className="lg:flex items-center mt-4">
<div className="flex">
<img
src={video.creator?.profile_picture || "https://placehold.co/48"}
alt={video.creator?.name || "Creator"}
className="w-12 h-12 rounded-full object-cover mr-3"
/>
<div>
<p className="text-white font-bold font-montserrat">{video.creator?.name}</p>
<p className="text-gray-300 text-sm">{video.creator?.subscribers || 0} abonnés</p>
</div>
<button className="ml-14 bg-primary text-white font-montserrat font-bold px-4 py-2 rounded-md cursor-pointer" onClick={handleSubscribe} >
s'abonner
</button>
</div>
</div>
<h1 className="mt-3 font-montserrat font-bold text-2xl text-white">{video.title}</h1>
{/* Channel and like */}
<div className="lg:flex items-center mt-4">
<div className="flex">
<img
src={video.creator?.profile_picture || "https://placehold.co/48"}
alt={video.creator?.name || "Creator"}
className="w-12 h-12 rounded-full object-cover mr-3 cursor-pointer"
onClick={() => {
window.location.href = `/channel/${video.creator?.id}`;
}}
/>
<div>
<p
className="text-white font-bold font-montserrat cursor-pointer"
onClick={() => {
window.location.href = `/channel/${video.creator?.id}`;
}}
>{video.creator?.name}</p>
<p className="text-gray-300 text-sm">{video.creator?.subscribers || 0} abonnés</p>
</div>
<button className="ml-14 bg-primary text-white font-montserrat font-bold px-4 py-2 rounded-md cursor-pointer" onClick={handleSubscribe} >
s'abonner
</button>
</div>
<div className="flex items-center lg:ml-auto mt-4 lg:mt-0">
<button className="lg:ml-4 cursor-pointer" onClick={handleLike}>
<svg width="32" height="32" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 31.5H7.5V12H6C5.20435 12 4.44129 12.3161 3.87868 12.8787C3.31607 13.4413 3 14.2044 3 15V28.5C3 29.2956 3.31607 30.0587 3.87868 30.6213C4.44129 31.1839 5.20435 31.5 6 31.5ZM30 12H19.5L21.183 6.948C21.3332 6.49712 21.3741 6.01702 21.3024 5.54723C21.2306 5.07745 21.0483 4.63142 20.7705 4.24589C20.4926 3.86036 20.1271 3.54636 19.7041 3.32975C19.2811 3.11314 18.8127 3.00012 18.3375 3H18L10.5 11.157V31.5H27L32.868 18.606L33 18V15C33 14.2044 32.6839 13.4413 32.1213 12.8787C31.5587 12.3161 30.7956 12 30 12Z" fill="white"/>
</svg>
</button>
<p className="font-montserrat text-white ml-2" >{video.likes}</p>
<div className="flex items-center lg:ml-auto mt-4 lg:mt-0">
<button className="lg:ml-4 cursor-pointer" onClick={handleLike}>
<svg width="32" height="32" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 31.5H7.5V12H6C5.20435 12 4.44129 12.3161 3.87868 12.8787C3.31607 13.4413 3 14.2044 3 15V28.5C3 29.2956 3.31607 30.0587 3.87868 30.6213C4.44129 31.1839 5.20435 31.5 6 31.5ZM30 12H19.5L21.183 6.948C21.3332 6.49712 21.3741 6.01702 21.3024 5.54723C21.2306 5.07745 21.0483 4.63142 20.7705 4.24589C20.4926 3.86036 20.1271 3.54636 19.7041 3.32975C19.2811 3.11314 18.8127 3.00012 18.3375 3H18L10.5 11.157V31.5H27L32.868 18.606L33 18V15C33 14.2044 32.6839 13.4413 32.1213 12.8787C31.5587 12.3161 30.7956 12 30 12Z" fill="white" />
</svg>
</button>
<p className="font-montserrat text-white ml-2" >{video.likes}</p>
{
isAuthenticated && (
<button className="relative ml-14">
<div className="bg-primary cursor-pointer px-4 py-2 rounded-md flex items-center gap-4" onClick={() => { setIsAddToPlaylistOpen(!isAddToPlaylistOpen) }} >
<p className="text-white font-montserrat font-bold" >playlist</p>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" className="fill-white"><path d="M19 11h-6V5h-2v6H5v2h6v6h2v-6h6z"></path></svg>
</div>
{
isAuthenticated && (
<button className="relative ml-14">
<div className="bg-primary cursor-pointer px-4 py-2 rounded-md flex items-center gap-4" onClick={() => { setIsAddToPlaylistOpen(!isAddToPlaylistOpen) }} >
<p className="text-white font-montserrat font-bold" >playlist</p>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" className="fill-white"><path d="M19 11h-6V5h-2v6H5v2h6v6h2v-6h6z"></path></svg>
</div>
{
playlists.length > 0 && isAddToPlaylistOpen && (
<div className="absolute inset-0 w-max h-max z-40 glassmorphism top-1/1 mt-2 left-0 rounded-2xl px-4 py-2 cursor-default">
<ul className="flex flex-col gap-2">
{playlists.map((playlist) => (
<li
key={playlist.id}
className="text-white font-montserrat font-medium text-sm cursor-pointer hover:underline flex items-center justify-between gap-4"
onClick={() => handleAddToPlaylist(playlist.id)}
>
<p className="text-start">{playlist.name}</p>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" className="fill-white"><path d="M19 11h-6V5h-2v6H5v2h6v6h2v-6h6z"></path></svg>
</li>
))}
</ul>
</div>
<div className="absolute inset-0 w-max h-max z-40 glassmorphism top-1/1 mt-2 left-0 rounded-2xl px-4 py-2 cursor-default">
<ul className="flex flex-col gap-2">
{playlists.map((playlist) => (
<li
key={playlist.id}
className="text-white font-montserrat font-medium text-sm cursor-pointer hover:underline flex items-center justify-between gap-4"
onClick={() => handleAddToPlaylist(playlist.id)}
>
<p className="text-start">{playlist.name}</p>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" className="fill-white"><path d="M19 11h-6V5h-2v6H5v2h6v6h2v-6h6z"></path></svg>
</li>
))}
</ul>
</div>
)
}
</button>
</button>
)
}
</div>
</div>
{/* Video details */}
<div className="glassmorphism rounded-md py-4 px-6 mt-7">
{/* Tags */}
{video.tags && video.tags.length > 0 && (
<div className="mb-3">
<div className="flex flex-wrap gap-2">
{video.tags.map((tag, index) => (
<Tag tag={tag} key={index} doShowControls={false} />
))}
</div>
</div>
)}
<p className="font-montserrat text-white">{video.views} vues - {formatDate(video.release_date)}</p>
<p className="font-montserrat text-white mt-2">{video.description}</p>
</div>
{/* Comments section */}
<div>
<div className="flex justify-between items-center mt-8 mb-2">
<h2 className="font-montserrat text-white text-2xl">Commentaires</h2>
<button className="text-white lg:hidden" onClick={() => setIsCommentVisible(!isCommentVisible)} >
Voir {isCommentVisible ? "moins" : "plus"}
</button>
</div>
<textarea
className="glassmorphism h-[100px] w-full font-inter text-white placeholder:text-[#9f9f9f] focus:outline-none py-4 px-6"
placeholder="Ajouter un commentaire..."
value={comment}
onChange={(e) => setComment(e.target.value)}
></textarea>
<button className="bg-primary text-white font-montserrat font-bold px-4 py-2 rounded-md mt-2 cursor-pointer" onClick={handleComment}>
Publier
</button>
{/* Comments list */}
{
isCommentVisible && (
<div className="mt-4">
{video.comments && video.comments.length > 0 ? (
video.comments.map((comment, index) => (
<Comment comment={comment} index={index} videoId={id} key={index} refetchVideo={fetchVideo} addAlert={addAlert} />
))
) : (
<p className="text-gray-400">Aucun commentaire pour le moment. Soyez le premier à en publier !</p>
)}
</div>
)
}
</div>
</div>
{/* Video details */}
<div className="glassmorphism rounded-md py-4 px-6 mt-7">
{/* Tags */}
{video.tags && video.tags.length > 0 && (
<div className="mb-3">
<div className="flex flex-wrap gap-2">
{video.tags.map((tag, index) => (
<Tag tag={tag} key={index} doShowControls={false} />
))}
</div>
</div>
)}
<p className="font-montserrat text-white">{video.views} vues - {formatDate(video.release_date)}</p>
<p className="font-montserrat text-white mt-2">{video.description}</p>
</div>
{/* Comments section */}
<div>
<div className="flex justify-between items-center mt-8 mb-2">
<h2 className="font-montserrat text-white text-2xl">Commentaires</h2>
<button className="text-white lg:hidden" onClick={() => setIsCommentVisible(!isCommentVisible)} >
Voir {isCommentVisible ? "moins" : "plus"}
</button>
</div>
<textarea
className="glassmorphism h-[100px] w-full font-inter text-white placeholder:text-[#9f9f9f] focus:outline-none py-4 px-6"
placeholder="Ajouter un commentaire..."
value={comment}
onChange={(e) => setComment(e.target.value)}
></textarea>
<button className="bg-primary text-white font-montserrat font-bold px-4 py-2 rounded-md mt-2 cursor-pointer" onClick={handleComment}>
Publier
</button>
{/* Comments list */}
{
isCommentVisible && (
<div className="mt-4">
{video.comments && video.comments.length > 0 ? (
video.comments.map((comment, index) => (
<Comment comment={comment} index={index} videoId={id} key={index} refetchVideo={fetchVideo} addAlert={addAlert} />
))
) : (
<p className="text-gray-400">Aucun commentaire pour le moment. Soyez le premier à en publier !</p>
)}
</div>
)
}
</div>
</div>
</div>
</div>
{/* Similar videos section */}
<div className="flex-1">
<div className="flex-1">
{
!isPlaylist ? (
<div className="flex flex-col items-center gap-2">
<h2 className="font-montserrat w-full lg:w-9/10 mt-8 text-white text-2xl">Recommandations</h2>
{similarVideos.length > 0 ? similarVideos.map((video, index) => (
<div className="w-full lg:w-9/10" key={index}>
<VideoCard video={video} />
</div>
)) : (
<p className="text-gray-500 text-left">Aucune recommandation disponible</p>
)}
</div>
<div className="flex flex-col items-center gap-2">
<h2 className="font-montserrat w-full lg:w-9/10 mt-8 text-white text-2xl">Recommandations</h2>
{similarVideos.length > 0 ? similarVideos.map((video, index) => (
<div className="w-full lg:w-9/10" key={index}>
<VideoCard video={video} />
</div>
)) : (
<p className="text-gray-500 text-left">Aucune recommandation disponible</p>
)}
</div>
) : (
<div className="flex flex-col items-center gap-2">
<div className="glassmorphism w-full lg:w-9/10 py-4 px-2" >
<div className="flex flex-col items-center gap-2">
<div className="glassmorphism w-full lg:w-9/10 py-4 px-2" >
<h2 className="font-montserrat text-white text-2xl">{currentPlaylist?.name}</h2>
{
currentPlaylist?.videos && currentPlaylist.videos.length > 0 ? (
@ -595,18 +602,18 @@ export default function Video() {
<p className="font-montserrat text-white mt-2">Aucune vidéo trouvée dans cette playlist.</p>
)
}
</div>
</div>
</div>
</div>
)
}
</div>
</>
): (
<p>Loading</p>
)}
</div>
</>
) : (
<p>Loading</p>
)}
</main>
</main>
</div>
);
</div>
);
}

2
frontend/src/services/channel.service.js

@ -56,7 +56,7 @@ export async function updateChannel(channelId, data, token, addAlert) {
export async function subscribe(channelId, addAlert) {
const token = localStorage.getItem('token');
const user = JSON.parse(localStorage.getItem('user'));
console.log("Subscribing to channel with ID:", channelId, "for user:", user.id);
return fetch(`/api/channels/${channelId}/subscribe`, {
method: 'POST',
headers: {

4
frontend/src/services/user.service.js

@ -11,7 +11,7 @@ export async function isSubscribed(channelId, addAlert) {
const request = await fetch(`/api/users/${channelId}/channel/subscribed`, { headers })
const result = await request.json();
console.log("Subscription status for channel ID", channelId, ":", result);
return result.subscribed;
}
@ -140,7 +140,7 @@ export async function searchByUsername(username, token, addAlert) {
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
console.log(errorData);
throw new Error(errorData.error || "Failed to search user");
}
const data = await response.json();

BIN
uploads/profiles/sacha.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Loading…
Cancel
Save