Browse Source

Merge pull request 'features/loading' (#11) from features/loading into developpement

Reviewed-on: #11
pull/12/head
astria 3 months ago
parent
commit
5ba9662557
  1. 764
      README.md
  2. 18
      backend/Dockerfile
  3. 9
      backend/app/controllers/playlist.controller.js
  4. 1
      backend/app/controllers/user.controller.js
  5. 4
      backend/app/controllers/video.controller.js
  6. 39
      backend/app/routes/channel.route.js
  7. BIN
      backend/app/uploads/profiles/test.jpg
  8. 8
      backend/app/utils/mail.js
  9. 670
      backend/logs/access.log
  10. 291
      backend/package-lock.json
  11. 5
      backend/package.json
  12. 21
      backend/server.js
  13. 2317
      backend/swagger.yaml
  14. 50
      developpement.yaml
  15. 36
      docker-compose.yaml
  16. 49
      documentation.md
  17. 7
      frontend/Docker.Development
  18. 14
      frontend/Dockerfile
  19. 68
      frontend/default.conf
  20. 23
      frontend/nginx-selfsigned.crt
  21. 28
      frontend/nginx-selfsigned.key
  22. 3
      frontend/package.json
  23. 1
      frontend/src/assets/svg/exit_fullscreen.svg
  24. 1
      frontend/src/assets/svg/fullscreen.svg
  25. 6
      frontend/src/components/Recommendations.jsx
  26. 6
      frontend/src/components/SeeLater.jsx
  27. 6
      frontend/src/components/TopCreators.jsx
  28. 6
      frontend/src/components/TrendingVideos.jsx
  29. 17
      frontend/src/modals/LoadingVideoModal.jsx
  30. 14
      frontend/src/pages/AddVideo.jsx
  31. 12
      frontend/src/pages/Register.jsx
  32. 91
      frontend/src/pages/Video.jsx
  33. 8
      nginx/Dockerfile

764
README.md

@ -1,436 +1,502 @@
# FreeTube - Video Sharing Platform
![FreeTube Logo](frontend/src/assets/img/hero.png)
## 📋 Table of Contents
- [Overview](#overview)
- [Features](#features)
- [Tech Stack](#tech-stack)
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Configuration](#configuration)
- [Usage](#usage)
- [API Documentation](#api-documentation)
- [Project Structure](#project-structure)
- [Development](#development)
- [Testing](#testing)
- [Troubleshooting](#troubleshooting)
- [Contributing](#contributing)
## 🎯 Overview
FreeTube is a modern video sharing platform built as a YouTube competitor. It allows users to upload, watch, and interact with videos through comments and likes. The platform features user authentication, video management, channel subscriptions, and a recommendation system.
This project is part of a 3-part development resit assignment focusing on:
- **Part 1**: HTTP server serving HTML pages
- **Part 2**: REST API for video, user, and comment management
- **Part 3**: Interactive frontend user interface
## ✨ Features
### 🔐 Authentication System
- User registration with profile picture upload
- Secure login with JWT tokens
- Persistent sessions with localStorage
- Protected routes and authentication guards
- Profile management and display
### 📹 Video Management
- Video upload with thumbnail generation
- Video streaming and playback
- Video metadata management
- Video search and filtering
### 👥 User Features
- User profiles with customizable avatars
- Channel creation and management
- Subscription system
- User activity tracking
### 💬 Social Features
- Video commenting system
- Like/dislike functionality
- Video recommendations
- Trending videos section
### 🎨 Frontend Features
- Responsive React-based UI
- Modern design with Tailwind CSS
- Client-side routing with React Router
- Real-time updates and interactions
## 🛠 Tech Stack
### Backend
- **Runtime**: Node.js
- **Framework**: Express.js
- **Database**: PostgreSQL
- **Authentication**: JWT (JSON Web Tokens)
- **File Upload**: Multer
- **Testing**: Vitest
### Frontend
- **Framework**: React 18
- **Build Tool**: Vite
- **Styling**: Tailwind CSS
- **Routing**: React Router
- **State Management**: React Context API
- **Fonts**: Montserrat, Inter
### Infrastructure
- **Containerization**: Docker & Docker Compose
- **Reverse Proxy**: Nginx
- **Development**: Hot reload for both frontend and backend
## 📋 Prerequisites
Before you begin, ensure you have the following installed:
- [Docker](https://docs.docker.com/get-docker/) (v20.10+)
- [Docker Compose](https://docs.docker.com/compose/install/) (v2.0+)
- [Git](https://git-scm.com/downloads)
## 🚀 Installation
### Quick Start with Docker (Recommended)
1. **Clone the repository**
```bash
git clone <repository-url>
cd 3RESIT_DOCKER
```
2. **Set up environment variables**
Create a `.env` file in the root directory:
```env
# Database Configuration
POSTGRES_USER=freetube_user
POSTGRES_PASSWORD=your_secure_password
POSTGRES_DB=freetube_db
POSTGRES_HOST=db
# Backend Configuration
BACKEND_PORT=8000
JWT_SECRET=your_jwt_secret_key_min_32_chars
LOG_FILE=/var/log/freetube/access.log
```
3. **Build and start the application**
```bash
docker-compose up --build
```
4. **Access the application**
- Frontend: http://localhost
- Backend API: http://localhost:8000
- Database: localhost:5432
### Manual Installation
<details>
<summary>Click to expand manual installation steps</summary>
#### Backend Setup
```bash
cd backend
npm install
npm run dev
```
#### Frontend Setup ## Sommaire
```bash
cd frontend 1) Introduction
npm install 2) Description du projet
npm run dev 3) La pile technologique
``` 1) Le serveur
2) Le site web
3) La base de données
4) Le serveur
1) Les dépendances
2) Le fonctionnement
5) Le site web
1) Les dépendances
2) Le fonctionnement
6) La base de données
7) Installation du projet
1) Docker Compose
2) Script Shell
3) Manuellement
8) Conclusion
## Introduction
Cette documentation est à destination des futurs développeurs travaillant sur Freetube. Elle a pour but d’expliquer le fonctionnement technique de toutes les couches de l’application et à justifier les choix pris pour chaque langage et framework. 
La documentation est une ressource indispensable pour tous ceux voulant comprendre le fonctionnement interne de Freetube. Les parties démontrées ici couvrent toutes les couches de l’application. Par ailleurs des diagrammes UML et schémas de base de données vous sont fournies pour une meilleure compréhension du code. Un indexe est disponible en fin de document. 
Cette documentation est à un but technique, elle rentre volontairement dans les détails du fonctionnement de chaque partie. Elle n’est pas faite pour être lu par un utilisateur, un manuel utilisateur vous a été fournies pour remplir ce besoin.
De plus les documentations externes sont disponible en fin de document et un **Swagger** est présent sur l'endpoint `/api/api-docs` pour une documentation de l'API plus approfondie.
## Description du projet
Il m’a été demandé de créer une plateforme concurrente à YouTube nommée Freetube. Cette alternative a pour but de mieux remplir les demandes des utilisateurs, pas d’abonnement ni publicité pour consommer ou poster des vidéos sur la plateforme. 
Le cahier des charges du projet demande certaines fonctionnalités à implémenter. Les utilisateurs doivent pourvoir regarder des vidéos sans avoir à se connecter où à créer de compte, ils doivent pouvoir créer un compte et le gérer, pouvoir créer une chaîne la gérer et y poster des vidéos. La fonctionnalité de vidéo privé a aussi été demandé. 
Pour ce projet j’avais la main libre sur la pile technologique à utiliser tout en respectant les demandes d’efficacité d’une plateforme de streaming vidéo.
## La pile technologique
### Le serveur
Le serveur a été codé en **Nodejs**, j’ai choisi ce langage car il permet d’implémenter une **API REST** efficacement grâce à son implémentation native de l’asynchrone indispensable pour une API REST. NodeJS étant basé sur **Javascript** il n’est pas le plus efficient mais ce n’est pas dérangeant car nous travaillons avec une API, le temps de réponse sera biaisé par la connexion internet de l’utilisateur.
### Le site web
Le site web a lui été codé en **ReactJS** une librairie Javascript permettant de créer des interfaces utilisateur. ReactJS étant lui aussi basé sur Javascript, cela permet une maintenabilité plus simple car les deux parties sont dans le même langages, de plus le site web bénéficie lui aussi de l’implémentation de l’asynchrone. J’ai choisi d’utiliser ReactJS car il permet l’utilisation de **components** permettant le live reload et évite la duplication de code inutile.
### La base de données
#### Database Setup La base de données est en **PostgreSQL**, un langage basé sur **SQL** largement utilisé pour communiquer avec une base de données. Cependant PostgreSQL possède quelques avantages par rapport à une base de données comme **MySQL**, il intègre une très bonne gestion du **JSON** que j’ai beaucoup utilisé dans ce projet et il est **Open Source**.
- Install PostgreSQL
- Create database and user
- Run migrations (if available)
</details>
## ⚙️ Configuration ## Le serveur
### Environment Variables ### Les dépendances
| Variable | Description | Default | Required | Le serveur NodeJS sert de plateforme entre le site web et la base de données, il doit donc pouvoir recevoir des requêtes HTTP et envoyer des requêtes SQL. Pour les requêtes HTTP j’ai choisis le framework **ExpressJS** car il est très connu et a donc beaucoup de contenu disponible sur internet. Pour les requêtes SQL j'ai utilisé la librairie **pg** qui permet de communiquer avec PostgreSQL, cette librairie peux gérer les fermetures de connexion inutile comme les time out ou les oublies. 
|----------|-------------|---------|----------|
| `POSTGRES_USER` | Database username | - | ✅ |
| `POSTGRES_PASSWORD` | Database password | - | ✅ |
| `POSTGRES_DB` | Database name | - | ✅ |
| `POSTGRES_HOST` | Database host | db | ✅ |
| `BACKEND_PORT` | Backend server port | 8000 | ✅ |
| `JWT_SECRET` | JWT signing secret | - | ✅ |
| `LOG_FILE` | Log file path | /var/log/freetube/access.log | ❌ |
### Docker Volumes Puisque l’API transmet des données sensibles j’ai dû sécuriser les endpoints en vérifiant les données entrantes, pour cela j’ai utilisé **express-validator** qui permet de créer des **middlewares** pour vérifier les données. Pour les images et les fichiers vidéo j’ai utilisé **multer** qui s’intègre très bien avec ExpressJS. Pour l’authentification par **Github** j’ai choisi de passé par **PassportJS** qui s’occupe du passage de token entre GitHub et le backend. 
The application uses the following volumes: Pour la gestion de l’authentification j’ai utilisé **Json Web Token** qui permet de générer et de vérifier des tokens d’authentifications. Pour l’encryption des mots de passe **Bcrypt** a été utilisé.
- `./backend/logs:/var/log/freetube` - Application logs
- `./backend/app/uploads:/app/app/uploads` - Uploaded files (videos, images)
- Database data volume for PostgreSQL persistence
## 📖 Usage ### Le fonctionnement
### Getting Started Chaque endpoints et diviser en trois parties distinctes, la définition de la route, qui va définir l’URL à appeler, les middlewares qui vont effectuer les vérifications et modifications des données avant leur utilisation, et les controllers qui vont faire les actions (appels de base données, déplacement de fichier...). Des diagrammes UML expliquant les routes unes-à-unes est disponible dans le dossier “Diagramme_UML”.
1. **Create an Account** Chaque endpoints qui doivent être protégées par l’utilisation d’un compte utilisent le middleware “auth.middleware.js” pour vérifier la validité d’un token.
- Navigate to http://localhost
- Click "Créer un compte"
- Fill in your details and optionally upload a profile picture
- Submit the form to register and automatically log in
2. **Login** ## Le site web
- Click "Se connecter" on the homepage
- Enter your username and password
- You'll be redirected to the authenticated homepage
3. **Upload Videos** ### Les dépendances
- Use the API endpoints to upload videos (see API documentation)
- Videos are stored in the uploads directory
4. **Browse Content** Le site web ne doit faire aucun calcul, tout passe donc par des requête HTTP, j'ai donc utilisé la librairie **fetch** intégré à NodeJS dans sa version 22. 
- View recommendations on the homepage
- Search for videos using the search bar
- Browse trending videos and top creators
### Authentication Flow Pour l’hébergement j’ai choisi **NGINX** car il permet de d’héberger un site et de faire des redirections, il m’a permis de rediriger les requêtes vers l’API en passant par la route “/api/” ce qui évite d’exposer des ports inutilement. 
The authentication system works as follows: NGINX permet aussi de mettre en place l’**HTTPS** avec des **certificats SSL** ce qui chiffre les requêtes du site et de l'API. 
1. User registers/logs in through the frontend forms
2. Backend validates credentials and returns JWT token
3. Token is stored in localStorage for persistence
4. Protected routes check authentication status
5. Navbar updates to show user profile and logout option
## 📚 API Documentation Le site fonctionnant avec ReactJS nécessite l’utilisation de **Vite** pour le développement et le déploiement. 
### Authentication Endpoints Freetube est un site multi-page et doit utiliser un système de routage. Pour cela j’ai utilisé **React Router 7** car il est très utilisé et donc très bien documenté.
#### Register User Pour la partie style du site web, j'ai utilisé TailwindCSS dans sa version 4.0. Tailwind permet de créer des classes CSS directement depuis le JSX et prend en charge le responsive grâce a des **breakpoints**. Il est notamment plus léger que ses concurrents car il créer ses classes CSS au moment du build contrairement, par exemple, a Bootstrap qui à besoin d'un fichier contenant toue les classes CSS de la librairie pour fonctionner.
```http
POST /api/users/
Content-Type: multipart/form-data
email: user@example.com ### Le fonctionnement
username: johndoe
password: securepassword Les éléments du site sont divisés en plusieurs parties, les pages sont dans le dossier “/src/pages” et servent à accueillir et à mettre en forme les composants et à appeler les services.  
picture: [file upload]
Les composants dans le dossier “/src/components” servent à diviser le code et à éviter la duplication, un composant peut être appelé plusieurs fois sur plusieurs pages différentes. Les composants ne font pas d'appel aux services, les événements liés aux composants sont passer en paramètre de ces dernier. 
Les modales sont dans le dossier “/src/modals” et sont toujours afficher au-dessus de la vue principale. Comme les composants elles ne fonts aucun appels aux services, les événements liés aux modales sont passer en paramètre de ces derniers. Elles sont toujours appelées en fin de fichier. 
Les services présent dans le dossier “/src/services” sont les seuls fichiers faisant appel à l’API (à l’exception du fichier AuthContext.jsx). Les services sont organisés de la même manière que les endpoints. Un service peut être appelé plusieurs fois dans plusieurs pages. 
Les routes utilisées par React Router sont présente dans le fichier “/src/routes/route.jsx”. Les routes ayant besoin d’un compte sont protégées par “ProtectedRoute” et rédigeront automatiquement à la page de connexion. React Router n’étant pas directement compatible avec le système de NGINX une configuration supplémentaire est nécessaire, elle est détaillée dans le fichier “/nginx/default.conf”.
## La base de données
La structure de la base de données est créée automatiquement par le serveur au lancement, chaque modification effectuée doit être modifié dans le fichier `/backend/src/utils/database.js` dans la fonction `initDb()`.  
La base de données étant relationnelle, elle repose sur beaucoup de clé étrangère détaillés dans le schéma fourni. A savoir que les enfants se détruisent automatiquement si le lien parent est supprimé grâce à la condition “ON CASCADE” présente dans chacun des liens.  
Le port de la base de données (5432 par defaut) ne doit jamais être exposé sans pare-feu, seul le serveur doit y avoir accès. Pour cela PostgreSQL propose deux fichiers de configuration. `pg_hba` créer des règles internes en fonction de l’utilisateur, la base cible et l’IP du client et `postgres.conf` qui permet de définir un schéma d’IP autorisé. A savoir que si le projet et lancé via Docker seul localhost peut avoir accès à cette base de données.
## Installation et lancement
**Ces instructions sont prévues pour un serveur tournant sous Ubuntu 24.04/Debian 12.** Par conséquent certaines commandes peuvent être incompatible avec votre système, cependant cotre système d'exploitation fournis des commandes alternatives.
Freetube peut être installé de trois manière différentes :
- Docker Compose
- Script Shell
- Manuellement
### Installation avec Docker Compose
#### Installer Docker et Docker Compose
De part la [documentation officielle de Docker](https://docs.docker.com/engine/install/ubuntu/)
```bash
# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository to Apt sources:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
``` ```
#### Login User #### Paramétrages NGINX
```http Quelque modification doivent être faites pour le fonctionnement de NGINX
POST /api/users/login ```nginx
Content-Type: application/json server {
#------------------------------ ici -------------------------------
server_name <url du serveur>;
#------------------------------------------------------------------
listen 80;
return 301 https://$host$request_uri;
}
server {
#------------------------------ ici -------------------------------
server_name <url du serveur>;
#------------------------------------------------------------------
listen 443 ssl;
root /usr/share/nginx/html;
index index.html index.htm;
client_max_body_size 500M;
ssl_certificate /etc/nginx/ssl/nginx-selfsigned.crt;
ssl_certificate_key /etc/nginx/ssl/nginx-selfsigned.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location /api/ {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '$http_origin' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' 1728000 always;
add_header 'Content-Type' 'text/plain; charset=utf-8' always;
add_header 'Content-Length' 0 always;
return 204;
}
proxy_pass http://resit_backend: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_set_header Origin $http_origin;
proxy_buffering off;
add_header 'Access-Control-Allow-Origin' '$http_origin' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
location ~* ^/(?!api/).*\.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
add_header Pragma "no-cache";
add_header Expires "0";
try_files $uri =404;
}
location / {
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
try_files $uri $uri/ /index.html;
}
{
"username": "johndoe",
"password": "securepassword"
} }
``` ```
### Media Endpoints #### Création de clé d'API Gmail
Rendez-vous sur [Gmail](https://gmail.com) et allez dans le panel d'administration de votre compte
Dans la barre de recherche tapez `Mot de passe des applications`
Créer un mot de passe et gardez le de côté il servira pour les variables d'environnements
#### Création Application OAuth Github
Rendez-vous sur [Github](https://github.com) et allez dans les paramètres de votre compte
En bas du menu à gauche, cliquez sur `Paramètres de développeur` puis cliquez sur `Application OAuth`
Créez une nouvelle application et gardez les clé de côté, elle serviront pour les variables d'environnements
#### Get Profile Picture #### Mise en place des variables d'environnements
```http
GET /api/media/profile/{filename} A la racine du projet créer un fichier `.env`
```bash
touch .env
``` ```
#### Get Video Thumbnail A l'aide de l'éditeur de votre choix entrez dans le fichier
```http ```
GET /api/media/thumbnail/{filename} nano .env
``` ```
### Additional Endpoints Rentrez les informations dans ce format
/!\ les valeurs non-entourées de chevrons **ne doivent pas être modifié**.
```env
POSTGRES_USER=<utilisateur_de_la_base>
POSTGRES_PASSWORD=<mot_de_passe>
POSTGRES_DB=<nom_de_la_base>
POSTGRES_HOST=db
- **Videos**: `/api/videos/` BACKEND_PORT=8000
- **Comments**: `/api/comments/`
- **Channels**: `/api/channels/`
- **Playlists**: `/api/playlists/`
- **Recommendations**: `/api/recommendations/`
For detailed API documentation, check the `.http` files in the `backend/requests/` directory. JWT_SECRET=<votre_clé_JWT>
## 📁 Project Structure LOG_FILE=/var/log/freetube/access.log
``` GMAIL_USER=<adresse e-mail>
3RESIT_DOCKER/ GMAIL_PASSWORD=<mot_de_passe_créer_precedemment>
├── backend/ # Node.js Express backend
│ ├── app/ FRONTEND_URL=<URL_HTTPS_de_votre_nginx>
│ │ ├── controllers/ # Request handlers
│ │ ├── middlewares/ # Express middlewares
│ │ ├── routes/ # API route definitions
│ │ ├── uploads/ # File storage
│ │ └── utils/ # Utility functions
│ ├── logs/ # Application logs
│ ├── requests/ # HTTP request examples
│ └── test/ # Test files
├── frontend/ # React frontend
│ ├── src/
│ │ ├── components/ # Reusable React components
│ │ ├── contexts/ # React Context providers
│ │ ├── pages/ # Page components
│ │ ├── routes/ # Route configuration
│ │ └── assets/ # Static assets
│ └── public/ # Public assets
├── nginx/ # Nginx configuration
└── docker-compose.yaml # Docker orchestration
```
## 🔧 Development GITHUB_ID=<ID_github>
### Available Scripts GITHUB_SECRET=<secret_github>
#### Backend
#### Lancement
Pour lancer le groupe de conteneur
```bash ```bash
npm run dev # Start development server with hot reload docker compose up -d # pour détacher de la session
npm run start # Start production server
npm run test # Run tests
``` ```
#### Frontend ### Installation via le Script Shell
#### Ajout des autorisations
Pour ajouter les autorisations nécessaire au lancement du script
```bash ```bash
npm run dev # Start development server chmod +x ./deploy.sh
npm run build # Build for production
npm run preview # Preview production build
npm run lint # Run ESLint
``` ```
#### Docker Commands #### Création de clé d'API Gmail
```bash
# Start all services
docker-compose up --build
# Stop all services Rendez-vous sur [Gmail](https://gmail.com) et allez dans le panel d'administration de votre compte
docker-compose down Dans la barre de recherche tapez `Mot de passe des applications`
Créer un mot de passe et gardez le de côté il servira pour les variables d'environnements
# View logs #### Création Application OAuth Github
docker-compose logs [service-name]
# Restart specific service Rendez-vous sur [Github](https://github.com) et allez dans les paramètres de votre compte
docker-compose restart [service-name] En bas du menu à gauche, cliquez sur `Paramètres de développeur` puis cliquez sur `Application OAuth`
Créez une nouvelle application et gardez les clé de côté, elle serviront pour les variables d'environnements
# Reset database #### Lancer l'installation
docker-compose down --volumes
Lancer le script et répondez au question
```bash
./deploy.sh
```
Lancer le projet
```bash
cd backend && npm run start
cd frontend && npx vite build
systemctl enable --now nginx # Pour un démarrage automatique au lancement de la machine
systemctl enable --now postgresql
``` ```
### Development Workflow ### Installation manuelle
1. **Backend Changes**: Automatically reload with nodemon #### Création de clé d'API Gmail
2. **Frontend Changes**: Hot module replacement with Vite
3. **Database Changes**: Restart containers to apply schema changes
4. **Nginx Changes**: Restart nginx service
### File Upload Testing Rendez-vous sur [Gmail](https://gmail.com) et allez dans le panel d'administration de votre compte
Dans la barre de recherche tapez `Mot de passe des applications`
Créer un mot de passe et gardez le de côté il servira pour les variables d'environnements
Use the provided `.http` files in `backend/requests/` to test API endpoints: #### Création Application OAuth Github
- `user.http` - User registration and authentication
- `video.http` - Video management
- `medias.http` - Media file serving
- `comment.http` - Comment system
## 🧪 Testing Rendez-vous sur [Github](https://github.com) et allez dans les paramètres de votre compte
En bas du menu à gauche, cliquez sur `Paramètres de développeur` puis cliquez sur `Application OAuth`
Créez une nouvelle application et gardez les clé de côté, elle serviront pour les variables d'environnements
### Running Tests #### Mise en place des variables d'environnements
A la racine du projet créer un fichier `.env`
```bash ```bash
# Backend tests touch .env
cd backend ```
npm test
# Frontend tests (if configured) A l'aide de l'éditeur de votre choix entrez dans le fichier
cd frontend
npm test
``` ```
nano .env
```
Rentrez les informations dans ce format
/!\ les valeurs non-entourées de chevrons **ne doivent pas être modifié**.
```env
POSTGRES_USER=<utilisateur_de_la_base>
POSTGRES_PASSWORD=<mot_de_passe>
POSTGRES_DB=<nom_de_la_base>
POSTGRES_HOST=db
### Test Structure BACKEND_PORT=8000
- **Unit Tests**: Individual component/function testing JWT_SECRET=<votre_clé_JWT>
- **Integration Tests**: API endpoint testing
- **E2E Tests**: Full application workflow testing
Current test coverage includes: LOG_FILE=/var/log/freetube/access.log
- User authentication
- Video management
- Comment system
- Channel operations
- Playlist functionality
## 🔍 Troubleshooting GMAIL_USER=<adresse e-mail>
GMAIL_PASSWORD=<mot_de_passe_créer_precedemment>
### Common Issues FRONTEND_URL=<URL_HTTPS_de_votre_nginx>
#### Authentication Problems GITHUB_ID=<ID_github>
- **Blank screen on reload**: Check browser console for context errors
- **Login not persisting**: Verify JWT token in localStorage
- **Registration fails**: Check file upload size limits
#### Media File Issues GITHUB_SECRET=<secret_github>
- **404 on images**: Verify nginx proxy configuration ```
- **Upload fails**: Check file permissions and upload directory
#### Docker Issues #### Installation des paquets
- **Containers won't start**: Check port conflicts
- **Database connection fails**: Verify environment variables
- **Build failures**: Clear Docker cache with `docker system prune`
### Debug Commands Pour installer PostgreSQL/NGINX
```bash
apt install nginx postgresql
```
Pour installer NodeJS de part la [documentation officielle](https://nodejs.org/en/download/)
```bash ```bash
# Check container status # Download and install nvm:
docker-compose ps curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
# View service logs # in lieu of restarting the shell
docker-compose logs -f [service-name] \. "$HOME/.nvm/nvm.sh"
# Access container shell # Download and install Node.js:
docker-compose exec [service-name] /bin/bash nvm install 22
# Reset everything # Verify the Node.js version:
docker-compose down --volumes --rmi all node -v # Should print "v22.19.0".
docker system prune -a nvm current # Should print "v22.19.0".
# Verify npm version:
npm -v # Should print "10.9.3".
``` ```
### Performance Optimization #### Installations des dépendances NodeJS
- Enable nginx caching for static assets Pour le serveur
- Implement image optimization for uploads ```bash
- Use CDN for media file delivery cd backend && npm i --production
- Database query optimization ```
- Frontend code splitting
## 🤝 Contributing Pour le site web
```bash
cd frontend && npm i --production
npx vite build # pour la construction du site
```
1. Fork the repository #### Configuration de NGINX
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
### Code Style Dans `/etc/nginx/conf.d/` ajouter le fichier `freetube.conf` avec cette configuration
```nginx
server {
server_name <url du serveur>;
listen 80;
return 301 https://$host$request_uri;
}
- **Backend**: ESLint with Node.js rules server {
- **Frontend**: ESLint with React rules server_name <url du serveur>;
- **Formatting**: Prettier for consistent code style listen 443 ssl;
- **Commits**: Conventional commit messages
root /usr/share/nginx/html;
index index.html index.htm;
client_max_body_size 500M;
ssl_certificate /etc/nginx/ssl/nginx-selfsigned.crt;
ssl_certificate_key /etc/nginx/ssl/nginx-selfsigned.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location /api/ {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '$http_origin' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' 1728000 always;
add_header 'Content-Type' 'text/plain; charset=utf-8' always;
add_header 'Content-Length' 0 always;
return 204;
}
proxy_pass http://resit_backend: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_set_header Origin $http_origin;
proxy_buffering off;
add_header 'Access-Control-Allow-Origin' '$http_origin' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
location ~* ^/(?!api/).*\.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
add_header Pragma "no-cache";
add_header Expires "0";
try_files $uri =404;
}
location / {
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
try_files $uri $uri/ /index.html;
}
## 📜 License }
```
This project is part of an educational assignment. All rights reserved. #### Création de la base de données
Pour créer l'utilisateur PostgreSQL
```postgresql
CREATE ROLE "<nom_utilisateur>" WITH PASSWORD "<mot_de_passe>";
```
Pour créer la base
```postgresql
CREATE DATABASE "<nom_de_la_base>" OWNER "<nom_utilisateur";
```
## 👥 Authors #### Créer le fichier de log
Pour créer le fichier de log
```bash
mkdir -p /var/log/freetube/
touch /var/log/freetube/access.log
```
- **Developer**: [Your Name] #### Activer les services
- **Institution**: [Institution Name] Pour activer et lancer les services
- **Course**: 3 RESIT - Web Development ```bash
systemctl enable --now postgresql
systemctl enable --now nginx
```
--- #### Lancement
Pour lancer Freetube
```bash
cd backend
npm run start
```
## 📞 Support ## Conclusion
For support and questions: ## Documentations externes
- Create an issue in the repository
- Check the troubleshooting section
- Review the API documentation
**Happy coding! 🚀** [NodeJS](https://nodejs.org/docs/latest/api/)
[ReactJS](https://react.dev/)
[Vite](https://vite.dev/guide/)
[ExpressJS](https://expressjs.com/en/guide/routing.html)
[NGINX](https://nginx.org/en/docs/)
[PostgreSQL](https://www.postgresql.org/docs/)
[Multer](https://www.npmjs.com/package/multer)
[TailwindCSS v4.0](https://tailwindcss.com/docs/installation/using-vite)
[PassportJS](https://www.passportjs.org/docs/)
[Swagger](https://swagger.io/docs/)

18
backend/Dockerfile

@ -1,22 +1,14 @@
FROM node:20-alpine FROM node:22-alpine
# Set the working directory
WORKDIR /app WORKDIR /app
# Copy package.json and package-lock.json
COPY package*.json ./ COPY package*.json ./
# Install dependencies
RUN npm install --production RUN npm install --production
# Copy the rest of the application code
COPY . .
# Expose the port the app runs on
EXPOSE 8000
# Install netcat for health checks COPY . .
RUN apk add --no-cache netcat-openbsd
# Install the cli tools EXPOSE 8000
RUN chmod +x ./freetube.sh
RUN cp ./freetube.sh /usr/local/bin/freetube
# Start the application # Start the application
CMD ["npm", "start"] CMD ["npm", "start"]

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

@ -238,6 +238,7 @@ export async function getSeeLater(req, res) {
const client = await getClient(); const client = await getClient();
const query = ` const query = `
SELECT SELECT
COALESCE(
JSON_AGG( JSON_AGG(
json_build_object( json_build_object(
'video_id', videos.id, 'video_id', videos.id,
@ -258,6 +259,8 @@ export async function getSeeLater(req, res) {
'description', channels.description 'description', channels.description
) )
) )
) FILTER (WHERE videos.id IS NOT NULL),
'[]'::json
) AS videos ) AS videos
FROM FROM
public.playlists public.playlists
@ -285,14 +288,14 @@ export async function getSeeLater(req, res) {
try { try {
const result = await client.query(query, [userId]); const result = await client.query(query, [userId]);
if (result.rows.length === 0) { if (result.rows.length === 0) {
logger.write("No 'See Later' playlist found for user with id " + userId, 404); logger.write("No 'See Later' playlist found for user with id " + userId, 200);
client.release(); client.release();
res.status(404).json({ error: "'See Later' playlist not found" }); res.status(200).json([]);
return; return;
} }
logger.write("'See Later' playlist retrieved for user with id " + userId, 200); logger.write("'See Later' playlist retrieved for user with id " + userId, 200);
client.release(); client.release();
res.status(200).json(result.rows[0].videos); res.status(200).json(result.rows[0].videos || []);
} catch (error) { } catch (error) {
logger.write("Error retrieving 'See Later' playlist: " + error.message, 500); logger.write("Error retrieving 'See Later' playlist: " + error.message, 500);
client.release(); client.release();

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

@ -130,7 +130,6 @@ export async function register(req, res) {
logger?.write("failed to register user", 500); logger?.write("failed to register user", 500);
res.status(500).json({ error: "Internal server error" }); res.status(500).json({ error: "Internal server error" });
} finally { } finally {
client.release();
} }
} }

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

@ -123,7 +123,7 @@ export async function upload(req, res) {
${video.description} ${video.description}
</p> </p>
<div style="text-align: center;"> <div style="text-align: center;">
<a href="${process.env.FRONTEND_URL}/videos/${video.id}" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #ffffff; text-decoration: none; padding: 12px 25px; border-radius: 6px; font-size: 16px; font-weight: bold; display: inline-block;"> <a href="${process.env.FRONTEND_URL}/videos/${id}" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #ffffff; text-decoration: none; padding: 12px 25px; border-radius: 6px; font-size: 16px; font-weight: bold; display: inline-block;">
Regarder la vidéo Regarder la vidéo
</a> </a>
</div> </div>
@ -492,7 +492,7 @@ export async function getSimilarVideos(req, res) {
JOIN video_tags vt ON v.id = vt.video JOIN video_tags vt ON v.id = vt.video
JOIN tags t ON vt.tag = t.id JOIN tags t ON vt.tag = t.id
JOIN channels c ON v.channel = c.id JOIN channels c ON v.channel = c.id
WHERE t.name = ANY($1) AND v.id != $2 WHERE t.name = ANY($1) AND v.id != $2 AND v.visibility = 'public'
GROUP BY v.id, c.name, c.id GROUP BY v.id, c.name, c.id
LIMIT 10; LIMIT 10;
`; `;

39
backend/app/routes/channel.route.js

@ -14,7 +14,44 @@ import {addLogger} from "../middlewares/logger.middleware.js";
const router = Router(); const router = Router();
// CREATE CHANNEL /**
* @swagger
* tags:
* name: Channels
* description: API for managing channels
* /:
* post:
* summary: Create a new channel
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* description:
* type: string
* owner:
* type: string
* responses:
* 201:
* description: Channel created successfully
* content:
* application/json:
* schema:
* type: object
* properties:
* id:
* type: string
* name:
* type: string
* description:
* type: string
* owner:
* type: string
*/
router.post("/", [addLogger, isTokenValid, ChannelCreate.name, ChannelCreate.description, ChannelCreate.owner, validator, doUserExistsBody, doUserHaveChannel, isOwnerBody, doChannelNameExists], create); router.post("/", [addLogger, isTokenValid, ChannelCreate.name, ChannelCreate.description, ChannelCreate.owner, validator, doUserExistsBody, doUserHaveChannel, isOwnerBody, doChannelNameExists], create);
// GET CHANNEL BY ID // GET CHANNEL BY ID

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

8
backend/app/utils/mail.js

@ -12,6 +12,14 @@ function getTransporter() {
}); });
}; };
/**
* Send an email
* @param {string} to
* @param {string} subject
* @param {string} text
* @param {string} html
* @return {Promise<object>}
*/
export function sendEmail(to, subject, text, html = null) { export function sendEmail(to, subject, text, html = null) {
const transporter = getTransporter(); const transporter = getTransporter();
const mailOptions = { const mailOptions = {

670
backend/logs/access.log

@ -11402,3 +11402,673 @@
[2025-08-27 14:02:56.751] [undefined] GET(/:id/history): successfully retrieved history of user 4 with status 200 [2025-08-27 14:02:56.751] [undefined] GET(/:id/history): successfully retrieved history of user 4 with status 200
[2025-08-27 14:02:56.755] [undefined] GET(/:id/channel): successfully retrieved channel of user 4 with status 200 [2025-08-27 14:02:56.755] [undefined] GET(/:id/channel): successfully retrieved channel of user 4 with status 200
[2025-08-27 14:02:56.761] [undefined] GET(/user/:id): Playlists retrieved for user with id 4 with status 200 [2025-08-27 14:02:56.761] [undefined] GET(/user/:id): Playlists retrieved for user with id 4 with status 200
[2025-08-31 10:18:51.355] [undefined] GET(/:id/history): try to retrieve history of user 1
[2025-08-31 10:18:51.361] [undefined] GET(/:id/channel): try to retrieve channel of user 1
[2025-08-31 10:18:51.367] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200
[2025-08-31 10:18:51.373] [undefined] GET(/:id/history): successfully retrieved history of user 1 with status 200
[2025-08-31 10:18:51.384] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-08-31 10:18:55.307] [undefined] GET(/:id): try to get channel with id 1
[2025-08-31 10:18:55.354] [undefined] GET(/:id/stats): try to get stats
[2025-08-31 10:18:55.360] [undefined] GET(/:id): Successfully get channel with id 1 with status 200
[2025-08-31 10:18:55.371] [undefined] GET(/:id/stats): Successfully get stats with status 200
[2025-08-31 10:19:10.594] [undefined] GET(/:id/channel): try to retrieve channel of user 1
[2025-08-31 10:19:10.599] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200
[2025-08-31 10:22:07.050] [undefined] GET(/:id/channel): try to retrieve channel of user 1
[2025-08-31 10:22:07.056] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200
[2025-08-31 10:22:30.580] [undefined] GET(/:id/channel): try to retrieve channel of user 1
[2025-08-31 10:22:30.586] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200
[2025-08-31 10:22:48.827] [undefined] GET(/:id/channel): try to retrieve channel of user 1
[2025-08-31 10:22:48.834] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200
[2025-08-31 10:22:56.007] [undefined] GET(/:id/channel): try to retrieve channel of user 1
[2025-08-31 10:22:56.012] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200
[2025-08-31 10:23:10.939] [undefined] GET(/:id/channel): try to retrieve channel of user 1
[2025-08-31 10:23:10.945] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200
[2025-08-31 10:23:11.668] [undefined] GET(/:id/channel): try to retrieve channel of user 1
[2025-08-31 10:23:11.673] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200
[2025-08-31 10:23:47.510] [undefined] GET(/:id/channel): try to retrieve channel of user 1
[2025-08-31 10:23:47.516] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200
[2025-08-31 10:23:53.769] [undefined] GET(/:id/channel): try to retrieve channel of user 1
[2025-08-31 10:23:53.775] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200
[2025-08-31 10:25:15.205] [undefined] GET(/:id/channel): try to retrieve channel of user 1
[2025-08-31 10:25:15.210] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200
[2025-08-31 10:25:35.308] [undefined] GET(/:id/channel): try to retrieve channel of user 1
[2025-08-31 10:25:35.313] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200
[2025-08-31 10:25:52.991] [undefined] GET(/:id/channel): try to retrieve channel of user 1
[2025-08-31 10:25:52.997] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200
[2025-08-31 10:26:48.298] [undefined] GET(/:id/channel): try to retrieve channel of user 1
[2025-08-31 10:26:48.304] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200
[2025-08-31 10:26:55.482] [undefined] GET(/:id/channel): try to retrieve channel of user 1
[2025-08-31 10:26:55.488] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200
[2025-08-31 10:27:21.463] [undefined] GET(/:id/channel): try to retrieve channel of user 1
[2025-08-31 10:27:21.469] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200
[2025-08-31 10:28:55.696] [undefined] GET(/:id/channel): try to retrieve channel of user 1
[2025-08-31 10:28:55.701] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200
[2025-08-31 10:29:12.334] [undefined] POST(/): try to upload video with status undefined
[2025-08-31 10:29:12.342] [undefined] POST(/): successfully uploaded video with status 200
[2025-08-31 10:29:12.457] [undefined] POST(/thumbnail): try to add thumbnail to video 4
[2025-08-31 10:29:12.464] [undefined] POST(/thumbnail): successfully uploaded thumbnail with status 200
[2025-08-31 10:29:12.492] [undefined] PUT(/:id/tags): try to add tags to video 4
[2025-08-31 10:29:12.505] [undefined] PUT(/:id/tags): successfully added tags to video 4 with status 200
[2025-08-31 10:29:45.158] [undefined] GET(/:id/channel): try to retrieve channel of user 1
[2025-08-31 10:29:45.164] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200
[2025-08-31 10:29:58.657] [undefined] POST(/): try to upload video with status undefined
[2025-08-31 10:29:58.663] [undefined] POST(/): successfully uploaded video with status 200
[2025-08-31 10:29:58.774] [undefined] POST(/thumbnail): try to add thumbnail to video 5
[2025-08-31 10:29:58.780] [undefined] POST(/thumbnail): successfully uploaded thumbnail with status 200
[2025-08-31 10:29:58.805] [undefined] PUT(/:id/tags): try to add tags to video 5
[2025-08-31 10:29:58.820] [undefined] PUT(/:id/tags): successfully added tags to video 5 with status 200
[2025-08-31 10:29:58.856] [undefined] GET(/:id): try to get channel with id 1
[2025-08-31 10:29:58.869] [undefined] GET(/:id/stats): try to get stats
[2025-08-31 10:29:58.875] [undefined] GET(/:id): Successfully get channel with id 1 with status 200
[2025-08-31 10:29:58.885] [undefined] GET(/:id/stats): Successfully get stats with status 200
[2025-08-31 10:30:58.389] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200
[2025-09-03 17:34:12.979] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200
[2025-09-03 17:34:15.647] [undefined] GET(/:id): failed due to invalid values with status 400
[2025-09-03 17:34:15.660] [undefined] GET(/:id/similar): failed due to invalid values with status 400
[2025-09-03 17:34:15.671] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-03 17:34:15.736] [undefined] GET(/:id/views): failed due to invalid values with status 400
[2025-09-03 17:34:24.176] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200
[2025-09-03 17:34:26.470] [undefined] GET(/:id): failed due to invalid values with status 400
[2025-09-03 17:34:26.478] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-03 17:34:26.485] [undefined] GET(/:id/similar): failed due to invalid values with status 400
[2025-09-03 17:34:26.494] [undefined] GET(/:id/views): failed due to invalid values with status 400
[2025-09-03 17:34:26.941] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200
[2025-09-03 17:34:29.468] [undefined] GET(/:id/history): try to retrieve history of user 1
[2025-09-03 17:34:29.523] [undefined] GET(/:id/history): successfully retrieved history of user 1 with status 200
[2025-09-03 17:34:29.535] [undefined] GET(/:id/channel): try to retrieve channel of user 1
[2025-09-03 17:34:29.541] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200
[2025-09-03 17:34:29.550] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-03 17:34:30.404] [undefined] GET(/:id): Playlist retrieved with id 1 with status 200
[2025-09-03 17:34:31.607] [undefined] DELETE(/:id/video/:videoId): Video deleted from playlist with id 1 with status 200
[2025-09-03 17:34:31.690] [undefined] GET(/:id): Playlist retrieved with id 1 with status 200
[2025-09-03 17:34:32.741] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200
[2025-09-03 17:34:41.091] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200
[2025-09-03 17:35:57.790] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200
[2025-09-03 17:44:19.296] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200
[2025-09-03 17:44:20.911] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200
[2025-09-03 17:50:33.774] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200
[2025-09-03 20:04:12.031] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200
[2025-09-03 20:04:13.537] [undefined] GET(/:id/history): try to retrieve history of user 1
[2025-09-03 20:04:13.544] [undefined] GET(/:id/history): failed to retrieve history of user 1 because it doesn't exist with status 404
[2025-09-03 20:04:13.556] [undefined] GET(/:id/channel): try to retrieve channel of user 1
[2025-09-03 20:04:13.561] [undefined] GET(/:id/channel): failed to retrieve channel of user 1 because it doesn't exist with status 404
[2025-09-03 20:04:13.566] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-03 20:04:18.757] [undefined] POST(/): try to create new channel with owner 1 and name c2lamerde
[2025-09-03 20:04:18.762] [undefined] POST(/): Successfully created new channel with name c2lamerde with status 200
[2025-09-03 20:04:18.857] [undefined] GET(/:id/channel): try to retrieve channel of user 1
[2025-09-03 20:04:18.862] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200
[2025-09-03 20:04:20.055] [undefined] GET(/:id): try to get channel with id 1
[2025-09-03 20:04:20.060] [undefined] GET(/:id/stats): try to get stats
[2025-09-03 20:04:20.065] [undefined] GET(/:id/stats): Successfully get stats with status 200
[2025-09-03 20:04:20.070] [undefined] GET(/:id): Successfully get channel with id 1 with status 200
[2025-09-03 20:04:20.845] [undefined] GET(/:id/channel): try to retrieve channel of user 1
[2025-09-03 20:04:20.849] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200
[2025-09-03 20:04:33.523] [undefined] POST(/): try to upload video with status undefined
[2025-09-03 20:04:33.528] [undefined] POST(/): successfully uploaded video with status 200
[2025-09-03 20:04:33.621] [undefined] POST(/thumbnail): try to add thumbnail to video 1
[2025-09-03 20:04:33.627] [undefined] POST(/thumbnail): successfully uploaded thumbnail with status 200
[2025-09-03 20:04:33.638] [undefined] PUT(/:id/tags): try to add tags to video 1
[2025-09-03 20:04:33.646] [undefined] PUT(/:id/tags): successfully added tags to video 1 with status 200
[2025-09-03 20:04:33.671] [undefined] GET(/:id): try to get channel with id 1
[2025-09-03 20:04:33.675] [undefined] GET(/:id/stats): try to get stats
[2025-09-03 20:04:33.679] [undefined] GET(/:id/stats): Successfully get stats with status 200
[2025-09-03 20:04:33.685] [undefined] GET(/:id): Successfully get channel with id 1 with status 200
[2025-09-03 20:04:35.555] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200
[2025-09-03 20:04:36.856] [undefined] GET(/:id): try to get video 1
[2025-09-03 20:04:36.860] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-03 20:04:36.867] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-03 20:04:36.877] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-03 20:04:36.882] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-03 20:04:36.893] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-03 20:04:36.900] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-04 20:32:34.492] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200
[2025-09-04 20:32:36.797] [undefined] GET(/:id/channel): try to retrieve channel of user 1
[2025-09-04 20:32:36.803] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200
[2025-09-04 20:32:36.815] [undefined] GET(/:id/history): try to retrieve history of user 1
[2025-09-04 20:32:36.822] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-04 20:32:36.826] [undefined] GET(/:id/history): successfully retrieved history of user 1 with status 200
[2025-09-04 20:32:47.165] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200
[2025-09-04 20:34:30.396] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200
[2025-09-04 20:34:31.179] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200
[2025-09-04 20:35:34.522] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200
[2025-09-04 20:36:03.193] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200
[2025-09-04 20:37:37.442] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200
[2025-09-04 20:37:42.936] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200
[2025-09-04 20:42:20.620] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200
[2025-09-04 20:42:31.482] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200
[2025-09-04 20:43:51.613] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200
[2025-09-04 20:44:03.089] [undefined] GET(/:id): try to get video 1
[2025-09-04 20:44:03.095] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-04 20:44:03.102] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-04 20:44:03.112] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-04 20:44:03.119] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-04 20:44:03.131] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-04 20:44:03.136] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-04 20:44:42.468] [undefined] GET(/:id): try to get video 1
[2025-09-04 20:44:42.523] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-04 20:44:42.534] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-04 20:44:42.544] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-04 20:44:42.553] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-04 20:44:42.569] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-04 20:44:42.573] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-04 20:45:08.656] [undefined] GET(/:id): try to get video 1
[2025-09-04 20:45:08.660] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-04 20:45:08.667] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-04 20:45:08.677] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-04 20:45:08.682] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-04 20:45:08.711] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-04 20:45:08.718] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-04 20:45:27.395] [undefined] GET(/:id): try to get video 1
[2025-09-04 20:45:27.403] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-04 20:45:27.413] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-04 20:45:27.418] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-04 20:45:27.424] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-04 20:45:27.455] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-04 20:45:27.465] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-04 20:45:40.775] [undefined] GET(/:id): try to get video 1
[2025-09-04 20:45:40.780] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-04 20:45:40.787] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-04 20:45:40.799] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-04 20:45:40.805] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-04 20:45:40.887] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-04 20:45:40.894] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-04 20:48:09.425] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-04 20:48:09.433] [undefined] GET(/:id): try to get video 1
[2025-09-04 20:48:09.442] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-04 20:48:09.454] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-04 20:48:09.461] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-04 20:48:09.487] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-04 20:48:09.495] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-04 20:48:14.450] [undefined] GET(/:id): try to get video 1
[2025-09-04 20:48:14.455] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-04 20:48:14.461] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-04 20:48:14.479] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-04 20:48:14.485] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-04 20:48:14.516] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-04 20:48:14.524] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-04 20:48:23.210] [undefined] GET(/:id): try to get video 1
[2025-09-04 20:48:23.215] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-04 20:48:23.221] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-04 20:48:23.232] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-04 20:48:23.237] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-04 20:48:23.268] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-04 20:48:23.277] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-04 20:48:28.466] [undefined] GET(/:id): try to get video 1
[2025-09-04 20:48:28.471] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-04 20:48:28.478] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-04 20:48:28.491] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-04 20:48:28.498] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-04 20:48:28.562] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-04 20:48:28.567] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-04 20:48:55.091] [undefined] GET(/:id): try to get video 1
[2025-09-04 20:48:55.099] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-04 20:48:55.109] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-04 20:48:55.116] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-04 20:48:55.122] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-04 20:48:55.142] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-04 20:48:55.150] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-04 20:49:01.822] [undefined] GET(/:id): try to get video 1
[2025-09-04 20:49:01.827] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-04 20:49:01.834] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-04 20:49:01.844] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-04 20:49:01.850] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-04 20:49:01.906] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-04 20:49:01.912] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-04 20:49:37.215] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-04 20:49:37.223] [undefined] GET(/:id): try to get video 1
[2025-09-04 20:49:37.230] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-04 20:49:37.241] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-04 20:49:37.248] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-04 20:49:37.271] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-04 20:49:37.277] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-04 20:50:00.804] [undefined] GET(/:id): try to get video 1
[2025-09-04 20:50:00.808] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-04 20:50:00.815] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-04 20:50:00.826] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-04 20:50:00.832] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-04 20:50:00.888] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-04 20:50:00.894] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-04 20:50:16.435] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-04 20:50:16.445] [undefined] GET(/:id): try to get video 1
[2025-09-04 20:50:16.451] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-04 20:50:16.462] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-04 20:50:16.468] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-04 20:50:16.539] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-04 20:50:16.545] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-04 20:51:09.495] [undefined] GET(/:id): try to get video 1
[2025-09-04 20:51:09.502] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-04 20:51:09.509] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-04 20:51:09.519] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-04 20:51:09.526] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-04 20:51:09.581] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-04 20:51:09.588] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-04 20:51:20.516] [undefined] GET(/:id): try to get video 1
[2025-09-04 20:51:20.520] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-04 20:51:20.527] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-04 20:51:20.539] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-04 20:51:20.545] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-04 20:51:20.597] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-04 20:51:20.604] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-04 20:51:38.559] [undefined] GET(/:id): try to get video 1
[2025-09-04 20:51:38.563] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-04 20:51:38.570] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-04 20:51:38.583] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-04 20:51:38.589] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-04 20:51:38.660] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-04 20:51:38.666] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-04 20:51:59.262] [undefined] GET(/:id): try to get video 1
[2025-09-04 20:51:59.269] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-04 20:51:59.278] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-04 20:51:59.284] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-04 20:51:59.290] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-04 20:51:59.312] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-04 20:51:59.320] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-04 20:52:08.947] [undefined] GET(/:id): try to get video 1
[2025-09-04 20:52:08.951] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-04 20:52:08.957] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-04 20:52:08.968] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-04 20:52:08.974] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-04 20:52:09.029] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-04 20:52:09.033] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-04 20:53:15.575] [undefined] GET(/:id): try to get video 1
[2025-09-04 20:53:15.583] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-04 20:53:15.590] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-04 20:53:15.596] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-04 20:53:15.602] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-04 20:53:15.694] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-04 20:53:15.699] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-04 20:53:40.778] [undefined] GET(/:id): try to get video 1
[2025-09-04 20:53:40.782] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-04 20:53:40.789] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-04 20:53:40.799] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-04 20:53:40.805] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-04 20:53:40.859] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-04 20:53:40.866] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-04 20:53:53.650] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-04 20:53:53.658] [undefined] GET(/:id): try to get video 1
[2025-09-04 20:53:53.666] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-04 20:53:53.675] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-04 20:53:53.681] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-04 20:53:53.748] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-04 20:53:53.754] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-04 20:57:38.645] [undefined] GET(/:id): try to get video 1
[2025-09-04 20:57:38.652] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-04 20:57:38.659] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-04 20:57:38.666] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-04 20:57:38.673] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-04 20:57:38.707] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-04 20:57:38.716] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-04 20:57:47.609] [undefined] GET(/:id): try to get video 1
[2025-09-04 20:57:47.614] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-04 20:57:47.621] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-04 20:57:47.635] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-04 20:57:47.641] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-04 20:57:47.693] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-04 20:57:47.699] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-04 20:57:54.157] [undefined] GET(/:id): try to get video 1
[2025-09-04 20:57:54.162] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-04 20:57:54.168] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-04 20:57:54.181] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-04 20:57:54.187] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-04 20:57:54.260] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-04 20:57:54.266] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-04 20:58:40.327] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-04 20:58:40.335] [undefined] GET(/:id): try to get video 1
[2025-09-04 20:58:40.342] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-04 20:58:40.353] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-04 20:58:40.359] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-04 20:58:40.380] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-04 20:58:40.389] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-04 20:59:06.564] [undefined] GET(/:id): try to get video 1
[2025-09-04 20:59:06.568] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-04 20:59:06.574] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-04 20:59:06.583] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-04 20:59:06.588] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-04 20:59:06.609] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-04 20:59:06.618] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-04 20:59:50.463] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-04 20:59:50.474] [undefined] GET(/:id): try to get video 1
[2025-09-04 20:59:50.481] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-04 20:59:50.495] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-04 20:59:50.501] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-04 20:59:50.530] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-04 20:59:50.537] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-04 21:01:19.527] [undefined] GET(/:id): try to get video 1
[2025-09-04 21:01:19.535] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-04 21:01:19.541] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-04 21:01:19.547] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-04 21:01:19.554] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-04 21:01:19.571] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-04 21:01:19.579] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-04 21:01:28.790] [undefined] GET(/:id): try to get video 1
[2025-09-04 21:01:28.846] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-04 21:01:28.853] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-04 21:01:28.882] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-04 21:01:28.888] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-04 21:01:28.935] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-04 21:01:28.941] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-04 21:01:30.079] [undefined] GET(/:id): try to get video 1
[2025-09-04 21:01:30.084] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
[2025-09-04 21:01:30.091] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-04 21:01:30.110] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-04 21:01:30.115] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-04 21:01:30.138] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-04 21:01:30.145] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-05 09:09:15.576] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 4 with status 200
[2025-09-05 09:09:24.015] [undefined] GET(/:id): try to get video 8
[2025-09-05 09:09:24.019] [undefined] GET(/user/:id): Playlists retrieved for user with id 4 with status 200
[2025-09-05 09:09:24.030] [undefined] GET(/:id): successfully get video 8 with status 200
[2025-09-05 09:09:24.040] [undefined] GET(/:id/similar): try to get similar videos for video 8
[2025-09-05 09:09:24.045] [undefined] GET(/:id/similar): successfully get similar videos for video 8 with status 200
[2025-09-05 09:09:24.066] [undefined] GET(/:id/views): try to add views for video 8
[2025-09-05 09:09:24.072] [undefined] GET(/:id/views): successfully added views for video 8 with status 200
[2025-09-05 09:09:35.618] [undefined] GET(/:id/channel): try to retrieve channel of user 4
[2025-09-05 09:09:35.623] [undefined] GET(/:id/history): try to retrieve history of user 4
[2025-09-05 09:09:35.629] [undefined] GET(/:id/channel): successfully retrieved channel of user 4 with status 200
[2025-09-05 09:09:35.636] [undefined] GET(/:id/history): successfully retrieved history of user 4 with status 200
[2025-09-05 09:09:35.640] [undefined] GET(/user/:id): Playlists retrieved for user with id 4 with status 200
[2025-09-05 09:09:37.238] [undefined] GET(/:id): try to get channel with id 2
[2025-09-05 09:09:37.243] [undefined] GET(/:id/stats): try to get stats
[2025-09-05 09:09:37.248] [undefined] GET(/:id/stats): Successfully get stats with status 200
[2025-09-05 09:09:37.254] [undefined] GET(/:id): Successfully get channel with id 2 with status 200
[2025-09-05 09:09:38.422] [undefined] GET(/:id/channel): try to retrieve channel of user 4
[2025-09-05 09:09:38.426] [undefined] GET(/:id/channel): successfully retrieved channel of user 4 with status 200
[2025-09-05 09:10:03.997] [undefined] POST(/): try to upload video with status undefined
[2025-09-05 09:10:04.013] [undefined] POST(/): successfully uploaded video with status 200
[2025-09-05 09:10:04.026] [undefined] POST(/thumbnail): try to add thumbnail to video 11
[2025-09-05 09:10:04.032] [undefined] POST(/thumbnail): successfully uploaded thumbnail with status 200
[2025-09-05 09:10:04.051] [undefined] PUT(/:id/tags): try to add tags to video 11
[2025-09-05 09:10:04.058] [undefined] PUT(/:id/tags): successfully added tags to video 11 with status 200
[2025-09-05 09:10:04.079] [undefined] GET(/:id): try to get channel with id 2
[2025-09-05 09:10:04.085] [undefined] GET(/:id/stats): try to get stats
[2025-09-05 09:10:04.091] [undefined] GET(/:id/stats): Successfully get stats with status 200
[2025-09-05 09:10:04.101] [undefined] GET(/:id): Successfully get channel with id 2 with status 200
[2025-09-05 09:10:09.459] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 4 with status 200
[2025-09-05 09:10:11.821] [undefined] GET(/:id): try to get video 8
[2025-09-05 09:10:11.825] [undefined] GET(/user/:id): Playlists retrieved for user with id 4 with status 200
[2025-09-05 09:10:11.836] [undefined] GET(/:id): successfully get video 8 with status 200
[2025-09-05 09:10:11.846] [undefined] GET(/:id/similar): try to get similar videos for video 8
[2025-09-05 09:10:11.852] [undefined] GET(/:id/similar): successfully get similar videos for video 8 with status 200
[2025-09-05 09:10:11.873] [undefined] GET(/:id/views): try to add views for video 8
[2025-09-05 09:10:11.881] [undefined] GET(/:id/views): successfully added views for video 8 with status 200
[2025-09-05 09:10:23.198] [undefined] GET(/:id/history): try to retrieve history of user 4
[2025-09-05 09:10:23.206] [undefined] GET(/:id/channel): try to retrieve channel of user 4
[2025-09-05 09:10:23.216] [undefined] GET(/:id/history): successfully retrieved history of user 4 with status 200
[2025-09-05 09:10:23.221] [undefined] GET(/:id/channel): successfully retrieved channel of user 4 with status 200
[2025-09-05 09:10:23.226] [undefined] GET(/user/:id): Playlists retrieved for user with id 4 with status 200
[2025-09-05 09:10:24.469] [undefined] GET(/:id): try to get channel with id 2
[2025-09-05 09:10:24.474] [undefined] GET(/:id/stats): try to get stats
[2025-09-05 09:10:24.478] [undefined] GET(/:id/stats): Successfully get stats with status 200
[2025-09-05 09:10:24.483] [undefined] GET(/:id): Successfully get channel with id 2 with status 200
[2025-09-05 09:10:28.356] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 4 with status 200
[2025-09-05 09:10:34.421] [undefined] GET(/:id/channel): try to retrieve channel of user 4
[2025-09-05 09:10:34.426] [undefined] GET(/:id/history): try to retrieve history of user 4
[2025-09-05 09:10:34.430] [undefined] GET(/:id/channel): successfully retrieved channel of user 4 with status 200
[2025-09-05 09:10:34.436] [undefined] GET(/:id/history): successfully retrieved history of user 4 with status 200
[2025-09-05 09:10:34.441] [undefined] GET(/user/:id): Playlists retrieved for user with id 4 with status 200
[2025-09-05 09:10:35.603] [undefined] GET(/:id): try to get channel with id 2
[2025-09-05 09:10:35.608] [undefined] GET(/:id/stats): try to get stats
[2025-09-05 09:10:35.612] [undefined] GET(/:id): Successfully get channel with id 2 with status 200
[2025-09-05 09:10:35.619] [undefined] GET(/:id/stats): Successfully get stats with status 200
[2025-09-05 09:10:36.921] [undefined] GET(/:id/channel): try to retrieve channel of user 4
[2025-09-05 09:10:36.925] [undefined] GET(/:id/channel): successfully retrieved channel of user 4 with status 200
[2025-09-05 09:10:59.327] [undefined] POST(/): try to upload video with status undefined
[2025-09-05 09:10:59.332] [undefined] POST(/): successfully uploaded video with status 200
[2025-09-05 09:10:59.377] [undefined] POST(/thumbnail): try to add thumbnail to video 12
[2025-09-05 09:10:59.382] [undefined] POST(/thumbnail): successfully uploaded thumbnail with status 200
[2025-09-05 09:10:59.398] [undefined] PUT(/:id/tags): try to add tags to video 12
[2025-09-05 09:10:59.402] [undefined] PUT(/:id/tags): Tag prive already exists for video 12 with status 200
[2025-09-05 09:10:59.413] [undefined] PUT(/:id/tags): successfully added tags to video 12 with status 200
[2025-09-05 09:10:59.431] [undefined] GET(/:id): try to get channel with id 2
[2025-09-05 09:10:59.436] [undefined] GET(/:id/stats): try to get stats
[2025-09-05 09:10:59.440] [undefined] GET(/:id/stats): Successfully get stats with status 200
[2025-09-05 09:10:59.451] [undefined] GET(/:id): Successfully get channel with id 2 with status 200
[2025-09-05 09:11:04.346] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 4 with status 200
[2025-09-05 09:11:07.011] [undefined] GET(/:id): try to get video 12
[2025-09-05 09:11:07.015] [undefined] GET(/user/:id): Playlists retrieved for user with id 4 with status 200
[2025-09-05 09:11:07.021] [undefined] GET(/:id): successfully get video 12 with status 200
[2025-09-05 09:11:07.030] [undefined] GET(/:id/similar): try to get similar videos for video 12
[2025-09-05 09:11:07.035] [undefined] GET(/:id/similar): successfully get similar videos for video 12 with status 200
[2025-09-05 09:11:07.050] [undefined] GET(/:id/views): try to add views for video 12
[2025-09-05 09:11:07.059] [undefined] GET(/:id/views): successfully added views for video 12 with status 200
[2025-09-05 09:12:29.703] [undefined] GET(/user/:id): Playlists retrieved for user with id 4 with status 200
[2025-09-05 09:12:29.711] [undefined] GET(/:id): try to get video 12
[2025-09-05 09:12:29.726] [undefined] GET(/:id): successfully get video 12 with status 200
[2025-09-05 09:12:29.737] [undefined] GET(/:id/similar): try to get similar videos for video 12
[2025-09-05 09:12:29.744] [undefined] GET(/:id/similar): successfully get similar videos for video 12 with status 200
[2025-09-05 09:12:29.760] [undefined] GET(/:id/views): try to add views for video 12
[2025-09-05 09:12:29.766] [undefined] GET(/:id/views): successfully added views for video 12 with status 200
[2025-09-05 09:13:57.318] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 4 with status 200
[2025-09-05 09:15:07.924] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 4 with status 200
[2025-09-05 09:15:28.583] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 4 with status 200
[2025-09-05 09:15:56.114] [undefined] GET(/:id): try to get video 12
[2025-09-05 09:15:56.117] [undefined] GET(/user/:id): Playlists retrieved for user with id 4 with status 200
[2025-09-05 09:15:56.129] [undefined] GET(/:id): successfully get video 12 with status 200
[2025-09-05 09:15:56.141] [undefined] GET(/:id/similar): try to get similar videos for video 12
[2025-09-05 09:15:56.146] [undefined] GET(/:id/similar): successfully get similar videos for video 12 with status 200
[2025-09-05 09:15:56.160] [undefined] GET(/:id/views): try to add views for video 12
[2025-09-05 09:15:56.164] [undefined] GET(/:id/views): successfully added views for video 12 with status 200
[2025-09-05 09:16:01.720] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 4 with status 200
[2025-09-05 09:18:55.271] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 4 with status 200
[2025-09-05 09:18:57.236] [undefined] GET(/:id/channel): try to retrieve channel of user 4
[2025-09-05 09:18:57.242] [undefined] GET(/:id/channel): successfully retrieved channel of user 4 with status 200
[2025-09-05 09:18:57.258] [undefined] GET(/:id/history): try to retrieve history of user 4
[2025-09-05 09:18:57.262] [undefined] GET(/user/:id): Playlists retrieved for user with id 4 with status 200
[2025-09-05 09:18:57.267] [undefined] GET(/:id/history): successfully retrieved history of user 4 with status 200
[2025-09-05 09:19:19.417] [undefined] GET(/:id/channel): try to retrieve channel of user 4
[2025-09-05 09:19:19.425] [undefined] GET(/:id/channel): successfully retrieved channel of user 4 with status 200
[2025-09-05 09:19:19.445] [undefined] GET(/:id/history): try to retrieve history of user 4
[2025-09-05 09:19:19.449] [undefined] GET(/user/:id): Playlists retrieved for user with id 4 with status 200
[2025-09-05 09:19:19.459] [undefined] GET(/:id/history): successfully retrieved history of user 4 with status 200
[2025-09-05 09:19:21.373] [undefined] GET(/:id): try to get channel with id 2
[2025-09-05 09:19:21.378] [undefined] GET(/:id/stats): try to get stats
[2025-09-05 09:19:21.382] [undefined] GET(/:id/stats): Successfully get stats with status 200
[2025-09-05 09:19:21.388] [undefined] GET(/:id): Successfully get channel with id 2 with status 200
[2025-09-05 09:19:22.607] [undefined] GET(/:id/channel): try to retrieve channel of user 4
[2025-09-05 09:19:22.611] [undefined] GET(/:id/channel): successfully retrieved channel of user 4 with status 200
[2025-09-05 09:19:31.231] [undefined] GET(/search): try to search user by username A
[2025-09-05 09:19:31.237] [undefined] GET(/search): successfully found user with username A with status 200
[2025-09-05 09:19:31.507] [undefined] GET(/search): try to search user by username As
[2025-09-05 09:19:31.513] [undefined] GET(/search): successfully found user with username As with status 200
[2025-09-05 09:19:37.369] [undefined] GET(/search): try to search user by username A
[2025-09-05 09:19:37.373] [undefined] GET(/search): successfully found user with username A with status 200
[2025-09-05 09:19:37.603] [undefined] GET(/search): try to search user by username As
[2025-09-05 09:19:37.608] [undefined] GET(/search): successfully found user with username As with status 200
[2025-09-05 09:19:37.809] [undefined] GET(/search): try to search user by username Ast
[2025-09-05 09:19:37.816] [undefined] GET(/search): successfully found user with username Ast with status 200
[2025-09-05 09:19:37.889] [undefined] GET(/search): try to search user by username Astr
[2025-09-05 09:19:37.892] [undefined] GET(/search): successfully found user with username Astr with status 200
[2025-09-05 09:19:49.328] [undefined] POST(/): try to upload video with status undefined
[2025-09-05 09:19:49.350] [undefined] POST(/): successfully uploaded video with status 200
[2025-09-05 09:19:49.397] [undefined] POST(/thumbnail): try to add thumbnail to video 13
[2025-09-05 09:19:49.401] [undefined] POST(/thumbnail): successfully uploaded thumbnail with status 200
[2025-09-05 09:19:49.416] [undefined] PUT(/:id/tags): try to add tags to video 13
[2025-09-05 09:19:49.422] [undefined] PUT(/:id/tags): successfully added tags to video 13 with status 200
[2025-09-05 09:19:49.443] [undefined] GET(/:id): try to get channel with id 2
[2025-09-05 09:19:49.448] [undefined] GET(/:id/stats): try to get stats
[2025-09-05 09:19:49.453] [undefined] GET(/:id/stats): Successfully get stats with status 200
[2025-09-05 09:19:49.467] [undefined] GET(/:id): Successfully get channel with id 2 with status 200
[2025-09-05 09:21:02.547] [undefined] GET(/:id/channel): try to retrieve channel of user 4
[2025-09-05 09:21:02.553] [undefined] GET(/:id/channel): successfully retrieved channel of user 4 with status 200
[2025-09-05 09:21:10.923] [undefined] GET(/search): try to search user by username a
[2025-09-05 09:21:10.928] [undefined] GET(/search): successfully found user with username a with status 200
[2025-09-05 09:21:11.146] [undefined] GET(/search): try to search user by username as
[2025-09-05 09:21:11.150] [undefined] GET(/search): successfully found user with username as with status 200
[2025-09-05 09:21:11.335] [undefined] GET(/search): try to search user by username ast
[2025-09-05 09:21:11.344] [undefined] GET(/search): successfully found user with username ast with status 200
[2025-09-05 09:21:11.382] [undefined] GET(/search): try to search user by username astr
[2025-09-05 09:21:11.388] [undefined] GET(/search): successfully found user with username astr with status 200
[2025-09-05 09:21:11.564] [undefined] GET(/search): try to search user by username astri
[2025-09-05 09:21:11.568] [undefined] GET(/search): successfully found user with username astri with status 200
[2025-09-05 09:21:15.516] [undefined] GET(/search): try to search user by username a
[2025-09-05 09:21:15.523] [undefined] GET(/search): successfully found user with username a with status 200
[2025-09-05 09:21:15.737] [undefined] GET(/search): try to search user by username as
[2025-09-05 09:21:15.741] [undefined] GET(/search): successfully found user with username as with status 200
[2025-09-05 09:21:15.918] [undefined] GET(/search): try to search user by username ast
[2025-09-05 09:21:15.924] [undefined] GET(/search): successfully found user with username ast with status 200
[2025-09-05 09:21:15.972] [undefined] GET(/search): try to search user by username astr
[2025-09-05 09:21:15.980] [undefined] GET(/search): successfully found user with username astr with status 200
[2025-09-05 09:21:27.039] [undefined] POST(/): try to upload video with status undefined
[2025-09-05 09:21:27.051] [undefined] POST(/): successfully uploaded video with status 200
[2025-09-05 09:21:27.068] [undefined] POST(/thumbnail): try to add thumbnail to video 14
[2025-09-05 09:21:27.073] [undefined] POST(/thumbnail): successfully uploaded thumbnail with status 200
[2025-09-05 09:21:27.093] [undefined] PUT(/:id/tags): try to add tags to video 14
[2025-09-05 09:21:27.101] [undefined] PUT(/:id/tags): successfully added tags to video 14 with status 200
[2025-09-05 09:21:27.122] [undefined] GET(/:id): try to get channel with id 2
[2025-09-05 09:21:27.130] [undefined] GET(/:id): Successfully get channel with id 2 with status 200
[2025-09-05 09:21:27.139] [undefined] GET(/:id/stats): try to get stats
[2025-09-05 09:21:27.143] [undefined] GET(/:id/stats): Successfully get stats with status 200
[2025-09-05 09:33:45.020] [undefined] GET(/:id/likes/day): try to get likes per day
[2025-09-05 09:33:45.025] [undefined] GET(/:id): try to get video 12
[2025-09-05 09:33:45.035] [undefined] GET(/:id): successfully get video 12 with status 200
[2025-09-05 09:33:45.042] [undefined] GET(/:id/likes/day): successfully retrieved likes per day with status 200
[2025-09-05 09:34:02.855] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 4 with status 200
[2025-09-05 09:36:29.635] [undefined] GET(/:id/subscriptions): try to retrieve all subscriptions of user 4
[2025-09-05 09:36:29.640] [undefined] GET(/:id/subscriptions): successfully retrieved all subscriptions of user 4 with status 200
[2025-09-05 09:36:29.649] [undefined] GET(/:id/subscriptions/videos): try to retrieve all subscriptions of user 4
[2025-09-05 09:36:29.655] [undefined] GET(/:id/subscriptions/videos): successfully retrieved all subscriptions of user 4 with status 200
[2025-09-05 09:36:32.965] [undefined] GET(/:id): try to get channel with id 5
[2025-09-05 09:36:32.976] [undefined] GET(/:id): Successfully get channel with id 5 with status 200
[2025-09-05 09:36:32.989] [undefined] GET(/:id/channel/subscribed): check if user 4 is subscribed to channel 5
[2025-09-05 09:36:32.994] [undefined] GET(/:id/channel/subscribed): user 4 is subscribed to channel 5 with status 200
[2025-09-05 09:37:01.587] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 4 with status 200
[2025-09-05 09:37:47.320] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 4 with status 200
[2025-09-05 09:38:00.125] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 4 with status 200
[2025-09-05 09:38:02.317] [undefined] GET(/:id/history): try to retrieve history of user 4
[2025-09-05 09:38:02.324] [undefined] GET(/:id/history): successfully retrieved history of user 4 with status 200
[2025-09-05 09:38:02.344] [undefined] GET(/:id/channel): try to retrieve channel of user 4
[2025-09-05 09:38:02.350] [undefined] GET(/:id/channel): successfully retrieved channel of user 4 with status 200
[2025-09-05 09:38:02.355] [undefined] GET(/user/:id): Playlists retrieved for user with id 4 with status 200
[2025-09-05 09:38:03.268] [undefined] GET(/:id): Playlist retrieved with id 4 with status 200
[2025-09-05 09:38:04.837] [undefined] DELETE(/:id/video/:videoId): Video deleted from playlist with id 4 with status 200
[2025-09-05 09:38:04.945] [undefined] GET(/:id): Playlist retrieved with id 4 with status 200
[2025-09-05 09:38:06.399] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 4 with status 200
[2025-09-05 09:38:22.999] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 4 with status 200
[2025-09-05 09:40:19.073] [undefined] GET(/user/:id): Playlists retrieved for user with id 4 with status 200
[2025-09-05 09:40:19.081] [undefined] GET(/:id): try to get video 8
[2025-09-05 09:40:19.087] [undefined] GET(/:id): successfully get video 8 with status 200
[2025-09-05 09:40:19.098] [undefined] GET(/:id/similar): try to get similar videos for video 8
[2025-09-05 09:40:19.109] [undefined] GET(/:id/similar): successfully get similar videos for video 8 with status 200
[2025-09-05 09:40:19.166] [undefined] GET(/:id/views): try to add views for video 8
[2025-09-05 09:40:19.172] [undefined] GET(/:id/views): successfully added views for video 8 with status 200
[2025-09-05 09:40:20.697] [undefined] GET(/:id): try to get video 8
[2025-09-05 09:40:20.700] [undefined] GET(/user/:id): Playlists retrieved for user with id 4 with status 200
[2025-09-05 09:40:20.711] [undefined] GET(/:id): successfully get video 8 with status 200
[2025-09-05 09:40:20.721] [undefined] GET(/:id/similar): try to get similar videos for video 8
[2025-09-05 09:40:20.726] [undefined] GET(/:id/similar): successfully get similar videos for video 8 with status 200
[2025-09-05 09:40:20.784] [undefined] GET(/:id/views): try to add views for video 8
[2025-09-05 09:40:20.788] [undefined] GET(/:id/views): successfully added views for video 8 with status 200
[2025-09-05 09:40:32.075] [undefined] GET(/:id): try to get video 8
[2025-09-05 09:40:32.079] [undefined] GET(/user/:id): Playlists retrieved for user with id 4 with status 200
[2025-09-05 09:40:32.089] [undefined] GET(/:id): successfully get video 8 with status 200
[2025-09-05 09:40:32.102] [undefined] GET(/:id/similar): try to get similar videos for video 8
[2025-09-05 09:40:32.108] [undefined] GET(/:id/similar): successfully get similar videos for video 8 with status 200
[2025-09-05 09:40:32.162] [undefined] GET(/:id/views): try to add views for video 8
[2025-09-05 09:40:32.167] [undefined] GET(/:id/views): successfully added views for video 8 with status 200
[2025-09-05 09:40:33.008] [undefined] GET(/:id): try to get video 8
[2025-09-05 09:40:33.013] [undefined] GET(/user/:id): Playlists retrieved for user with id 4 with status 200
[2025-09-05 09:40:33.023] [undefined] GET(/:id): successfully get video 8 with status 200
[2025-09-05 09:40:33.050] [undefined] GET(/:id/similar): try to get similar videos for video 8
[2025-09-05 09:40:33.059] [undefined] GET(/:id/similar): successfully get similar videos for video 8 with status 200
[2025-09-05 09:40:33.162] [undefined] GET(/:id/views): try to add views for video 8
[2025-09-05 09:40:33.166] [undefined] GET(/:id/views): successfully added views for video 8 with status 200
[2025-09-05 09:41:02.165] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 4 with status 200
[2025-09-05 09:50:00.863] [undefined] GET(/:id): try to get channel with id 6
[2025-09-05 09:50:00.876] [undefined] GET(/:id): Successfully get channel with id 6 with status 200
[2025-09-05 09:50:08.677] [undefined] GET(/:id): try to get channel with id 2
[2025-09-05 09:50:08.689] [undefined] GET(/:id): Successfully get channel with id 2 with status 200
[2025-09-05 09:50:14.446] [undefined] GET(/:id): try to get channel with id 5
[2025-09-05 09:50:14.458] [undefined] GET(/:id): Successfully get channel with id 5 with status 200
[2025-09-05 09:50:16.410] [undefined] GET(/:id): try to get video 8
[2025-09-05 09:50:16.421] [undefined] GET(/:id): successfully get video 8 with status 200
[2025-09-05 09:50:16.430] [undefined] GET(/:id/similar): try to get similar videos for video 8
[2025-09-05 09:50:16.435] [undefined] GET(/:id/similar): successfully get similar videos for video 8 with status 200
[2025-09-05 09:59:02.737] [undefined] GET(/:id): try to get video 12
[2025-09-05 09:59:02.743] [undefined] GET(/:id): successfully get video 12 with status 200
[2025-09-05 09:59:02.756] [undefined] GET(/:id/similar): try to get similar videos for video 12
[2025-09-05 09:59:02.761] [undefined] GET(/:id/similar): successfully get similar videos for video 12 with status 200
[2025-09-05 10:02:49.939] [undefined] GET(/:id): try to get video 12
[2025-09-05 10:02:49.952] [undefined] GET(/:id): successfully get video 12 with status 200
[2025-09-05 10:02:49.963] [undefined] GET(/:id/similar): try to get similar videos for video 12
[2025-09-05 10:02:49.969] [undefined] GET(/:id/similar): successfully get similar videos for video 12 with status 200
[2025-09-05 10:04:14.469] [undefined] GET(/:id): try to get video 12
[2025-09-05 10:04:14.479] [undefined] GET(/:id): successfully get video 12 with status 200
[2025-09-05 10:04:14.491] [undefined] GET(/:id/similar): try to get similar videos for video 12
[2025-09-05 10:04:14.498] [undefined] GET(/:id/similar): successfully get similar videos for video 12 with status 200
[2025-09-05 10:06:26.495] [undefined] GET(/:id): try to get channel with id 6
[2025-09-05 10:06:26.509] [undefined] GET(/:id): Successfully get channel with id 6 with status 200
[2025-09-05 10:06:37.217] [undefined] GET(/:id): try to get channel with id 5
[2025-09-05 10:06:37.227] [undefined] GET(/:id): Successfully get channel with id 5 with status 200
[2025-09-05 16:39:37.274] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200
[2025-09-05 16:41:47.205] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200
[2025-09-05 17:01:58.738] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200
[2025-09-05 17:04:07.324] [undefined] POST(/): try to register a user with username: test and email: test@test.com
[2025-09-05 17:04:08.497] [undefined] POST(/): successfully registered with status 200
[2025-09-05 17:04:46.774] [undefined] POST(/): try to register a user with username: teste and email: teste@test.com
[2025-09-05 17:04:47.739] [undefined] POST(/): successfully registered with status 200
[2025-09-05 17:05:50.681] [undefined] POST(/): try to register a user with username: testre and email: testre@test.com
[2025-09-05 17:05:51.611] [undefined] POST(/): successfully registered with status 200
[2025-09-05 17:06:30.976] [undefined] POST(/login): try to login with username 'test'
[2025-09-05 17:06:31.027] [undefined] POST(/login): Successfully logged in with status 200
[2025-09-05 17:10:52.763] [undefined] POST(/login): try to login with username 'test'
[2025-09-05 17:10:52.861] [undefined] POST(/login): Successfully logged in with status 200
[2025-09-05 17:11:15.670] [undefined] GET(/search): Invalid token with status 401
[2025-09-05 17:11:27.652] [undefined] POST(/login): try to login with username 'test'
[2025-09-05 17:11:27.751] [undefined] POST(/login): Successfully logged in with status 200
[2025-09-05 17:11:41.375] [undefined] GET(/search): try to search user by username test
[2025-09-05 17:11:41.432] [undefined] GET(/search): successfully found user with username test with status 200
[2025-09-05 17:11:51.707] [undefined] GET(/:id): try to retrieve user 2
[2025-09-05 17:11:51.762] [undefined] GET(/:id): successfully retrieved user 2 with status 200
[2025-09-05 17:12:15.622] [undefined] PUT(/:id): try to update user 2
[2025-09-05 17:12:15.630] [undefined] PUT(/:id): successfully updated user 2 with status 200
[2025-09-05 17:12:28.053] [undefined] DELETE(/:id): failed because he wasn't the owner of the user with status 403
[2025-09-05 17:12:38.114] [undefined] GET(/username/:username): try to retrieve user string
[2025-09-05 17:12:38.169] [undefined] GET(/username/:username): successfully retrieved user string with status 200
[2025-09-05 17:12:46.243] [undefined] GET(/:id/channel): try to retrieve channel of user 2
[2025-09-05 17:12:46.249] [undefined] GET(/:id/channel): failed to retrieve channel of user 2 because it doesn't exist with status 404
[2025-09-05 17:12:52.671] [undefined] GET(/:id/history): try to retrieve history of user 2
[2025-09-05 17:12:52.726] [undefined] GET(/:id/history): failed to retrieve history of user 2 because it doesn't exist with status 404
[2025-09-05 17:12:58.929] [undefined] GET(/:id/subscriptions): try to retrieve all subscriptions of user 2
[2025-09-05 17:12:58.936] [undefined] GET(/:id/subscriptions): no subscriptions found for user 2 with status 404
[2025-09-05 17:13:04.939] [undefined] GET(/:id/subscriptions/videos): try to retrieve all subscriptions of user 2
[2025-09-05 17:13:04.998] [undefined] GET(/:id/subscriptions/videos): no subscriptions found for user 2 with status 404
[2025-09-05 17:13:25.706] [undefined] POST(/): try to create new channel with owner 2 and name chien
[2025-09-05 17:13:25.710] [undefined] POST(/): Successfully created new channel with name chien with status 200
[2025-09-05 17:13:41.559] [undefined] GET(/): try to get all channels
[2025-09-05 17:13:41.564] [undefined] GET(/): Successfully get all channels with status 200
[2025-09-05 17:13:52.689] [undefined] GET(/:id): try to get channel with id 2
[2025-09-05 17:13:52.751] [undefined] GET(/:id): Successfully get channel with id 2 with status 200
[2025-09-05 17:14:09.069] [undefined] PUT(/:id): try to update channel with id 2
[2025-09-05 17:14:09.130] [undefined] PUT(/:id): Successfully updated channel with status 200
[2025-09-05 17:14:19.755] [undefined] DELETE(/:id): failed because user do not own the channel with status 403
[2025-09-05 17:14:29.922] [undefined] POST(/:id/subscribe): try to toggle subscription for channel with id 1
[2025-09-05 17:53:33.682] [undefined] POST(/:id/subscribe): Invalid token with status 401
[2025-09-05 17:53:46.173] [undefined] POST(/:id/subscribe): Invalid token with status 401
[2025-09-05 17:53:56.975] [undefined] POST(/:id/subscribe): try to toggle subscription for channel with id 1
[2025-09-05 17:55:37.631] [undefined] POST(/:id/subscribe): try to toggle subscription for channel with id 2
[2025-09-05 17:55:37.693] [undefined] POST(/:id/subscribe): Successfully subscribed to channel with status 200
[2025-09-05 17:55:57.145] [undefined] GET(/:id/stats): try to get stats
[2025-09-05 17:55:57.151] [undefined] GET(/:id/stats): Successfully get stats with status 200
[2025-09-05 17:56:13.319] [undefined] GET(/:id): try to get video 1
[2025-09-05 17:56:13.350] [undefined] GET(/:id): successfully get video 1 with status 200
[2025-09-05 17:56:24.622] [undefined] GET(/channel/:id): try to get video from channel 1
[2025-09-05 17:56:24.627] [undefined] GET(/channel/:id): successfully get video from channel 1 with status 200
[2025-09-05 17:56:30.445] [undefined] GET(/:id/like): try to toggle like on video 1
[2025-09-05 17:56:30.453] [undefined] GET(/:id/like): no likes found adding likes for video 1 with status 200
[2025-09-05 17:56:37.689] [undefined] GET(/:id/similar): try to get similar videos for video 1
[2025-09-05 17:56:37.696] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200
[2025-09-05 17:56:43.181] [undefined] GET(/:id/views): try to add views for video 1
[2025-09-05 17:56:43.230] [undefined] GET(/:id/views): successfully added views for video 1 with status 200
[2025-09-05 17:56:48.002] [undefined] GET(/:id/likes/day): try to get likes per day
[2025-09-05 17:56:48.063] [undefined] GET(/:id/likes/day): successfully retrieved likes per day with status 200
[2025-09-05 17:57:00.119] [undefined] POST(/): try to post comment
[2025-09-05 17:57:00.126] [undefined] POST(/): successfully post comment with status 200
[2025-09-05 17:57:09.337] [undefined] GET(/video/:id): try to get comment from video 1
[2025-09-05 17:57:09.343] [undefined] GET(/video/:id): successfully get comment with status 200
[2025-09-05 17:57:15.838] [undefined] GET(/:id): try to get comment 1
[2025-09-05 17:57:15.843] [undefined] GET(/:id): successfully get comment with status 200
[2025-09-05 17:57:26.069] [undefined] POST(/): Playlist created with id 5 with status 200
[2025-09-05 17:57:32.550] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 2 with status 200
[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

291
backend/package-lock.json

@ -22,7 +22,10 @@
"nodemailer": "^7.0.5", "nodemailer": "^7.0.5",
"passport": "^0.7.0", "passport": "^0.7.0",
"passport-github2": "^0.1.12", "passport-github2": "^0.1.12",
"pg": "^8.16.3" "pg": "^8.16.3",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"yaml": "^2.8.1"
}, },
"devDependencies": { "devDependencies": {
"chai": "^5.2.0", "chai": "^5.2.0",
@ -35,6 +38,50 @@
"vitest": "^3.2.4" "vitest": "^3.2.4"
} }
}, },
"node_modules/@apidevtools/json-schema-ref-parser": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz",
"integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==",
"license": "MIT",
"dependencies": {
"@jsdevtools/ono": "^7.1.3",
"@types/json-schema": "^7.0.6",
"call-me-maybe": "^1.0.1",
"js-yaml": "^4.1.0"
}
},
"node_modules/@apidevtools/openapi-schemas": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz",
"integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/@apidevtools/swagger-methods": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz",
"integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==",
"license": "MIT"
},
"node_modules/@apidevtools/swagger-parser": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz",
"integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==",
"license": "MIT",
"dependencies": {
"@apidevtools/json-schema-ref-parser": "^9.0.6",
"@apidevtools/openapi-schemas": "^2.0.4",
"@apidevtools/swagger-methods": "^3.0.2",
"@jsdevtools/ono": "^7.1.3",
"call-me-maybe": "^1.0.1",
"z-schema": "^5.0.1"
},
"peerDependencies": {
"openapi-types": ">=7"
}
},
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.25.5", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
@ -485,6 +532,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@jsdevtools/ono": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
"license": "MIT"
},
"node_modules/@noble/hashes": { "node_modules/@noble/hashes": {
"version": "1.8.0", "version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
@ -799,6 +852,13 @@
"win32" "win32"
] ]
}, },
"node_modules/@scarf/scarf": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
"integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==",
"hasInstallScript": true,
"license": "Apache-2.0"
},
"node_modules/@types/chai": { "node_modules/@types/chai": {
"version": "5.2.2", "version": "5.2.2",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz",
@ -830,6 +890,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"license": "MIT"
},
"node_modules/@types/methods": { "node_modules/@types/methods": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
@ -1038,7 +1104,6 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0" "license": "Python-2.0"
}, },
"node_modules/asap": { "node_modules/asap": {
@ -1069,7 +1134,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/base64url": { "node_modules/base64url": {
@ -1132,7 +1196,6 @@
"version": "1.1.12", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
@ -1230,6 +1293,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/call-me-maybe": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
"integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==",
"license": "MIT"
},
"node_modules/camelcase": { "node_modules/camelcase": {
"version": "6.3.0", "version": "6.3.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
@ -1480,6 +1549,15 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/commander": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz",
"integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==",
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/component-emitter": { "node_modules/component-emitter": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
@ -1494,7 +1572,6 @@
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/concat-stream": { "node_modules/concat-stream": {
@ -1679,6 +1756,18 @@
"node": ">=0.3.1" "node": ">=0.3.1"
} }
}, },
"node_modules/doctrine": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
"license": "Apache-2.0",
"dependencies": {
"esutils": "^2.0.2"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "17.0.1", "version": "17.0.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.0.1.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.0.1.tgz",
@ -1876,6 +1965,15 @@
"@types/estree": "^1.0.0" "@types/estree": "^1.0.0"
} }
}, },
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/etag": { "node_modules/etag": {
"version": "1.8.1", "version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
@ -2147,6 +2245,12 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"license": "ISC"
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -2407,6 +2511,17 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"license": "ISC",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": { "node_modules/inherits": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@ -2584,7 +2699,6 @@
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"argparse": "^2.0.1" "argparse": "^2.0.1"
@ -2658,6 +2772,13 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
"deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.",
"license": "MIT"
},
"node_modules/lodash.includes": { "node_modules/lodash.includes": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
@ -2670,6 +2791,13 @@
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
"license": "MIT"
},
"node_modules/lodash.isinteger": { "node_modules/lodash.isinteger": {
"version": "4.0.4", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
@ -2694,6 +2822,12 @@
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.mergewith": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
"integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==",
"license": "MIT"
},
"node_modules/lodash.once": { "node_modules/lodash.once": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
@ -2875,7 +3009,6 @@
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
@ -3262,6 +3395,13 @@
"wrappy": "1" "wrappy": "1"
} }
}, },
"node_modules/openapi-types": {
"version": "12.1.3",
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
"license": "MIT",
"peer": true
},
"node_modules/p-limit": { "node_modules/p-limit": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@ -3377,6 +3517,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/path-key": { "node_modules/path-key": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@ -4273,6 +4422,92 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/swagger-jsdoc": {
"version": "6.2.8",
"resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz",
"integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==",
"license": "MIT",
"dependencies": {
"commander": "6.2.0",
"doctrine": "3.0.0",
"glob": "7.1.6",
"lodash.mergewith": "^4.6.2",
"swagger-parser": "^10.0.3",
"yaml": "2.0.0-1"
},
"bin": {
"swagger-jsdoc": "bin/swagger-jsdoc.js"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/swagger-jsdoc/node_modules/glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/swagger-jsdoc/node_modules/yaml": {
"version": "2.0.0-1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz",
"integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==",
"license": "ISC",
"engines": {
"node": ">= 6"
}
},
"node_modules/swagger-parser": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz",
"integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==",
"license": "MIT",
"dependencies": {
"@apidevtools/swagger-parser": "10.0.3"
},
"engines": {
"node": ">=10"
}
},
"node_modules/swagger-ui-dist": {
"version": "5.28.1",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.28.1.tgz",
"integrity": "sha512-IvPrtNi8MvjiuDgoSmPYgg27Lvu38fnLD1OSd8Y103xXsPAqezVNnNeHnVCZ/d+CMXJblflGaIyHxAYIF3O71w==",
"license": "Apache-2.0",
"dependencies": {
"@scarf/scarf": "=1.4.0"
}
},
"node_modules/swagger-ui-express": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz",
"integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==",
"license": "MIT",
"dependencies": {
"swagger-ui-dist": ">=5.0.0"
},
"engines": {
"node": ">= v0.10.32"
},
"peerDependencies": {
"express": ">=4.0.0 || >=5.0.0-beta"
}
},
"node_modules/time-span": { "node_modules/time-span": {
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/time-span/-/time-span-5.1.0.tgz", "resolved": "https://registry.npmjs.org/time-span/-/time-span-5.1.0.tgz",
@ -4879,6 +5114,18 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/yaml": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
}
},
"node_modules/yargs": { "node_modules/yargs": {
"version": "17.7.2", "version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
@ -4981,6 +5228,36 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
},
"node_modules/z-schema": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz",
"integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==",
"license": "MIT",
"dependencies": {
"lodash.get": "^4.4.2",
"lodash.isequal": "^4.5.0",
"validator": "^13.7.0"
},
"bin": {
"z-schema": "bin/z-schema"
},
"engines": {
"node": ">=8.0.0"
},
"optionalDependencies": {
"commander": "^9.4.1"
}
},
"node_modules/z-schema/node_modules/commander": {
"version": "9.5.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": "^12.20.0 || >=14"
}
} }
} }
} }

5
backend/package.json

@ -26,7 +26,10 @@
"nodemailer": "^7.0.5", "nodemailer": "^7.0.5",
"passport": "^0.7.0", "passport": "^0.7.0",
"passport-github2": "^0.1.12", "passport-github2": "^0.1.12",
"pg": "^8.16.3" "pg": "^8.16.3",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"yaml": "^2.8.1"
}, },
"devDependencies": { "devDependencies": {
"chai": "^5.2.0", "chai": "^5.2.0",

21
backend/server.js

@ -1,4 +1,7 @@
import express from "express"; import express from "express";
import swaggerui from "swagger-ui-express";
import fs from "fs";
import YAML from "yaml";
import dotenv from "dotenv"; import dotenv from "dotenv";
import UserRoute from "./app/routes/user.route.js"; import UserRoute from "./app/routes/user.route.js";
import ChannelRoute from "./app/routes/channel.route.js"; import ChannelRoute from "./app/routes/channel.route.js";
@ -26,7 +29,6 @@ const app = express();
// Increase body size limits for file uploads // Increase body size limits for file uploads
app.use(express.urlencoded({extended: true, limit: '500mb'})); app.use(express.urlencoded({extended: true, limit: '500mb'}));
app.use(express.json({limit: '500mb'})); app.use(express.json({limit: '500mb'}));
app.use(cors())
app.use(session({ app.use(session({
secret: "your-secret", secret: "your-secret",
@ -34,6 +36,23 @@ app.use(session({
saveUninitialized: false, saveUninitialized: false,
})); }));
// Swagger setup
const file = fs.readFileSync('./swagger.yaml', 'utf8');
const swaggerDocument = YAML.parse(file);
// Swagger UI options
const swaggerOptions = {
explorer: true,
swaggerOptions: {
requestInterceptor: (req) => {
req.headers['Content-Type'] = 'application/json';
return req;
}
}
};
app.use('/api/api-docs', swaggerui.serve, swaggerui.setup(swaggerDocument, swaggerOptions));
// --- Passport setup --- // --- Passport setup ---
app.use(passport.initialize()); app.use(passport.initialize());
app.use(passport.session()); app.use(passport.session());

2317
backend/swagger.yaml

File diff suppressed because it is too large

50
developpement.yaml

@ -1,32 +1,35 @@
services: services:
backend: resit_backend:
build: build:
context: ./backend context: ./backend
dockerfile: Dockerfile dockerfile: Dockerfile
network: host network: host
container_name: resit_backend container_name: resit_backend
ports: ports:
- "${BACKEND_PORT}:${BACKEND_PORT}" - "8000:8000"
environment: environment:
DB_USER: ${POSTGRES_USER} POSTGRES_USER: ${POSTGRES_USER}
DB_NAME: ${POSTGRES_DB} POSTGRES_DB: ${POSTGRES_DB}
DB_HOST: ${POSTGRES_HOST} POSTGRES_HOST: db
DB_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
JWT_SECRET: ${JWT_SECRET} JWT_SECRET: ${JWT_SECRET}
LOG_FILE: ${LOG_FILE} LOG_FILE: ${LOG_FILE}
PORT: ${BACKEND_PORT} PORT: ${BACKEND_PORT}
GMAIL_USER: ${GMAIL_USER}
GMAIL_PASSWORD: ${GMAIL_PASSWORD}
GITHUB_ID: ${GITHUB_ID}
GITHUB_SECRET: ${GITHUB_SECRET}
FRONTEND_URL: ${FRONTEND_URL}
volumes: volumes:
- ./backend/logs:/var/log/freetube - ./backend/logs:/var/log/freetube
- ./backend/app/uploads:/app/app/uploads - ./backend:/app
- ./backend/app/utils/wait-for-it.sh:/wait-for-it.sh
depends_on: depends_on:
- db db:
command: ["/wait-for-it.sh", "${POSTGRES_HOST}:5432", "--", "npm", "start"] condition: service_healthy
db: db:
image: postgres:latest image: postgres:latest
container_name: resit_db
ports: ports:
- "5432:5432" - "5432:5432"
environment: environment:
@ -35,19 +38,28 @@ services:
POSTGRES_DB: ${POSTGRES_DB} POSTGRES_DB: ${POSTGRES_DB}
volumes: volumes:
- db_data:/var/lib/postgresql/data - db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
frontend: frontend:
build: image: nginx:alpine
context: ./frontend container_name: resit_frontend
dockerfile: Dockerfile
network: host
ports: ports:
- "5173:5173" - "80:80"
- "443:443"
volumes: volumes:
- ./frontend/:/app - ./frontend/dist:/usr/share/nginx/html
- /app/node_modules - ./frontend/nginx-selfsigned.crt:/etc/nginx/ssl/nginx-selfsigned.crt
- ./frontend/nginx-selfsigned.key:/etc/nginx/ssl/nginx-selfsigned.key
- ./frontend/default.conf:/etc/nginx/conf.d/default.conf
environment:
- VITE_API_BASE_URL=https://localhost/api
depends_on: depends_on:
- backend - resit_backend
volumes: volumes:
db_data: db_data:

36
docker-compose.yaml

@ -20,11 +20,13 @@ services:
GMAIL_PASSWORD: ${GMAIL_PASSWORD} GMAIL_PASSWORD: ${GMAIL_PASSWORD}
GITHUB_ID: ${GITHUB_ID} GITHUB_ID: ${GITHUB_ID}
GITHUB_SECRET: ${GITHUB_SECRET} GITHUB_SECRET: ${GITHUB_SECRET}
FRONTEND_URL: ${FRONTEND_URL}
volumes: volumes:
- ./backend/logs:/var/log/freetube - ./backend/logs:/var/log/freetube
- ./backend:/app - ./backend:/app
depends_on: depends_on:
- db db:
condition: service_healthy
db: db:
image: postgres:latest image: postgres:latest
@ -36,34 +38,26 @@ services:
POSTGRES_DB: ${POSTGRES_DB} POSTGRES_DB: ${POSTGRES_DB}
volumes: volumes:
- db_data:/var/lib/postgresql/data - db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
frontend: frontend:
image: nginx:latest build:
context: ./frontend
dockerfile: Dockerfile
container_name: resit_frontend
ports: ports:
- "80:80" - "80:80"
- "443:443" - "443:443"
volumes: environment:
- ./frontend/dist:/usr/share/nginx/html - VITE_API_BASE_URL=https://localhost/api
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf
- ./nginx/nginx-selfsigned.crt:/etc/nginx/ssl/nginx-selfsigned.crt
- ./nginx/nginx-selfsigned.key:/etc/nginx/ssl/nginx-selfsigned.key
depends_on: depends_on:
- resit_backend - resit_backend
mailpit:
image: axllent/mailpit:latest
ports:
- "8025:8025" # Web UI
- "1025:1025" # SMTP
volumes:
- mailpit-data:/data
environment:
# set where to store the database
MP_DATABASE: /data/mailpit.db
restart: unless-stopped
volumes: volumes:
db_data: db_data:
driver: local driver: local
mailpit-data:
driver: local

49
documentation.md

@ -0,0 +1,49 @@
# 3RESIT Freetube
## Sommaire
1) Introduction au projet
2) Organisation
2) Les technologies utilisées
1) Le serveur
2) Le site web
3) La base de données
3) Le serveur
1) Les dépendances
2) Le fonctionnement
4) Le site web
1) Les dépendances
2) Le fonctionnement
5) La base de données
1) Diagramme des tables
6) Installation
1) Docker Compose
2) Script Shell
3) Manuelle
## Introduction
Pour ce projet il nous a été demandé de recréer une plateforme similaire a Youtube nommée Freetube. Cette application web devait être gratuite et sans publicités. Nous avions la main libre sur le choix de la pile technologique utilisée et sur l'organisation du projet.
## Organisation
J'ai commencé par faire un plan détaillés de toute les routes et vérifications a effectué pour chaque fonctionnalité, puis faire un plan de la base de données. Ceci allait définir toute la structure du projet.
Pour ce qui est du développement, j'ai choisis de procéder fonctionnalité par fonctionnalité en commençant par le serveur et la base de données pour les intégrer ensuite dans le site web. Pour séparer toute ces parties j'ai utilisé **git** en créant plusieurs branches, une par fonctionnalité.
## Les technologies utilisées
### Le serveur
Le serveur utilise **NodeJS**, j'ai choisis ce langage car il permet d'implementer une **API REST** efficacement grâce a son système d'asynchronisation natif très performant. Etant créer a partir du **Javascript** il est aussi plus simple a comprendre et a écrire. Même si NodeJS n'est pas le plus connu en terme de rapidité d'éxecution ce n'est pas un problème dans notre situation car nous travaillons avec une **API** qui ajoutera un temps de latence supplémentaire.
### Le site web
Le site web est programmé en **ReactJS** avec **Vite** ce qui donne un **backend** et un **frontend** dans le même langage ce qui permet une maintenance et des mises à jour plus simple. Tout comme NodeJS, ReactJS et créé a partir de Javascript, il bénéficie donc de la même intégration de l'asynchrone natif. Le système de **composant** de ReactJS permet aussi une gestion de mise à jour de l'interface en temps réel plus simple a mettre en place et évite la duplication de code.
### La base données
La base de données et une base **PostgreSQL**, un système basé sur le langage SQL largement connu. PostgreSQL est une alternative **OpenSource** à **MySQL**. Il possède une très bonne intégration du **JSON** très utilisé comme moyen d'envoyer des données via **requête HTTP** utilisé dans les API REST.
## Le serveur
### Les dépendances
Pour l'API REST j'ai choisis d'utiliser **ExpressJS** couplé avec **express-validator**

7
frontend/Docker.Development

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

14
frontend/Dockerfile

@ -1,9 +1,17 @@
FROM node:21-alpine3.20 # Build stage
FROM node:22-alpine3.20 as build-stage
WORKDIR /app WORKDIR /app
COPY package*.json ./ COPY package*.json ./
RUN npm install RUN npm install
COPY . . COPY . .
EXPOSE 5173 RUN npm run build
CMD ["npm", "run", "dev"]
# Production stage
FROM nginx:alpine
RUN mkdir -p /etc/nginx/ssl
COPY --from=build-stage /app/dist /usr/share/nginx/html
COPY default.conf /etc/nginx/conf.d/default.conf
COPY nginx-selfsigned.crt nginx-selfsigned.key /etc/nginx/ssl/
EXPOSE 80 443
CMD ["nginx", "-g", "daemon off;"]

68
frontend/default.conf

@ -0,0 +1,68 @@
server {
server_name localhost;
listen 80;
return 301 https://$host$request_uri;
}
server {
server_name localhost;
listen 443 ssl;
root /usr/share/nginx/html;
index index.html index.htm;
# Allow large file uploads for videos (up to 500MB)
client_max_body_size 500M;
ssl_certificate /etc/nginx/ssl/nginx-selfsigned.crt;
ssl_certificate_key /etc/nginx/ssl/nginx-selfsigned.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# API routes - proxy to backend (MUST come before static file rules)
location /api/ {
# Handle preflight OPTIONS requests
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '$http_origin' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' 1728000 always;
add_header 'Content-Type' 'text/plain; charset=utf-8' always;
add_header 'Content-Length' 0 always;
return 204;
}
proxy_pass http://resit_backend: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_set_header Origin $http_origin;
proxy_buffering off;
# CORS headers for actual requests
add_header 'Access-Control-Allow-Origin' '$http_origin' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
# Also set timeout for large uploads
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
# Static assets - NO CACHING for development
location ~* ^/(?!api/).*\.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
add_header Pragma "no-cache";
add_header Expires "0";
try_files $uri =404;
}
# Handle React Router - all other routes should serve index.html
location / {
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
try_files $uri $uri/ /index.html;
}
}

23
frontend/nginx-selfsigned.crt

@ -0,0 +1,23 @@
-----BEGIN CERTIFICATE-----
MIID5zCCAs+gAwIBAgIUXzNzqa/12lyIcoxXf+v371J3fWkwDQYJKoZIhvcNAQEL
BQAwgYIxCzAJBgNVBAYTAkZSMREwDwYDVQQIDAhOb3JtYW5keTENMAsGA1UEBwwE
Q2FlbjERMA8GA1UECgwIRnJhbWluZm8xFTATBgNVBAMMDFNhY2hhIEdVRVJJTjEn
MCUGCSqGSIb3DQEJARYYc2FjaGEuZ3VlcmluQHN1cGluZm8uY29tMB4XDTI1MDcy
MTEzMzgwMVoXDTI2MDcyMTEzMzgwMVowgYIxCzAJBgNVBAYTAkZSMREwDwYDVQQI
DAhOb3JtYW5keTENMAsGA1UEBwwEQ2FlbjERMA8GA1UECgwIRnJhbWluZm8xFTAT
BgNVBAMMDFNhY2hhIEdVRVJJTjEnMCUGCSqGSIb3DQEJARYYc2FjaGEuZ3Vlcmlu
QHN1cGluZm8uY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvLg7
nR0UqRZ7UadhI8jrUjRMV1SZj+ljxEnV6tDOVMsvafsym1MhDZHb+cyv8769yqPv
CKtIOQKhMH0PkSqau8szNlF1Tg/1UzT+Mkd4zvLvGE5+aW/oDMg7E2LMJZuCyO4X
9SzWDVA5+b1QFIw6vvb3mCkUOtVDkOFreBBwryZKcWJ0b8o1hT60oB2wr18P14j0
0C2/TmHMtim0o4r3gKGvpatqt1fXJo0UlYOwTvfMrYhu2VHqsQ2qP7ocazXEWt5u
Alf1vNPkAenF0ZV/2UiaL41Q8GMoV1enDP7k7/qfgXvta/hOeYnLtmv5Qpi4XiWz
xKjSukTUD2sRtSX+YQIDAQABo1MwUTAdBgNVHQ4EFgQUVj9KtmjLFy4xWzkNI9Kq
NAxNsfUwHwYDVR0jBBgwFoAUVj9KtmjLFy4xWzkNI9KqNAxNsfUwDwYDVR0TAQH/
BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAGpUPMoF/ASIqOOfX5anDtaPvnslj
DuEVbU7Uoyc4EuSD343DPV7iMUKoFvVLPxJJqMLhSo3aEGJyqOF6q3fvq/vX2VE7
9MhwS1t2DBGb5foWRosnT1EuqFU1/S0RJ/Y+GNcoY1PrUES4+r7zqqJJjwKOzneV
ktUVCdKl0C1gtw6W4Ajxse3fm9DNLxnZZXbyNqn+KbI8QdO0xSEl+gyiycvPu/NT
+EesdlFoYjO7gdA8dXkmu+Z7R61MYhE9Zvyop5KVMqgU8/Ym04UUWjWQYWWLMyuu
bxngE4XNEI5fhg+0e/I25xJJ9wVV/ZNAF4+XOylHz/CmU8V/SPKuGXBGHg==
-----END CERTIFICATE-----

28
frontend/nginx-selfsigned.key

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC8uDudHRSpFntR
p2EjyOtSNExXVJmP6WPESdXq0M5Uyy9p+zKbUyENkdv5zK/zvr3Ko+8Iq0g5AqEw
fQ+RKpq7yzM2UXVOD/VTNP4yR3jO8u8YTn5pb+gMyDsTYswlm4LI7hf1LNYNUDn5
vVAUjDq+9veYKRQ61UOQ4Wt4EHCvJkpxYnRvyjWFPrSgHbCvXw/XiPTQLb9OYcy2
KbSjiveAoa+lq2q3V9cmjRSVg7BO98ytiG7ZUeqxDao/uhxrNcRa3m4CV/W80+QB
6cXRlX/ZSJovjVDwYyhXV6cM/uTv+p+Be+1r+E55icu2a/lCmLheJbPEqNK6RNQP
axG1Jf5hAgMBAAECggEAAj+hmDRx6jafAAf67sqi3ZgEGEmBkXNeeLGBTPc/qhxd
ip6krTELnz8TE26RG5LYXzslasUNrn42nIImvBT5ZkcjcosKpWfEqQEAjc1PQovC
9eyKnKfw4TpUvvmiveT4T98vCYEOOqHE0/WTdlOoaBY/f+sZKQYu+1NMtAjFcg2r
vVqwsZb5vGyh7CKmIHZnz3UP8P+7G5digiNRne18pGnE2oTnSoQ3/QIqUWBs69DS
k5ew+CSyTLiUFFnMnE4adwyg6wAud5fBlzowF6UF2agToX7pxEaGxGvpBGG034kk
1UXaB/d5YwcsBeH+x5cNMLKZy4zqjoxEEW31Q466NQKBgQDtKk1R/slpTpRqvtBT
NC7InvjcCBXkXttylQHJRN9glqhmflEOe8iMW1/qRwBPlQgK1wq/sXySanVv2+gO
JGq8XNRLbHyG3YRyshdnJHP1HoWQE0uedD/rfqgkNaW5S1IvHrD7Q7tOvCrF+KbS
612pmIgNVzn+inafDXPhMZc4pQKBgQDLtQGAu2eK58ewndyL8+7+VHZSTEtKpt+h
G/U/ccv+6NGqdxI5YUkrJ7k6vV81VeRMvmN9uUS/i8znORFQmm6noRVkhXytwW5B
HXq2co4WRvv9b/XqcqS0GSYVPJ1u4YNH6lvtDZ4UWPyBzYl700GdHrGa+erT44yL
tnibHx9GDQKBgFW1J+Qt85O+9hvtgVPQU+fkq4K42VCCh0PNXavi2+cICyufEqPt
T/iJPQxpRE9+SD3CoPvNpHs1ReN60U3rEzenRIFNX2NNwoPAoHyBy/YVZac/keBd
mov8Zb9QM+fWtIiaytLDE3nMvph017T5ogucN+66SxcV6vBn6CzFwySRAoGAcUf2
Tv1ohkGAtgIDrLx5cmvL5NZSpHAKOpDOoHqLA/W66v4OX2RviRUtF7JJ6OIb9GWH
9Fl8Fr0KtKbyrw1CbevRdrYY8JN52bIoFJ+9zjupVHXXnookd5boq7SqpAe6ttpo
RnplJ1GZEiIXy4lemp6AC/vhD/YhqWxOw4zaGl0CgYBslhqVt5F0EHf94p7NrCuY
hNHKHaNaULYP0VXKefQamt/ssDuktqb6DNSIvx2rbbB5+33nTlLTya67gimY1lKt
WeNB33/yBkCjfSP/J5UDD9mE/oPLt3vAOkOUgMCfp2IpC2Wez1QGqLHS260zpotP
VpgalHuSWtn8D4nO2pk1hg==
-----END PRIVATE KEY-----

3
frontend/package.json

@ -5,7 +5,8 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --host", "dev": "vite --host",
"build": "vite build --watch", "build": "vite build",
"build:watch": "vite build --watch",
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview" "preview": "vite preview"
}, },

1
frontend/src/assets/svg/exit_fullscreen.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M10 4H8v4H4v2h6zM8 20h2v-6H4v2h4zm12-6h-6v6h2v-4h4zm0-6h-4V4h-2v6h6z"></path></svg>

After

Width:  |  Height:  |  Size: 175 B

1
frontend/src/assets/svg/fullscreen.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M5 5h5V3H3v7h2zm5 14H5v-5H3v7h7zm11-5h-2v5h-5v2h7zm-2-4h2V3h-7v2h5z"></path></svg>

After

Width:  |  Height:  |  Size: 174 B

6
frontend/src/components/Recommendations.jsx

@ -7,9 +7,11 @@ export default function Recommendations({videos}) {
<h2 className="text-3xl font-bold mb-4 text-white">Recommandations</h2> <h2 className="text-3xl font-bold mb-4 text-white">Recommandations</h2>
<div> <div>
<div className="grid grid-cols-1 lg:grid-cols-5 gap-8 mt-2"> <div className="grid grid-cols-1 lg:grid-cols-5 gap-8 mt-2">
{videos && videos.map((video, index) => ( {videos && videos.length > 0 ? videos.map((video, index) => (
<VideoCard key={video.id || index} video={video} /> <VideoCard key={video.id || index} video={video} />
))} )) : (
<p className="text-gray-500">Aucune recommandation disponible</p>
)}
</div> </div>
</div> </div>
</div> </div>

6
frontend/src/components/SeeLater.jsx

@ -8,9 +8,11 @@ export default function SeeLater({videos}) {
<h2 className="text-3xl font-bold mb-4 text-white">A regarder plus tard</h2> <h2 className="text-3xl font-bold mb-4 text-white">A regarder plus tard</h2>
<div> <div>
<div className="grid grid-cols-1 lg:grid-cols-5 gap-8 mt-2"> <div className="grid grid-cols-1 lg:grid-cols-5 gap-8 mt-2">
{videos && videos.map((video, index) => ( {videos && videos.length > 0 ? videos.map((video, index) => (
<VideoCard key={video.id || index} video={video} /> <VideoCard key={video.id || index} video={video} />
))} )) : (
<p className="text-gray-500">Aucune vidéo à regarder plus tard</p>
)}
</div> </div>
</div> </div>
</div> </div>

6
frontend/src/components/TopCreators.jsx

@ -5,7 +5,7 @@ export default function TopCreators({ creators, navigate }) {
<div className="mt-10"> <div className="mt-10">
<h2 className="text-3xl font-bold mb-4 text-white">Top Créateurs</h2> <h2 className="text-3xl font-bold mb-4 text-white">Top Créateurs</h2>
<div className="grid grid-cols-1 lg:grid-cols-5 gap-8 mt-8"> <div className="grid grid-cols-1 lg:grid-cols-5 gap-8 mt-8">
{creators && creators.map((creator, index) => ( {creators && creators.length > 0 ? creators.map((creator, index) => (
<div <div
key={creator.id || index} key={creator.id || index}
className="flex flex-col items-center glassmorphism py-2" className="flex flex-col items-center glassmorphism py-2"
@ -18,7 +18,9 @@ export default function TopCreators({ creators, navigate }) {
<span>{creator.description.slice(0, 100) + (creator.description.length > 100 ? '...' : '')}</span> <span>{creator.description.slice(0, 100) + (creator.description.length > 100 ? '...' : '')}</span>
</p> </p>
</div> </div>
))} )) : (
<p className="text-gray-500">Aucun créateur disponible</p>
)}
</div> </div>
</div> </div>
); );

6
frontend/src/components/TrendingVideos.jsx

@ -7,9 +7,11 @@ export default function TrendingVideos({ videos }) {
<div className="mt-10"> <div className="mt-10">
<h2 className="text-3xl font-bold mb-4 text-white">Tendances</h2> <h2 className="text-3xl font-bold mb-4 text-white">Tendances</h2>
<div className="grid grid-cols-1 lg:grid-cols-5 gap-8 mt-2"> <div className="grid grid-cols-1 lg:grid-cols-5 gap-8 mt-2">
{videos && videos.map((video, index) => ( {videos && videos.length > 0 ? videos.map((video, index) => (
<VideoCard video={video} key={index} /> <VideoCard video={video} key={index} />
))} )) : (
<p className="text-gray-500">Aucune vidéo tendance disponible</p>
)}
</div> </div>
</div> </div>
); );

17
frontend/src/modals/LoadingVideoModal.jsx

@ -0,0 +1,17 @@
export default function LoadingVideoModal({state, message}) {
return state === "loading" && (
<div className="fixed inset-0 bg-[rgba(0,0,0,0.5)] flex items-center justify-center z-50">
<div className="glassmorphism p-8 flex flex-col items-center space-y-4">
{/* Spinner */}
<div className="relative w-16 h-16">
<div className="absolute inset-0 border-4 border-gray-300 border-opacity-30 rounded-full"></div>
<div className="absolute inset-0 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
</div>
<p className="text-white text-lg font-medium">{message}</p>
</div>
</div>
);
}

14
frontend/src/pages/AddVideo.jsx

@ -4,13 +4,15 @@ import Tag from "../components/Tag.jsx";
import { getChannel, searchByUsername } from "../services/user.service.js"; import { getChannel, searchByUsername } from "../services/user.service.js";
import { uploadVideo, uploadThumbnail, uploadTags } from "../services/video.service.js"; import { uploadVideo, uploadThumbnail, uploadTags } from "../services/video.service.js";
import UserCard from "../components/UserCard.jsx"; import UserCard from "../components/UserCard.jsx";
import LoadingVideoModal from "../modals/LoadingVideoModal.jsx";
import { useNavigate } from "react-router-dom";
export default function AddVideo() { export default function AddVideo() {
const storedUser = localStorage.getItem("user"); const storedUser = localStorage.getItem("user");
const user = storedUser ? JSON.parse(storedUser) : null; const user = storedUser ? JSON.parse(storedUser) : null;
const token = localStorage.getItem("token"); const token = localStorage.getItem("token");
const navigation = useNavigate();
const [videoTitle, setVideoTitle] = useState(""); const [videoTitle, setVideoTitle] = useState("");
const [videoDescription, setVideoDescription] = useState(""); const [videoDescription, setVideoDescription] = useState("");
@ -23,6 +25,8 @@ export default function AddVideo() {
const [authorizedUsers, setAuthorizedUsers] = useState([]); const [authorizedUsers, setAuthorizedUsers] = useState([]);
const [searchResults, setSearchResults] = useState([]); const [searchResults, setSearchResults] = useState([]);
const [alerts, setAlerts] = useState([]); const [alerts, setAlerts] = useState([]);
const [loadingState, setLoadingState] = useState("none");
const [loadingMessage, setLoadingMessage] = useState("");
useEffect(() => { useEffect(() => {
fetchChannel(); fetchChannel();
@ -53,6 +57,8 @@ export default function AddVideo() {
e.preventDefault(); e.preventDefault();
console.log(channel) console.log(channel)
setLoadingState("loading");
setLoadingMessage("Envoie de la vidéo...");
if (!videoTitle || !videoDescription || !videoThumbnail || !videoFile) { if (!videoTitle || !videoDescription || !videoThumbnail || !videoFile) {
addAlert('error', 'Veuillez remplir tous les champs requis.'); addAlert('error', 'Veuillez remplir tous les champs requis.');
@ -77,6 +83,8 @@ export default function AddVideo() {
const request = await uploadVideo(formData, token, addAlert); const request = await uploadVideo(formData, token, addAlert);
setLoadingMessage("Envoie de la miniature...");
// If the video was successfully created, we can now upload the thumbnail // If the video was successfully created, we can now upload the thumbnail
const response = await request.json(); const response = await request.json();
const videoId = response.id; const videoId = response.id;
@ -86,6 +94,7 @@ export default function AddVideo() {
thumbnailFormData.append("channel", channel.id.toString()); thumbnailFormData.append("channel", channel.id.toString());
await uploadThumbnail(thumbnailFormData, token, addAlert); await uploadThumbnail(thumbnailFormData, token, addAlert);
setLoadingMessage("Envoie des tags...");
// if the thumbnail was successfully uploaded, we can send the tags // if the thumbnail was successfully uploaded, we can send the tags
const body = { const body = {
tags: videoTags, tags: videoTags,
@ -93,6 +102,7 @@ export default function AddVideo() {
}; };
await uploadTags(body, videoId, token, addAlert); await uploadTags(body, videoId, token, addAlert);
// If everything is successful, redirect to the video management page // If everything is successful, redirect to the video management page
navigation("/manage-channel/" + channel.id);
addAlert('success', 'Vidéo ajoutée avec succès !'); addAlert('success', 'Vidéo ajoutée avec succès !');
@ -345,7 +355,7 @@ export default function AddVideo() {
</div> </div>
</main> </main>
<LoadingVideoModal state={loadingState} message={loadingMessage} />
</div> </div>
); );

12
frontend/src/pages/Register.jsx

@ -170,6 +170,16 @@ export default function Register() {
)} )}
</button> </button>
</div> </div>
<div>
<p className="text-xs text-gray-400 mt-1">Le mot de passe doit contenir au moins :</p>
<ul className="list-disc list-inside text-xs text-gray-400">
<li>8 caractères</li>
<li>Une lettre majuscule</li>
<li>Une lettre minuscule</li>
<li>Un chiffre</li>
<li>Un caractère spécial (ex: !@#$%^&*)</li>
</ul>
</div>
</div> </div>
<div> <div>
@ -207,7 +217,7 @@ export default function Register() {
<div> <div>
<label htmlFor="profile" className="block text-sm font-medium font-montserrat text-white mb-1"> <label htmlFor="profile" className="block text-sm font-medium font-montserrat text-white mb-1">
Photo de profil (optionnel) Photo de profil
</label> </label>
<input <input
type="file" type="file"

91
frontend/src/pages/Video.jsx

@ -18,6 +18,7 @@ export default function Video() {
const { user, isAuthenticated } = useAuth(); const { user, isAuthenticated } = useAuth();
const videoRef = useRef(null); const videoRef = useRef(null);
const controllerRef = useRef(null); const controllerRef = useRef(null);
const hideControlsTimeoutRef = useRef(null);
const navigation = useNavigate(); const navigation = useNavigate();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const playlistId = searchParams.get("playlistId"); const playlistId = searchParams.get("playlistId");
@ -35,6 +36,7 @@ export default function Video() {
const [playlists, setPlaylists] = useState([]); const [playlists, setPlaylists] = useState([]);
const [isAddToPlaylistOpen, setIsAddToPlaylistOpen] = useState(false); const [isAddToPlaylistOpen, setIsAddToPlaylistOpen] = useState(false);
const [currentPlaylist, setCurrentPlaylist] = useState(null); const [currentPlaylist, setCurrentPlaylist] = useState(null);
const [isFullscreen, setIsFullscreen] = useState(false);
const [isCommentVisible, setIsCommentVisible] = useState(window.innerWidth >= 1024); // Show comments by default on large screens const [isCommentVisible, setIsCommentVisible] = useState(window.innerWidth >= 1024); // Show comments by default on large screens
const fetchVideo = useCallback(async () => { const fetchVideo = useCallback(async () => {
@ -127,6 +129,19 @@ export default function Video() {
fetchNextVideo(); fetchNextVideo();
}, [currentPlaylist]); }, [currentPlaylist]);
// Handle fullscreen state changes
useEffect(() => {
if (!isFullscreen) {
// Clear timeout when exiting fullscreen
if (hideControlsTimeoutRef.current) {
clearTimeout(hideControlsTimeoutRef.current);
hideControlsTimeoutRef.current = null;
}
// Reset controls visibility for normal mode
setShowControls(false);
}
}, [isFullscreen]);
const handlePlayPause = () => { const handlePlayPause = () => {
if (videoRef.current) { if (videoRef.current) {
if (videoRef.current.paused) { if (videoRef.current.paused) {
@ -199,11 +214,31 @@ export default function Video() {
} }
const handleMouseEnter = () => { const handleMouseEnter = () => {
if (!isFullscreen) {
setShowControls(true); setShowControls(true);
}
}; };
const handleMouseLeave = () => { const handleMouseLeave = () => {
if (!isFullscreen) {
setShowControls(false);
}
};
const handleMouseMove = () => {
if (isFullscreen) {
setShowControls(true);
// Clear existing timeout
if (hideControlsTimeoutRef.current) {
clearTimeout(hideControlsTimeoutRef.current);
}
// Hide controls after 3 seconds of no mouse movement
hideControlsTimeoutRef.current = setTimeout(() => {
setShowControls(false); setShowControls(false);
}, 3000);
}
}; };
const handleSubscribe = async () => { const handleSubscribe = async () => {
@ -318,6 +353,7 @@ export default function Video() {
className="relative w-full aspect-video mx-auto rounded-lg overflow-hidden" className="relative w-full aspect-video mx-auto rounded-lg overflow-hidden"
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
onMouseMove={handleMouseMove}
> >
<video <video
key={video.id} key={video.id}
@ -327,7 +363,7 @@ export default function Video() {
onTimeUpdate={handleTimeUpdate} onTimeUpdate={handleTimeUpdate}
onLoadedMetadata={handleLoadedMetadata} onLoadedMetadata={handleLoadedMetadata}
onEnded={passToNextVideo} onEnded={passToNextVideo}
className="w-full h-full object-cover" 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 controls={window.innerWidth < 1024} // Show native controls on small screens
> >
<source src={`${video.file}`} type="video/mp4" /> <source src={`${video.file}`} type="video/mp4" />
@ -336,14 +372,31 @@ export default function Video() {
{/* Video controls */} {/* Video controls */}
<div <div
className={`absolute bottom-4 left-4 right-4 glassmorphism-rounded-md p-4 hidden lg:flex items-center transition-opacity duration-300 ${ className={`glassmorphism-rounded-md p-4 hidden lg:flex gap-4 items-center transition-opacity duration-300 ${
showControls ? 'opacity-100' : 'opacity-0' 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} 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}> <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"/> <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> </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"> <div className="flex-1 mx-4">
{/* Time display */} {/* Time display */}
@ -362,7 +415,33 @@ export default function Video() {
style={{ width: `${progress}%` }} style={{ width: `${progress}%` }}
></div> ></div>
</div> </div>
</div> </div>
{
!isFullscreen ? (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
className="fill-white"
viewBox="0 0 24 24"
onClick={() => setIsFullscreen(true)}
>
<path d="M5 5h5V3H3v7h2zm5 14H5v-5H3v7h7zm11-5h-2v5h-5v2h7zm-2-4h2V3h-7v2h5z"></path>
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
className="fill-white"
onClick={() => setIsFullscreen(false)}
>
<path d="M10 4H8v4H4v2h6zM8 20h2v-6H4v2h4zm12-6h-6v6h2v-4h4zm0-6h-4V4h-2v6h6z"></path>
</svg>
)
}
</div> </div>
</div> </div>
@ -491,11 +570,13 @@ export default function Video() {
!isPlaylist ? ( !isPlaylist ? (
<div className="flex flex-col items-center gap-2"> <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> <h2 className="font-montserrat w-full lg:w-9/10 mt-8 text-white text-2xl">Recommandations</h2>
{similarVideos.map((video, index) => ( {similarVideos.length > 0 ? similarVideos.map((video, index) => (
<div className="w-full lg:w-9/10" key={index}> <div className="w-full lg:w-9/10" key={index}>
<VideoCard video={video} /> <VideoCard video={video} />
</div> </div>
))} )) : (
<p className="text-gray-500 text-left">Aucune recommandation disponible</p>
)}
</div> </div>
) : ( ) : (
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-2">

8
nginx/Dockerfile

@ -0,0 +1,8 @@
FROM nginx:latest
COPY ../frontend/dist/ /usr/share/nginx/html
COPY ./default.conf /etc/nginx/conf.d
EXPOSE 80:80
Loading…
Cancel
Save