diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6812b12 --- /dev/null +++ b/Makefile @@ -0,0 +1,23 @@ +up: + docker compose up --build -d + +down: + docker compose down + +logs: + docker compose logs -f + +dev: + docker compose -f developpement.yaml up --build -d + +dev-down: + docker compose -f developpement.yaml down + +dev-logs: + docker compose -f developpement.yaml logs -f + +dev-volumes: + docker compose -f developpement.yaml down -v + +dev-build: + docker compose -f developpement.yaml build \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile index 993ee06..1470f39 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20 +FROM node:20-alpine # Set the working directory WORKDIR /app # Copy package.json and package-lock.json @@ -12,10 +12,11 @@ COPY . . EXPOSE 8000 # Install netcat for health checks -RUN apt-get update && apt-get install -y netcat-openbsd +RUN apk add --no-cache netcat-openbsd # Install the cli tools - +RUN chmod +x ./freetube.sh +RUN cp ./freetube.sh /usr/local/bin/freetube # Start the application CMD ["npm", "start"] \ No newline at end of file diff --git a/backend/app/controllers/media.controller.js b/backend/app/controllers/media.controller.js new file mode 100644 index 0000000..ad8bf7d --- /dev/null +++ b/backend/app/controllers/media.controller.js @@ -0,0 +1,57 @@ +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export async function getProfilePicture(req, res) { + const file = req.params.file; + + // Try different path approaches + const possiblePaths = [ + path.join(__dirname, '../../uploads/profiles', file), + path.join(process.cwd(), 'app/uploads/profiles', file), + path.join('/app/app/uploads/profiles', file), + 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); + + try { + res.sendFile(filePath, (err) => { + if (err) { + 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) { + console.error("Error fetching profile picture:", error); + res.status(500).json({ error: "Internal server error while fetching profile picture." }); + } +} + +export async function getThumbnail(req, res) { + const file = req.params.file; + const filePath = path.join('/app/app/uploads/thumbnails', file); + + try { + res.sendFile(filePath, (err) => { + if (err) { + console.error("Error sending thumbnail:", err); + res.status(404).json({ error: "Thumbnail not found." }); + } else { + console.log("Thumbnail sent successfully."); + } + }); + } catch (error) { + console.error("Error fetching thumbnail:", error); + res.status(500).json({ error: "Internal server error while fetching thumbnail." }); + } +} \ No newline at end of file diff --git a/backend/app/controllers/recommendation.controller.js b/backend/app/controllers/recommendation.controller.js index 026e06d..7567cb8 100644 --- a/backend/app/controllers/recommendation.controller.js +++ b/backend/app/controllers/recommendation.controller.js @@ -17,6 +17,45 @@ export async function getRecommendations(req, res) { } else { + // Recuperer les 20 derniere vu de l'historique + let client = await getClient(); + let queryLastVideos = `SELECT video_id FROM history WHERE user_id = $1 ORDER BY viewed_at DESC LIMIT 20;`; + // TODO: Implement retrieval of recommendations based on user history and interactions + + // Recuperer les likes de l'utilisateur sur les 20 derniere videos recuperees + + // Recuperer les commentaires de l'utilisateur sur les 20 derniere videos recuperees + + // Recuperer les 3 tags avec lesquels l'utilisateur a le plus interagi + + // Recuperer 10 videos avec les 3 tags ayant le plus d'interaction avec l'utilisateur + + res.status(200).json({ + message: "Recommendations based on user history and interactions are not yet implemented." + }); } +} + +export async function getTrendingVideos(req, res) { + try { + // GET 10 VIDEOS WITH THE MOST LIKES AND COMMENTS + let client = await getClient(); + let queryTrendingVideos = ` + SELECT v.id, v.title, v.description, v.release_date, v.thumbnail, + COUNT(DISTINCT l.id) AS like_count, COUNT(DISTINCT c.id) AS comment_count + FROM videos v + LEFT JOIN likes l ON v.id = l.video + LEFT JOIN comments c ON v.id = c.video + GROUP BY v.id + ORDER BY like_count DESC, comment_count DESC + LIMIT 10; + `; + let result = await client.query(queryTrendingVideos); + const trendingVideos = result.rows; + res.status(200).json(trendingVideos); + } catch (error) { + console.error("Error fetching trending videos:", error); + res.status(500).json({error: "Internal server error while fetching trending videos."}); + } } \ No newline at end of file diff --git a/backend/app/controllers/user.controller.js b/backend/app/controllers/user.controller.js index f91cec0..6412c7c 100644 --- a/backend/app/controllers/user.controller.js +++ b/backend/app/controllers/user.controller.js @@ -60,7 +60,7 @@ export async function login(req, res) { const client = await getClient(); - let query = `SELECT * FROM users WHERE username = '${user.username}'`; + let query = `SELECT id, username, email, picture, password FROM users WHERE username = '${user.username}'`; const result = await client.query(query); @@ -87,8 +87,15 @@ export async function login(req, res) { const token = jwt.sign(payload, process.env.JWT_SECRET); + const userData = { + id: userInBase.id, + username: userInBase.username, + email: userInBase.email, + picture: userInBase.picture + } + logger.write("Successfully logged in", 200); - res.status(200).json({token: token}); + res.status(200).json({token: token, user: userData}); } diff --git a/backend/app/middlewares/media.middleware.js b/backend/app/middlewares/media.middleware.js new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/routes/media.routes.js b/backend/app/routes/media.routes.js new file mode 100644 index 0000000..0d25e83 --- /dev/null +++ b/backend/app/routes/media.routes.js @@ -0,0 +1,12 @@ +import {Router} from 'express'; +import {getProfilePicture, getThumbnail} from "../controllers/media.controller.js"; + +const router = Router(); + +router.get("/profile/:file", getProfilePicture); + +//router.get("/video/:file", getVideo); + +router.get("/thumbnail/:file", getThumbnail); + +export default router; \ No newline at end of file diff --git a/backend/app/routes/redommendation.route.js b/backend/app/routes/redommendation.route.js index b08c915..dbb7f2c 100644 --- a/backend/app/routes/redommendation.route.js +++ b/backend/app/routes/redommendation.route.js @@ -1,8 +1,10 @@ import { Router } from 'express'; -import {getRecommendations} from "../controllers/recommendation.controller.js"; +import {getRecommendations, getTrendingVideos} from "../controllers/recommendation.controller.js"; const router = Router(); router.get('/', [], getRecommendations); +router.get('/trending', [], getTrendingVideos); + export default router; \ No newline at end of file diff --git a/backend/app/utils/database.js b/backend/app/utils/database.js index 49c0b1a..f2610d4 100644 --- a/backend/app/utils/database.js +++ b/backend/app/utils/database.js @@ -100,6 +100,14 @@ export async function initDb() { )` await client.query(query); + query = `CREATE TABLE IF NOT EXISTS history ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id), + video INTEGER NOT NULL REFERENCES videos(id), + viewed_at TIMESTAMP NOT NULL DEFAULT NOW() + )`; + await client.query(query); + } catch (e) { console.error("Error initializing database:", e); } diff --git a/backend/logs/access.log b/backend/logs/access.log index 4aef878..c01bf83 100644 --- a/backend/logs/access.log +++ b/backend/logs/access.log @@ -79,3 +79,43 @@ [2025-07-17 21:18:20.739] [172.18.0.1] PUT(/:id/tags): try to add tags to video 1 [2025-07-17 21:18:20.746] [172.18.0.1] PUT(/:id/tags): Tag test already exists for video 1 with status 200 [2025-07-17 21:18:20.749] [172.18.0.1] PUT(/:id/tags): successfully added tags to video 1 with status 200 +[2025-07-18 18:48:42.083] [172.18.0.1] POST(/): try to register a user with username: astria and email: sacha@gmail.com +[2025-07-18 18:49:34.055] [172.18.0.1] POST(/): try to register a user with username: astria and email: sacha@gmail.com +[2025-07-18 18:49:34.103] [172.18.0.1] POST(/): successfully registered with status 200 +[2025-07-18 18:49:43.361] [172.18.0.1] POST(/login): try to login with username 'astria' +[2025-07-18 18:49:43.412] [172.18.0.1] POST(/login): Successfully logged in with status 200 +[2025-07-18 18:49:59.235] [172.18.0.1] POST(/): failed because user doesn't exists with status 404 +[2025-07-18 18:50:09.387] [172.18.0.1] POST(/): failed because user doesn't exists with status 404 +[2025-07-18 18:50:30.825] [172.18.0.1] POST(/): try to create new channel with owner 2 and name Machin +[2025-07-18 18:50:30.826] [172.18.0.1] POST(/): Successfully created new channel with name Machin with status 200 +[2025-07-18 18:51:08.407] [172.18.0.1] POST(/): try to upload video with status undefined +[2025-07-18 18:51:08.411] [172.18.0.1] POST(/): successfully uploaded video with status 200 +[2025-07-18 20:13:20.785] [undefined] POST(/): failed because username already exists with status 400 +[2025-07-18 20:13:49.607] [undefined] POST(/): failed because username already exists with status 400 +[2025-07-18 20:13:59.733] [undefined] POST(/): try to register a user with username: sacha and email: sachaguerin.Sg@gmail.com +[2025-07-18 20:13:59.785] [undefined] POST(/): successfully registered with status 200 +[2025-07-18 20:13:59.799] [undefined] POST(/login): try to login with username 'sacha' +[2025-07-18 20:13:59.848] [undefined] POST(/login): Successfully logged in with status 200 +[2025-07-18 20:18:47.900] [undefined] POST(/login): failed due to invalid values with status 400 +[2025-07-18 20:18:51.693] [undefined] POST(/login): failed due to invalid values with status 400 +[2025-07-18 20:19:07.145] [undefined] POST(/login): failed due to invalid values with status 400 +[2025-07-18 20:19:19.460] [undefined] POST(/login): try to login with username 'sacha' +[2025-07-18 20:19:19.509] [undefined] POST(/login): Successfully logged in with status 200 +[2025-07-18 20:50:28.272] [undefined] POST(/thumbnail): failed because user is not owner with status 403 +[2025-07-18 20:50:59.028] [undefined] POST(/login): try to login with username 'astria' +[2025-07-18 20:50:59.079] [undefined] POST(/login): Successfully logged in with status 200 +[2025-07-18 20:51:07.827] [undefined] POST(/thumbnail): try to add thumbnail to video 2 +[2025-07-18 20:51:07.829] [undefined] POST(/thumbnail): successfully uploaded thumbnail with status 200 +[2025-07-18 21:06:45.009] [undefined] POST(/login): try to login with username 'sacha' +[2025-07-18 21:06:45.057] [undefined] POST(/login): failed to login with status 401 +[2025-07-18 21:06:57.128] [undefined] POST(/login): try to login with username 'astria' +[2025-07-18 21:06:57.174] [undefined] POST(/login): Successfully logged in with status 200 +[2025-07-18 21:09:06.545] [undefined] POST(/login): try to login with username 'astria' +[2025-07-18 21:09:13.475] [undefined] POST(/login): try to login with username 'astria' +[2025-07-18 21:09:21.212] [undefined] POST(/login): try to login with username 'astria' +[2025-07-18 21:10:17.503] [undefined] POST(/login): try to login with username 'astria' +[2025-07-18 21:10:17.558] [undefined] POST(/login): Successfully logged in with status 200 +[2025-07-18 21:10:44.202] [undefined] POST(/login): try to login with username 'astria' +[2025-07-18 21:10:44.250] [undefined] POST(/login): Successfully logged in with status 200 +[2025-07-18 21:24:17.066] [undefined] POST(/login): try to login with username 'sacha' +[2025-07-18 21:24:17.118] [undefined] POST(/login): Successfully logged in with status 200 diff --git a/backend/requests/channel.http b/backend/requests/channel.http index e85393d..940cb73 100644 --- a/backend/requests/channel.http +++ b/backend/requests/channel.http @@ -1,4 +1,4 @@ -@token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhc3RyaWEiLCJpYXQiOjE3NTI3ODY2Njh9._naj-ip2UeBxbflzGOWLxjHNTXJobLJ9s_70xL6ylKw +@token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidXNlcm5hbWUiOiJhc3RyaWEiLCJpYXQiOjE3NTI4NjQ1ODN9.yyKcb1vLhAxEftBN1Z27wV7uM1pSruVEMb4dtiqDTrg ### CREATE CHANNEL POST http://localhost:8000/api/channels @@ -8,7 +8,7 @@ Content-Type: application/json { "name": "Machin", "description": "kljsdfjklsdfjkl", - "owner": 1 + "owner": 2 } ### GET CHANNEL BY ID diff --git a/backend/requests/medias.http b/backend/requests/medias.http new file mode 100644 index 0000000..ebb1152 --- /dev/null +++ b/backend/requests/medias.http @@ -0,0 +1,11 @@ +### 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 diff --git a/backend/requests/recommendation.http b/backend/requests/recommendation.http index 5ad6591..7758524 100644 --- a/backend/requests/recommendation.http +++ b/backend/requests/recommendation.http @@ -1,2 +1,5 @@ ### GET NON-AUTHENTICATED RECOMMENDATIONS -GET http://localhost:8000/api/recommendations/ \ No newline at end of file +GET http://localhost:8000/api/recommendations/ + +### GET TRENDING VIDS +GET http://localhost:8000/api/recommendations/trending/ \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index 89f33f5..c588fb8 100644 --- a/backend/server.js +++ b/backend/server.js @@ -9,6 +9,7 @@ import * as http from "node:http"; import cors from "cors"; import PlaylistRoute from "./app/routes/playlist.route.js"; import {initDb} from "./app/utils/database.js"; +import MediaRoutes from "./app/routes/media.routes.js"; console.clear(); dotenv.config(); @@ -30,12 +31,13 @@ app.use("/api/videos/", VideoRoute); app.use("/api/comments/", CommentRoute); app.use("/api/playlists", PlaylistRoute); app.use("/api/recommendations", RecommendationRoute); +app.use("/api/media", MediaRoutes); const port = process.env.PORT; if (process.env.NODE_ENV !== "test") { const server = http.createServer(app); - server.listen(port, async () => { + server.listen(port, '0.0.0.0', async () => { console.log("Server's listening on port " + port); console.log("Initializing database..."); await initDb(); diff --git a/docker-compose.yaml b/docker-compose.yaml index 0477c6b..997e29b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,20 +1,21 @@ services: - backend: + resit_backend: build: context: ./backend dockerfile: Dockerfile + network: host container_name: resit_backend ports: - "8000:8000" environment: - DB_USER: astria - DB_NAME: freetube - DB_HOST: db - DB_PASSWORD: 9Zv1Y1MAfWKKmtP7JlSQPX7ZmHkS1J6iTlOFQb6OFZEzWjQGeJIKictaGvKyOBIz - JWT_SECRET: Aed4GPa8BtriElyuwy65bf598D8MgxWCiE6Xzc4riV0J7AiLpxeu2DexjQPx4cBO - LOG_FILE: /var/log/freetube/access.log - PORT: 8000 + DB_USER: ${POSTGRES_USER} + DB_NAME: ${POSTGRES_DB} + DB_HOST: ${POSTGRES_HOST} + DB_PASSWORD: ${POSTGRES_PASSWORD} + JWT_SECRET: ${JWT_SECRET} + LOG_FILE: ${LOG_FILE} + PORT: ${BACKEND_PORT} volumes: - ./backend/logs:/var/log/freetube - ./backend/app/uploads:/app/app/uploads @@ -26,14 +27,15 @@ services: ports: - "5432:5432" environment: - POSTGRES_USER: astria - POSTGRES_PASSWORD: 9Zv1Y1MAfWKKmtP7JlSQPX7ZmHkS1J6iTlOFQb6OFZEzWjQGeJIKictaGvKyOBIz - POSTGRES_DB: freetube + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} volumes: - db_data:/var/lib/postgresql/data frontend: image: nginx:latest + network_mode: host ports: - "80:80" volumes: diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index c1ab4f2..d3f7916 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,13 +1,28 @@ import React from 'react'; import { useRoutes } from 'react-router-dom'; +import { AuthProvider, useAuth } from './contexts/AuthContext'; import routes from './routes/routes.jsx'; -const App = () => { +const AppContent = () => { + const { loading } = useAuth(); const routing = useRoutes(routes); + + if (loading) { + return ( +