├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── backend ├── .eslintignore ├── .eslintrc.js ├── .jshintignore ├── Dockerfile ├── Dockerfile-test ├── __tests__ │ └── backend.test.js ├── app.js ├── jest.config.js ├── package-lock.json ├── package.json ├── setupTests.js ├── src │ ├── functions │ │ ├── user.js │ │ └── video.js │ ├── models │ │ ├── user.js │ │ └── video.js │ └── routes.js └── stryker.conf.js ├── docker-compose.test.yml ├── docker-compose.yml ├── frontend ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── Dockerfile ├── babel.config.js ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── manifest.json │ └── robots.txt └── src │ ├── App.js │ ├── __tests__ │ └── App.test.js │ ├── assets │ └── favicon.ico │ ├── components │ ├── Home.js │ ├── Navbar.js │ ├── Subscriptions.js │ ├── VideoDisplay.js │ ├── VideoList.js │ └── user │ │ ├── Login.js │ │ ├── Profile.js │ │ └── Upload.js │ ├── images │ ├── icon.svg │ └── logo.svg │ ├── index.css │ ├── index.js │ ├── logo.svg │ ├── serviceWorker.js │ └── styles.css ├── nginx ├── Dockerfile └── config.conf └── test.sh /.gitignore: -------------------------------------------------------------------------------- 1 | backend/config/config.js 2 | frontend/src/config/config.js 3 | backend/config/storage_config.json 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimCoderYoutube/YoutubeClone/23d2fe13f34785a262c12559800be04c940446b0/CHANGELOG.md -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 SimCoder 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Youtube Clone 2 | 3 | Youtube clone developed on the simcoder youtube chanel. It is made using MERN and firebase for the Auth system. 4 | 5 | ## Getting Started 6 | 7 | These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. 8 | 9 | 10 | ### Prerequisites 11 | 12 | What things you need to install the software and how to install them 13 | 14 | 1. Docker-Compose 15 | 16 | 17 | > check the following [link](https://docs.docker.com/compose/install/) on how to install docker-compose 18 | 19 | ## Setting up firebase 20 | 21 | 1. Go to your firebase dashboard -> authentication -> sign-in method and enable google 22 | 23 | 2. Go to firebase dashboard -> project Settings -> add web app and get the contents of firebaseConfig pasting them onto the variable firebaseConfig in frontend/src/config/config.js 24 | 25 | 3. Go to your firebase dashboard -> Project Settings -> Service accounts 26 | 1. Generate new private key copy the content and paste it into backend/src/config/serviceAccountKey.json 27 | 2. Go to backend/config/config.js and change the admin.initializeApp() content for the one in the service account page. 28 | 29 | ## Deployment 30 | 31 | A step by step series of examples that tell you how to get a development env running. 32 | 33 | ``` 34 | > sudo systemctl start docker (or the equivalemnt for your OS) 35 | > sudo docker-compose up --build 36 | ``` 37 | 38 | ## Authors 39 | 40 | * **SimCoder** - *Main Dev* - [Simcoder](https://simcoder.com) 41 | 42 | 43 | ## License 44 | 45 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /backend/.eslintignore: -------------------------------------------------------------------------------- 1 | *.test* -------------------------------------------------------------------------------- /backend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | }, 6 | extends: [ 7 | 'airbnb-base', 8 | ], 9 | globals: { 10 | Atomics: 'readonly', 11 | SharedArrayBuffer: 'readonly', 12 | }, 13 | parserOptions: { 14 | ecmaVersion: 2018, 15 | sourceType: 'module', 16 | }, 17 | rules: { 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /backend/.jshintignore: -------------------------------------------------------------------------------- 1 | src/ -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | # base image 2 | FROM node:12.2.0 3 | 4 | # set working directory 5 | WORKDIR /app 6 | 7 | # install and cache app dependencies 8 | COPY package*.json ./ 9 | 10 | RUN npm install --quiet 11 | 12 | COPY ./src ./src 13 | COPY ./config ./config 14 | COPY app.js ./ 15 | 16 | RUN npm install -g nodemon 17 | 18 | RUN npm -g config set user root 19 | 20 | EXPOSE 6200 21 | 22 | # start app 23 | CMD ["npm", "start"] 24 | -------------------------------------------------------------------------------- /backend/Dockerfile-test: -------------------------------------------------------------------------------- 1 | FROM node:8 2 | # set working directory 3 | WORKDIR /app 4 | 5 | 6 | 7 | # install and cache app dependencies 8 | COPY package*.json ./ 9 | 10 | RUN npm install 11 | 12 | COPY ./src ./src 13 | COPY ./__tests__ ./__tests__ 14 | COPY .env* ./ 15 | 16 | COPY .eslintignore/ ./ 17 | COPY .eslintrc.js/ ./ 18 | COPY .jshintignore/ ./ 19 | COPY stryker.conf.js/ ./ 20 | COPY jest.config.js . 21 | COPY setupTests.js . 22 | 23 | 24 | EXPOSE 6200 25 | 26 | # start app 27 | CMD ["npm", "run", "test-docker"] -------------------------------------------------------------------------------- /backend/__tests__/backend.test.js: -------------------------------------------------------------------------------- 1 | jest.setTimeout(40000); 2 | 3 | 4 | describe("Testing backend", () => { 5 | 6 | it("Clear Everything", async () => { 7 | }); 8 | 9 | }); 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /backend/app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var mongoose = require('mongoose'); 3 | var bodyParser = require('body-parser'); 4 | var cors = require('cors'); 5 | 6 | 7 | //server configuration 8 | var basePath = '/'; 9 | var port = 6200; 10 | 11 | 12 | // Connection to DB 13 | mongoose.connect('mongodb://mongodb') 14 | .then(() => { 15 | console.log('Backend Started'); 16 | }) 17 | .catch(err => { 18 | console.error('Backend error:', err.stack); 19 | process.exit(1); 20 | }); 21 | 22 | 23 | var app = express(); 24 | 25 | 26 | app.use((req, res, next) => { 27 | res.header('Access-Control-Allow-Origin', '*'); 28 | next(); 29 | }); 30 | 31 | app.use(express.static('public')); 32 | app.options(cors()); 33 | app.use(bodyParser.urlencoded({extended: true})); 34 | app.use(bodyParser.json()); 35 | // Routes and Backend Functionalities 36 | var routes = require('./src/routes'); 37 | app.use(basePath, routes); 38 | 39 | 40 | app.listen(port, () => { 41 | console.log('Backend running on Port: ',port); 42 | }); 43 | 44 | 45 | -------------------------------------------------------------------------------- /backend/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | coverageDirectory: 'coverage', 4 | setupFilesAfterEnv: [ 5 | '/setupTests.js', 6 | ], 7 | testEnvironment: 'node', 8 | }; 9 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "nodemon", 8 | "start": "nodemon app.js", 9 | "test-react": "react-scripts test", 10 | "lint": "eslint . ", 11 | "lint:fix": "eslint . --fix", 12 | "test": "jest", 13 | "test-docker": "jest --detectOpenHandles --collectCoverage --runInBand", 14 | "test-coverage": "jest --coverage", 15 | "pre:push": "set CI=true && npm run test && npm run lint", 16 | "pre:commit": "npm run lint && npm run test" 17 | }, 18 | "husky": { 19 | "hooks": {} 20 | }, 21 | "keywords": [], 22 | "author": "", 23 | "license": "ISC", 24 | "devDependencies": { 25 | "@stryker-mutator/core": "^2.1.0", 26 | "@stryker-mutator/html-reporter": "^2.1.0", 27 | "@stryker-mutator/javascript-mutator": "^2.1.0", 28 | "@stryker-mutator/jest-runner": "^2.1.0", 29 | "babel-jest": "^24.9.0", 30 | "eslint": "^6.5.1", 31 | "eslint-config-airbnb-base": "^14.0.0", 32 | "eslint-plugin-import": "^2.18.2", 33 | "eslint-plugin-jsx-a11y": "^6.2.3", 34 | "eslint-plugin-react-hooks": "^1.7.0", 35 | "husky": "^2.7.0", 36 | "jest": "^24.9.0", 37 | "nodemon": "^2.0.4", 38 | "supertest": "^4.0.2" 39 | }, 40 | "dependencies": { 41 | "@ffmpeg-installer/ffmpeg": "^1.0.20", 42 | "@ffprobe-installer/ffprobe": "^1.0.12", 43 | "@google-cloud/storage": "^5.0.1", 44 | "autoprefixer": "^8.2.0", 45 | "axios": "^0.19.0", 46 | "babel-loader": "^8.0.0-beta.2", 47 | "babel-plugin-transform-class-properties": "^6.24.1", 48 | "bcryptjs": "^2.4.3", 49 | "body-parser": "^1.19.0", 50 | "bootstrap": "^4.3.1", 51 | "config": "^3.2.2", 52 | "connect-history-api-fallback": "^1.5.0", 53 | "cookie-parser": "^1.4.4", 54 | "copy-webpack-plugin": "^4.5.1", 55 | "cors": "^2.8.5", 56 | "crypto-random-string": "^3.2.0", 57 | "dotenv-flow": "^3.1.0", 58 | "eslint-friendly-formatter": "^4.0.1", 59 | "eslint-plugin-html": "^5.0.5", 60 | "express": "^4.16.3", 61 | "express-session": "^1.16.2", 62 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 63 | "ffmpeg": "0.0.4", 64 | "ffmpeg-static": "^4.2.4", 65 | "ffprobe": "^1.1.0", 66 | "ffprobe-client": "^1.1.6", 67 | "ffprobe-static": "^3.0.0", 68 | "file-loader": "^4.2.0", 69 | "filehound": "^1.17.4", 70 | "firebase": "^7.2.2", 71 | "firebase-admin": "^8.6.1", 72 | "fluent-ffmpeg": "^2.1.2", 73 | "jade": "^1.11.0", 74 | "js-cookie": "^2.2.1", 75 | "jsonwebtoken": "^8.5.1", 76 | "mongoose": "^5.0.11", 77 | "morgan": "^1.9.1", 78 | "multer": "^1.4.2", 79 | "nodemon": "^2.0.4", 80 | "prop-types": "^15.7.2", 81 | "query-string": "^6.8.2", 82 | "url-loader": "^2.1.0", 83 | "whatwg-fetch": "^2.0.4" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /backend/setupTests.js: -------------------------------------------------------------------------------- 1 | //---------------------------------- 2 | // Base file for unit test utilities 3 | //---------------------------------- 4 | 5 | // Setting up the end-to-end request testing helper methods 6 | const supertestRequest = require('supertest'); 7 | const mongoose = require('mongoose'); 8 | const app = require('./app.js'); 9 | 10 | // During the test the env variable must be set to test 11 | if (process.env.NODE_ENV !== 'test') { 12 | console.error('Entered test files without being in the test environment, aborting!'); 13 | process.exit(process.env.NODE_ENV); 14 | } 15 | 16 | 17 | // Occurs before all the tests, only once 18 | beforeAll(async () => { 19 | await new Promise((resolve) => { 20 | mongoose.connection.once('open', () => { 21 | resolve(); 22 | }); 23 | }); 24 | }); 25 | 26 | 27 | afterAll(async () => { 28 | await (mongoose.connection.close()); 29 | }); 30 | 31 | 32 | const request = () => supertestRequest(app); 33 | const agent = () => supertestRequest.agent(app); 34 | 35 | global.request = request; 36 | global.agent = agent; 37 | -------------------------------------------------------------------------------- /backend/src/functions/user.js: -------------------------------------------------------------------------------- 1 | const User = require('../models/user'); 2 | 3 | const firebaseAdmin = require('firebase-admin'); 4 | 5 | const serviceAccount = require('../../config/config').serviceAccount; 6 | const databaseURL = require('../../config/config').databaseURL; 7 | 8 | firebaseAdmin.initializeApp({ 9 | credential: firebaseAdmin.credential.cert(serviceAccount), 10 | databaseURL 11 | }) 12 | 13 | module.exports = { 14 | 15 | /** 16 | * validate token for a user 17 | * 18 | * @param {Object} user - user to be validated' object 19 | * @param {String} idToken - Session token of the user 20 | * 21 | * @returns {Promise} Promise object represents the user object and, in case of failure, returns an error 22 | */ 23 | verifyAccount: (user, idToken) => new Promise((resolve, reject) => { 24 | if (user === undefined) { 25 | return reject(new Error('auth denied')); 26 | } 27 | 28 | //parse the json into an object 29 | const userJson = JSON.parse(user); 30 | 31 | // Validate the token with firebase 32 | firebaseAdmin.auth().verifyIdToken(idToken) 33 | .then(() => { 34 | //find a user that contains the firebase_id equal to that of the user object 35 | User.findOne({ firebase_id: userJson.uid }) 36 | .then(user => { 37 | if (!user) { // if the user does not exist in the database then create a new entry 38 | new User({ 39 | firebase_id: userJson.uid, 40 | name: userJson.email, 41 | }).save() 42 | .then(user => { 43 | resolver(user) 44 | }) 45 | .catch(error => { 46 | reject(new Error(error)) 47 | }) 48 | } else { //If the user exists then resolve the promise with the user's object 49 | resolve(user) 50 | } 51 | }) 52 | }) 53 | .catch(error => { // called if the token validation fails 54 | reject(new Error(error)) 55 | }) 56 | }), 57 | 58 | 59 | /** 60 | * Gets a object of a user by id 61 | * 62 | * @param {String} id - id of the user to fetch the object of. 63 | * 64 | * @returns {Promise} Promise object represents the use object and, in case of failure, returns an error 65 | */ 66 | getById: (id) => new Promise((resolve, reject) => { 67 | if (id != null) { 68 | User.findById(id) 69 | .then((result) => { 70 | resolve(result); 71 | }) 72 | .catch(error => { 73 | reject(new Error(error)) 74 | }) 75 | } else { 76 | reject(new Error('id is null')) 77 | } 78 | }), 79 | 80 | /** 81 | * handles the (un)subscription of a user to another user 82 | * 83 | * @param {Object} user - current user's object 84 | * @param {String} idToken - Session token of the user 85 | * @param {String} id - id of the user the current user wants to (un)subscribe to. 86 | * 87 | * @returns {Promise} Promise object represents the use String of current subscription status and , in case of failure, returns an error 88 | */ 89 | handleSubscribe: (user, idToken, id) => new Promise((resolve, reject) => { 90 | module.exports.verifyAccount(user, idToken) 91 | .then((result) => { 92 | User.findOne({ _id: result._id, subscriptions: { $in: [id] } }, function (error, user) { 93 | if (user == null) { // if user is null then the user is not subscribed 94 | // update the subscription array of the current user adding the "id" user from the list 95 | User.updateOne({ _id: result._id }, { "$push": { subscriptions: id } }, { useFindAndModify: true }).then(() => { 96 | // update the subscribers array of the "id" user adding the current user from the list 97 | User.updateOne({ _id: id }, { "$push": { subscribers: result._id } }, { useFindAndModify: true }).then(() => { 98 | resolve('user subscribed successfully'); 99 | }) 100 | }) 101 | } else {// if user not is null then the user is subscribed 102 | // update the subscription array of the current user removing the "id" user from the list 103 | User.updateOne({ _id: result._id }, { "$pull": { subscriptions: id } }, { useFindAndModify: true }).then(() => { 104 | // update the subscribers array of the "id" user removing the current user from the list 105 | User.updateOne({ _id: id }, { "$pull": { subscribers: result._id } }, { useFindAndModify: true }).then(() => { 106 | resolve('user unsubscribed successfully'); 107 | }) 108 | }) 109 | } 110 | }) 111 | 112 | }).catch(error => { 113 | reject(new Error(error)) 114 | }) 115 | }) 116 | } -------------------------------------------------------------------------------- /backend/src/functions/video.js: -------------------------------------------------------------------------------- 1 | const CLOUD_BUCKET = 'youtube_clone_bucket'; 2 | 3 | const userFunc = require('./user') 4 | const Video = require('../models/video'); 5 | 6 | const { Storage } = require('@google-cloud/storage'); 7 | const user = require('../models/user'); 8 | 9 | const storage = new Storage({ projectId: CLOUD_BUCKET, keyFilename: '/app/config/storage_config.json' }); 10 | const bucket = storage.bucket(CLOUD_BUCKET); 11 | 12 | module.exports = { 13 | 14 | /** 15 | * Upload files to the google cloud storage, only returns when every promise (for each file) has been fulfilled 16 | * 17 | * @param {Object} req - Express request object 18 | * @param {Object} res - Express response object 19 | * @param {Function} next - Express next middleware function 20 | */ 21 | upload: (req, res, next) => { 22 | const { user } = req.query; 23 | 24 | //parse the json into an object 25 | const userJson = JSON.parse(user); 26 | 27 | //in case no files have been uploaded then return the function 28 | if (req.files.length == 0) { 29 | res.send('No files uploaded.') 30 | return; 31 | } 32 | 33 | // Gets current timestamp wich will be the id of the file 34 | const timestamp = new Date().getTime(); 35 | let promises = []; 36 | 37 | // Loop over every file (image and video) 38 | req.files.forEach((file, index) => { 39 | //create a promise for current file iteration 40 | const promise = new Promise((resolve, reject) => { 41 | //get type of file (video, image) 42 | const type = file.mimetype.substring(0, file.mimetype.indexOf('/')); 43 | //builds the name of the file to upload 44 | const name = `${userJson.uid}/${timestamp}/${type}`; 45 | //get the bucket object 46 | const bucket_filename = bucket.file(name); 47 | 48 | //create write stream for the file to be uploaded 49 | const stream = bucket_filename.createWriteStream({ 50 | metadata: { 51 | contentType: file.mimetype, 52 | } 53 | }) 54 | 55 | //on error reject the promise of the file 56 | stream.on('error', (error) => { 57 | req.files[index].cloudStorageError = error; 58 | reject(error); 59 | }) 60 | 61 | //on finish treat the file and save the name to the file object 62 | stream.on('finish', async () => { 63 | try { 64 | 65 | // save the cloud storage object name to the file object 66 | req.files[index].cloudStorageObject = name; 67 | 68 | // if file is image then make it publicly accessible 69 | if (type == 'image') { 70 | await bucket_filename.makePublic(); 71 | } 72 | req.files[index].cloudStoragePublicUrl = `https://storage.googleapis.com/${CLOUD_BUCKET}/${name}` 73 | resolve(); 74 | } catch (error) { 75 | reject(error) 76 | } 77 | }) 78 | 79 | //end stream 80 | stream.end(file.buffer) 81 | }) 82 | promises.push(promise) 83 | }) 84 | 85 | // called when every promise has been fulfilled 86 | Promise.all(promises) 87 | .then(() => { 88 | promises = []; 89 | next(); 90 | }) 91 | .catch(next) 92 | }, 93 | 94 | 95 | /** 96 | * Saves a video object in the database, usually called after upload(req, res, next) succseeds 97 | * 98 | * @param {String} descritpion - description of the video 99 | * @param {String} name - name of the video 100 | * @param {String[]} files - array with the files of the video (image and video) 101 | * @param {Object} user - user to be validated' object 102 | * @param {String} idToken - Session token of the user 103 | * 104 | * @returns {Promise} Promise object represents the video object and, in case of failure, returns an error 105 | */ 106 | save: (description, name, files, user, idToken) => new Promise((resolve, reject) => { 107 | 108 | //validate the user 109 | userFunc.verifyAccount(user, idToken) 110 | .then(result => { 111 | 112 | //Build the video object 113 | const mVideo = new Video(); 114 | 115 | mVideo.name = name; 116 | mVideo.description = description; 117 | mVideo.image = files[1].cloudStoragePublicUrl; 118 | mVideo.video = files[0].cloudStorageObject; 119 | mVideo.creator = result._id; 120 | 121 | // store it in the ddatabase 122 | mVideo.save().then(result => { 123 | resolve(result); 124 | }).catch(error => { 125 | reject(error); 126 | }) 127 | }).catch(error => { 128 | reject(error); 129 | }) 130 | }), 131 | 132 | /** 133 | * Gets a list of every video in the database 134 | * 135 | * @returns {Promise} Promise object represents the list of videos and, in case of failure, returns an error 136 | */ 137 | list: () => new Promise((resolve, reject) => { 138 | Video 139 | .find() 140 | .populate('creator') 141 | .exec() 142 | .then(result => { 143 | resolve(result); 144 | }).catch(error => { 145 | reject(error); 146 | }) 147 | }), 148 | 149 | /** 150 | * Gets a list of videos of a user 151 | * 152 | * @param {String} id - id of the user to fetch the videos of. 153 | * 154 | * @returns {Promise} Promise object represents the list of videos and, in case of failure, returns an error 155 | */ 156 | getByUser: (id) => new Promise((resolve, reject) => { 157 | Video 158 | .find({ creator: id }) 159 | .populate('creator') 160 | .exec() 161 | .then(result => { 162 | resolve(result); 163 | }).catch(error => { 164 | reject(error); 165 | }) 166 | }), 167 | 168 | 169 | /** 170 | * Gets a list of videos according to the user's subscriptions 171 | * 172 | * @param {String} id - id of the user to fetch the subscribers of. 173 | * 174 | * @returns {Promise} Promise object represents the list of videos and, in case of failure, returns an error 175 | */ 176 | getBySubscriptions: (id) => new Promise((resolve, reject) => { 177 | //get user object 178 | user.findOne({ firebase_id: id }).then((result) => { 179 | // Get all videos that contains ids that exist in the user's subscribers' list 180 | Video 181 | .find({ creator: result.subscriptions }) 182 | .populate('creator') 183 | .exec() 184 | .then(result => { 185 | resolve(result); 186 | }).catch(error => { 187 | reject(error); 188 | }) 189 | }) 190 | 191 | }), 192 | 193 | 194 | /** 195 | * Gets a object of a video by id 196 | * 197 | * @param {String} id - id of the video to fetch the object of. 198 | * 199 | * @returns {Promise} Promise object represents the use object and, in case of failure, returns an error 200 | */ 201 | getById: (id) => new Promise((resolve, reject) => { 202 | Video 203 | .findById(id) 204 | .populate('creator') 205 | .exec() 206 | .then(video => { 207 | //generate the signed url and add it to the video object 208 | module.exports.generateSignedUrl(video.video).then(result => { 209 | video.video = result; 210 | resolve(video); 211 | }).catch(error => { 212 | reject(error); 213 | }) 214 | }).catch(error => { 215 | reject(error); 216 | }) 217 | }), 218 | 219 | /** 220 | * Generates a signed url for google storage bucket with a validity of 1 day 221 | * 222 | * @param {String} filename - name of the file to generate the url of. 223 | * 224 | * @returns {Promise} Promise object represents the url String of the google storage object , in case of failure, returns an error 225 | */ 226 | generateSignedUrl: (filename) => new Promise((resolve, reject) => { 227 | 228 | //get the current date and add one day to it 229 | const date = new Date(); 230 | const month = date.getUTCMonth() + 1; 231 | const day = date.getUTCDate() + 1; 232 | const year = date.getUTCFullYear(); 233 | 234 | const expires = month + '-' + day + '-' + year; 235 | const file = bucket.file(filename); 236 | 237 | //gets signed url using the google storage function 238 | file.getSignedUrl({ 239 | action: 'read', 240 | expires 241 | }).then(result => { 242 | resolve(result[0]); 243 | }) 244 | .catch(error => { 245 | reject(error); 246 | }) 247 | }) 248 | } -------------------------------------------------------------------------------- /backend/src/models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const UserSchema = new mongoose.Schema({ 4 | firebase_id: {type: String, required: true, unique: true}, 5 | name: {type: String, default: ''}, 6 | subscriptions: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }], 7 | subscribers: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }] 8 | }) 9 | 10 | module.exports = mongoose.model('User', UserSchema) -------------------------------------------------------------------------------- /backend/src/models/video.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const VideoSchema = new mongoose.Schema({ 4 | name: {type: String, default: ''}, 5 | description: {type: String, default: ''}, 6 | video: {type: String}, 7 | image: {type: String}, 8 | creator: {type: mongoose.Schema.Types.ObjectId, ref: 'User'} 9 | }) 10 | 11 | module.exports = mongoose.model('Video', VideoSchema) -------------------------------------------------------------------------------- /backend/src/routes.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const userFunc = require('./functions/user') 3 | const videoFunc = require('./functions/video') 4 | 5 | const multer = require('multer') 6 | 7 | const upload = multer().array('files') 8 | 9 | router.route('/').get(function (req, res) { 10 | res.json("Backend Connected"); 11 | }); 12 | 13 | /** 14 | * @api {post} /api/user/verify verify user session token 15 | * @apiName verifyUser 16 | * @apiGroup User 17 | * 18 | * @apiParam {Object} user - firebase user object. 19 | * @apiParam {String} idToken - session id token. 20 | * 21 | * @apiSuccess {Object} user object. 22 | * @apiError {Error} error object. 23 | */ 24 | router.route('/api/user/verify').post(function (req, res) { 25 | const { user, idToken } = req.query; 26 | userFunc.verifyAccount(user, idToken) 27 | .then(result => { 28 | res.json(result); 29 | }) 30 | .catch(error => { 31 | res.json(error); 32 | }) 33 | }); 34 | 35 | /** 36 | * @api {get} /api/user/get Request User information 37 | * @apiName getUser 38 | * @apiGroup User 39 | * 40 | * @apiParam {String} id - user's id. 41 | * 42 | * @apiSuccess {Object} user object. 43 | * @apiError {Error} error object. 44 | */ 45 | router.route('/api/user/get').get(function (req, res) { 46 | const { id } = req.query; 47 | userFunc.getById(id) 48 | .then(result => { 49 | res.json(result); 50 | }) 51 | .catch(error => { 52 | res.json(error); 53 | }) 54 | }); 55 | 56 | /** 57 | * @api {post} /api/user/subscribe subscribe user to another user 58 | * @apiName subscribeUser 59 | * @apiGroup User 60 | * 61 | * @apiParam {Object} user - firebase user object. 62 | * @apiParam {String} idToken - session id token. 63 | * @apiParam {String} id - user id to (un)subscribe. 64 | * 65 | * @apiSuccess {String} subscription status. 66 | * @apiError {Error} error object. 67 | */ 68 | router.route('/api/user/subscribe').post(function (req, res) { 69 | const { user, idToken, id } = req.query; 70 | userFunc.handleSubscribe(user, idToken, id) 71 | .then(result => { 72 | res.json(result); 73 | }) 74 | .catch(error => { 75 | res.json(error); 76 | }) 77 | }); 78 | 79 | /** 80 | * @api {post} /api/video/upload upload video with all the relevant info 81 | * @apiName uploadVideo 82 | * @apiGroup Video 83 | * 84 | * @apiParam {Object} user - firebase user object. 85 | * @apiParam {String} idToken - session id token. 86 | * @apiParam {String} description - description of the video. 87 | * @apiParam {String} name - name of the video. 88 | * 89 | * @apiSuccess {Object} video object. 90 | * @apiError {Error} error object. 91 | */ 92 | router.route('/api/video/upload').post(upload, videoFunc.upload, function (req, res) { 93 | const { description, name, user, idToken } = req.query; 94 | 95 | videoFunc.save(description, name, req.files, user, idToken) 96 | .then(result => { 97 | res.json(result); 98 | }) 99 | .catch(error => { 100 | res.json(error); 101 | }) 102 | }); 103 | 104 | 105 | /** 106 | * @api {post} /api/video/list List every video in the database. 107 | * @apiName listVideo 108 | * @apiGroup Video 109 | * 110 | * @apiSuccess {Object[]} list of video objects. 111 | * @apiError {Error} error object. 112 | */ 113 | router.route('/api/video/list').get(function (req, res) { 114 | videoFunc.list() 115 | .then(result => { 116 | res.json(result); 117 | }) 118 | .catch(error => { 119 | res.json(error); 120 | }) 121 | }); 122 | 123 | /** 124 | * @api {get} /api/video/user get every video of a user. 125 | * @apiName listUserVideo 126 | * @apiGroup Video 127 | * 128 | * @apiParam {String} id - user id to get the videos of. 129 | * 130 | * @apiSuccess {Object[]} list of video objects. 131 | * @apiError {Error} error object. 132 | */ 133 | router.route('/api/video/user').get(function (req, res) { 134 | const { id } = req.query; 135 | videoFunc.getByUser(id) 136 | .then(result => { 137 | res.json(result); 138 | }) 139 | .catch(error => { 140 | res.json(error); 141 | }) 142 | }); 143 | 144 | /** 145 | * @api {get} /api/video/subscriptions get every video of a user subscriptions. 146 | * @apiName listSubscritpionsVideo 147 | * @apiGroup Video 148 | * 149 | * @apiParam {String} id - user id to get the subscriptions' videos of. 150 | * 151 | * @apiSuccess {Object[]} list of video objects. 152 | * @apiError {Error} error object. 153 | */ 154 | router.route('/api/video/subscriptions').get(function (req, res) { 155 | const { id } = req.query; 156 | videoFunc.getBySubscriptions(id) 157 | .then(result => { 158 | res.json(result); 159 | }) 160 | .catch(error => { 161 | res.json(error); 162 | }) 163 | }); 164 | 165 | 166 | 167 | /** 168 | * @api {get} /api/video/get Fetches a video by id. 169 | * @apiName getVideo 170 | * @apiGroup Video 171 | * 172 | * @apiParam {String} id - id of the video to get. 173 | * 174 | * @apiSuccess {Object video object. 175 | * @apiError {Error} error object. 176 | */ 177 | router.route('/api/video/get').get(function (req, res) { 178 | const {id} = req.query 179 | 180 | videoFunc.getById(id) 181 | .then(result => { 182 | res.json(result); 183 | }) 184 | .catch(error => { 185 | res.json(error); 186 | }) 187 | }); 188 | 189 | module.exports = router; 190 | 191 | 192 | -------------------------------------------------------------------------------- /backend/stryker.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | mutator: "javascript", 4 | packageManager: "npm", 5 | reporters: ["html", "clear-text", "progress", "dashboard"], 6 | testRunner: "jest", 7 | transpilers: [], 8 | coverageAnalysis: "off", 9 | files: [ 10 | "**/*", 11 | "!node_modules/**/*" 12 | ], 13 | mutate: [ "__tests__/*"], 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /docker-compose.test.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | 3 | services: 4 | 5 | mongodb: 6 | image: "mongo" 7 | ports: 8 | - "27017:27017" 9 | test: 10 | container_name: test 11 | build: 12 | context: ./backend/. 13 | dockerfile: ./Dockerfile-test 14 | image: test:latest 15 | ports: 16 | - '6201:6201' 17 | environment: 18 | - NODE_ENV=test 19 | volumes: 20 | - ./backend/__tests__:/app/__tests__ 21 | - ./backend/src:/app/src 22 | - ./backend/setupTests.js:/app/setupTests.js 23 | depends_on: 24 | - mongodb 25 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | services: 4 | mongodb: 5 | image: "mongo" 6 | environment: 7 | - VIRTUAL_PORT=27017 8 | 9 | backend: 10 | build: ./backend/ 11 | expose: 12 | - 6200 13 | ports: 14 | - "6200:6200" 15 | volumes: 16 | - ./backend/src:/app/src 17 | environment: 18 | - NODE_ENV=development 19 | # - VIRTUAL_HOST=api.example.com,www.api.example.com 20 | # - VIRTUAL_PORT=6200 21 | # - LETSENCRYPT_HOST=api.example.com 22 | # - LETSENCRYPT_EMAIL=example@mail.com 23 | 24 | depends_on: 25 | - mongodb 26 | - frontend 27 | 28 | 29 | frontend: 30 | build: ./frontend/ 31 | expose: 32 | - 3000 33 | ports: 34 | - "3000:3000" 35 | volumes: 36 | - ./frontend/src:/app/src 37 | - ./frontend/public:/app/public 38 | environment: 39 | - NODE_ENV=development 40 | # - VIRTUAL_HOST=example.com,www.example.com 41 | # - VIRTUAL_PORT=3000 42 | # - LETSENCRYPT_HOST=example.com 43 | # - LETSENCRYPT_EMAIL=example@mail.com 44 | 45 | 46 | # nginx-proxy: 47 | # image: jwilder/nginx-proxy 48 | # ports: 49 | # - "80:80" 50 | # - "443:443" 51 | # volumes: 52 | # - "/etc/nginx/vhost.d" 53 | # - "/usr/share/nginx/html" 54 | # - "/var/run/docker.sock:/tmp/docker.sock:ro" 55 | # - "/etc/nginx/certs" 56 | 57 | # letsencrypt-nginx-proxy-companion: 58 | # image: jrcs/letsencrypt-nginx-proxy-companion 59 | # volumes: 60 | # - "/var/run/docker.sock:/var/run/docker.sock:ro" 61 | # volumes_from: 62 | # - "nginx-proxy" 63 | -------------------------------------------------------------------------------- /frontend/.eslintignore: -------------------------------------------------------------------------------- 1 | src/serviceWorker.js 2 | -------------------------------------------------------------------------------- /frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | jest:true, 5 | es6: true, 6 | }, 7 | extends: [ 8 | 'airbnb', 9 | ], 10 | globals: { 11 | Atomics: 'readonly', 12 | SharedArrayBuffer: 'readonly', 13 | }, 14 | parserOptions: { 15 | ecmaFeatures: { 16 | jsx: true, 17 | }, 18 | ecmaVersion: 2018, 19 | sourceType: 'module', 20 | }, 21 | plugins: [ 22 | 'react', 23 | ], 24 | rules: { 25 | "react/jsx-filename-extension":"off", 26 | }, 27 | 28 | }; 29 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | node_modules 3 | scripts/flow/*/.flowconfig 4 | *~ 5 | *.pyc 6 | .grunt 7 | _SpecRunner.html 8 | __benchmarks__ 9 | build/ 10 | remote-repo/ 11 | coverage/ 12 | .module-cache 13 | fixtures/dom/public/react-dom.js 14 | fixtures/dom/public/react.js 15 | test/the-files-to-test.generated.js 16 | *.log* 17 | chrome-user-data 18 | *.sublime-project 19 | *.sublime-workspace 20 | .idea 21 | *.iml 22 | .vscode 23 | *.swp 24 | *.swo 25 | 26 | packages/react-devtools-core/dist 27 | packages/react-devtools-extensions/chrome/build 28 | packages/react-devtools-extensions/chrome/*.crx 29 | packages/react-devtools-extensions/chrome/*.pem 30 | packages/react-devtools-extensions/firefox/build 31 | packages/react-devtools-extensions/firefox/*.xpi 32 | packages/react-devtools-extensions/firefox/*.pem 33 | packages/react-devtools-extensions/shared/build 34 | packages/react-devtools-inline/dist 35 | packages/react-devtools-shell/dist -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | # base image 2 | FROM node:12.2.0-alpine 3 | 4 | # set working directory 5 | WORKDIR /app 6 | 7 | # install and cache app dependencies 8 | COPY package*.json ./ 9 | 10 | RUN npm install --quiet 11 | 12 | 13 | COPY ./src ./src 14 | COPY ./public ./public 15 | 16 | # start app 17 | CMD ["npm", "start"] 18 | -------------------------------------------------------------------------------- /frontend/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/preset-env', '@babel/preset-react'], 3 | }; 4 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^4.9.7", 7 | "@material-ui/icons": "^4.9.1", 8 | "axios": "^0.19.0", 9 | "bootstrap": "^4.4.1", 10 | "eslint-friendly-formatter": "^4.0.1", 11 | "eslint-plugin-html": "^5.0.5", 12 | "firebase": "^7.2.2", 13 | "firebase-admin": "^8.6.1", 14 | "history": "^4.10.1", 15 | "js-cookie": "^2.2.1", 16 | "material-table": "^1.57.2", 17 | "postcss-loader": "^2.1.3", 18 | "react": "^16.12.0", 19 | "react-aspect-ratio": "^1.0.42", 20 | "react-device-detect": "^1.11.14", 21 | "react-dnd": "^10.0.2", 22 | "react-dnd-html5-backend": "^10.0.2", 23 | "react-dom": "^16.12.0", 24 | "react-firebaseui": "^4.0.0", 25 | "react-flippy": "^0.1.5", 26 | "react-hexagon": "^1.1.3", 27 | "react-hexagon-grid": "^1.1.1", 28 | "react-hexgrid": "^1.0.3", 29 | "react-html5video": "^2.5.1", 30 | "react-icons": "^3.9.0", 31 | "react-lazy-load-image-component": "^1.4.1", 32 | "react-router-dom": "^5.1.2", 33 | "react-scripts": "^3.4.0", 34 | "react-scroll-trigger": "^0.6.2", 35 | "react-tag-input": "^6.4.2", 36 | "react-tagsinput": "^3.19.0", 37 | "react-truncate": "^2.4.0" 38 | }, 39 | "scripts": { 40 | "start": "react-scripts start", 41 | "build": "react-scripts build", 42 | "test-react": "react-scripts test", 43 | "eject": "react-scripts eject", 44 | "lint": "eslint . ", 45 | "lint-fix": "eslint . --fix", 46 | "pre:push": "set CI=true && npm run test && npm run lint", 47 | "pre:commit": "npm run lint && npm run test", 48 | "test": "jest " 49 | }, 50 | "husky": { 51 | "hooks": {} 52 | }, 53 | "eslintConfig": { 54 | "extends": "react-app" 55 | }, 56 | "browserslist": { 57 | "production": [ 58 | ">0.2%", 59 | "not dead", 60 | "not op_mini all" 61 | ], 62 | "development": [ 63 | "last 1 chrome version", 64 | "last 1 firefox version", 65 | "last 1 safari version" 66 | ] 67 | }, 68 | "devDependencies": { 69 | "babel-jest": "^24.9.0", 70 | "enzyme": "^3.10.0", 71 | "enzyme-adapter-react-16": "^1.15.1", 72 | "enzyme-to-json": "^3.4.2", 73 | "eslint": "^6.5.1", 74 | "eslint-config-airbnb": "^18.0.1", 75 | "eslint-plugin-import": "^2.18.2", 76 | "eslint-plugin-jsx-a11y": "^6.2.3", 77 | "eslint-plugin-react": "^7.16.0", 78 | "eslint-plugin-react-hooks": "^1.7.0", 79 | "husky": "^3.0.9", 80 | "jest": "^24.9.0", 81 | "react-test-renderer": "^16.10.2" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimCoderYoutube/YoutubeClone/23d2fe13f34785a262c12559800be04c940446b0/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Youtube Clone 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Simcoder_Airbnb", 3 | "name": "SimCoder - Airbnb Clone", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": "/", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import {BrowserRouter as Router, Route} from 'react-router-dom' 3 | import firebase from 'firebase' 4 | import axios from 'axios' 5 | 6 | 7 | import Home from './components/Home' 8 | import Subscriptions from './components/Subscriptions' 9 | import Login from './components/user/Login' 10 | import Upload from './components/user/Upload' 11 | import VideoDisplay from './components/VideoDisplay' 12 | import Profile from './components/user/Profile' 13 | import 'bootstrap/dist/css/bootstrap.css'; 14 | 15 | import './styles.css'; 16 | 17 | 18 | 19 | export class App extends Component { 20 | constructor(props){ 21 | super(props); 22 | 23 | this.state = { 24 | loaded: false 25 | } 26 | } 27 | 28 | componentDidMount(){ 29 | firebase.auth().onAuthStateChanged(user => { 30 | if(!user){ 31 | this.setState({loaded: true}); 32 | } 33 | else{ 34 | firebase.auth().currentUser.getIdToken(true) 35 | .then(idToken =>{ 36 | axios.post('http://127.0.0.1:6200/api/user/verify', null, { 37 | params: { 38 | user: firebase.auth().currentUser, 39 | idToken 40 | } 41 | }).then(result => { 42 | console.log(result) 43 | }) 44 | }) 45 | 46 | this.setState({loaded: true}); 47 | } 48 | }) 49 | } 50 | render() { 51 | if(this.state.loaded){ 52 | return ( 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | ) 62 | } 63 | 64 | return( 65 |
66 | Loading . . . 67 |
68 | ) 69 | 70 | 71 | } 72 | } 73 | 74 | export default App 75 | -------------------------------------------------------------------------------- /frontend/src/__tests__/App.test.js: -------------------------------------------------------------------------------- 1 | 2 | describe('Examining the syntax of Jest tests', () => { 3 | it('sums numbers', () => { 4 | expect(1 + 2).toEqual(3); 5 | expect(2 + 2).toEqual(4); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /frontend/src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimCoderYoutube/YoutubeClone/23d2fe13f34785a262c12559800be04c940446b0/frontend/src/assets/favicon.ico -------------------------------------------------------------------------------- /frontend/src/components/Home.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import Navbar from './Navbar' 3 | import VideoList from './VideoList' 4 | 5 | export class Home extends Component { 6 | render() { 7 | return ( 8 |
9 | 10 | this is home 11 | 12 | 13 |
14 | ) 15 | } 16 | } 17 | 18 | export default Home 19 | -------------------------------------------------------------------------------- /frontend/src/components/Navbar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import {Link} from "react-router-dom" 3 | import firebase from 'firebase' 4 | 5 | export class Navbar extends Component { 6 | 7 | onSignOut() { 8 | firebase.auth().signOut().catch(error =>{ 9 | console.log(error) 10 | }) 11 | } 12 | render() { 13 | return ( 14 |
15 | Login 16 | Upload 17 | Subscriptions 18 | 19 |
20 | ) 21 | } 22 | } 23 | 24 | export default Navbar 25 | -------------------------------------------------------------------------------- /frontend/src/components/Subscriptions.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import Navbar from './Navbar' 3 | import VideoList from './VideoList' 4 | 5 | export class Subscriptions extends Component { 6 | render() { 7 | return ( 8 |
9 | 10 | 11 | This are your subscriptions' videos 12 | 13 | 14 |
15 | ) 16 | } 17 | } 18 | 19 | export default Subscriptions 20 | -------------------------------------------------------------------------------- /frontend/src/components/VideoDisplay.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import axios from 'axios'; 3 | import { DefaultPlayer as Video } from 'react-html5video' 4 | import 'react-html5video/dist/styles.css' 5 | import Navbar from './Navbar' 6 | 7 | import { Link } from 'react-router-dom' 8 | 9 | export class VideoDisplay extends Component { 10 | 11 | constructor(props) { 12 | super(props); 13 | 14 | this.state = { 15 | video: null 16 | } 17 | } 18 | 19 | componentDidMount() { 20 | axios.get('http://127.0.0.1:6200/api/video/get', { 21 | params: { 22 | id: this.props.match.params.id 23 | } 24 | }).then(result => { 25 | this.setState({ video: result.data }); 26 | }) 27 | } 28 | render() { 29 | const { video } = this.state 30 | 31 | 32 | if (video == null) { 33 | return (<>) 34 | } 35 | 36 | 37 | console.log(video) 38 | return ( 39 |
40 | 41 | 42 |
43 | 46 | 47 | 48 |

{video.creator.name}

49 | 50 |

{video.name}

51 |

{video.description}

52 |
53 |
54 | ) 55 | } 56 | } 57 | 58 | export default VideoDisplay 59 | -------------------------------------------------------------------------------- /frontend/src/components/VideoList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import axios from 'axios'; 3 | import { LazyLoadImage } from 'react-lazy-load-image-component' 4 | import { Link } from 'react-router-dom' 5 | import firebase from 'firebase' 6 | 7 | export class VideoList extends Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | videoList: [], 13 | location: this.props.location, 14 | id: this.props.id 15 | } 16 | } 17 | componentDidMount() { 18 | let that = this 19 | 20 | switch (this.state.location) { 21 | case 'home': 22 | axios.get('http://127.0.0.1:6200/api/video/list') 23 | .then(result => { 24 | that.setState({ videoList: result.data }); 25 | console.log(that.state.videoList); 26 | }) 27 | break; 28 | 29 | case 'profile': 30 | axios.get('http://127.0.0.1:6200/api/video/user', { params: { id: this.state.id } }) 31 | .then(result => { 32 | that.setState({ videoList: result.data }); 33 | console.log(that.state.videoList); 34 | }) 35 | break; 36 | 37 | case 'subscriptions': 38 | axios.get('http://127.0.0.1:6200/api/video/subscriptions', { params: { id: firebase.auth().currentUser.uid } }) 39 | .then(result => { 40 | that.setState({ videoList: result.data }); 41 | console.log(that.state.videoList); 42 | }) 43 | break; 44 | 45 | 46 | } 47 | } 48 | render() { 49 | return ( 50 |
51 |
52 | {this.state.videoList.map(currentVideo => { 53 | if (currentVideo.creator == null) { 54 | return; 55 | } 56 | return ( 57 | 58 | 59 |

{currentVideo.name}

60 |

{currentVideo.creator.name}

61 | 62 | ) 63 | })} 64 | 65 |
66 |
67 | 68 | ) 69 | } 70 | } 71 | 72 | export default VideoList 73 | -------------------------------------------------------------------------------- /frontend/src/components/user/Login.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Redirect } from 'react-router-dom' 3 | 4 | import Navbar from '../Navbar' 5 | import firebase from 'firebase' 6 | 7 | export class Login extends Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | email: "", 13 | password: "" 14 | } 15 | 16 | this.onSignIn = this.onSignIn.bind(this) 17 | this.onSignUp = this.onSignUp.bind(this) 18 | } 19 | 20 | onSignUp() { 21 | firebase.auth().createUserWithEmailAndPassword(this.state.email, this.state.password).catch(function(error) { 22 | console.log(error) 23 | }); 24 | } 25 | 26 | onSignIn() { 27 | firebase.auth().signInWithEmailAndPassword(this.state.email, this.state.password).catch(function(error) { 28 | console.log(error) 29 | }); 30 | } 31 | 32 | render() { 33 | if(firebase.auth().currentUser == undefined){ 34 | return ( 35 |
36 | 37 |
38 | this.setState({ email: e.target.value })} /> 44 | this.setState({ password: e.target.value })} /> 50 | 51 | 52 |
53 |
54 | ) 55 | } 56 | 57 | 58 | return( 59 | 60 | ) 61 | 62 | } 63 | } 64 | 65 | export default Login 66 | -------------------------------------------------------------------------------- /frontend/src/components/user/Profile.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import axios from 'axios' 3 | import VideoList from '../VideoList' 4 | import Navbar from '../Navbar' 5 | import firebase from 'firebase' 6 | 7 | export class Profile extends Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | user: null 13 | } 14 | 15 | this.handleSubscribe = this.handleSubscribe.bind(this); 16 | } 17 | 18 | componentDidMount() { 19 | let that = this; 20 | axios.get('http://127.0.0.1:6200/api/user/get', { params: { id: this.props.match.params.id } }) 21 | .then((response) => { 22 | that.setState({ 23 | user: response.data 24 | }) 25 | }) 26 | .catch((error) => { 27 | console.log(error); 28 | }) 29 | } 30 | 31 | handleSubscribe() { 32 | 33 | let that = this; 34 | 35 | firebase.auth().currentUser.getIdToken(true).then((idToken) => { 36 | axios.post('http://127.0.0.1:6200/api/user/subscribe', null, 37 | { 38 | params: 39 | { 40 | user: firebase.auth().currentUser, 41 | idToken, 42 | id: this.props.match.params.id 43 | } 44 | }).then((result) => { 45 | console.log(result); 46 | }).catch((error) => { 47 | console.log(error); 48 | }) 49 | }) 50 | } 51 | render() { 52 | const { user } = this.state; 53 | 54 | if (user == null) { 55 | return (<>) 56 | } 57 | 58 | return ( 59 |
60 | 61 |
62 |

{user.name}

63 | 64 | 65 | 66 |
67 |
68 | 69 | ) 70 | } 71 | } 72 | 73 | export default Profile 74 | -------------------------------------------------------------------------------- /frontend/src/components/user/Upload.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Navbar from '../Navbar'; 3 | import firebase from 'firebase'; 4 | import axios from 'axios'; 5 | 6 | export class Upload extends Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.state = { 11 | name: '', 12 | description: '', 13 | video: '', 14 | image: '' 15 | } 16 | 17 | this.handleUpload = this.handleUpload.bind(this); 18 | } 19 | 20 | handleUpload() { 21 | if (this.state.video == null || this.state.video == null) { 22 | return; 23 | } 24 | 25 | const data = new FormData(); 26 | data.append('files', this.state.video) 27 | data.append('files', this.state.image) 28 | 29 | const { description, name } = this.state 30 | 31 | firebase.auth().currentUser.getIdToken(true) 32 | .then(idToken => { 33 | axios.post('http://127.0.0.1:6200/api/video/upload', data, { params: { description, name, user: firebase.auth().currentUser, idToken } }) 34 | .then(result => { 35 | console.log(result) 36 | }) 37 | .catch(error => { 38 | console.log(error) 39 | }) 40 | }) 41 | .catch(error => { 42 | console.log(error) 43 | }) 44 | } 45 | render() { 46 | return ( 47 |
48 | 49 | this.setState({ name: e.target.value })} /> 54 | this.setState({ description: e.target.value })} /> 59 | this.setState({ video: e.target.files[0] })} /> 65 | this.setState({ image: e.target.files[0] })} /> 71 | 72 | 77 | 78 |
79 | ) 80 | } 81 | } 82 | 83 | export default Upload 84 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 12 | monospace; 13 | } 14 | 15 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | import firebase from 'firebase'; 8 | 9 | const firebaseConfig = require('./config/config').firebaseConfig; 10 | firebase.initializeApp(firebaseConfig) 11 | 12 | 13 | ReactDOM.render(, document.getElementById('root')); 14 | 15 | // If you want your app to work offline and load faster, you can change 16 | // unregister() to register() below. Note this comes with some pitfalls. 17 | // Learn more about service workers: https://bit.ly/CRA-PWA 18 | serviceWorker.unregister(); 19 | -------------------------------------------------------------------------------- /frontend/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' 15 | // [::1] is the IPv6 localhost address. 16 | || window.location.hostname === '[::1]' 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | || window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/, 20 | ), 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' 46 | + 'worker. To learn more, visit https://bit.ly/CRA-PWA', 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then((registration) => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' 74 | + 'tabs for this page are closed. See https://bit.ly/CRA-PWA.', 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch((error) => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then((response) => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 109 | || (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then((registration) => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.', 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then((registration) => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /frontend/src/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 4 | -webkit-font-smoothing: antialiased; 5 | -moz-osx-font-smoothing: grayscale; 6 | background: #ffffff !important; 7 | min-height: 100vh; 8 | font: normal 16px sans-serif; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; 13 | } 14 | 15 | .container { 16 | margin-top: 100px; 17 | } 18 | 19 | .video-list-img{ 20 | max-height: 140px; 21 | object-fit: contain; 22 | } 23 | -------------------------------------------------------------------------------- /nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:latest 2 | 3 | COPY nginx.conf /etc/nginx/nginx.conf -------------------------------------------------------------------------------- /nginx/config.conf: -------------------------------------------------------------------------------- 1 | 2 | worker_processes 1; 3 | 4 | events { worker_connections 1024; } 5 | 6 | 7 | http { 8 | 9 | log_format compression '$remote_addr - $remote_user [$time_local] ' 10 | '"$request" $status $upstream_addr ' 11 | '"$http_referer" "$http_user_agent" "$gzip_ratio"'; 12 | 13 | upstream frontend { 14 | server frontend:3000; 15 | } 16 | 17 | server { 18 | listen 8080; 19 | access_log /var/log/nginx/access.log compression; 20 | 21 | location /hello/ { 22 | proxy_pass http://frontend/; 23 | proxy_redirect off; 24 | proxy_set_header Host $host; 25 | proxy_set_header X-Real-IP $remote_addr; 26 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 27 | proxy_set_header X-Forwarded-Host $server_name; 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | docker-compose -f docker-compose.test.yml up --exit-code-from test $1 --------------------------------------------------------------------------------