diff --git a/README.md b/README.md index 6db5d56..0359ba0 100644 --- a/README.md +++ b/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 - 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 - -
-Click to expand manual installation steps - -#### Backend Setup -```bash -cd backend -npm install -npm run dev -``` -#### Frontend Setup -```bash -cd frontend -npm install -npm run dev -``` +## Sommaire + +1) Introduction +2) Description du projet +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 -- Install PostgreSQL -- Create database and user -- Run migrations (if available) -
+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**. -## ⚙ Configuration +## Le serveur -### Environment Variables +### Les dĂ©pendances -| Variable | Description | Default | Required | -|----------|-------------|---------|----------| -| `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 | ❌ | +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.  -### 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: -- `./backend/logs:/var/log/freetube` - Application logs -- `./backend/app/uploads:/app/app/uploads` - Uploaded files (videos, images) -- Database data volume for PostgreSQL persistence +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Ă©. -## 📖 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** - - 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 +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. -2. **Login** - - Click "Se connecter" on the homepage - - Enter your username and password - - You'll be redirected to the authenticated homepage +## Le site web -3. **Upload Videos** - - Use the API endpoints to upload videos (see API documentation) - - Videos are stored in the uploads directory +### Les dĂ©pendances -4. **Browse Content** - - View recommendations on the homepage - - Search for videos using the search bar - - Browse trending videos and top creators +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.  -### 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: -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 +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.  -## 📚 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 -```http -POST /api/users/ -Content-Type: multipart/form-data +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. -email: user@example.com -username: johndoe -password: securepassword -picture: [file upload] +### Le fonctionnement + +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.   + +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 -```http -POST /api/users/login -Content-Type: application/json +#### ParamĂ©trages NGINX +Quelque modification doivent ĂȘtre faites pour le fonctionnement de NGINX +```nginx +server { + #------------------------------ ici ------------------------------- + server_name ; + #------------------------------------------------------------------ + listen 80; + return 301 https://$host$request_uri; +} + +server { + #------------------------------ ici ------------------------------- + server_name ; + #------------------------------------------------------------------ + 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 -```http -GET /api/media/profile/{filename} +#### Mise en place des variables d'environnements + +A la racine du projet crĂ©er un fichier `.env` +```bash +touch .env ``` -#### Get Video Thumbnail -```http -GET /api/media/thumbnail/{filename} +A l'aide de l'Ă©diteur de votre choix entrez dans le fichier +``` +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= +POSTGRES_PASSWORD= +POSTGRES_DB= +POSTGRES_HOST=db -- **Videos**: `/api/videos/` -- **Comments**: `/api/comments/` -- **Channels**: `/api/channels/` -- **Playlists**: `/api/playlists/` -- **Recommendations**: `/api/recommendations/` +BACKEND_PORT=8000 -For detailed API documentation, check the `.http` files in the `backend/requests/` directory. +JWT_SECRET= -## 📁 Project Structure +LOG_FILE=/var/log/freetube/access.log -``` -3RESIT_DOCKER/ -├── backend/ # Node.js Express backend -│ ├── app/ -│ │ ├── 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 -``` +GMAIL_USER= +GMAIL_PASSWORD= + +FRONTEND_URL= -## 🔧 Development +GITHUB_ID= -### Available Scripts +GITHUB_SECRET= -#### Backend + +#### Lancement + +Pour lancer le groupe de conteneur ```bash -npm run dev # Start development server with hot reload -npm run start # Start production server -npm run test # Run tests +docker compose up -d # pour dĂ©tacher de la session ``` -#### Frontend +### Installation via le Script Shell + +#### Ajout des autorisations + +Pour ajouter les autorisations nĂ©cessaire au lancement du script ```bash -npm run dev # Start development server -npm run build # Build for production -npm run preview # Preview production build -npm run lint # Run ESLint +chmod +x ./deploy.sh ``` -#### Docker Commands -```bash -# Start all services -docker-compose up --build +#### CrĂ©ation de clĂ© d'API Gmail -# Stop all services -docker-compose down +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 -# View logs -docker-compose logs [service-name] +#### CrĂ©ation Application OAuth Github -# Restart specific service -docker-compose restart [service-name] +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 -# Reset database -docker-compose down --volumes +#### Lancer l'installation + +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 -2. **Frontend Changes**: Hot module replacement with Vite -3. **Database Changes**: Restart containers to apply schema changes -4. **Nginx Changes**: Restart nginx service +#### CrĂ©ation de clĂ© d'API Gmail -### 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: -- `user.http` - User registration and authentication -- `video.http` - Video management -- `medias.http` - Media file serving -- `comment.http` - Comment system +#### CrĂ©ation Application OAuth Github -## đŸ§Ș 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 -# Backend tests -cd backend -npm test +touch .env +``` -# Frontend tests (if configured) -cd frontend -npm test +A l'aide de l'Ă©diteur de votre choix entrez dans le fichier ``` +nano .env +``` + +Rentrez les informations dans ce format +/!\ les valeurs non-entourĂ©es de chevrons **ne doivent pas ĂȘtre modifiĂ©**. +```env +POSTGRES_USER= +POSTGRES_PASSWORD= +POSTGRES_DB= +POSTGRES_HOST=db -### Test Structure +BACKEND_PORT=8000 -- **Unit Tests**: Individual component/function testing -- **Integration Tests**: API endpoint testing -- **E2E Tests**: Full application workflow testing +JWT_SECRET= -Current test coverage includes: -- User authentication -- Video management -- Comment system -- Channel operations -- Playlist functionality +LOG_FILE=/var/log/freetube/access.log -## 🔍 Troubleshooting +GMAIL_USER= +GMAIL_PASSWORD= -### Common Issues +FRONTEND_URL= -#### Authentication Problems -- **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 +GITHUB_ID= -#### Media File Issues -- **404 on images**: Verify nginx proxy configuration -- **Upload fails**: Check file permissions and upload directory +GITHUB_SECRET= +``` -#### Docker Issues -- **Containers won't start**: Check port conflicts -- **Database connection fails**: Verify environment variables -- **Build failures**: Clear Docker cache with `docker system prune` +#### Installation des paquets -### 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 -# Check container status -docker-compose ps +# Download and install nvm: +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash -# View service logs -docker-compose logs -f [service-name] +# in lieu of restarting the shell +\. "$HOME/.nvm/nvm.sh" -# Access container shell -docker-compose exec [service-name] /bin/bash +# Download and install Node.js: +nvm install 22 -# Reset everything -docker-compose down --volumes --rmi all -docker system prune -a +# Verify the Node.js version: +node -v # Should print "v22.19.0". +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 -- Implement image optimization for uploads -- Use CDN for media file delivery -- Database query optimization -- Frontend code splitting +Pour le serveur +```bash +cd backend && npm i --production +``` -## đŸ€ Contributing +Pour le site web +```bash +cd frontend && npm i --production +npx vite build # pour la construction du site +``` -1. Fork the repository -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 +#### Configuration de NGINX -### Code Style +Dans `/etc/nginx/conf.d/` ajouter le fichier `freetube.conf` avec cette configuration +```nginx +server { + server_name ; + listen 80; + return 301 https://$host$request_uri; +} -- **Backend**: ESLint with Node.js rules -- **Frontend**: ESLint with React rules -- **Formatting**: Prettier for consistent code style -- **Commits**: Conventional commit messages +server { + server_name ; + 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; + } -## 📜 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 "" WITH PASSWORD ""; +``` + +Pour crĂ©er la base +```postgresql +CREATE DATABASE "" OWNER " @@ -492,7 +492,7 @@ export async function getSimilarVideos(req, res) { JOIN video_tags vt ON v.id = vt.video JOIN tags t ON vt.tag = t.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 LIMIT 10; `; diff --git a/backend/app/routes/channel.route.js b/backend/app/routes/channel.route.js index 3663063..063ce3b 100644 --- a/backend/app/routes/channel.route.js +++ b/backend/app/routes/channel.route.js @@ -14,7 +14,44 @@ import {addLogger} from "../middlewares/logger.middleware.js"; 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); // GET CHANNEL BY ID diff --git a/backend/app/uploads/profiles/test.jpg b/backend/app/uploads/profiles/test.jpg deleted file mode 100644 index d8e9979..0000000 Binary files a/backend/app/uploads/profiles/test.jpg and /dev/null differ diff --git a/backend/app/utils/mail.js b/backend/app/utils/mail.js index 9fb04c1..0197e21 100644 --- a/backend/app/utils/mail.js +++ b/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} + */ export function sendEmail(to, subject, text, html = null) { const transporter = getTransporter(); const mailOptions = { diff --git a/backend/logs/access.log b/backend/logs/access.log index 3fcfa30..3ab8444 100644 --- a/backend/logs/access.log +++ b/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.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-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 diff --git a/backend/package-lock.json b/backend/package-lock.json index a7ab9bf..9a88007 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -22,7 +22,10 @@ "nodemailer": "^7.0.5", "passport": "^0.7.0", "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": { "chai": "^5.2.0", @@ -35,6 +38,50 @@ "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": { "version": "0.25.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", @@ -485,6 +532,12 @@ "dev": true, "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": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", @@ -799,6 +852,13 @@ "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": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", @@ -830,6 +890,12 @@ "dev": true, "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": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -1038,7 +1104,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/asap": { @@ -1069,7 +1134,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/base64url": { @@ -1132,7 +1196,6 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -1230,6 +1293,12 @@ "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": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", @@ -1480,6 +1549,15 @@ "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": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", @@ -1494,7 +1572,6 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, "node_modules/concat-stream": { @@ -1679,6 +1756,18 @@ "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": { "version": "17.0.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.0.1.tgz", @@ -1876,6 +1965,15 @@ "@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": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -2147,6 +2245,12 @@ "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": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2407,6 +2511,17 @@ "dev": true, "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": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -2584,7 +2699,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -2658,6 +2772,13 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "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": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -2670,6 +2791,13 @@ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", "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": { "version": "4.0.4", "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==", "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": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", @@ -2875,7 +3009,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -3262,6 +3395,13 @@ "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": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -3377,6 +3517,15 @@ "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": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -4273,6 +4422,92 @@ "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": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/time-span/-/time-span-5.1.0.tgz", @@ -4879,6 +5114,18 @@ "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": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -4981,6 +5228,36 @@ "funding": { "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" + } } } } diff --git a/backend/package.json b/backend/package.json index c7247bd..93c9231 100644 --- a/backend/package.json +++ b/backend/package.json @@ -26,7 +26,10 @@ "nodemailer": "^7.0.5", "passport": "^0.7.0", "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": { "chai": "^5.2.0", diff --git a/backend/server.js b/backend/server.js index 1b3aeb0..ab22864 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,4 +1,7 @@ import express from "express"; +import swaggerui from "swagger-ui-express"; +import fs from "fs"; +import YAML from "yaml"; import dotenv from "dotenv"; import UserRoute from "./app/routes/user.route.js"; import ChannelRoute from "./app/routes/channel.route.js"; @@ -26,7 +29,6 @@ const app = express(); // Increase body size limits for file uploads app.use(express.urlencoded({extended: true, limit: '500mb'})); app.use(express.json({limit: '500mb'})); -app.use(cors()) app.use(session({ secret: "your-secret", @@ -34,6 +36,23 @@ app.use(session({ 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 --- app.use(passport.initialize()); app.use(passport.session()); diff --git a/backend/swagger.yaml b/backend/swagger.yaml new file mode 100644 index 0000000..ba6adf4 --- /dev/null +++ b/backend/swagger.yaml @@ -0,0 +1,2317 @@ +openapi: 3.0.0 +info: + title: FreeTube - Video Sharing Platform API + version: 1.0.0 + description: API documentation for the FreeTube Video Sharing Platform + contact: + name: FreeTube Team + email: contact@freetube.com +servers: + - url: https://localhost + description: Local development server + +components: + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + + schemas: + User: + type: object + properties: + id: + type: integer + username: + type: string + email: + type: string + format: email + picture: + type: string + created_at: + type: string + format: date-time + + Channel: + type: object + properties: + id: + type: integer + name: + type: string + description: + type: string + owner: + type: integer + subscribers: + type: integer + created_at: + type: string + format: date-time + + Video: + type: object + properties: + id: + type: integer + title: + type: string + description: + type: string + file: + type: string + thumbnail: + type: string + visibility: + type: string + enum: [public, private, unlisted] + channel: + type: integer + views: + type: integer + likes: + type: integer + release_date: + type: string + format: date-time + tags: + type: array + items: + type: string + + Comment: + type: object + properties: + id: + type: integer + content: + type: string + video: + type: integer + author: + type: integer + created_at: + type: string + format: date-time + + Playlist: + type: object + properties: + id: + type: integer + name: + type: string + owner: + type: integer + created_at: + type: string + format: date-time + + Error: + type: object + properties: + message: + type: string + error: + type: string + +paths: + # USER ENDPOINTS + /api/users: + post: + tags: + - Users + summary: Register a new user + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + username: + type: string + email: + type: string + format: email + password: + type: string + minLength: 6 + profile: + type: string + format: binary + required: + - username + - email + - password + responses: + '201': + description: User created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '400': + description: Bad request - validation failed + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '409': + description: User already exists + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/users/login: + post: + tags: + - Users + summary: Login user + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + username: + type: string + password: + type: string + required: + - username + - password + responses: + '200': + description: Login successful + content: + application/json: + schema: + type: object + properties: + token: + type: string + user: + $ref: '#/components/schemas/User' + '401': + description: Invalid credentials + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/users/search: + get: + tags: + - Users + summary: Search users by username + security: + - BearerAuth: [] + parameters: + - name: username + in: query + required: true + schema: + type: string + responses: + '200': + description: Users found + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/users/{id}: + get: + tags: + - Users + summary: Get user by ID + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: User found + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '404': + description: User not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + put: + tags: + - Users + summary: Update user + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + username: + type: string + email: + type: string + format: email + responses: + '200': + description: User updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Forbidden - not the owner + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: User not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + delete: + tags: + - Users + summary: Delete user + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: User deleted successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Forbidden - not the owner + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: User not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/users/username/{username}: + get: + tags: + - Users + summary: Get user by username + security: + - BearerAuth: [] + parameters: + - name: username + in: path + required: true + schema: + type: string + responses: + '200': + description: User found + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '404': + description: User not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/users/{id}/channel: + get: + tags: + - Users + summary: Get user's channel + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Channel found + content: + application/json: + schema: + $ref: '#/components/schemas/Channel' + '404': + description: Channel not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/users/{id}/history: + get: + tags: + - Users + summary: Get user's watch history + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: History retrieved + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Video' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/users/{id}/subscriptions: + get: + tags: + - Users + summary: Get user's subscriptions + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Subscriptions retrieved + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Channel' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: User not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/users/{id}/subscriptions/videos: + get: + tags: + - Users + summary: Get videos from subscribed channels + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Subscription videos retrieved + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Video' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/users/verify-email: + post: + tags: + - Users + summary: Verify user email + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + token: + type: string + email: + type: string + format: email + required: + - token + - email + responses: + '200': + description: Email verified successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + '400': + description: Invalid or expired token + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + # CHANNEL ENDPOINTS + /api/channels: + post: + tags: + - Channels + summary: Create a new channel + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: + type: string + owner: + type: integer + required: + - name + - description + - owner + responses: + '201': + description: Channel created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Channel' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '409': + description: Channel name already exists + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + get: + tags: + - Channels + summary: Get all channels + security: + - BearerAuth: [] + responses: + '200': + description: Channels retrieved successfully + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Channel' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/channels/{id}: + get: + tags: + - Channels + summary: Get channel by ID + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Channel found + content: + application/json: + schema: + $ref: '#/components/schemas/Channel' + '404': + description: Channel not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + put: + tags: + - Channels + summary: Update channel + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: + type: string + responses: + '200': + description: Channel updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Channel' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Forbidden - not the owner + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Channel not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + delete: + tags: + - Channels + summary: Delete channel + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Channel deleted successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Forbidden - not the owner + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Channel not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/channels/{id}/subscribe: + post: + tags: + - Channels + summary: Toggle subscription to channel + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + userId: + type: integer + required: + - userId + responses: + '200': + description: Subscription toggled successfully + content: + application/json: + schema: + type: object + properties: + subscribed: + type: boolean + subscriptions: + type: integer + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Channel not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/channels/{id}/stats: + get: + tags: + - Channels + summary: Get channel statistics + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Channel statistics retrieved + content: + application/json: + schema: + type: object + properties: + total_views: + type: integer + subscribers: + type: integer + videos_count: + type: integer + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Forbidden - not the owner + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Channel not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + # VIDEO ENDPOINTS + /api/videos: + post: + tags: + - Videos + summary: Upload a new video + security: + - BearerAuth: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + title: + type: string + description: + type: string + visibility: + type: string + enum: [public, private, unlisted] + channel: + type: integer + authorizedUsers: + type: array + items: + type: integer + required: + - file + - title + - description + - visibility + - channel + responses: + '201': + description: Video uploaded successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Video' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Forbidden - not the channel owner + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/videos/thumbnail: + post: + tags: + - Videos + summary: Upload or update video thumbnail + security: + - BearerAuth: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + video: + type: integer + channel: + type: integer + required: + - file + - video + - channel + responses: + '200': + description: Thumbnail uploaded successfully + content: + application/json: + schema: + type: object + properties: + thumbnail: + type: string + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Forbidden - not the channel owner + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Video not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/videos/{id}: + get: + tags: + - Videos + summary: Get video by ID + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Video found + content: + application/json: + schema: + $ref: '#/components/schemas/Video' + '404': + description: Video not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Forbidden - no access to video + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + put: + tags: + - Videos + summary: Update video metadata + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + title: + type: string + description: + type: string + visibility: + type: string + enum: [public, private, unlisted] + channel: + type: integer + responses: + '200': + description: Video updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Video' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Forbidden - not the owner + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Video not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + delete: + tags: + - Videos + summary: Delete video + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Video deleted successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Forbidden - not the owner + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Video not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/videos/{id}/video: + put: + tags: + - Videos + summary: Update video file + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + channel: + type: integer + required: + - file + - channel + responses: + '200': + description: Video file updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Video' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Forbidden - not the owner + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Video not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/videos/channel/{id}: + get: + tags: + - Videos + summary: Get videos by channel + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Videos retrieved successfully + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Video' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Channel not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/videos/{id}/like: + get: + tags: + - Videos + summary: Toggle like on video + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Like toggled successfully + content: + application/json: + schema: + type: object + properties: + liked: + type: boolean + likes: + type: integer + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Video not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/videos/{id}/tags: + put: + tags: + - Videos + summary: Update video tags + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + tags: + type: array + items: + type: string + responses: + '200': + description: Tags updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Video' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Forbidden - not the owner + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Video not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/videos/{id}/similar: + get: + tags: + - Videos + summary: Get similar videos + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Similar videos retrieved + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Video' + '404': + description: Video not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/videos/{id}/views: + get: + tags: + - Videos + summary: Add view to video + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: View added successfully + content: + application/json: + schema: + type: object + properties: + views: + type: integer + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Video not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/videos/{id}/likes/day: + get: + tags: + - Videos + summary: Get likes per day for video + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Likes per day retrieved + content: + application/json: + schema: + type: array + items: + type: object + properties: + date: + type: string + format: date + likes: + type: integer + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Video not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/videos/{id}/authorized-users: + put: + tags: + - Videos + summary: Update authorized users for private video + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + authorizedUsers: + type: array + items: + type: integer + responses: + '200': + description: Authorized users updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Video' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Forbidden - not the owner + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Video not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + # COMMENT ENDPOINTS + /api/comments: + post: + tags: + - Comments + summary: Create a new comment + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + content: + type: string + video: + type: integer + required: + - content + - video + responses: + '201': + description: Comment created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Comment' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Video not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/comments/video/{id}: + get: + tags: + - Comments + summary: Get comments for a video + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Comments retrieved successfully + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Comment' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Video not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/comments/{id}: + get: + tags: + - Comments + summary: Get comment by ID + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Comment found + content: + application/json: + schema: + $ref: '#/components/schemas/Comment' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Comment not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + put: + tags: + - Comments + summary: Update comment + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + content: + type: string + video: + type: integer + required: + - content + - video + responses: + '200': + description: Comment updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Comment' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Forbidden - not the author + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Comment not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + delete: + tags: + - Comments + summary: Delete comment + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Comment deleted successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Forbidden - not the author + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Comment not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + # PLAYLIST ENDPOINTS + /api/playlists: + post: + tags: + - Playlists + summary: Create a new playlist + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + required: + - name + responses: + '200': + description: Playlist created successfully + content: + application/json: + schema: + type: object + properties: + id: + type: integer + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/playlists/see-later: + get: + tags: + - Playlists + summary: Get "See Later" playlist + security: + - BearerAuth: [] + responses: + '200': + description: See Later playlist retrieved + content: + application/json: + schema: + $ref: '#/components/schemas/Playlist' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Playlist not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/playlists/{id}: + post: + tags: + - Playlists + summary: Add video to playlist + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + video: + type: integer + required: + - video + responses: + '200': + description: Video added to playlist successfully + content: + application/json: + schema: + type: object + properties: + id: + type: integer + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Forbidden - not the owner + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Playlist or video not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + get: + tags: + - Playlists + summary: Get playlist by ID + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Playlist retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Playlist' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Forbidden - not the owner + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Playlist not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + put: + tags: + - Playlists + summary: Update playlist + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + required: + - name + responses: + '200': + description: Playlist updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Playlist' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Forbidden - not the owner + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Playlist not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + delete: + tags: + - Playlists + summary: Delete playlist + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Playlist deleted successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Forbidden - not the owner + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Playlist not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/playlists/user/{id}: + get: + tags: + - Playlists + summary: Get playlists by user + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: User playlists retrieved successfully + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Playlist' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Forbidden - not the owner + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: User not found or no playlists found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/playlists/{id}/video/{videoId}: + delete: + tags: + - Playlists + summary: Remove video from playlist + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + - name: videoId + in: path + required: true + schema: + type: integer + responses: + '200': + description: Video removed from playlist successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Forbidden - not the owner + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Playlist or video not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + # SEARCH ENDPOINTS + /api/search: + get: + tags: + - Search + summary: Search videos and channels + parameters: + - name: q + in: query + required: true + schema: + type: string + description: Search query + - name: type + in: query + schema: + type: string + enum: [videos, channel] + default: videos + description: Type of search (videos or channel) + responses: + '200': + description: Search results retrieved successfully + content: + application/json: + schema: + oneOf: + - type: array + items: + $ref: '#/components/schemas/Video' + - type: array + items: + $ref: '#/components/schemas/Channel' + '400': + description: Bad request - query parameter required or invalid type + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + # RECOMMENDATION ENDPOINTS + /api/recommendations: + get: + tags: + - Recommendations + summary: Get personalized recommendations + responses: + '200': + description: Recommendations retrieved successfully + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Video' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/recommendations/trending: + get: + tags: + - Recommendations + summary: Get trending videos + responses: + '200': + description: Trending videos retrieved successfully + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Video' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/recommendations/creators: + get: + tags: + - Recommendations + summary: Get top creators + responses: + '200': + description: Top creators retrieved successfully + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Channel' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + # MEDIA ENDPOINTS + /api/media/profile/{file}: + get: + tags: + - Media + summary: Get profile picture + parameters: + - name: file + in: path + required: true + schema: + type: string + responses: + '200': + description: Profile picture retrieved successfully + content: + image/*: + schema: + type: string + format: binary + '404': + description: File not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/media/video/{file}: + get: + tags: + - Media + summary: Get video file + parameters: + - name: file + in: path + required: true + schema: + type: string + responses: + '200': + description: Video file retrieved successfully + content: + video/*: + schema: + type: string + format: binary + '404': + description: File not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/media/thumbnail/{file}: + get: + tags: + - Media + summary: Get video thumbnail + parameters: + - name: file + in: path + required: true + schema: + type: string + responses: + '200': + description: Thumbnail retrieved successfully + content: + image/*: + schema: + type: string + format: binary + '404': + description: File not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + # OAUTH ENDPOINTS + /api/oauth/github: + get: + tags: + - OAuth + summary: Initiate GitHub OAuth login + responses: + '302': + description: Redirect to GitHub OAuth + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/oauth/callback: + get: + tags: + - OAuth + summary: GitHub OAuth callback + parameters: + - name: code + in: query + required: true + schema: + type: string + - name: state + in: query + schema: + type: string + responses: + '302': + description: Redirect after successful authentication + '400': + description: OAuth error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/oauth/me: + get: + tags: + - OAuth + summary: Get current authenticated user info + security: + - BearerAuth: [] + responses: + '200': + description: User info retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' \ No newline at end of file diff --git a/developpement.yaml b/developpement.yaml index 5b12e0b..35ed232 100644 --- a/developpement.yaml +++ b/developpement.yaml @@ -1,32 +1,35 @@ services: - backend: + resit_backend: build: context: ./backend dockerfile: Dockerfile network: host container_name: resit_backend ports: - - "${BACKEND_PORT}:${BACKEND_PORT}" + - "8000:8000" environment: - DB_USER: ${POSTGRES_USER} - DB_NAME: ${POSTGRES_DB} - DB_HOST: ${POSTGRES_HOST} - DB_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_HOST: db + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} JWT_SECRET: ${JWT_SECRET} LOG_FILE: ${LOG_FILE} PORT: ${BACKEND_PORT} + GMAIL_USER: ${GMAIL_USER} + GMAIL_PASSWORD: ${GMAIL_PASSWORD} + GITHUB_ID: ${GITHUB_ID} + GITHUB_SECRET: ${GITHUB_SECRET} + FRONTEND_URL: ${FRONTEND_URL} volumes: - ./backend/logs:/var/log/freetube - - ./backend/app/uploads:/app/app/uploads - - ./backend/app/utils/wait-for-it.sh:/wait-for-it.sh + - ./backend:/app depends_on: - - db - command: ["/wait-for-it.sh", "${POSTGRES_HOST}:5432", "--", "npm", "start"] + db: + condition: service_healthy db: image: postgres:latest - container_name: resit_db ports: - "5432:5432" environment: @@ -35,20 +38,29 @@ services: POSTGRES_DB: ${POSTGRES_DB} volumes: - 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: - build: - context: ./frontend - dockerfile: Dockerfile - network: host + image: nginx:alpine + container_name: resit_frontend ports: - - "5173:5173" + - "80:80" + - "443:443" volumes: - - ./frontend/:/app - - /app/node_modules + - ./frontend/dist:/usr/share/nginx/html + - ./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: - - backend + - resit_backend volumes: db_data: - driver: local + driver: local \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 9daba49..ab63c0f 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -20,11 +20,13 @@ services: GMAIL_PASSWORD: ${GMAIL_PASSWORD} GITHUB_ID: ${GITHUB_ID} GITHUB_SECRET: ${GITHUB_SECRET} + FRONTEND_URL: ${FRONTEND_URL} volumes: - ./backend/logs:/var/log/freetube - ./backend:/app depends_on: - - db + db: + condition: service_healthy db: image: postgres:latest @@ -36,34 +38,26 @@ services: POSTGRES_DB: ${POSTGRES_DB} volumes: - 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: - image: nginx:latest + build: + context: ./frontend + dockerfile: Dockerfile + container_name: resit_frontend ports: - "80:80" - "443:443" - volumes: - - ./frontend/dist:/usr/share/nginx/html - - ./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 + environment: + - VITE_API_BASE_URL=https://localhost/api depends_on: - 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: db_data: - driver: local - mailpit-data: driver: local \ No newline at end of file diff --git a/documentation.md b/documentation.md new file mode 100644 index 0000000..3796d59 --- /dev/null +++ b/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** \ No newline at end of file diff --git a/frontend/Docker.Development b/frontend/Docker.Development new file mode 100644 index 0000000..2bc8951 --- /dev/null +++ b/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 . . diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 5b114a4..e0a2a74 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,9 +1,17 @@ -FROM node:21-alpine3.20 +# Build stage +FROM node:22-alpine3.20 as build-stage WORKDIR /app COPY package*.json ./ RUN npm install COPY . . -EXPOSE 5173 -CMD ["npm", "run", "dev"] +RUN npm run build +# 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;"] diff --git a/frontend/default.conf b/frontend/default.conf new file mode 100644 index 0000000..314181e --- /dev/null +++ b/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; + } +} \ No newline at end of file diff --git a/frontend/nginx-selfsigned.crt b/frontend/nginx-selfsigned.crt new file mode 100644 index 0000000..29fac8a --- /dev/null +++ b/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----- diff --git a/frontend/nginx-selfsigned.key b/frontend/nginx-selfsigned.key new file mode 100644 index 0000000..0ac5345 --- /dev/null +++ b/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----- diff --git a/frontend/package.json b/frontend/package.json index adc5c94..2af0127 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,7 +5,8 @@ "type": "module", "scripts": { "dev": "vite --host", - "build": "vite build --watch", + "build": "vite build", + "build:watch": "vite build --watch", "lint": "eslint .", "preview": "vite preview" }, diff --git a/frontend/src/assets/svg/exit_fullscreen.svg b/frontend/src/assets/svg/exit_fullscreen.svg new file mode 100644 index 0000000..d6f4973 --- /dev/null +++ b/frontend/src/assets/svg/exit_fullscreen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/svg/fullscreen.svg b/frontend/src/assets/svg/fullscreen.svg new file mode 100644 index 0000000..a316690 --- /dev/null +++ b/frontend/src/assets/svg/fullscreen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/Recommendations.jsx b/frontend/src/components/Recommendations.jsx index f7ce8d1..d59b791 100644 --- a/frontend/src/components/Recommendations.jsx +++ b/frontend/src/components/Recommendations.jsx @@ -7,9 +7,11 @@ export default function Recommendations({videos}) {

Recommandations

- {videos && videos.map((video, index) => ( + {videos && videos.length > 0 ? videos.map((video, index) => ( - ))} + )) : ( +

Aucune recommandation disponible

+ )}
diff --git a/frontend/src/components/SeeLater.jsx b/frontend/src/components/SeeLater.jsx index 05b2d8d..bbb421c 100644 --- a/frontend/src/components/SeeLater.jsx +++ b/frontend/src/components/SeeLater.jsx @@ -8,9 +8,11 @@ export default function SeeLater({videos}) {

A regarder plus tard

- {videos && videos.map((video, index) => ( + {videos && videos.length > 0 ? videos.map((video, index) => ( - ))} + )) : ( +

Aucune vidéo à regarder plus tard

+ )}
diff --git a/frontend/src/components/TopCreators.jsx b/frontend/src/components/TopCreators.jsx index 85cfa94..32efd4e 100644 --- a/frontend/src/components/TopCreators.jsx +++ b/frontend/src/components/TopCreators.jsx @@ -5,7 +5,7 @@ export default function TopCreators({ creators, navigate }) {

Top Créateurs

- {creators && creators.map((creator, index) => ( + {creators && creators.length > 0 ? creators.map((creator, index) => (
{creator.description.slice(0, 100) + (creator.description.length > 100 ? '...' : '')}

- ))} + )) : ( +

Aucun créateur disponible

+ )}
); diff --git a/frontend/src/components/TrendingVideos.jsx b/frontend/src/components/TrendingVideos.jsx index c24f191..955c99b 100644 --- a/frontend/src/components/TrendingVideos.jsx +++ b/frontend/src/components/TrendingVideos.jsx @@ -7,9 +7,11 @@ export default function TrendingVideos({ videos }) {

Tendances

- {videos && videos.map((video, index) => ( + {videos && videos.length > 0 ? videos.map((video, index) => ( - ))} + )) : ( +

Aucune vidéo tendance disponible

+ )}
); diff --git a/frontend/src/modals/LoadingVideoModal.jsx b/frontend/src/modals/LoadingVideoModal.jsx new file mode 100644 index 0000000..3416584 --- /dev/null +++ b/frontend/src/modals/LoadingVideoModal.jsx @@ -0,0 +1,17 @@ + + +export default function LoadingVideoModal({state, message}) { + return state === "loading" && ( +
+
+ {/* Spinner */} +
+
+
+
+

{message}

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

Le mot de passe doit contenir au moins :

+
    +
  • 8 caractĂšres
  • +
  • Une lettre majuscule
  • +
  • Une lettre minuscule
  • +
  • Un chiffre
  • +
  • Un caractĂšre spĂ©cial (ex: !@#$%^&*)
  • +
+
@@ -207,7 +217,7 @@ export default function Register() {
= 1024); // Show comments by default on large screens const fetchVideo = useCallback(async () => { @@ -127,6 +129,19 @@ export default function Video() { fetchNextVideo(); }, [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 = () => { if (videoRef.current) { if (videoRef.current.paused) { @@ -199,11 +214,31 @@ export default function Video() { } const handleMouseEnter = () => { - setShowControls(true); + if (!isFullscreen) { + setShowControls(true); + } }; const handleMouseLeave = () => { - setShowControls(false); + 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); + }, 3000); + } }; const handleSubscribe = async () => { @@ -318,6 +353,7 @@ export default function Video() { className="relative w-full aspect-video mx-auto rounded-lg overflow-hidden" onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} + onMouseMove={handleMouseMove} >
+ { + !isFullscreen ? ( + setIsFullscreen(true)} + > + + + ) : ( + setIsFullscreen(false)} + > + + + ) + }
@@ -491,11 +570,13 @@ export default function Video() { !isPlaylist ? (

Recommandations

- {similarVideos.map((video, index) => ( + {similarVideos.length > 0 ? similarVideos.map((video, index) => (
- ))} + )) : ( +

Aucune recommandation disponible

+ )}
) : (
diff --git a/nginx/Dockerfile b/nginx/Dockerfile new file mode 100644 index 0000000..f73a4e7 --- /dev/null +++ b/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 +