Browse Source

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
fix/clean
Astri4-4 5 months ago
parent
commit
ccc415bd94
  1. 23
      Makefile
  2. 7
      backend/Dockerfile
  3. 57
      backend/app/controllers/media.controller.js
  4. 39
      backend/app/controllers/recommendation.controller.js
  5. 11
      backend/app/controllers/user.controller.js
  6. 0
      backend/app/middlewares/media.middleware.js
  7. 12
      backend/app/routes/media.routes.js
  8. 4
      backend/app/routes/redommendation.route.js
  9. 8
      backend/app/utils/database.js
  10. 40
      backend/logs/access.log
  11. 4
      backend/requests/channel.http
  12. 11
      backend/requests/medias.http
  13. 5
      backend/requests/recommendation.http
  14. 4
      backend/server.js
  15. 24
      docker-compose.yaml
  16. 23
      frontend/src/App.jsx
  17. 57
      frontend/src/components/Navbar.jsx
  18. 27
      frontend/src/components/ProtectedRoute.jsx
  19. 2
      frontend/src/components/Recommendations.jsx
  20. 112
      frontend/src/contexts/AuthContext.jsx
  21. 74
      frontend/src/pages/Home.jsx
  22. 107
      frontend/src/pages/Login.jsx
  23. 195
      frontend/src/pages/Register.jsx
  24. 19
      frontend/src/routes/routes.jsx
  25. 22
      nginx/default.conf

23
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

7
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"]

57
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." });
}
}

39
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."});
}
}

11
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});
}

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

12
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;

4
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;

8
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);
}

40
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

4
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

11
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

5
backend/requests/recommendation.http

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

4
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();

24
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:

23
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 (
<div className="min-h-screen bg-linear-to-br from-left-gradient to-right-gradient flex items-center justify-center">
<div className="text-white text-2xl font-montserrat">Chargement...</div>
</div>
);
}
return <div>{routing}</div>;
};
const App = () => {
return (
<div>
{routing}
</div>
<AuthProvider>
<AppContent />
</AuthProvider>
);
};

57
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 (
<nav className="flex justify-between items-center p-4 text-white absolute top-0 left-0 w-screen">
<div>
<h1 className="font-montserrat text-5xl font-black">FreeTube</h1>
<h1 className="font-montserrat text-5xl font-black">
<a href="/">FreeTube</a>
</h1>
</div>
<div>
<ul className="flex items-center space-x-[44px] font-montserrat text-2xl font-black">
<li><a href="/">Abonnements</a></li>
<li><a href="/search">Se connecter</a></li>
<li className="bg-primary p-3 rounded-sm text-white font-montserrat text-2xl font-black cursor-pointer">
<a className="">
<p>Créer un compte</p>
</a>
</li>
{isAuthenticated ? (
<>
<li><a href="/">Abonnements</a></li>
<li className="flex items-center space-x-4">
<span className="text-2xl">{user?.username}</span>
{user?.picture && (
<img
src={`${user.picture}`}
alt="Profile"
className="w-8 h-8 rounded-full object-cover"
/>
)}
</li>
<li>
<button
onClick={handleLogout}
className="bg-red-600 p-3 rounded-sm text-white font-montserrat text-2xl font-black cursor-pointer hover:bg-red-700"
>
Déconnexion
</button>
</li>
</>
) : (
<>
<li><a href="/login">Se connecter</a></li>
<li className="bg-primary p-3 rounded-sm text-white font-montserrat text-2xl font-black cursor-pointer">
<a href="/register">
<p>Créer un compte</p>
</a>
</li>
</>
)}
<li className="bg-glass backdrop-blur-glass border-glass-full border rounded-sm text-white font-montserrat text-2xl font-black cursor-pointer w-[600/1920] h-[50px] px-3 flex items-center justify-center">
<input type="text" name="search" id="searchbar" placeholder="Rechercher" className="font-inter text-2xl font-normal focus:outline-none"/>
<input type="text" name="search" id="searchbar" placeholder="Rechercher" className="font-inter text-2xl font-normal focus:outline-none bg-transparent"/>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
<circle cx="18.381" cy="13.619" r="12.119" stroke="white" stroke-width="3"/>
<line x1="9.63207" y1="22.2035" x2="1.06064" y2="30.7749" stroke="white" stroke-width="3"/>
<circle cx="18.381" cy="13.619" r="12.119" stroke="white" strokeWidth="3"/>
<line x1="9.63207" y1="22.2035" x2="1.06064" y2="30.7749" stroke="white" strokeWidth="3"/>
</svg>
</li>
</ul>
@ -28,5 +60,4 @@ export default function Navbar(isSearchPage) {
</nav>
)
}

27
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 (
<div className="min-h-screen flex items-center justify-center">
<div className="text-white text-xl">Chargement...</div>
</div>
);
}
if (requireAuth && !isAuthenticated) {
return <Navigate to="/login" replace />;
}
if (!requireAuth && isAuthenticated) {
return <Navigate to="/" replace />;
}
return children;
};
export default ProtectedRoute;

2
frontend/src/components/Recommendations.jsx

@ -1,6 +1,6 @@
export default function Recommendations() {
export default function Recommendations({videos}) {
return (
<div>

112
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 (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
};

74
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 (
<div className="min-w-screen min-h-screen bg-linear-to-br from-left-gradient to-right-gradient">
@ -32,25 +34,33 @@ export default function Home() {
<div className="flex flex-col items-center w-screen pt-[304px]">
<img src={HeroImage} alt="" className="w-1046/1920" />
<h1 className="font-montserrat 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-28 -translate-y-[100px] mt-10">
<button className="bg-white text-black font-montserrat p-3 rounded-sm 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-2xl font-bold cursor-pointer">
<a href="/register">
<p>Créer un compte</p>
</a>
</button>
</div>
{isAuthenticated ? (
<h1 className="font-montserrat text-8xl font-black w-1200/1920 text-center text-white -translate-y-1/2">
Bienvenue {user?.username} !
</h1>
) : (
<>
<h1 className="font-montserrat 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-28 -translate-y-[100px] mt-10">
<button className="bg-white text-black font-montserrat p-3 rounded-sm 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-2xl font-bold cursor-pointer">
<a href="/register">
<p>Créer un compte</p>
</a>
</button>
</div>
</>
)}
</div>
<Recommendations/>
</div>
)
);
}

107
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 (
<div className="min-w-screen min-h-screen bg-linear-to-br from-left-gradient to-right-gradient">
<Navbar isSearchPage={false} />
<div className="flex justify-center items-center min-h-screen pt-20">
<div className="bg-white p-8 rounded-lg shadow-lg w-full max-w-md">
<h2 className="text-3xl font-bold text-center mb-6 font-montserrat">Connexion</h2>
{error && (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="username" className="block text-sm font-medium text-gray-700 mb-1">
Nom d'utilisateur
</label>
<input
type="text"
id="username"
name="username"
value={formData.username}
onChange={handleChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Entrez votre nom d'utilisateur"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
Mot de passe
</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Entrez votre mot de passe"
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 font-montserrat"
>
{loading ? 'Connexion...' : 'Se connecter'}
</button>
</form>
<div className="mt-6 text-center">
<p className="text-gray-600">
Pas encore de compte ?{' '}
<a href="/register" className="text-blue-600 hover:underline">
Créer un compte
</a>
</p>
</div>
</div>
</div>
</div>
);
}

195
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 (
<div className="min-w-screen min-h-screen bg-linear-to-br from-left-gradient to-right-gradient">
<Navbar isSearchPage={false} />
<div className="flex justify-center items-center min-h-screen pt-20 pb-10">
<div className="bg-white p-8 rounded-lg shadow-lg w-full max-w-md">
<h2 className="text-3xl font-bold text-center mb-6 font-montserrat">Créer un compte</h2>
{error && (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
Email
</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Entrez votre email"
/>
</div>
<div>
<label htmlFor="username" className="block text-sm font-medium text-gray-700 mb-1">
Nom d'utilisateur
</label>
<input
type="text"
id="username"
name="username"
value={formData.username}
onChange={handleChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Entrez votre nom d'utilisateur"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
Mot de passe
</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Entrez votre mot de passe"
/>
</div>
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700 mb-1">
Confirmer le mot de passe
</label>
<input
type="password"
id="confirmPassword"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Confirmez votre mot de passe"
/>
</div>
<div>
<label htmlFor="profile" className="block text-sm font-medium text-gray-700 mb-1">
Photo de profil (optionnel)
</label>
<input
type="file"
id="profile"
name="profile"
accept="image/*"
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
{previewImage && (
<div className="mt-2">
<img
src={previewImage}
alt="Aperçu"
className="w-20 h-20 object-cover rounded-full mx-auto"
/>
</div>
)}
</div>
<button
type="submit"
disabled={loading}
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 font-montserrat"
>
{loading ? 'Création du compte...' : 'Créer un compte'}
</button>
</form>
<div className="mt-6 text-center">
<p className="text-gray-600">
Déjà un compte ?{' '}
<a href="/login" className="text-blue-600 hover:underline">
Se connecter
</a>
</p>
</div>
</div>
</div>
</div>
);
}

19
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: <Home/> },
{
path: "/login",
element: (
<ProtectedRoute requireAuth={false}>
<Login />
</ProtectedRoute>
)
},
{
path: "/register",
element: (
<ProtectedRoute requireAuth={false}>
<Register />
</ProtectedRoute>
)
},
]
export default routes;

22
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;
}
}
Loading…
Cancel
Save