├── .babelrc ├── .nsprc ├── server ├── public │ ├── doge.jpg │ ├── login.png │ ├── logout.png │ └── account_circle.png ├── search.js ├── server.js └── db.js ├── .editorconfig ├── reset.sh ├── src ├── index.js ├── index.html └── index.sass ├── webpack.config.js ├── .eslintrc.json ├── LICENSE ├── .gitignore ├── components ├── Doge.jsx ├── VideoSelectionCard.jsx ├── Trending.jsx ├── Feedback.jsx ├── SearchResults.jsx ├── Navbar.jsx ├── TrendingVideo.jsx ├── VideoUploadCard.jsx ├── ThumbnailRow.jsx ├── ConfirmDelete.jsx ├── Sidebar.jsx ├── Upload.jsx ├── NewLogin.jsx ├── RoutesSwitch.jsx ├── App.jsx ├── Register.jsx ├── Login.jsx ├── PublicProfile.jsx ├── Comments.jsx ├── SettingsPage.jsx └── WatchPage.jsx ├── package.json ├── Creating_DB.sql └── README.md /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | "env" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.nsprc: -------------------------------------------------------------------------------- 1 | { 2 | "exceptions": [ 3 | "https://nodesecurity.io/advisories/532" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /server/public/doge.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yrahul3910/video-sharing-site/HEAD/server/public/doge.jpg -------------------------------------------------------------------------------- /server/public/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yrahul3910/video-sharing-site/HEAD/server/public/login.png -------------------------------------------------------------------------------- /server/public/logout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yrahul3910/video-sharing-site/HEAD/server/public/logout.png -------------------------------------------------------------------------------- /server/public/account_circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yrahul3910/video-sharing-site/HEAD/server/public/account_circle.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /reset.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Delete all indices from search 4 | echo "Deleting search indices..." 5 | curl -XDELETE http://localhost:9200/qtube > /dev/null 2> /dev/null 6 | 7 | # Delete folders 8 | echo "Deleting user data..." 9 | rm -rf videos/ 10 | rm -rf users/ 11 | 12 | # Re-run MySQL script 13 | echo "Running MySQL script..." 14 | mysql -u $1 -p < Creating_DB.sql 15 | 16 | echo "Project reset complete." 17 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | // App entry point 3 | import "./index.sass"; 4 | import React from "react"; 5 | import ReactDOM from "react-dom"; 6 | import {HashRouter as Router} from "react-router-dom"; 7 | 8 | import RoutesSwitch from "../components/RoutesSwitch.jsx"; 9 | 10 | ReactDOM.render( 11 | 12 | 13 | , document.getElementById("app") 14 | ); 15 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | OpenVideo 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import HtmlWebpackPlugin from "html-webpack-plugin"; 3 | 4 | export default { 5 | entry: [ 6 | path.resolve(__dirname, "src/index") 7 | ], 8 | output: { 9 | path: path.resolve(__dirname, "src"), 10 | publicPath: "/", 11 | filename: "bundle.js" 12 | }, 13 | plugins: [ 14 | new HtmlWebpackPlugin({ 15 | template: "src/index.html", 16 | inject: true 17 | }) 18 | ], 19 | module: { 20 | loaders: [ 21 | {test: /\.jsx$/, exclude: /node_modules/, loaders: ["babel-loader"]}, 22 | {test: /\.js$/, exclude: /node_modules/, loaders: ["babel-loader"]}, 23 | {test: /\.css$/, loaders: ["style-loader","css-loader"]}, 24 | {test: /\.sass$/, loaders: ["style-loader", "css-loader", "sass-loader"]} 25 | ] 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "mocha": true 7 | }, 8 | "extends": ["eslint:recommended", "plugin:react/recommended"], 9 | "plugins": [ 10 | "react" 11 | ], 12 | "parserOptions": { 13 | "ecmaVersion": 7, 14 | "ecmaFeatures": { 15 | "experimentalObjectRestSpread": true, 16 | "jsx": true 17 | }, 18 | "sourceType": "module" 19 | }, 20 | "rules": { 21 | "indent": [ 22 | "error", 23 | 4 24 | ], 25 | "linebreak-style": [ 26 | "error", 27 | "unix" 28 | ], 29 | "quotes": [ 30 | "error", 31 | "double" 32 | ], 33 | "semi": [ 34 | "error", 35 | "always" 36 | ], 37 | "no-console": 1 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Rahul Yedida 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | videos/ 61 | users/ 62 | -------------------------------------------------------------------------------- /components/Doge.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import {Link} from "react-router-dom"; 4 | 5 | import Navbar from "./Navbar.jsx"; 6 | import Sidebar from "./Sidebar.jsx"; 7 | 8 | class Doge extends React.Component { 9 | render() { 10 | return ( 11 |
12 | 13 | 14 |
15 |
16 | 17 |
18 |
19 |

20 | It seems you haven't subscribed to any users yet. Why not head over to the 21 | trending page to find some inspiration? 22 |

23 |
24 |
25 |
26 | ); 27 | } 28 | } 29 | 30 | Doge.propTypes = { 31 | user: PropTypes.object, 32 | toggleLogin: PropTypes.func.isRequired 33 | }; 34 | 35 | export default Doge; 36 | -------------------------------------------------------------------------------- /components/VideoSelectionCard.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | class VideoSelectionCard extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | } 8 | 9 | render() { 10 | return ( 11 |
12 |
13 | 14 | cloud_upload 15 | 16 |
17 |
18 | 20 |
21 |
{this.props.text}
22 |
23 |
24 |
25 |
26 |
27 | ); 28 | } 29 | } 30 | 31 | VideoSelectionCard.propTypes = { 32 | selectClick: PropTypes.func.isRequired, 33 | changeClick: PropTypes.func.isRequired, 34 | text: PropTypes.string.isRequired 35 | }; 36 | 37 | export default VideoSelectionCard; 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "YoutubeClone", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "npm-run-all --parallel server security-check lint:watch", 8 | "server": "babel-node server/server.js", 9 | "security-check": "nsp check", 10 | "lint": "esw", 11 | "lint:watch": "npm run lint -- --watch" 12 | }, 13 | "dependencies": { 14 | "bcrypt": "^1.0.3", 15 | "body-parser": "^1.18.2", 16 | "elasticsearch": "^14.0.0", 17 | "eslint-watch": "^3.1.3", 18 | "helmet": "^3.9.0", 19 | "jsonwebtoken": "^8.1.0", 20 | "lodash.chunk": "^4.2.0", 21 | "lodash.groupby": "^4.6.0", 22 | "moment": "^2.19.2", 23 | "numeral": "^2.0.6", 24 | "react-router-dom": "^4.2.2" 25 | }, 26 | "devDependencies": { 27 | "babel-cli": "^6.26.0", 28 | "babel-core": "^6.26.0", 29 | "babel-loader": "^7.1.2", 30 | "babel-preset-env": "^1.6.1", 31 | "babel-preset-react": "^6.24.1", 32 | "babel-register": "^6.26.0", 33 | "compression": "^1.7.1", 34 | "cors": "^2.8.4", 35 | "css-loader": "^0.28.7", 36 | "dotenv": "^4.0.0", 37 | "eslint": "^4.12.0", 38 | "eslint-plugin-import": "^2.8.0", 39 | "eslint-plugin-react": "^7.5.1", 40 | "express": "^4.16.2", 41 | "formidable": "^1.1.1", 42 | "html-webpack-plugin": "^2.30.1", 43 | "mysql": "^2.15.0", 44 | "node-sass": "^4.7.2", 45 | "npm-run-all": "^4.1.2", 46 | "nsp": "^3.1.0", 47 | "open": "^0.0.5", 48 | "prop-types": "^15.6.0", 49 | "react": "^16.1.1", 50 | "react-dom": "^16.1.1", 51 | "request": "^2.83.0", 52 | "sass-loader": "^6.0.6", 53 | "style-loader": "^0.19.0", 54 | "webpack": "^3.8.1", 55 | "webpack-dev-middleware": "^1.12.2" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /server/search.js: -------------------------------------------------------------------------------- 1 | const elasticsearch = require("elasticsearch"); 2 | const esClient = new elasticsearch.Client({ 3 | host: "127.0.0.1:9200", 4 | log: "error" 5 | }); 6 | 7 | // Index new object to ElasticSearch 8 | exports.index = (index, type, data) => { 9 | /*if(type == "user") { 10 | esClient.index({ 11 | index, 12 | type, 13 | id: data.username, 14 | body: { 15 | name: data.name, 16 | username: data.username 17 | } 18 | }, (error) => { 19 | if (error) 20 | throw error; 21 | }); 22 | } */ 23 | if (type == "video") { 24 | esClient.index({ 25 | index, 26 | type, 27 | id: data.video_id, 28 | body: { 29 | description: data.description, 30 | title: data.title, 31 | username: data.username, 32 | thumbnail: data.thumbnail 33 | } 34 | }, (error) => { 35 | if (error) 36 | throw error; 37 | }); 38 | } 39 | }; 40 | 41 | // Get all the indices from ElasticSearch 42 | exports.indices = () => { 43 | return esClient.cat.indices({v: true}) 44 | .then(console.log) // eslint-disable-line no-console 45 | .catch(err => { 46 | throw err; 47 | }); 48 | }; 49 | 50 | // Delete document from index 51 | exports.deleteDoc = (index, type, id, func) => { 52 | esClient.delete({ 53 | index, 54 | type, 55 | id 56 | }, () => { 57 | func(); 58 | }); 59 | }; 60 | 61 | // Search for documents 62 | exports.search = (index, body) => { 63 | return esClient.search({index, body}); 64 | }; 65 | 66 | module.exports = exports; 67 | 68 | -------------------------------------------------------------------------------- /components/Trending.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | import React from "react"; 3 | import PropTypes from "prop-types"; 4 | 5 | import Navbar from "./Navbar.jsx"; 6 | import TrendingVideo from "./TrendingVideo.jsx"; 7 | import Sidebar from "./Sidebar.jsx"; 8 | 9 | class Trending extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | this.state = {videos: null}; 13 | } 14 | 15 | componentDidMount() { 16 | $.getJSON("http://localhost:8000/api/trending", (data) => { 17 | if (data.success) 18 | this.setState({videos: data.videos}); 19 | }); 20 | } 21 | 22 | render() { 23 | let content; 24 | if (!this.state.videos) 25 | content =
Loading...
; 26 | else { 27 | content = this.state.videos.map((element, index) => { 28 | return ; 36 | }); 37 | } 38 | return ( 39 |
40 | 41 | 42 |
43 | {content} 44 |
45 |
46 | ); 47 | } 48 | } 49 | 50 | Trending.propTypes = { 51 | user: PropTypes.object, 52 | toggleLogin: PropTypes.func.isRequired 53 | }; 54 | 55 | export default Trending; 56 | -------------------------------------------------------------------------------- /components/Feedback.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | import React from "react"; 3 | import PropTypes from "prop-types"; 4 | import {Redirect} from "react-router-dom"; 5 | 6 | import Navbar from "./Navbar.jsx"; 7 | 8 | class Feedback extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = {loggedIn: (this.props.user ? true : false)}; 12 | this.click = this.click.bind(this); 13 | } 14 | 15 | click() { 16 | let feedback = $("#feedback").val(); 17 | if (feedback.trim() == "") { 18 | Materialize.toast("You may not submit empty feedback.", 2000, "rounded"); 19 | return; 20 | } 21 | $.post("http://localhost:8000/api/feedback", { 22 | token: localStorage.getItem("token"), 23 | feedback 24 | }, (res) => { 25 | Materialize.toast(res.message, 4000); 26 | }); 27 | } 28 | 29 | render() { 30 | if (!this.state.loggedIn) { 31 | Materialize.toast("You need to be logged in!", 4000); 32 | return ( 33 | 34 | ); 35 | } 36 | return ( 37 |
38 | 39 |
40 |
Tell us how we’re doing!
41 |
42 |
43 | 44 | 45 |
46 |
47 |
48 | Submit 49 |
50 |
51 |
52 | ); 53 | } 54 | } 55 | 56 | Feedback.propTypes = { 57 | user: PropTypes.object 58 | }; 59 | 60 | export default Feedback; 61 | -------------------------------------------------------------------------------- /components/SearchResults.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | import React from "react"; 3 | import PropTypes from "prop-types"; 4 | 5 | import Navbar from "./Navbar.jsx"; 6 | import Sidebar from "./Sidebar.jsx"; 7 | import TrendingVideo from "./TrendingVideo.jsx"; 8 | 9 | class SearchResults extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | this.state = {results: []}; 13 | } 14 | 15 | componentDidMount() { 16 | $.post("http://localhost:8000/api/search", { 17 | query: this.props.match.params.q 18 | }, (data) => { 19 | this.setState({results: data}); 20 | }); 21 | } 22 | 23 | render() { 24 | let {time, results} = this.state.results; 25 | let timeDiv =
Fetched {results ? results.length : 0} results in {time} ms.
; 26 | // Results div 27 | let div = this.state.results.results ? this.state.results.results.map((result, i) => { 28 | return ; 34 | }) :
; 35 | 36 | return ( 37 |
38 | 39 | 40 |
41 |
42 | {timeDiv} 43 |
44 |
45 | {div} 46 |
47 |
48 |
49 | ); 50 | } 51 | } 52 | 53 | SearchResults.propTypes = { 54 | user: PropTypes.object, 55 | toggleLogin: PropTypes.func.isRequired, 56 | match: PropTypes.object 57 | }; 58 | 59 | export default SearchResults; 60 | -------------------------------------------------------------------------------- /components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | import React from "react"; 3 | import PropTypes from "prop-types"; 4 | 5 | import {Link, Redirect} from "react-router-dom"; 6 | 7 | class Navbar extends React.Component { 8 | /* 9 | props: 10 | dp: base64 encoded image string 11 | */ 12 | constructor(props) { 13 | super(props); 14 | this.state = {search: 0, q: ""}; 15 | this.search = this.search.bind(this); 16 | this.change = this.change.bind(this); 17 | } 18 | 19 | change() { 20 | if ($(".search-input").val()) 21 | this.setState({q: $(".search-input").val()}); 22 | } 23 | 24 | search() { 25 | window.location = "/#/search/" + this.state.q; 26 | window.location.reload(); 27 | } 28 | 29 | render() { 30 | return ( 31 |
32 |
33 | 34 |

OpenVideo

36 | 37 |
38 |
39 |
40 | 41 | 43 | search 44 | 45 |
46 |
47 |
48 | 49 |
50 |
51 | ); 52 | } 53 | } 54 | 55 | Navbar.propTypes = { 56 | dp: PropTypes.string 57 | }; 58 | 59 | export default Navbar; 60 | -------------------------------------------------------------------------------- /components/TrendingVideo.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import numeral from "numeral"; 4 | import {Link} from "react-router-dom"; 5 | 6 | class TrendingVideo extends React.Component { 7 | render() { 8 | return ( 9 |
10 |
11 | 12 |
13 |
14 |
15 | 16 |

17 | {this.props.title} 18 |

19 | 20 |
21 |
22 | 23 |

24 | {this.props.user + 25 | (this.props.views ? (" • " + numeral(this.props.views).format("0.0a") + " views") : "") + 26 | (this.props.age ? ((typeof(this.props.age) == "number") ? (" • " + this.props.age + " days ago") 27 | : (" • " + this.props.age)) 28 | : "") } 29 |

30 | 31 |
32 |
33 |

34 | {this.props.desc ? this.props.desc : ""} 35 |

36 |
37 |
38 |
39 | ); 40 | } 41 | } 42 | 43 | TrendingVideo.propTypes = { 44 | thumbnail: PropTypes.string.isRequired, 45 | title: PropTypes.string.isRequired, 46 | user: PropTypes.string.isRequired, // this is the username 47 | views: PropTypes.string, 48 | age: PropTypes.any, 49 | desc: PropTypes.string, 50 | video_id: PropTypes.number.isRequired 51 | }; 52 | 53 | export default TrendingVideo; 54 | -------------------------------------------------------------------------------- /components/VideoUploadCard.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | class VideoUploadCard extends React.Component { 5 | render() { 6 | return ( 7 |
8 |
9 | 10 | Upload Options 11 | 12 |
13 |
14 |
15 |
16 | 17 | 18 |
19 |
20 |
21 |
22 | 215 | 216 |
217 |
218 | 220 | COMMENT 221 | 222 |
223 | ) :
; 224 | 225 | let recommendDiv = this.state.recommendations ? this.state.recommendations.map((val, i) => 226 |
227 | 231 |
232 | ) :
; 233 | 234 | return ( 235 |
236 | 237 |
238 |
239 |
240 | 244 |
245 |
246 |

247 | {this.state.video.title} 248 |

249 |

251 | {this.state.video.views + " views"} 252 |

253 |
254 |
255 | 257 | thumb_up 258 | 259 | 260 | {numeral(this.state.video.upvotes).format("0a")} 261 | 262 | 264 | thumb_down 265 | 266 | {" " + numeral(this.state.video.downvotes).format("0a")} 267 |
268 |
269 |
270 |
271 | 273 |
274 | 275 | {this.state.video.name} 276 | 277 |

Published on {new Date(this.state.video.upload_date).toDateString()}

278 |

{this.state.video.description}

279 |
280 |
281 |
282 |

Comments

283 | {commentBox} 284 | 285 |
286 |
287 |

Videos by this user

288 | {recommendDiv} 289 |
290 |
291 |
292 |
293 | ); 294 | } 295 | } 296 | 297 | WatchPage.propTypes = { 298 | match: PropTypes.object, 299 | user: PropTypes.object 300 | }; 301 | 302 | export default WatchPage; 303 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | import cors from "cors"; 2 | import express from "express"; 3 | import path from "path"; 4 | import open from "open"; 5 | import compression from "compression"; 6 | import bodyParser from "body-parser"; 7 | import dotenv from "dotenv"; 8 | import jwt from "jsonwebtoken"; 9 | import formidable from "formidable"; 10 | import fs from "fs"; 11 | import helmet from "helmet"; 12 | import numeral from "numeral"; 13 | 14 | // Used for transpiling 15 | import webpack from "webpack"; 16 | import config from "../webpack.config"; 17 | 18 | import dbUtils from "./db"; 19 | import searchUtils from "./search"; 20 | 21 | const port = 8000; 22 | const app = express(); 23 | const compiler = webpack(config); 24 | const illegalCharsFormat = /[!@#$%^&*()+\-=[\]{};':"\\|,.<>/?]/; 25 | dotenv.config(); 26 | 27 | // gzip files 28 | app.use(helmet()); 29 | app.use(compression()); 30 | app.use(bodyParser.json()); 31 | app.use(bodyParser({extended: true})); 32 | app.use(cors()); 33 | app.use(express.static(__dirname + "/public")); 34 | app.use("/videos", express.static(__dirname + "/../videos")); 35 | app.use("/users", express.static(__dirname + "/../users")); 36 | 37 | // Use Webpack middleware 38 | app.use(require("webpack-dev-middleware")(compiler, { 39 | noInfo: true, 40 | publicPath: config.output.publicPath 41 | })); 42 | 43 | app.get("/api/trending", (req, res) => { 44 | res.writeHead(200, {"Content-Type": "application/json"}); 45 | 46 | // Define trending as videos uploaded in the last 5 days, with maximum views. 47 | dbUtils.init(); 48 | dbUtils.trending((err, videos) => { 49 | if (err) throw err; 50 | res.end(JSON.stringify({ 51 | success: true, 52 | videos 53 | })); 54 | }); 55 | }); 56 | 57 | app.get("/api/user/:username", (req, res) => { 58 | res.writeHead(200, {"Content-Type": "application/json"}); 59 | 60 | dbUtils.init(); 61 | dbUtils.userDetails(req.params.username, (err, results) => { 62 | if (err) 63 | res.end(JSON.stringify({ 64 | success: false, 65 | message: "Username does not exist." 66 | })); 67 | else 68 | res.end(JSON.stringify({ 69 | success: true, 70 | data: results 71 | })); 72 | }); 73 | }); 74 | 75 | app.get("/*", (req, res) => { 76 | res.sendFile(path.join(__dirname, "../src/index.html")); 77 | }); 78 | 79 | app.post("/api/upload", (req, res) => { 80 | res.writeHead(200, {"Content-Type": "application/json"}); 81 | 82 | let form = new formidable.IncomingForm(); 83 | form.parse(req, (err, fields, files) => { 84 | let {video, thumbnail} = files; 85 | let {title, desc, token} = fields; 86 | // Verify file types 87 | if (!thumbnail.type.match(/image\/.*/)) { 88 | res.end(JSON.stringify({success: false, message: "Thumbnail must be an image."})); 89 | return; 90 | } 91 | if (!video.type.match(/video\/.*/)) { 92 | res.end(JSON.stringify({success: false, message: "Upload must be a video type."})); 93 | return; 94 | } 95 | if (!title) { 96 | res.end(JSON.stringify({success: false, message: "Title cannot be empty."})); 97 | return; 98 | } 99 | if (token) { 100 | jwt.verify(token, process.env.SESSION_SECRET, (e_, decoded) => { 101 | if (e_) { 102 | res.end(JSON.stringify({success: false, message: "No token provided."})); 103 | return; 104 | } 105 | let username = decoded.username; 106 | 107 | // First check if a video with the same details has already been uploaded. 108 | if (fs.exists(`./videos/${username}/${title}`)) { 109 | res.end(JSON.stringify({ 110 | success: false, 111 | message: "You have uploaded another video with the same details." 112 | })); 113 | return; 114 | } 115 | 116 | if (!fs.existsSync("./videos")) { 117 | fs.mkdirSync("./videos"); 118 | fs.mkdirSync(`./videos/${username}`); 119 | } else if (!fs.existsSync(`./videos/${username}`)) { 120 | fs.mkdirSync(`./videos/${username}`); 121 | } 122 | 123 | let path = `./videos/${username}/${title}`; 124 | fs.mkdirSync(path); 125 | fs.rename(video.path, path + `/${video.name}`, (e) => { 126 | if (e) { 127 | res.end(JSON.stringify({success: false, message: "Unknown error while saving video."})); 128 | return; 129 | } 130 | 131 | fs.rename(thumbnail.path, path + `/${thumbnail.name}`, (e) => { 132 | if (e) { 133 | res.end(JSON.stringify({success: false, message: "Unknown error while saving video."})); 134 | return; 135 | } 136 | 137 | // Write data to database. 138 | dbUtils.init(); 139 | dbUtils.upload(username, title, path + `/${video.name}`, 140 | path + `/${thumbnail.name}`, new Date(), desc, (err, id) => { 141 | // Upload successful, so now add it to index. 142 | searchUtils.index("qtube", "video", { 143 | title, 144 | username, 145 | thumbnail: path + `/${thumbnail.name}`, 146 | description: desc, 147 | video_id: id.toString() 148 | }); 149 | 150 | res.end(JSON.stringify({success: true, message: "Successfully uploaded!"})); 151 | }); 152 | }); 153 | }); 154 | }); 155 | } 156 | }); 157 | }); 158 | 159 | app.post("/api/authenticate", (req, res) => { 160 | res.writeHead(200, {"Content-Type": "application/json"}); 161 | let {username, password} = req.body; 162 | 163 | if (!username || !password) 164 | res.end(JSON.stringify({ 165 | success: false, 166 | message: "Fields cannot be empty" 167 | })); 168 | else { 169 | dbUtils.init(); 170 | dbUtils.authenticate(username, password, (err, authResult) => { 171 | if (err) throw err; 172 | 173 | if (authResult.success) { 174 | let user = { 175 | username: authResult.results.username, 176 | name: authResult.results.name, 177 | dp: authResult.results.dp 178 | }; 179 | let token = jwt.sign(user, process.env.SESSION_SECRET, { 180 | expiresIn: "1 day" 181 | }); 182 | res.end(JSON.stringify({ 183 | success: true, 184 | message: "Logged in successfully!", 185 | user, 186 | token 187 | })); 188 | } else 189 | res.end(JSON.stringify({ 190 | success: false, 191 | message: authResult.message 192 | })); 193 | }); 194 | } 195 | }); 196 | 197 | app.post("/api/register", (req, res) => { 198 | res.writeHead(200, {"Content-Type": "application/json"}); 199 | let {username, password, name} = req.body; 200 | if (!username || !password || !name) { 201 | res.end(JSON.stringify({ 202 | success: false, 203 | message: "Fields cannot be empty" 204 | })); 205 | return; 206 | } 207 | if (illegalCharsFormat.test(username) || 208 | illegalCharsFormat.test(name)) { 209 | res.end(JSON.stringify({ 210 | success: false, 211 | message: "Special characters aren't allowed in usernames and names." 212 | })); 213 | return; 214 | } 215 | if (username.includes(" ")) { 216 | res.end(JSON.stringify({ 217 | success: false, 218 | message: "Spaces aren't allowed in usernames." 219 | })); 220 | return; 221 | } 222 | 223 | dbUtils.init(); 224 | dbUtils.register(username, password, name, (e, regResult) => { 225 | if (e) throw e; 226 | 227 | if (regResult.success) { 228 | let user = { 229 | username, 230 | name, 231 | dp: null 232 | }; 233 | let token = jwt.sign(user, process.env.SESSION_SECRET, { 234 | expiresIn: "1 day" 235 | }); 236 | 237 | // Registration successful, add to index and then end response. 238 | searchUtils.index("qtube", "user", { 239 | name, 240 | username 241 | }); 242 | 243 | res.end(JSON.stringify({ 244 | success: regResult.success, 245 | message: regResult.message, 246 | token 247 | })); 248 | } else 249 | res.end(JSON.stringify(regResult)); 250 | }); 251 | }); 252 | 253 | app.post("/api/feedback", (req, res) => { 254 | res.writeHead(200, {"Content-Type": "application/json"}); 255 | let {token, feedback} = req.body; 256 | 257 | jwt.verify(token, process.env.SESSION_SECRET, (err, decoded) => { 258 | dbUtils.init(); 259 | dbUtils.feedback(decoded.username, feedback, (e, result) => { 260 | if (e) throw e; 261 | 262 | res.end(JSON.stringify(result)); 263 | }); 264 | }); 265 | }); 266 | 267 | app.post("/api/whoami", (req, res) => { 268 | res.writeHead(200, {"Content-Type": "application/json"}); 269 | let {token} = req.body; 270 | jwt.verify(token, process.env.SESSION_SECRET, (err, decoded) => { 271 | if (err) 272 | res.end(JSON.stringify({ 273 | success: false 274 | })); 275 | else 276 | res.end(JSON.stringify({ 277 | success: true, 278 | user: decoded 279 | })); 280 | }); 281 | }); 282 | 283 | app.post("/api/search", (req, res) => { 284 | res.writeHead(200, {"Content-Type": "application/json"}); 285 | let queryString = req.body.query; 286 | let body = { 287 | size: 10, // Number of results 288 | from: 0, // Start index of results returned 289 | query: { 290 | multi_match: { 291 | query: queryString, 292 | fields: ["username", "name", "description", "title"], 293 | minimum_should_match: 1, 294 | fuzziness: 2 295 | } 296 | } 297 | }; 298 | searchUtils.search("qtube", body).then(results => { 299 | let response = { 300 | time: results.took, 301 | results: results.hits.hits 302 | }; 303 | 304 | res.end(JSON.stringify(response)); 305 | }); 306 | }); 307 | 308 | app.post("/api/video/details", (req, res) => { 309 | res.writeHead(200, {"Content-Type": "application/json"}); 310 | let {id} = req.body; 311 | 312 | dbUtils.init(); 313 | dbUtils.details(id, (err, results) => { 314 | if (err) 315 | res.end(JSON.stringify({ 316 | success: false 317 | })); 318 | else 319 | res.end(JSON.stringify({ 320 | success: true, 321 | age: results[0].age, 322 | views: numeral(results[0].views).format("0.0a") 323 | })); 324 | }); 325 | }); 326 | 327 | app.delete("/api/video/:id", (req, res) => { 328 | res.writeHead(200, {"Content-Type": "application/json"}); 329 | 330 | jwt.verify(req.body.token, process.env.SESSION_SECRET, (err, decoded) => { 331 | if (err) 332 | res.end({success: false}); 333 | else { 334 | dbUtils.init(); 335 | dbUtils.deleteVideo(decoded.username, req.params.id, (err) => { 336 | if (err) 337 | res.end(JSON.stringify({ 338 | success: false, 339 | message: "Couldn't delete the video." 340 | })); 341 | else { 342 | // Now delete the video from search index 343 | searchUtils.deleteDoc("qtube", "video", req.params.id, (e) => { 344 | if (e) throw e; 345 | res.end(JSON.stringify({success: true})); 346 | }); 347 | } 348 | }); 349 | } 350 | }); 351 | }); 352 | 353 | app.delete("/api/user", (req, res) => { 354 | res.writeHead(200, {"Content-Type": "application/json"}); 355 | 356 | let {token, pwd} = req.body; 357 | /* Decode the user from the token, and verify the password. If it's right, 358 | delete the user account, as well as all the user's content. */ 359 | jwt.verify(token, process.env.SESSION_SECRET, (err, decoded) => { 360 | if (err) throw err; 361 | 362 | let {username} = decoded; 363 | dbUtils.authenticate(username, pwd, (e, authResult) => { 364 | if (e) throw e; 365 | if (authResult.success) { 366 | dbUtils.init(); 367 | dbUtils.deleteUser(username, (e_) => { 368 | if (e_) throw e_; 369 | 370 | res.end(JSON.stringify({success: true})); 371 | }); 372 | } else 373 | res.end(JSON.stringify({ 374 | success: false, 375 | message: "Incorrect password" 376 | })); 377 | }); 378 | }); 379 | }); 380 | 381 | app.post("/api/video", (req, res) => { 382 | res.writeHead(200, {"Content-Type": "application/json"}); 383 | 384 | let {id, token} = req.body; 385 | let username; 386 | 387 | dbUtils.init(); 388 | jwt.verify(token, process.env.SESSION_SECRET, (err, decoded) => { 389 | if (err) { 390 | // User can still watch the video. 391 | username = null; 392 | } else 393 | username = decoded.username; 394 | }); 395 | dbUtils.videoDetails(username, id, (err, results) => { 396 | if (err) 397 | throw err; 398 | res.end(JSON.stringify({ 399 | success: true, 400 | details: results 401 | })); 402 | }); 403 | }); 404 | 405 | app.post("/api/comments", (req, res) => { 406 | res.writeHead(200, {"Content-Type": "application/json"}); 407 | 408 | let {id} = req.body; 409 | dbUtils.init(); 410 | dbUtils.comments(id, (err, result) => { 411 | if (err) 412 | res.end(JSON.stringify({success: false})); 413 | else 414 | res.end(JSON.stringify({success: true, data: result})); 415 | }); 416 | }); 417 | 418 | app.post("/api/comment", (req, res) => { 419 | res.writeHead(200, {"Content-Type": "application/json"}); 420 | let {token, comment, video_id} = req.body; 421 | 422 | jwt.verify(token, process.env.SESSION_SECRET, (err, decoded) => { 423 | dbUtils.init(); 424 | dbUtils.addComment(video_id, comment, decoded.username, (e) => { 425 | if (e) 426 | res.end(JSON.stringify({success: false})); 427 | else 428 | res.end(JSON.stringify({success: true})); 429 | }); 430 | }); 431 | }); 432 | 433 | app.post("/api/votes", (req, res) => { 434 | res.writeHead(200, {"Content-Type": "application/json"}); 435 | let {token, action, video_id, vote} = req.body; 436 | 437 | jwt.verify(token, process.env.SESSION_SECRET, (err, decoded) => { 438 | if (err) { 439 | res.end(JSON.stringify({success: false})); 440 | return; 441 | } 442 | 443 | let {username} = decoded; 444 | dbUtils.init(); 445 | switch (action) { 446 | case "add": 447 | dbUtils.addVote(video_id, username, vote, (err) => { 448 | if (err) { 449 | res.end(JSON.stringify({success: false})); 450 | return; 451 | } 452 | }); 453 | break; 454 | 455 | case "update": 456 | dbUtils.swapVote(video_id, username, (err) => { 457 | if (err) { 458 | res.end(JSON.stringify({success: false})); 459 | return; 460 | } 461 | }); 462 | break; 463 | 464 | case "remove": 465 | dbUtils.removeVote(video_id, username, (err) => { 466 | if (err) { 467 | res.end(JSON.stringify({success: false})); 468 | return; 469 | } 470 | }); 471 | break; 472 | } 473 | res.end(JSON.stringify({success: true})); 474 | }); 475 | }); 476 | 477 | app.post("/api/toggle_subscription", (req, res) => { 478 | res.writeHead(200, {"Content-Type": "application/json"}); 479 | 480 | let {token, profile} = req.body; 481 | jwt.verify(token, process.env.SESSION_SECRET, (err, decoded) => { 482 | if (err) { 483 | res.end(JSON.stringify({ 484 | success: false, 485 | message: err.message 486 | })); 487 | return; 488 | } 489 | 490 | let {username} = decoded; 491 | dbUtils.init(); 492 | dbUtils.toggleSubscription(profile, username, (e) => { 493 | if (e) { 494 | res.end(JSON.stringify({ 495 | success: false, 496 | message: err 497 | })); 498 | return; 499 | } else 500 | res.end(JSON.stringify({success: true})); 501 | }); 502 | }); 503 | }); 504 | 505 | app.post("/api/reply", (req, res) => { 506 | res.writeHead(200, {"Content-Type": "application/json"}); 507 | 508 | let {comment_id, text, token} = req.body; 509 | jwt.verify(token, process.env.SESSION_SECRET, (err, decoded) => { 510 | if (err) { 511 | res.end(JSON.stringify({success: false})); 512 | return; 513 | } 514 | 515 | let {username} = decoded; 516 | dbUtils.init(); 517 | dbUtils.addReply(comment_id, username, text, (e) => { 518 | if (e) 519 | throw e; 520 | 521 | res.end(JSON.stringify({success: true})); 522 | }); 523 | }); 524 | }); 525 | 526 | app.post("/api/video/add_view", (req, res) => { 527 | res.writeHead(200, {"Content-Type": "application/json"}); 528 | 529 | let {video_id} = req.body; 530 | dbUtils.init(); 531 | dbUtils.incrementViews(video_id, (err) => { 532 | if (err) throw err; 533 | 534 | res.end(JSON.stringify({success: true})); 535 | }); 536 | }); 537 | 538 | app.post("/api/change_dp", (req, res) => { 539 | res.writeHead(200, {"Content-Type": "application/json"}); 540 | 541 | let form = new formidable.IncomingForm(); 542 | form.parse(req, (err, fields, files) => { 543 | if (err) { 544 | res.end(JSON.stringify({success: false, message: "Unknown error occurred"})); 545 | return; 546 | } 547 | 548 | let {dp} = files; 549 | let {token} = fields; 550 | if (!dp.type.match(/image\/.*/)) { 551 | res.end(JSON.stringify({success: false, message: "Thumbnail must be an image."})); 552 | return; 553 | } 554 | 555 | jwt.verify(token, process.env.SESSION_SECRET, (err, decoded) => { 556 | if (err) { 557 | res.end(JSON.stringify({success: false})); 558 | return; 559 | } 560 | let {username} = decoded; 561 | 562 | if (!fs.existsSync("./users")) 563 | fs.mkdirSync("./users"); 564 | if (!fs.existsSync(`./users/${username}`)) 565 | fs.mkdirSync(`./users/${username}`); 566 | 567 | fs.rename(dp.path, `./users/${username}/${dp.name}`, (e) => { 568 | if (e) { 569 | res.end(JSON.stringify({success: false, message: "Failed to upload video."})); 570 | return; 571 | } 572 | 573 | dbUtils.init(); 574 | dbUtils.updateDp(username, `./users/${username}/${dp.name}`, (e) => { 575 | if (e) { 576 | res.end(JSON.stringify({success: false, message: "Failed to save video."})); 577 | return; 578 | } 579 | 580 | res.end(JSON.stringify({success: true})); 581 | }); 582 | }); 583 | }); 584 | }); 585 | }); 586 | 587 | app.post("/api/feed", (req, res) => { 588 | // Just use res.json instead in this 589 | let {token} = req.body; 590 | 591 | jwt.verify(token, process.env.SESSION_SECRET, (err, decoded) => { 592 | if (err) { 593 | res.json({success: false}); 594 | return; 595 | } 596 | 597 | let {username} = decoded; 598 | dbUtils.init(); 599 | dbUtils.getSubscribedVideos(username, (e, results) => { 600 | if (e) 601 | throw e; 602 | 603 | res.json({success: true, details: results}); 604 | }); 605 | }); 606 | }); 607 | 608 | app.listen(port, (err) => { 609 | if (err) throw err; 610 | open("http://localhost:" + port); 611 | }); 612 | -------------------------------------------------------------------------------- /server/db.js: -------------------------------------------------------------------------------- 1 | // Database functions module 2 | import mysql from "mysql"; 3 | import dotenv from "dotenv"; 4 | import bcrypt from "bcrypt"; 5 | import search from "./search.js"; 6 | import groupBy from "lodash.groupby"; 7 | import { Object } from "core-js/library/web/timers"; 8 | 9 | let connection; 10 | 11 | /** 12 | * Establishes a connection to the database. 13 | */ 14 | exports.init = () => { 15 | dotenv.config(); 16 | connection = mysql.createConnection({ 17 | host: process.env.DB_HOST, 18 | user: process.env.DB_USER, 19 | password: process.env.DB_PWD, 20 | database: "video_sharing" 21 | }); 22 | connection.connect(); 23 | }; 24 | 25 | /** 26 | * Terminates connection to the database. 27 | */ 28 | exports.terminate = () => { 29 | connection.end(); 30 | }; 31 | 32 | /** 33 | * Checks if the user has entered the right set of authentication details. 34 | * @param {string} username - The username entered 35 | * @param {string} pwd - The plaintext password entered 36 | * @param {Function} func - The callback function 37 | */ 38 | exports.authenticate = (username, pwd, func) => { 39 | connection.query("SELECT * FROM users WHERE BINARY username = ?", [username], (err, results) => { 40 | if (err) { 41 | func(err); 42 | return; 43 | } 44 | 45 | if (!results.length) { 46 | func(null, { 47 | success: false, 48 | message: "User does not exist." 49 | }); 50 | return; 51 | } 52 | 53 | bcrypt.compare(pwd, results[0].pwd, (e, res) => { 54 | if (e) throw e; 55 | 56 | if (!res) 57 | func(null, { 58 | success: false, 59 | message: "Username and password do not match." 60 | }); 61 | 62 | func(null, { 63 | success: true, 64 | results: results[0] 65 | }); 66 | }); 67 | }); 68 | }; 69 | 70 | /** 71 | * Registers a user by adding the details to the users table. Also adds user to 72 | * subscriptions table, since by default, every user subscribes to him/herself. 73 | * @param {string} username - The username of the new user 74 | * @param {string} pwd - The plaintext password of the user. This will be hashed. 75 | * @param {string} name - The user's name 76 | * @param {Function} func - The callback function 77 | */ 78 | exports.register = (username, pwd, name, func) => { 79 | connection.query("SELECT * FROM users WHERE BINARY username = ?", [username], (err, results) => { 80 | if (err) { 81 | func(err); 82 | return; 83 | } 84 | 85 | // First check if user exists already. 86 | if (results.length) { 87 | func(null, { 88 | success: false, 89 | message: "Username already exists." 90 | }); 91 | return; 92 | } 93 | }); 94 | 95 | // Gotta hash the password first! 96 | bcrypt.hash(pwd, 10, (e_, hash) => { 97 | if (e_) { 98 | func(e_); 99 | return; 100 | } 101 | 102 | connection.query("INSERT INTO users (name, username, pwd) VALUES (?, ?, ?)", [name, username, hash], (err) => { 103 | if (err) { 104 | // Can't simply throw an error here, return an error message instead. 105 | func(null, { 106 | success: false, 107 | message: "Unknown error occurred, try again." 108 | }); 109 | return; 110 | } 111 | 112 | func(null, { 113 | success: true, 114 | message: "Successfully registered!", 115 | username 116 | }); 117 | }); 118 | }); 119 | }; 120 | 121 | /** 122 | * Inserts the parameters of the video into the videos table. The actual 123 | * uploading must be done by the server. 124 | * @param {string} username - The uploader username 125 | * @param {string} title - The video title 126 | * @param {string} path - The path to the video file in the disk 127 | * @param {string} thumbnail - The path to the thumbnail file on disk 128 | * @param {Date} date - Date of uploading 129 | * @param {string} desc - Video description 130 | * @param {Function} func - The callback function 131 | */ 132 | exports.upload = (username, title, path, thumbnail, date, desc, func) => { 133 | connection.query("INSERT INTO videos (username, title, video_path,\ 134 | thumbnail, upload_date, description) VALUES (?, ?, ?, ?, ?, ?)", 135 | [username, title, path, thumbnail, date, desc], (err, results) => { 136 | if (err) throw err; 137 | 138 | func(null, results.insertId); // func takes no arguments, a call indicates success. 139 | } 140 | ); 141 | }; 142 | 143 | /** 144 | * Submits feedback by adding to the feedback table 145 | * @param {string} username - The username of the person submitting the feedback 146 | * @param {string} feedback - The submitted feedback 147 | * @param {Function} func - The callback function 148 | */ 149 | exports.feedback = (username, feedback, func) => { 150 | connection.query("SELECT * FROM feedback WHERE BINARY username = ?", [username], (err, results) => { 151 | if (err) { 152 | func(err); 153 | return; 154 | } 155 | 156 | if (results.length) { 157 | func(null, { 158 | success: false, 159 | message: "You have already submitted feedback!" 160 | }); 161 | return; 162 | } 163 | 164 | connection.query("INSERT INTO feedback VALUES (?, ?)", [username, feedback], (err) => { 165 | if (err) { 166 | func(err); 167 | return; 168 | } 169 | 170 | func(null, { 171 | success: true, 172 | message: "Thanks for your feedback!" 173 | }); 174 | }); 175 | }); 176 | }; 177 | 178 | /** 179 | * Gives a list of trending videos 180 | * @param {Function} func - The callback function 181 | */ 182 | exports.trending = (func) => { 183 | let sql = "SELECT *, DATEDIFF(?, upload_date) AS age \ 184 | FROM videos \ 185 | WHERE DATEDIFF(?, upload_date) < 6"; 186 | let date = new Date().toISOString(); 187 | connection.query(sql, [date, date], (err, results) => { 188 | if (err) { 189 | func(err); 190 | return; 191 | } 192 | func(null, results); 193 | }); 194 | }; 195 | 196 | /** 197 | * Returns the views and age of a video. 198 | * @param {number} id - The video id in the database 199 | * @param {Function} func - The callback function 200 | */ 201 | exports.details = (id, func) => { 202 | let sql = "SELECT COUNT(video_views.username) AS views, DATEDIFF(?, upload_date) AS age \ 203 | FROM videos, video_views \ 204 | WHERE videos.video_id = ? \ 205 | AND video_views.video_id = ?"; 206 | connection.query(sql, [new Date().toISOString(), id, id], (err, results) => { 207 | if (err) { 208 | func(err); 209 | return; 210 | } 211 | func(null, results); 212 | }); 213 | }; 214 | 215 | /** 216 | * Returns details of user with given username 217 | * @param {string} username - The username whose details are required. 218 | * @param {Function} func - The callback function 219 | */ 220 | exports.userDetails = (username, func) => { 221 | let sql = "SELECT u.name, u.username, u.dp, COUNT(*) AS subscribers \ 222 | FROM users AS u \ 223 | NATURAL JOIN subscriptions AS s \ 224 | WHERE BINARY u.username = ?"; 225 | connection.query(sql, [username], (err, results) => { 226 | if (err) { 227 | func(err); 228 | return; 229 | } 230 | 231 | if (!results.length) 232 | func(new Error("Username does not exist.")); 233 | 234 | sql = "SELECT * \ 235 | FROM videos \ 236 | WHERE username = ?"; 237 | connection.query(sql, [username], (e, r) => { 238 | if (err) 239 | func(err); 240 | 241 | func(null, { 242 | user: results, 243 | videos: r 244 | }); 245 | }); 246 | }); 247 | }; 248 | 249 | /** 250 | * Delete a video with the given video_id. Make sure the video was uploaded by 251 | * the user with the given username. 252 | * @param {string} username - The user's username 253 | * @param {number} id - The video_id to delete 254 | * @param {Function} func - The callback function, taking only one argument. 255 | */ 256 | exports.deleteVideo = (username, id, func) => { 257 | // First verify if the username is right 258 | let sql = "SELECT BINARY username = ? AS valid \ 259 | FROM videos \ 260 | WHERE video_id = ?"; 261 | connection.query(sql, [username, id], (err, results) => { 262 | if (err) { 263 | func(err); 264 | return; 265 | } 266 | 267 | if (results[0].valid != "1") 268 | func(new Error("Authorization failed.")); 269 | else { 270 | sql = "DELETE FROM videos WHERE video_id = ?"; 271 | connection.query(sql, [id], (err) => { 272 | if (err) { 273 | func(err); 274 | return; 275 | } 276 | search.deleteDoc("qtube", "video", id, (err) => { 277 | if(err) { 278 | func(err); 279 | return; 280 | } 281 | }); 282 | 283 | func(); 284 | }); 285 | } 286 | }); 287 | }; 288 | 289 | /** 290 | * Deletes a user and all relevant information. 291 | * @param {string} username - The username whose details must be deleted 292 | * @param {Function} func - The callback function 293 | */ 294 | exports.deleteUser = (username, func) => { 295 | let sql = "SELECT video_id \ 296 | FROM videos \ 297 | WHERE username = ?"; 298 | connection.query(sql, [username], (err, results) => { 299 | if (err) { 300 | func(err); 301 | return; 302 | } 303 | 304 | // First delete all search indices 305 | results.forEach((result) => { 306 | search.deleteDoc("qtube", "video", result.video_id, (err) => { 307 | if(err) { 308 | func(err); 309 | return; 310 | } 311 | }); 312 | }); 313 | 314 | // Delete search index for the user 315 | search.deleteDoc("qtube", "user", username, (err) => { 316 | if(err) { 317 | func(err); 318 | return; 319 | } 320 | 321 | // Delete the user from the database 322 | sql = "DELETE FROM users WHERE username = ?"; 323 | connection.query(sql, [username], (err) => { 324 | if (err) { 325 | func(err); 326 | return; 327 | } 328 | 329 | func(); 330 | }); 331 | }); 332 | }); 333 | }; 334 | 335 | /** 336 | * Gets all video details 337 | * @param {string} username - The user's username 338 | * @param {number} id - The video_id 339 | * @param {Function} func - The callback function 340 | */ 341 | exports.videoDetails = (username, id, func) => { 342 | let sql = "SELECT v.*, u.name AS name, u.dp AS dp, vv.views AS views \ 343 | FROM videos v, users u, video_views vv \ 344 | WHERE v.username = u.username \ 345 | AND vv.video_id = v.video_id \ 346 | AND v.video_id = ?"; 347 | connection.query(sql, [id], (err, results) => { 348 | if (err) { 349 | func(err); 350 | return; 351 | } 352 | 353 | sql = "SELECT COUNT(*) AS ? \ 354 | FROM video_ratings \ 355 | WHERE rating = ?"; 356 | connection.query(sql, ["upvotes", 1], (e, r) => { 357 | if (e) { 358 | func(e); 359 | return; 360 | } 361 | 362 | connection.query(sql, ["downvotes", -1], (e_, r_) => { 363 | if (e_) { 364 | func(e_); 365 | return; 366 | } 367 | 368 | // And finally, get details of whether the user has voted on this or not. 369 | sql = "SELECT COUNT(*) AS count_, rating AS user_rating \ 370 | FROM users AS u \ 371 | NATURAL JOIN video_ratings \ 372 | WHERE video_id = ? \ 373 | AND username = ?"; 374 | connection.query(sql, [id, username], (_e, _r) => { 375 | if (_e) { 376 | func(_e); 377 | return; 378 | } 379 | 380 | // Merge the properties of all the results (ES6) 381 | let finalResults = Object.assign({}, results[0], r[0], r_[0], _r[0]); 382 | func(null, finalResults); 383 | }); 384 | }); 385 | }); 386 | }); 387 | }; 388 | 389 | /** 390 | * Gets the details of all comments and videos on a video. 391 | * @param {number} video_id - The video_id whose comment details are required. 392 | * @param {Function} func - The callback function. 393 | */ 394 | exports.comments = (video_id, func) => { 395 | let sql = "SELECT u.name, u.username, u.dp, c.comment_date, c.comment, c.comment_id \ 396 | FROM users AS u \ 397 | NATURAL JOIN comments AS c \ 398 | WHERE video_id = ?"; 399 | connection.query(sql, [video_id], (err, results) => { 400 | if (err) { 401 | func(err); 402 | return; 403 | } 404 | 405 | sql = "SELECT u.name, u.username, u.dp, r.reply_date, r.reply_text, r.comment_id \ 406 | FROM users AS u \ 407 | NATURAL JOIN replies AS r \ 408 | \ 409 | JOIN comments AS c \ 410 | ON r.comment_id = c.comment_id \ 411 | WHERE c.video_id = ?"; 412 | connection.query(sql, [video_id], (e, r) => { 413 | if (e) { 414 | func(e); 415 | return; 416 | } 417 | 418 | let finalResult = {comments: results, replies: r}; 419 | func(null, finalResult); 420 | }); 421 | }); 422 | }; 423 | 424 | /** 425 | * Adds a comment on the given video 426 | * @param {number} video_id - The video_id of the video 427 | * @param {string} comment - The comment to add 428 | * @param {string} username - The user's username 429 | * @param {Function} func - The callback function. Accepts only one argument, the error. 430 | */ 431 | exports.addComment = (video_id, comment, username, func) => { 432 | let sql = "INSERT INTO comments (username, video_id, comment, comment_date) \ 433 | VALUES (?, ?, ?, ?)"; 434 | connection.query(sql, [username, video_id, comment, new Date()], (err) => { 435 | if (err) { 436 | func(err); 437 | return; 438 | } 439 | 440 | func(); 441 | }); 442 | }; 443 | 444 | /** 445 | * Adds a new upvote to the video_ratings table 446 | * @param {number} video_id - The video_id of the video 447 | * @param {string} username - The user who's voting 448 | * @param {number} vote - The vote to add to the table 449 | * @param {Function} func - The callback function. Accepts only one argument. 450 | */ 451 | exports.addVote = (video_id, username, vote, func) => { 452 | let sql = "INSERT INTO video_ratings \ 453 | VALUES (?, ?, ?)"; 454 | connection.query(sql, [username, video_id, vote], (err) => { 455 | if (err) { 456 | func(err); 457 | return; 458 | } 459 | 460 | func(); 461 | }); 462 | }; 463 | 464 | /** 465 | * Swap the upvote with a downvote or vice versa 466 | * @param {number} video_id - The video_id of the video 467 | * @param {string} username - The username of the user 468 | * @param {Function} func - The callback function. Accepts only one argument. 469 | */ 470 | exports.swapVote = (video_id, username, func) => { 471 | let sql = "UPDATE video_ratings \ 472 | SET rating = IF (rating = 1, -1, 1) \ 473 | WHERE username = ? \ 474 | AND video_id = ?"; 475 | connection.query(sql, [username, video_id], (err) => { 476 | if (err) { 477 | func(err); 478 | return; 479 | } 480 | 481 | func(); 482 | }); 483 | }; 484 | 485 | /** 486 | * 487 | * @param {number} video_id - The video_id of the video 488 | * @param {string} username - The username of the user 489 | * @param {Function} func - The callback function. Accepts only one argument. 490 | */ 491 | exports.removeVote = (video_id, username, func) => { 492 | let sql = "DELETE \ 493 | FROM video_ratings \ 494 | WHERE video_id = ? \ 495 | AND username = ?"; 496 | connection.query(sql, [video_id, username], (err) => { 497 | if (err) { 498 | func(err); 499 | return; 500 | } 501 | 502 | func(); 503 | }); 504 | }; 505 | 506 | /** 507 | * Toggles the subscription status of a user to another. 508 | * @param {string} username - The user who is being subscribed to (profile) 509 | * @param {string} subscriber - The user who is subscribing 510 | * @param {Function} func - The callback function. Only accepts one argument 511 | */ 512 | exports.toggleSubscription = (username, subscriber, func) => { 513 | let sql = "SELECT COUNT(*) AS count \ 514 | FROM subscriptions \ 515 | WHERE username = ? \ 516 | AND subscriber = ?"; 517 | connection.query(sql, [username, subscriber], (err, results) => { 518 | if (err) { 519 | func(err); 520 | return; 521 | } 522 | 523 | let count = results[0].count; 524 | if (count == 1) { 525 | // Remove the subscriptions 526 | sql = "DELETE \ 527 | FROM subscriptions \ 528 | WHERE username = ? \ 529 | AND subscriber = ?"; 530 | connection.query(sql, [username, subscriber], (e) => { 531 | if (e) { 532 | func(e); 533 | return; 534 | } 535 | 536 | func(); 537 | }); 538 | } else { 539 | // Subscribe the user 540 | sql = "INSERT INTO subscriptions (username, subscriber) \ 541 | VALUES (?, ?)"; 542 | connection.query(sql, [username, subscriber], (e) => { 543 | if (e) { 544 | func(e); 545 | return; 546 | } 547 | 548 | func(); 549 | }); 550 | } 551 | }); 552 | }; 553 | 554 | /** 555 | * Adds a reply to the specified comment on the given video 556 | * @param {number} comment_id - The comment id which is being replied to 557 | * @param {string} username - The user adding the reply 558 | * @param {string} reply - The reply text 559 | * @param {Function} func - The callback function. Accepts only one argument 560 | */ 561 | exports.addReply = (comment_id, username, reply, func) => { 562 | let sql = "INSERT INTO replies (comment_id, username, reply_text, reply_date) \ 563 | VALUES (?, ?, ?, ?)"; 564 | connection.query(sql, [comment_id, username, reply, new Date()], (err) => { 565 | if (err) { 566 | func(err); 567 | return; 568 | } 569 | 570 | func(); 571 | }); 572 | }; 573 | 574 | /** 575 | * Increments the views for a video 576 | * @param {number} video_id - The id of the video 577 | * @param {Function} func - The callback function. Accepts only one argument 578 | */ 579 | exports.incrementViews = (video_id, func) => { 580 | let sql = "CALL increment_views(?)"; 581 | connection.query(sql, [video_id], (err) => { 582 | if (err) { 583 | func(err); 584 | return; 585 | } 586 | 587 | func(); 588 | }); 589 | }; 590 | 591 | /** 592 | * Updates the DP of a user. 593 | * @param {string} username - The username of the user 594 | * @param {string} dp - The new DP, in base64 595 | * @param {Function} func - The callback function. Only accepts one argument. 596 | */ 597 | exports.updateDp = (username, dp, func) => { 598 | let sql = "UPDATE users \ 599 | SET dp = ? \ 600 | WHERE username = ?"; 601 | connection.query(sql, [dp, username], (err) => { 602 | if (err) { 603 | func(err); 604 | return; 605 | } 606 | 607 | func(); 608 | }); 609 | }; 610 | 611 | /** 612 | * Gets all the videos uploaded by users that the subscriber has subscribed to. 613 | * @param {string} subscriber - The user whose feed must be fetched 614 | * @param {Function} func - The callback function. 615 | */ 616 | exports.getSubscribedVideos = (subscriber, func) => { 617 | let sql = "SELECT * \ 618 | FROM videos AS v \ 619 | NATURAL JOIN video_views AS vv \ 620 | WHERE username IN (SELECT username \ 621 | FROM subscriptions \ 622 | WHERE subscriber = ?) \ 623 | AND username <> ? \ 624 | ORDER BY vv.views"; 625 | connection.query(sql, [subscriber, subscriber], (err, results) => { 626 | if (err) { 627 | func(err); 628 | return; 629 | } 630 | 631 | let groups = groupBy(results, (val) => val.username); 632 | func(null, groups); 633 | }); 634 | }; 635 | 636 | module.exports = exports; 637 | --------------------------------------------------------------------------------