From ccc415bd94a84935d4c56ed4e1038050e647109c Mon Sep 17 00:00:00 2001 From: Astri4-4 Date: Fri, 18 Jul 2025 21:26:45 +0000 Subject: [PATCH] feat: Add complete authentication system with frontend and backend integration - Add login and register pages with form validation and file upload support - Implement AuthContext for global authentication state management - Create ProtectedRoute component for route-based authentication - Update Navbar to show user profile and authentication status - Add media controller debugging and fix file serving paths - Fix nginx configuration for proper API proxying and client-side routing - Update Home page to display personalized content for authenticated users - Integrate profile picture display in navigation bar Technical improvements: - Fix nginx rule priority to handle API routes before static assets - Add proper ES modules support in media controller - Implement JWT token persistence with localStorage - Add loading states and error handling throughout auth flow --- Makefile | 23 +++ backend/Dockerfile | 7 +- backend/app/controllers/media.controller.js | 57 +++++ .../controllers/recommendation.controller.js | 39 ++++ backend/app/controllers/user.controller.js | 11 +- backend/app/middlewares/media.middleware.js | 0 backend/app/routes/media.routes.js | 12 ++ backend/app/routes/redommendation.route.js | 4 +- backend/app/utils/database.js | 8 + backend/logs/access.log | 40 ++++ backend/requests/channel.http | 4 +- backend/requests/medias.http | 11 + backend/requests/recommendation.http | 5 +- backend/server.js | 4 +- docker-compose.yaml | 24 ++- frontend/src/App.jsx | 23 ++- frontend/src/components/Navbar.jsx | 57 +++-- frontend/src/components/ProtectedRoute.jsx | 27 +++ frontend/src/components/Recommendations.jsx | 2 +- frontend/src/contexts/AuthContext.jsx | 112 ++++++++++ frontend/src/pages/Home.jsx | 74 ++++--- frontend/src/pages/Login.jsx | 107 ++++++++++ frontend/src/pages/Register.jsx | 195 ++++++++++++++++++ frontend/src/routes/routes.jsx | 19 ++ nginx/default.conf | 22 ++ 25 files changed, 816 insertions(+), 71 deletions(-) create mode 100644 Makefile create mode 100644 backend/app/controllers/media.controller.js create mode 100644 backend/app/middlewares/media.middleware.js create mode 100644 backend/app/routes/media.routes.js create mode 100644 backend/requests/medias.http create mode 100644 frontend/src/components/ProtectedRoute.jsx create mode 100644 frontend/src/contexts/AuthContext.jsx create mode 100644 frontend/src/pages/Login.jsx create mode 100644 frontend/src/pages/Register.jsx 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 ( +
+
Chargement...
+
+ ); + } + + return
{routing}
; +}; + +const App = () => { return ( -
- {routing} -
+ + + ); }; diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index 34b7808..174f22a 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -1,26 +1,58 @@ +import { useAuth } from '../contexts/AuthContext'; +export default function Navbar({ isSearchPage = false }) { + const { user, logout, isAuthenticated } = useAuth(); -export default function Navbar(isSearchPage) { + const handleLogout = () => { + logout(); + }; return ( ) - } \ No newline at end of file diff --git a/frontend/src/components/ProtectedRoute.jsx b/frontend/src/components/ProtectedRoute.jsx new file mode 100644 index 0000000..379bcd2 --- /dev/null +++ b/frontend/src/components/ProtectedRoute.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { Navigate } from 'react-router-dom'; +import { useAuth } from '../contexts/AuthContext'; + +const ProtectedRoute = ({ children, requireAuth = true }) => { + const { isAuthenticated, loading } = useAuth(); + + if (loading) { + return ( +
+
Chargement...
+
+ ); + } + + if (requireAuth && !isAuthenticated) { + return ; + } + + if (!requireAuth && isAuthenticated) { + return ; + } + + return children; +}; + +export default ProtectedRoute; diff --git a/frontend/src/components/Recommendations.jsx b/frontend/src/components/Recommendations.jsx index 22157d1..108ce3f 100644 --- a/frontend/src/components/Recommendations.jsx +++ b/frontend/src/components/Recommendations.jsx @@ -1,6 +1,6 @@ -export default function Recommendations() { +export default function Recommendations({videos}) { return (
diff --git a/frontend/src/contexts/AuthContext.jsx b/frontend/src/contexts/AuthContext.jsx new file mode 100644 index 0000000..087df4d --- /dev/null +++ b/frontend/src/contexts/AuthContext.jsx @@ -0,0 +1,112 @@ +import React, { createContext, useContext, useState, useEffect } from 'react'; + +const AuthContext = createContext(); + +export const useAuth = () => { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +}; + +export const AuthProvider = ({ children }) => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + // Check if user is logged in on app start + useEffect(() => { + const token = localStorage.getItem('token'); + const userData = localStorage.getItem('user'); + + if (token && userData) { + setUser(JSON.parse(userData)); + } + setLoading(false); + }, []); + + const login = async (username, password) => { + try { + const response = await fetch('/api/users/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ username, password }), + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || 'Erreur de connexion'); + } + + // Store token and user data + localStorage.setItem('token', data.token); + localStorage.setItem('user', JSON.stringify(data.user)); + setUser(data.user); + + return data; + } catch (error) { + throw error; + } + }; + + const register = async (email, username, password, profileImage) => { + try { + const formData = new FormData(); + formData.append('email', email); + formData.append('username', username); + formData.append('password', password); + + if (profileImage) { + formData.append('profile', profileImage); + } + + const response = await fetch('/api/users/', { + method: 'POST', + body: formData, + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || 'Erreur lors de la création du compte'); + } + + // After successful registration, log the user in + await login(username, password); + + return data; + } catch (error) { + throw error; + } + }; + + const logout = () => { + localStorage.removeItem('token'); + localStorage.removeItem('user'); + setUser(null); + }; + + const getAuthHeaders = () => { + const token = localStorage.getItem('token'); + return token ? { Authorization: `Bearer ${token}` } : {}; + }; + + const value = { + user, + login, + register, + logout, + getAuthHeaders, + isAuthenticated: !!user, + loading + }; + + return ( + + {children} + + ); +}; diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx index f96fcaf..c8afa3c 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -1,29 +1,31 @@ import Navbar from '../components/Navbar.jsx'; import HeroImage from '../assets/img/hero.png'; import Recommendations from "../components/Recommendations.jsx"; -import {useState} from "react"; +import {useState, useEffect} from "react"; +import { useAuth } from '../contexts/AuthContext'; export default function Home() { - + const { isAuthenticated, user } = useAuth(); const [recommendations, setRecommendations] = useState([]); const [loading, setLoading] = useState(true); const [topCreators, setTopCreators] = useState([]); const [trendingVideos, setTrendingVideos] = useState([]); - // Fetch recommendations, top creators, and trending videos - const fetchData = async () => { - try { - const response = await fetch('/api/home'); - const data = await response.json(); - setRecommendations(data.recommendations); - setTopCreators(data.topCreators); - setTrendingVideos(data.trendingVideos); - } catch (error) { - console.error('Error fetching data:', error); - } finally { - setLoading(false); - } - }; + useEffect(() => { + // Fetch recommendations, top creators, and trending videos + const fetchData = async () => { + try { + const response = await fetch('/api/recommendations'); + const data = await response.json(); + setRecommendations(data.recommendations); + } catch (error) { + console.error('Error fetching data:', error); + } finally { + setLoading(false); + } + }; + fetchData(); + }, []); return (
@@ -32,25 +34,33 @@ export default function Home() {
-

Regarder des vidéos comme jamais auparavant

- - + {isAuthenticated ? ( +

+ Bienvenue {user?.username} ! +

+ ) : ( + <> +

+ Regarder des vidéos comme jamais auparavant +

+ + + )}
- ) - + ); } \ No newline at end of file diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx new file mode 100644 index 0000000..d111c7d --- /dev/null +++ b/frontend/src/pages/Login.jsx @@ -0,0 +1,107 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAuth } from '../contexts/AuthContext'; +import Navbar from '../components/Navbar'; + +export default function Login() { + const [formData, setFormData] = useState({ + username: '', + password: '' + }); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + + const navigate = useNavigate(); + const { login } = useAuth(); + + const handleChange = (e) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value + }); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + setError(''); + setLoading(true); + + try { + await login(formData.username, formData.password); + navigate('/'); + } catch (err) { + setError(err.message || 'Erreur de connexion'); + } finally { + setLoading(false); + } + }; + + return ( +
+ + +
+
+

Connexion

+ + {error && ( +
+ {error} +
+ )} + +
+
+ + +
+ +
+ + +
+ + +
+ +
+

+ Pas encore de compte ?{' '} + + Créer un compte + +

+
+
+
+
+ ); +} diff --git a/frontend/src/pages/Register.jsx b/frontend/src/pages/Register.jsx new file mode 100644 index 0000000..bf74d9e --- /dev/null +++ b/frontend/src/pages/Register.jsx @@ -0,0 +1,195 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAuth } from '../contexts/AuthContext'; +import Navbar from '../components/Navbar'; + +export default function Register() { + const [formData, setFormData] = useState({ + email: '', + username: '', + password: '', + confirmPassword: '', + profile: null + }); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + const [previewImage, setPreviewImage] = useState(null); + + const navigate = useNavigate(); + const { register } = useAuth(); + + const handleChange = (e) => { + if (e.target.name === 'profile') { + const file = e.target.files[0]; + setFormData({ + ...formData, + profile: file + }); + + // Create preview + if (file) { + const reader = new FileReader(); + reader.onload = (e) => setPreviewImage(e.target.result); + reader.readAsDataURL(file); + } else { + setPreviewImage(null); + } + } else { + setFormData({ + ...formData, + [e.target.name]: e.target.value + }); + } + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + setError(''); + + // Validation + if (formData.password !== formData.confirmPassword) { + setError('Les mots de passe ne correspondent pas'); + return; + } + + if (formData.password.length < 6) { + setError('Le mot de passe doit contenir au moins 6 caractères'); + return; + } + + setLoading(true); + + try { + await register(formData.email, formData.username, formData.password, formData.profile); + navigate('/'); + } catch (err) { + setError(err.message || 'Erreur lors de la création du compte'); + } finally { + setLoading(false); + } + }; + + return ( +
+ + +
+
+

Créer un compte

+ + {error && ( +
+ {error} +
+ )} + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + {previewImage && ( +
+ Aperçu +
+ )} +
+ + +
+ +
+

+ Déjà un compte ?{' '} + + Se connecter + +

+
+
+
+
+ ); +} diff --git a/frontend/src/routes/routes.jsx b/frontend/src/routes/routes.jsx index 43fc69b..3d75fc9 100644 --- a/frontend/src/routes/routes.jsx +++ b/frontend/src/routes/routes.jsx @@ -1,7 +1,26 @@ import Home from '../pages/Home.jsx' +import Login from '../pages/Login.jsx' +import Register from '../pages/Register.jsx' +import ProtectedRoute from '../components/ProtectedRoute.jsx' const routes = [ { path: "/", element: }, + { + path: "/login", + element: ( + + + + ) + }, + { + path: "/register", + element: ( + + + + ) + }, ] export default routes; \ No newline at end of file diff --git a/nginx/default.conf b/nginx/default.conf index a3d207b..9a7f5ef 100644 --- a/nginx/default.conf +++ b/nginx/default.conf @@ -4,4 +4,26 @@ server { root /usr/share/nginx/html; index index.html index.htm; + + # API routes - proxy to backend (MUST come before static file rules) + location /api/ { + proxy_pass http://127.0.0.1:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_buffering off; + } + + # Static assets caching (only for frontend assets, not API) + location ~* ^/(?!api/).*\.(js|css|png|jpg|jpeg|gif|ico|svg)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + # Handle React Router - all other routes should serve index.html + location / { + try_files $uri $uri/ /index.html; + } } \ No newline at end of file