diff --git a/.gitignore b/.gitignore index ec5b4b2f03e602cd8f4fe84d055fa86263d7c1d4..7ca89dca8532625749c8e09c466040913187a17b 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,11 @@ uploads thumbnails config/production.yaml ffmpeg +<<<<<<< HEAD torrents +======= +.tags +*.sublime-project +*.sublime-workspace +torrents/ +>>>>>>> master diff --git a/.travis.yml b/.travis.yml index e6a92d883134ef4b0d9d577c96a0464b982fbe45..7b025f0b98964c96760179cc147514d65f8b2b68 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: node_js node_js: - - "4.4" - - "6.2" + - "4.5" + - "6.6" env: - CXX=g++-4.8 @@ -19,8 +19,10 @@ sudo: false services: - mongodb +before_install: if [[ `npm -v` != 3* ]]; then npm i -g npm@3; fi + before_script: - - npm install electron-prebuilt -g + - npm install electron -g - npm run build - wget --no-check-certificate "https://download.cpy.re/ffmpeg/ffmpeg-release-3.0.2-64bit-static.tar.xz" - tar xf ffmpeg-release-3.0.2-64bit-static.tar.xz diff --git a/README.md b/README.md index 1a3470711be0f82feeca1548fbd036eecce22383..777df6d7d54f448fbfa7f7689c2c21b024d7833b 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ Want to see in action? * You can directly test in your browser with this [demo server](http://peertube.cpy.re). Don't forget to use the latest version of Firefox/Chromium/(Opera?) and check your firewall configuration (for WebRTC) * You can find [a video](https://vimeo.com/164881662 "Yes Vimeo, please don't judge me") to see how the "decentralization feature" looks like + * Experimental demo servers that share videos (they are in the same network): [peertube2](http://peertube2.cpy.re), [peertube3](http://peertube3.cpy.re). Since I do experiments with them, sometimes they might not work correctly. ## Why @@ -95,10 +96,12 @@ Thanks to [WebTorrent](https://github.com/feross/webtorrent), we can make P2P (t - [ ] Validate the prototype (test PeerTube in a real world with many pods and videos) - [ ] Manage API breaks - [ ] Add "DDOS" security (check if a pod don't send too many requests for example) -- [ ] Admin panel - - [ ] Stats about the network (how many friends, how many requests per hour...) - - [ ] Stats about videos - - [ ] Manage users (create/remove) +- [X] Admin panel + - [X] Stats + - [X] Friends list + - [X] Manage users (create/remove) +- [ ] User playlists +- [ ] User subscriptions (by tags, author...) ## Installation @@ -111,6 +114,7 @@ Thanks to [WebTorrent](https://github.com/feross/webtorrent), we can make P2P (t ### Dependencies * **NodeJS >= 4.2** + * **npm >= 3.0** * OpenSSL (cli) * MongoDB * ffmpeg xvfb-run libgtk2.0-0 libgconf-2-4 libnss3 libasound2 libxtst6 libxss1 libnotify-bin (for electron) @@ -123,7 +127,8 @@ Thanks to [WebTorrent](https://github.com/feross/webtorrent), we can make P2P (t # apt-get update # apt-get install ffmpeg mongodb openssl xvfb curl sudo git build-essential libgtk2.0-0 libgconf-2-4 libnss3 libasound2 libxtst6 libxss1 libnotify-bin - # npm install -g electron-prebuilt + # npm install -g npm@3 + # npm install -g electron #### Other distribution... (PR welcome) @@ -160,6 +165,10 @@ Finally, run the server with the `production` `NODE_ENV` variable set. $ NODE_ENV=production npm start +**Nginx template** (reverse proxy): https://github.com/Chocobozzz/PeerTube/tree/master/support/nginx + +**Systemd template**: https://github.com/Chocobozzz/PeerTube/tree/master/support/systemd + ### Other commands To print all available command run: diff --git a/client/config/helpers.js b/client/config/helpers.js index 24d7dae9f406b7e8c4f7fc603c62c8ef96ce6cdb..6268d2628fbfdf067e1e7a773e6f6c83f29a4216 100644 --- a/client/config/helpers.js +++ b/client/config/helpers.js @@ -8,10 +8,15 @@ function hasProcessFlag (flag) { return process.argv.join('').indexOf(flag) > -1 } +function isWebpackDevServer () { + return process.argv[1] && !!(/webpack-dev-server$/.exec(process.argv[1])) +} + function root (args) { args = Array.prototype.slice.call(arguments, 0) return path.join.apply(path, [ROOT].concat(args)) } exports.hasProcessFlag = hasProcessFlag +exports.isWebpackDevServer = isWebpackDevServer exports.root = root diff --git a/client/config/webpack.common.js b/client/config/webpack.common.js index 2ff3a1506497687a4ba6ec3e3f0500c52fab5ceb..882013a9e1b6f7c481e2d35d23ab3529fd23e89e 100644 --- a/client/config/webpack.common.js +++ b/client/config/webpack.common.js @@ -5,9 +5,11 @@ const helpers = require('./helpers') * Webpack Plugins */ -var CopyWebpackPlugin = (CopyWebpackPlugin = require('copy-webpack-plugin'), CopyWebpackPlugin.default || CopyWebpackPlugin) +const CopyWebpackPlugin = require('copy-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin') const ForkCheckerPlugin = require('awesome-typescript-loader').ForkCheckerPlugin +const AssetsPlugin = require('assets-webpack-plugin') +const ContextReplacementPlugin = require('webpack/lib/ContextReplacementPlugin') const WebpackNotifierPlugin = require('webpack-notifier') /* @@ -15,7 +17,8 @@ const WebpackNotifierPlugin = require('webpack-notifier') */ const METADATA = { title: 'PeerTube', - baseUrl: '/' + baseUrl: '/', + isDevServer: helpers.isWebpackDevServer() } /* @@ -23,247 +26,241 @@ const METADATA = { * * See: http://webpack.github.io/docs/configuration.html#cli */ -module.exports = { - /* - * Static metadata for index.html - * - * See: (custom attribute) - */ - metadata: METADATA, +module.exports = function (options) { + var isProd = options.env === 'production' - /* - * Cache generated modules and chunks to improve performance for multiple incremental builds. - * This is enabled by default in watch mode. - * You can pass false to disable it. - * - * See: http://webpack.github.io/docs/configuration.html#cache - */ - // cache: false, - - /* - * The entry point for the bundle - * Our Angular.js app - * - * See: http://webpack.github.io/docs/configuration.html#entry - */ - entry: { - 'polyfills': './src/polyfills.ts', - 'vendor': './src/vendor.ts', - 'main': './src/main.ts' - }, + return { + /* + * Static metadata for index.html + * + * See: (custom attribute) + */ + metadata: METADATA, - /* - * Options affecting the resolving of modules. - * - * See: http://webpack.github.io/docs/configuration.html#resolve - */ - resolve: { /* - * An array of extensions that should be used to resolve modules. + * Cache generated modules and chunks to improve performance for multiple incremental builds. + * This is enabled by default in watch mode. + * You can pass false to disable it. * - * See: http://webpack.github.io/docs/configuration.html#resolve-extensions + * See: http://webpack.github.io/docs/configuration.html#cache */ - extensions: [ '', '.ts', '.js', '.scss' ], + // cache: false, - // Make sure root is src - root: helpers.root('src'), + /* + * The entry point for the bundle + * Our Angular.js app + * + * See: http://webpack.github.io/docs/configuration.html#entry + */ + entry: { + 'polyfills': './src/polyfills.ts', + 'vendor': './src/vendor.ts', + 'main': './src/main.ts' + }, - // remove other default values - modulesDirectories: [ 'node_modules' ], + /* + * Options affecting the resolving of modules. + * + * See: http://webpack.github.io/docs/configuration.html#resolve + */ + resolve: { + /* + * An array of extensions that should be used to resolve modules. + * + * See: http://webpack.github.io/docs/configuration.html#resolve-extensions + */ + extensions: [ '', '.ts', '.js', '.scss' ], - packageAlias: 'browser' + // Make sure root is src + root: helpers.root('src'), - }, + // remove other default values + modulesDirectories: [ 'node_modules' ] + }, - output: { - publicPath: '/client/' - }, + output: { + publicPath: '/client/' + }, - /* - * Options affecting the normal modules. - * - * See: http://webpack.github.io/docs/configuration.html#module - */ - module: { /* - * An array of applied pre and post loaders. + * Options affecting the normal modules. * - * See: http://webpack.github.io/docs/configuration.html#module-preloaders-module-postloaders + * See: http://webpack.github.io/docs/configuration.html#module */ - preLoaders: [ - + module: { /* - * Tslint loader support for *.ts files + * An array of applied pre and post loaders. * - * See: https://github.com/wbuchwalter/tslint-loader + * See: http://webpack.github.io/docs/configuration.html#module-preloaders-module-postloaders */ - // { test: /\.ts$/, loader: 'tslint-loader', exclude: [ helpers.root('node_modules') ] }, + preLoaders: [ + { + test: /\.ts$/, + loader: 'string-replace-loader', + query: { + search: '(System|SystemJS)(.*[\\n\\r]\\s*\\.|\\.)import\\((.+)\\)', + replace: '$1.import($3).then(mod => (mod.__esModule && mod.default) ? mod.default : mod)', + flags: 'g' + }, + include: [helpers.root('src')] + } + ], /* - * Source map loader support for *.js files - * Extracts SourceMaps for source files that as added as sourceMappingURL comment. + * An array of automatically applied loaders. + * + * IMPORTANT: The loaders here are resolved relative to the resource which they are applied to. + * This means they are not resolved relative to the configuration file. * - * See: https://github.com/webpack/source-map-loader + * See: http://webpack.github.io/docs/configuration.html#module-loaders */ - { - test: /\.js$/, - loader: 'source-map-loader', - exclude: [ - // these packages have problems with their sourcemaps - helpers.root('node_modules/rxjs'), - helpers.root('node_modules/@angular') - ] - } - - ], + loaders: [ + + /* + * Typescript loader support for .ts and Angular 2 async routes via .async.ts + * + * See: https://github.com/s-panferov/awesome-typescript-loader + */ + { + test: /\.ts$/, + loaders: [ + '@angularclass/hmr-loader?pretty=' + !isProd + '&prod=' + isProd, + 'awesome-typescript-loader', + 'angular2-template-loader' + ], + exclude: [/\.(spec|e2e)\.ts$/] + }, + + /* + * Json loader support for *.json files. + * + * See: https://github.com/webpack/json-loader + */ + { + test: /\.json$/, + loader: 'json-loader' + }, + + { + test: /\.(sass|scss)$/, + loaders: ['css-to-string-loader', 'css-loader?sourceMap', 'resolve-url', 'sass-loader?sourceMap'] + }, + { test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'url?limit=10000&minetype=application/font-woff' }, + { test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'file' }, + + /* Raw loader support for *.html + * Returns file content as string + * + * See: https://github.com/webpack/raw-loader + */ + { + test: /\.html$/, + loader: 'raw-loader', + exclude: [ helpers.root('src/index.html') ] + } + + ] + + }, + + sassLoader: { + precision: 10 + }, /* - * An array of automatically applied loaders. - * - * IMPORTANT: The loaders here are resolved relative to the resource which they are applied to. - * This means they are not resolved relative to the configuration file. + * Add additional plugins to the compiler. * - * See: http://webpack.github.io/docs/configuration.html#module-loaders + * See: http://webpack.github.io/docs/configuration.html#plugins */ - loaders: [ + plugins: [ + new AssetsPlugin({ + path: helpers.root('dist'), + filename: 'webpack-assets.json', + prettyPrint: true + }), /* - * Typescript loader support for .ts and Angular 2 async routes via .async.ts + * Plugin: ForkCheckerPlugin + * Description: Do type checking in a separate process, so webpack don't need to wait. * - * See: https://github.com/s-panferov/awesome-typescript-loader + * See: https://github.com/s-panferov/awesome-typescript-loader#forkchecker-boolean-defaultfalse */ - { - test: /\.ts$/, - loader: 'awesome-typescript-loader', - exclude: [/\.(spec|e2e)\.ts$/] - }, + new ForkCheckerPlugin(), /* - * Json loader support for *.json files. + * Plugin: CommonsChunkPlugin + * Description: Shares common code between the pages. + * It identifies common modules and put them into a commons chunk. * - * See: https://github.com/webpack/json-loader + * See: https://webpack.github.io/docs/list-of-plugins.html#commonschunkplugin + * See: https://github.com/webpack/docs/wiki/optimization#multi-page-app */ - { - test: /\.json$/, - loader: 'json-loader' - }, + new webpack.optimize.CommonsChunkPlugin({ + name: [ 'polyfills', 'vendor' ].reverse() + }), - { - test: /\.scss$/, - exclude: /node_modules/, - loaders: [ 'raw-loader', 'sass-loader' ] - }, - - { - test: /\.(woff2?|ttf|eot|svg)$/, - loader: 'url?limit=10000&name=assets/fonts/[hash].[ext]' - }, + /** + * Plugin: ContextReplacementPlugin + * Description: Provides context to Angular's use of System.import + * + * See: https://webpack.github.io/docs/list-of-plugins.html#contextreplacementplugin + * See: https://github.com/angular/angular/issues/11580 + */ + new ContextReplacementPlugin( + // The (\\|\/) piece accounts for path separators in *nix and Windows + /angular(\\|\/)core(\\|\/)(esm(\\|\/)src|src)(\\|\/)linker/, + helpers.root('src') // location of your src + ), - /* Raw loader support for *.html - * Returns file content as string + /* + * Plugin: CopyWebpackPlugin + * Description: Copy files and directories in webpack. * - * See: https://github.com/webpack/raw-loader + * Copies project static assets. + * + * See: https://www.npmjs.com/package/copy-webpack-plugin */ - { - test: /\.html$/, - loader: 'raw-loader', - exclude: [ helpers.root('src/index.html') ] - } - - ] - - }, + new CopyWebpackPlugin([ + { + from: 'src/assets', + to: 'assets' + }, + { + from: 'node_modules/webtorrent/webtorrent.min.js', + to: 'assets/webtorrent' + } + ]), - sassLoader: { - precision: 10 - }, - - /* - * Add additional plugins to the compiler. - * - * See: http://webpack.github.io/docs/configuration.html#plugins - */ - plugins: [ - - /* - * Plugin: ForkCheckerPlugin - * Description: Do type checking in a separate process, so webpack don't need to wait. - * - * See: https://github.com/s-panferov/awesome-typescript-loader#forkchecker-boolean-defaultfalse - */ - new ForkCheckerPlugin(), - - /* - * Plugin: OccurenceOrderPlugin - * Description: Varies the distribution of the ids to get the smallest id length - * for often used ids. - * - * See: https://webpack.github.io/docs/list-of-plugins.html#occurrenceorderplugin - * See: https://github.com/webpack/docs/wiki/optimization#minimize - */ - new webpack.optimize.OccurenceOrderPlugin(true), - - /* - * Plugin: CommonsChunkPlugin - * Description: Shares common code between the pages. - * It identifies common modules and put them into a commons chunk. - * - * See: https://webpack.github.io/docs/list-of-plugins.html#commonschunkplugin - * See: https://github.com/webpack/docs/wiki/optimization#multi-page-app - */ - new webpack.optimize.CommonsChunkPlugin({ - name: [ 'polyfills', 'vendor' ].reverse() - }), + /* + * Plugin: HtmlWebpackPlugin + * Description: Simplifies creation of HTML files to serve your webpack bundles. + * This is especially useful for webpack bundles that include a hash in the filename + * which changes every compilation. + * + * See: https://github.com/ampedandwired/html-webpack-plugin + */ + new HtmlWebpackPlugin({ + template: 'src/index.html', + chunksSortMode: 'dependency' + }), - /* - * Plugin: CopyWebpackPlugin - * Description: Copy files and directories in webpack. - * - * Copies project static assets. - * - * See: https://www.npmjs.com/package/copy-webpack-plugin - */ - new CopyWebpackPlugin([ - { - from: 'src/assets', - to: 'assets' - }, - { - from: 'node_modules/webtorrent/webtorrent.min.js', - to: 'assets/webtorrent' - } - ]), + new WebpackNotifierPlugin({ alwaysNotify: true }) + ], /* - * Plugin: HtmlWebpackPlugin - * Description: Simplifies creation of HTML files to serve your webpack bundles. - * This is especially useful for webpack bundles that include a hash in the filename - * which changes every compilation. + * Include polyfills or mocks for various node stuff + * Description: Node configuration * - * See: https://github.com/ampedandwired/html-webpack-plugin + * See: https://webpack.github.io/docs/configuration.html#node */ - new HtmlWebpackPlugin({ - template: 'src/index.html', - chunksSortMode: 'dependency' - }), - - new WebpackNotifierPlugin({ alwaysNotify: true }) - ], - - /* - * Include polyfills or mocks for various node stuff - * Description: Node configuration - * - * See: https://webpack.github.io/docs/configuration.html#node - */ - node: { - global: 'window', - crypto: 'empty', - fs: 'empty', - events: true, - module: false, - clearImmediate: false, - setImmediate: false + node: { + global: 'window', + crypto: 'empty', + fs: 'empty', + events: true, + module: false, + clearImmediate: false, + setImmediate: false + } } - } diff --git a/client/config/webpack.dev.js b/client/config/webpack.dev.js index 50193bf58f18c7f2e61e4eb48c271083853c5f27..0b6c00cbde54eca23acc9718fbbe97134e858a4c 100644 --- a/client/config/webpack.dev.js +++ b/client/config/webpack.dev.js @@ -6,15 +6,18 @@ const commonConfig = require('./webpack.common.js') // the settings that are com * Webpack Plugins */ const DefinePlugin = require('webpack/lib/DefinePlugin') +const NamedModulesPlugin = require('webpack/lib/NamedModulesPlugin') /** * Webpack Constants */ const ENV = process.env.ENV = process.env.NODE_ENV = 'development' +const HOST = process.env.HOST || 'localhost' +const PORT = process.env.PORT || 3000 const HMR = helpers.hasProcessFlag('hot') -const METADATA = webpackMerge(commonConfig.metadata, { - host: 'localhost', - port: 3000, +const METADATA = webpackMerge(commonConfig({env: ENV}).metadata, { + host: HOST, + port: PORT, ENV: ENV, HMR: HMR }) @@ -24,119 +27,136 @@ const METADATA = webpackMerge(commonConfig.metadata, { * * See: http://webpack.github.io/docs/configuration.html#cli */ -module.exports = webpackMerge(commonConfig, { - /** - * Merged metadata from webpack.common.js for index.html - * - * See: (custom attribute) - */ - metadata: METADATA, - - /** - * Switch loaders to debug mode. - * - * See: http://webpack.github.io/docs/configuration.html#debug - */ - debug: true, - - /** - * Developer tool to enhance debugging - * - * See: http://webpack.github.io/docs/configuration.html#devtool - * See: https://github.com/webpack/docs/wiki/build-performance#sourcemaps - */ - devtool: 'cheap-module-source-map', - - /** - * Options affecting the output of the compilation. - * - * See: http://webpack.github.io/docs/configuration.html#output - */ - output: { +module.exports = function (env) { + return webpackMerge(commonConfig({env: ENV}), { /** - * The output directory as absolute path (required). + * Merged metadata from webpack.common.js for index.html * - * See: http://webpack.github.io/docs/configuration.html#output-path + * See: (custom attribute) */ - path: helpers.root('dist'), + metadata: METADATA, /** - * Specifies the name of each output file on disk. - * IMPORTANT: You must not specify an absolute path here! + * Switch loaders to debug mode. * - * See: http://webpack.github.io/docs/configuration.html#output-filename + * See: http://webpack.github.io/docs/configuration.html#debug */ - filename: '[name].bundle.js', + debug: true, /** - * The filename of the SourceMaps for the JavaScript files. - * They are inside the output.path directory. + * Developer tool to enhance debugging * - * See: http://webpack.github.io/docs/configuration.html#output-sourcemapfilename + * See: http://webpack.github.io/docs/configuration.html#devtool + * See: https://github.com/webpack/docs/wiki/build-performance#sourcemaps */ - sourceMapFilename: '[name].map', + devtool: 'cheap-module-source-map', - /** The filename of non-entry chunks as relative path - * inside the output.path directory. + /** + * Options affecting the output of the compilation. * - * See: http://webpack.github.io/docs/configuration.html#output-chunkfilename + * See: http://webpack.github.io/docs/configuration.html#output */ - chunkFilename: '[id].chunk.js' - - }, - - externals: { - webtorrent: 'WebTorrent' - }, + output: { + /** + * The output directory as absolute path (required). + * + * See: http://webpack.github.io/docs/configuration.html#output-path + */ + path: helpers.root('dist'), + + /** + * Specifies the name of each output file on disk. + * IMPORTANT: You must not specify an absolute path here! + * + * See: http://webpack.github.io/docs/configuration.html#output-filename + */ + filename: '[name].bundle.js', + + /** + * The filename of the SourceMaps for the JavaScript files. + * They are inside the output.path directory. + * + * See: http://webpack.github.io/docs/configuration.html#output-sourcemapfilename + */ + sourceMapFilename: '[name].map', + + /** The filename of non-entry chunks as relative path + * inside the output.path directory. + * + * See: http://webpack.github.io/docs/configuration.html#output-chunkfilename + */ + chunkFilename: '[id].chunk.js', + + library: 'ac_[name]', + libraryTarget: 'var' + + }, + + externals: { + webtorrent: 'WebTorrent' + }, + + plugins: [ + + /** + * Plugin: DefinePlugin + * Description: Define free variables. + * Useful for having development builds with debug logging or adding global constants. + * + * Environment helpers + * + * See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin + */ + // NOTE: when adding more properties, make sure you include them in custom-typings.d.ts + new DefinePlugin({ + 'ENV': JSON.stringify(METADATA.ENV), + 'HMR': METADATA.HMR, + 'process.env': { + 'ENV': JSON.stringify(METADATA.ENV), + 'NODE_ENV': JSON.stringify(METADATA.ENV), + 'HMR': METADATA.HMR + } + }), - plugins: [ + new NamedModulesPlugin() + ], /** - * Plugin: DefinePlugin - * Description: Define free variables. - * Useful for having development builds with debug logging or adding global constants. + * Static analysis linter for TypeScript advanced options configuration + * Description: An extensible linter for the TypeScript language. * - * Environment helpers + * See: https://github.com/wbuchwalter/tslint-loader + */ + tslint: { + emitErrors: false, + failOnHint: false, + resourcePath: 'src' + }, + + devServer: { + port: METADATA.port, + host: METADATA.host, + historyApiFallback: true, + watchOptions: { + aggregateTimeout: 300, + poll: 1000 + }, + outputPath: helpers.root('dist') + }, + + /* + * Include polyfills or mocks for various node stuff + * Description: Node configuration * - * See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin + * See: https://webpack.github.io/docs/configuration.html#node */ - // NOTE: when adding more properties, make sure you include them in custom-typings.d.ts - new DefinePlugin({ - 'ENV': JSON.stringify(METADATA.ENV), - 'HMR': METADATA.HMR, - 'process.env': { - 'ENV': JSON.stringify(METADATA.ENV), - 'NODE_ENV': JSON.stringify(METADATA.ENV), - 'HMR': METADATA.HMR - } - }) - ], - - /** - * Static analysis linter for TypeScript advanced options configuration - * Description: An extensible linter for the TypeScript language. - * - * See: https://github.com/wbuchwalter/tslint-loader - */ - tslint: { - emitErrors: false, - failOnHint: false, - resourcePath: 'src' - }, - - /* - * Include polyfills or mocks for various node stuff - * Description: Node configuration - * - * See: https://webpack.github.io/docs/configuration.html#node - */ - node: { - global: 'window', - crypto: 'empty', - process: true, - module: false, - clearImmediate: false, - setImmediate: false - } - -}) + node: { + global: 'window', + crypto: 'empty', + process: true, + module: false, + clearImmediate: false, + setImmediate: false + } + }) +} diff --git a/client/config/webpack.prod.js b/client/config/webpack.prod.js index 7ce5727d32bbfed1d6c7bad41c71c5035d39fb9e..46db5448216381cc24c09b7b053797d0b7daadee 100644 --- a/client/config/webpack.prod.js +++ b/client/config/webpack.prod.js @@ -9,10 +9,12 @@ const commonConfig = require('./webpack.common.js') // the settings that are com /** * Webpack Plugins */ +// const ProvidePlugin = require('webpack/lib/ProvidePlugin') const DefinePlugin = require('webpack/lib/DefinePlugin') -const DedupePlugin = require('webpack/lib/optimize/DedupePlugin') +const NormalModuleReplacementPlugin = require('webpack/lib/NormalModuleReplacementPlugin') +// const IgnorePlugin = require('webpack/lib/IgnorePlugin') +// const DedupePlugin = require('webpack/lib/optimize/DedupePlugin') const UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin') -const CompressionPlugin = require('compression-webpack-plugin') const WebpackMd5Hash = require('webpack-md5-hash') /** @@ -21,211 +23,210 @@ const WebpackMd5Hash = require('webpack-md5-hash') const ENV = process.env.NODE_ENV = process.env.ENV = 'production' const HOST = process.env.HOST || 'localhost' const PORT = process.env.PORT || 8080 -const METADATA = webpackMerge(commonConfig.metadata, { +const METADATA = webpackMerge(commonConfig({env: ENV}).metadata, { host: HOST, port: PORT, ENV: ENV, HMR: false }) -module.exports = webpackMerge(commonConfig, { - /** - * Switch loaders to debug mode. - * - * See: http://webpack.github.io/docs/configuration.html#debug - */ - debug: false, - - /** - * Developer tool to enhance debugging - * - * See: http://webpack.github.io/docs/configuration.html#devtool - * See: https://github.com/webpack/docs/wiki/build-performance#sourcemaps - */ - devtool: 'source-map', - - /** - * Options affecting the output of the compilation. - * - * See: http://webpack.github.io/docs/configuration.html#output - */ - output: { +module.exports = function (env) { + return webpackMerge(commonConfig({env: ENV}), { /** - * The output directory as absolute path (required). + * Switch loaders to debug mode. * - * See: http://webpack.github.io/docs/configuration.html#output-path + * See: http://webpack.github.io/docs/configuration.html#debug */ - path: helpers.root('dist'), + debug: false, /** - * Specifies the name of each output file on disk. - * IMPORTANT: You must not specify an absolute path here! + * Developer tool to enhance debugging * - * See: http://webpack.github.io/docs/configuration.html#output-filename + * See: http://webpack.github.io/docs/configuration.html#devtool + * See: https://github.com/webpack/docs/wiki/build-performance#sourcemaps */ - filename: '[name].[chunkhash].bundle.js', + devtool: 'source-map', /** - * The filename of the SourceMaps for the JavaScript files. - * They are inside the output.path directory. + * Options affecting the output of the compilation. * - * See: http://webpack.github.io/docs/configuration.html#output-sourcemapfilename + * See: http://webpack.github.io/docs/configuration.html#output */ - sourceMapFilename: '[name].[chunkhash].bundle.map', + output: { + /** + * The output directory as absolute path (required). + * + * See: http://webpack.github.io/docs/configuration.html#output-path + */ + path: helpers.root('dist'), + + /** + * Specifies the name of each output file on disk. + * IMPORTANT: You must not specify an absolute path here! + * + * See: http://webpack.github.io/docs/configuration.html#output-filename + */ + filename: '[name].[chunkhash].bundle.js', + + /** + * The filename of the SourceMaps for the JavaScript files. + * They are inside the output.path directory. + * + * See: http://webpack.github.io/docs/configuration.html#output-sourcemapfilename + */ + sourceMapFilename: '[name].[chunkhash].bundle.map', + + /** + * The filename of non-entry chunks as relative path + * inside the output.path directory. + * + * See: http://webpack.github.io/docs/configuration.html#output-chunkfilename + */ + chunkFilename: '[id].[chunkhash].chunk.js' + + }, + + externals: { + webtorrent: 'WebTorrent' + }, /** - * The filename of non-entry chunks as relative path - * inside the output.path directory. + * Add additional plugins to the compiler. * - * See: http://webpack.github.io/docs/configuration.html#output-chunkfilename + * See: http://webpack.github.io/docs/configuration.html#plugins */ - chunkFilename: '[id].[chunkhash].chunk.js' - - }, - - externals: { - webtorrent: 'WebTorrent' - }, - - /** - * Add additional plugins to the compiler. - * - * See: http://webpack.github.io/docs/configuration.html#plugins - */ - plugins: [ - - /** - * Plugin: WebpackMd5Hash - * Description: Plugin to replace a standard webpack chunkhash with md5. - * - * See: https://www.npmjs.com/package/webpack-md5-hash - */ - new WebpackMd5Hash(), + plugins: [ + + /** + * Plugin: WebpackMd5Hash + * Description: Plugin to replace a standard webpack chunkhash with md5. + * + * See: https://www.npmjs.com/package/webpack-md5-hash + */ + new WebpackMd5Hash(), + + /** + * Plugin: DedupePlugin + * Description: Prevents the inclusion of duplicate code into your bundle + * and instead applies a copy of the function at runtime. + * + * See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin + * See: https://github.com/webpack/docs/wiki/optimization#deduplication + */ + // new DedupePlugin(), + + /** + * Plugin: DefinePlugin + * Description: Define free variables. + * Useful for having development builds with debug logging or adding global constants. + * + * Environment helpers + * + * See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin + */ + // NOTE: when adding more properties make sure you include them in custom-typings.d.ts + new DefinePlugin({ + 'ENV': JSON.stringify(METADATA.ENV), + 'HMR': METADATA.HMR, + 'process.env': { + 'ENV': JSON.stringify(METADATA.ENV), + 'NODE_ENV': JSON.stringify(METADATA.ENV), + 'HMR': METADATA.HMR + } + }), + + /** + * Plugin: UglifyJsPlugin + * Description: Minimize all JavaScript output of chunks. + * Loaders are switched into minimizing mode. + * + * See: https://webpack.github.io/docs/list-of-plugins.html#uglifyjsplugin + */ + // NOTE: To debug prod builds uncomment //debug lines and comment //prod lines + new UglifyJsPlugin({ + // beautify: true, //debug + // mangle: false, //debug + // dead_code: false, //debug + // unused: false, //debug + // deadCode: false, //debug + // compress: { + // screw_ie8: true, + // keep_fnames: true, + // drop_debugger: false, + // dead_code: false, + // unused: false + // }, // debug + // comments: true, //debug + + beautify: false, // prod + mangle: { screw_ie8: true, keep_fnames: true }, // prod + compress: { screw_ie8: true }, // prod + comments: false // prod + }), + + new NormalModuleReplacementPlugin( + /angular2-hmr/, + helpers.root('config/modules/angular2-hmr-prod.js') + ) + + /** + * Plugin: CompressionPlugin + * Description: Prepares compressed versions of assets to serve + * them with Content-Encoding + * + * See: https://github.com/webpack/compression-webpack-plugin + */ + // new CompressionPlugin({ + // regExp: /\.css$|\.html$|\.js$|\.map$/, + // threshold: 2 * 1024 + // }) - /** - * Plugin: DedupePlugin - * Description: Prevents the inclusion of duplicate code into your bundle - * and instead applies a copy of the function at runtime. - * - * See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin - * See: https://github.com/webpack/docs/wiki/optimization#deduplication - */ - new DedupePlugin(), + ], /** - * Plugin: DefinePlugin - * Description: Define free variables. - * Useful for having development builds with debug logging or adding global constants. - * - * Environment helpers + * Static analysis linter for TypeScript advanced options configuration + * Description: An extensible linter for the TypeScript language. * - * See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin + * See: https://github.com/wbuchwalter/tslint-loader */ - // NOTE: when adding more properties make sure you include them in custom-typings.d.ts - new DefinePlugin({ - 'ENV': JSON.stringify(METADATA.ENV), - 'HMR': METADATA.HMR, - 'process.env': { - 'ENV': JSON.stringify(METADATA.ENV), - 'NODE_ENV': JSON.stringify(METADATA.ENV), - 'HMR': METADATA.HMR - } - }), + tslint: { + emitErrors: true, + failOnHint: true, + resourcePath: 'src' + }, /** - * Plugin: UglifyJsPlugin - * Description: Minimize all JavaScript output of chunks. - * Loaders are switched into minimizing mode. + * Html loader advanced options * - * See: https://webpack.github.io/docs/list-of-plugins.html#uglifyjsplugin + * See: https://github.com/webpack/html-loader#advanced-options */ - // NOTE: To debug prod builds uncomment //debug lines and comment //prod lines - new UglifyJsPlugin({ - // beautify: true, //debug - // mangle: false, //debug - // dead_code: false, //debug - // unused: false, //debug - // deadCode: false, //debug - // compress: { - // screw_ie8: true, - // keep_fnames: true, - // drop_debugger: false, - // dead_code: false, - // unused: false - // }, // debug - // comments: true, //debug - - beautify: false, // prod - - mangle: { - screw_ie8: true, - keep_fnames: true - }, // prod - - compress: { - screw_ie8: true - }, // prod - - comments: false // prod - }), - - /** - * Plugin: CompressionPlugin - * Description: Prepares compressed versions of assets to serve - * them with Content-Encoding + // TODO: Need to workaround Angular 2's html syntax => #id [bind] (event) *ngFor + htmlLoader: { + minimize: true, + removeAttributeQuotes: false, + caseSensitive: true, + customAttrSurround: [ + [/#/, /(?:)/], + [/\*/, /(?:)/], + [/\[?\(?/, /(?:)/] + ], + customAttrAssign: [/\)?\]?=/] + }, + + /* + * Include polyfills or mocks for various node stuff + * Description: Node configuration * - * See: https://github.com/webpack/compression-webpack-plugin + * See: https://webpack.github.io/docs/configuration.html#node */ - new CompressionPlugin({ - regExp: /\.css$|\.html$|\.js$|\.map$/, - threshold: 2 * 1024 - }) - - ], - - /** - * Static analysis linter for TypeScript advanced options configuration - * Description: An extensible linter for the TypeScript language. - * - * See: https://github.com/wbuchwalter/tslint-loader - */ - tslint: { - emitErrors: true, - failOnHint: true, - resourcePath: 'src' - }, - - /** - * Html loader advanced options - * - * See: https://github.com/webpack/html-loader#advanced-options - */ - // TODO: Need to workaround Angular 2's html syntax => #id [bind] (event) *ngFor - htmlLoader: { - minimize: true, - removeAttributeQuotes: false, - caseSensitive: true, - customAttrSurround: [ - [/#/, /(?:)/], - [/\*/, /(?:)/], - [/\[?\(?/, /(?:)/] - ], - customAttrAssign: [/\)?\]?=/] - }, - - /* - * Include polyfills or mocks for various node stuff - * Description: Node configuration - * - * See: https://webpack.github.io/docs/configuration.html#node - */ - node: { - global: 'window', - crypto: 'empty', - process: false, - module: false, - clearImmediate: false, - setImmediate: false - } - -}) + node: { + global: 'window', + crypto: 'empty', + process: false, + module: false, + clearImmediate: false, + setImmediate: false + } + + }) +} diff --git a/client/package.json b/client/package.json index a5c5d092b258872053f775614467244a44b7817b..cc116f3e56522399ab24d50934804b3a607addb3 100644 --- a/client/package.json +++ b/client/package.json @@ -13,61 +13,72 @@ "url": "git://github.com/Chocobozzz/PeerTube.git" }, "scripts": { - "postinstall": "typings install", "test": "standard && tslint -c ./tslint.json src/**/*.ts", "webpack": "webpack" }, "license": "GPLv3", "dependencies": { - "@angular/common": "2.0.0-rc.4", - "@angular/compiler": "2.0.0-rc.4", - "@angular/core": "2.0.0-rc.4", - "@angular/http": "2.0.0-rc.4", - "@angular/platform-browser": "2.0.0-rc.4", - "@angular/platform-browser-dynamic": "2.0.0-rc.4", - "@angular/router": "3.0.0-beta.2", - "angular-pipes": "^2.0.0", - "awesome-typescript-loader": "^0.17.0", - "bootstrap-loader": "^1.0.8", + "@angular/common": "^2.0.0", + "@angular/compiler": "^2.0.0", + "@angular/core": "^2.0.0", + "@angular/forms": "^2.0.0", + "@angular/http": "^2.0.0", + "@angular/platform-browser": "^2.0.0", + "@angular/platform-browser-dynamic": "^2.0.0", + "@angular/router": "^3.0.0", + "@angularclass/hmr": "^1.2.0", + "@angularclass/hmr-loader": "^3.0.2", + "@types/core-js": "^0.9.28", + "@types/node": "^6.0.38", + "@types/source-map": "^0.1.26", + "@types/uglify-js": "^2.0.27", + "@types/webpack": "^1.12.29", + "angular-pipes": "^3.0.0", + "angular2-template-loader": "^0.5.0", + "assets-webpack-plugin": "^3.4.0", + "awesome-typescript-loader": "^2.2.1", + "bootstrap-loader": "^2.0.0-beta.11", "bootstrap-sass": "^3.3.6", "compression-webpack-plugin": "^0.3.1", "copy-webpack-plugin": "^3.0.1", - "core-js": "^2.4.0", - "css-loader": "^0.23.1", + "core-js": "^2.4.1", + "css-loader": "^0.25.0", + "css-to-string-loader": "https://github.com/Chocobozzz/css-to-string-loader#patch-1", "es6-promise": "^3.0.2", "es6-promise-loader": "^1.0.1", "es6-shim": "^0.35.0", - "file-loader": "^0.8.5", + "extract-text-webpack-plugin": "^2.0.0-beta.4", + "file-loader": "^0.9.0", "html-webpack-plugin": "^2.19.0", "ie-shim": "^0.1.0", "intl": "^1.2.4", "json-loader": "^0.5.4", - "ng2-bootstrap": "1.0.16", + "ng2-bootstrap": "^1.1.5", "ng2-file-upload": "^1.0.3", - "node-sass": "^3.7.0", + "node-sass": "^3.10.0", "normalize.css": "^4.1.1", "raw-loader": "^0.5.1", "reflect-metadata": "0.1.3", - "resolve-url-loader": "^1.4.3", - "rxjs": "5.0.0-beta.6", - "sass-loader": "^3.2.0", + "resolve-url-loader": "^1.6.0", + "rxjs": "5.0.0-beta.12", + "sass-loader": "^4.0.2", "source-map-loader": "^0.1.5", + "string-replace-loader": "^1.0.3", "style-loader": "^0.13.1", "ts-helpers": "^1.1.1", - "tslint": "^3.7.4", + "tslint": "3.15.1", "tslint-loader": "^2.1.4", - "typescript": "^1.8.10", - "typings": "^1.0.4", + "typescript": "^2.0.0", "url-loader": "^0.5.7", - "webpack": "^1.13.1", + "webpack": "2.1.0-beta.22", "webpack-md5-hash": "0.0.5", - "webpack-merge": "^0.13.0", + "webpack-merge": "^0.14.1", "webpack-notifier": "^1.3.0", - "webtorrent": "^0.95.2", - "zone.js": "0.6.12" + "webtorrent": "^0.96.0", + "zone.js": "0.6.23" }, "devDependencies": { - "codelyzer": "0.0.19", - "standard": "^7.0.1" + "codelyzer": "0.0.28", + "standard": "^8.0.0" } } diff --git a/client/src/app/account/account.component.html b/client/src/app/account/account.component.html new file mode 100644 index 0000000000000000000000000000000000000000..5a8847acda812bf45b0970197ba72bf86451da70 --- /dev/null +++ b/client/src/app/account/account.component.html @@ -0,0 +1,27 @@ +<h3>Account</h3> + +<div *ngIf="information" class="alert alert-success">{{ information }}</div> +<div *ngIf="error" class="alert alert-danger">{{ error }}</div> + +<form role="form" (ngSubmit)="changePassword()" [formGroup]="form"> + <div class="form-group"> + <label for="new-password">New password</label> + <input + type="password" class="form-control" id="new-password" + formControlName="new-password" + > + <div *ngIf="formErrors['new-password']" class="alert alert-danger"> + {{ formErrors['new-password'] }} + </div> + </div> + + <div class="form-group"> + <label for="name">Confirm new password</label> + <input + type="password" class="form-control" id="new-confirmed-password" + formControlName="new-confirmed-password" + > + </div> + + <input type="submit" value="Change password" class="btn btn-default" [disabled]="!form.valid"> +</form> diff --git a/client/src/app/account/account.component.ts b/client/src/app/account/account.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..851eaf198e1ae73dc9b4d84e6d95c815a7d76e80 --- /dev/null +++ b/client/src/app/account/account.component.ts @@ -0,0 +1,67 @@ +import { } from '@angular/common'; +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Router } from '@angular/router'; + +import { AccountService } from './account.service'; +import { FormReactive, USER_PASSWORD } from '../shared'; + +@Component({ + selector: 'my-account', + templateUrl: './account.component.html' +}) + +export class AccountComponent extends FormReactive implements OnInit { + information: string = null; + error: string = null; + + form: FormGroup; + formErrors = { + 'new-password': '', + 'new-confirmed-password': '' + }; + validationMessages = { + 'new-password': USER_PASSWORD.MESSAGES, + 'new-confirmed-password': USER_PASSWORD.MESSAGES + }; + + constructor( + private accountService: AccountService, + private formBuilder: FormBuilder, + private router: Router + ) { + super(); + } + + buildForm() { + this.form = this.formBuilder.group({ + 'new-password': [ '', USER_PASSWORD.VALIDATORS ], + 'new-confirmed-password': [ '', USER_PASSWORD.VALIDATORS ], + }); + + this.form.valueChanges.subscribe(data => this.onValueChanged(data)); + } + + ngOnInit() { + this.buildForm(); + } + + changePassword() { + const newPassword = this.form.value['new-password']; + const newConfirmedPassword = this.form.value['new-confirmed-password']; + + this.information = null; + this.error = null; + + if (newPassword !== newConfirmedPassword) { + this.error = 'The new password and the confirmed password do not correspond.'; + return; + } + + this.accountService.changePassword(newPassword).subscribe( + ok => this.information = 'Password updated.', + + err => this.error = err + ); + } +} diff --git a/client/src/app/account/account.routes.ts b/client/src/app/account/account.routes.ts new file mode 100644 index 0000000000000000000000000000000000000000..e348c6ebed9a22b689a7cfd230906fa76e7878cf --- /dev/null +++ b/client/src/app/account/account.routes.ts @@ -0,0 +1,5 @@ +import { AccountComponent } from './account.component'; + +export const AccountRoutes = [ + { path: 'account', component: AccountComponent } +]; diff --git a/client/src/app/account/account.service.ts b/client/src/app/account/account.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..355bcef742ebd9fffca1cefb27bd440e3551133f --- /dev/null +++ b/client/src/app/account/account.service.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@angular/core'; + +import { AuthHttp, AuthService, RestExtractor } from '../shared'; + +@Injectable() +export class AccountService { + private static BASE_USERS_URL = '/api/v1/users/'; + + constructor( + private authHttp: AuthHttp, + private authService: AuthService, + private restExtractor: RestExtractor + ) {} + + changePassword(newPassword: string) { + const url = AccountService.BASE_USERS_URL + this.authService.getUser().id; + const body = { + password: newPassword + }; + + return this.authHttp.put(url, body) + .map(this.restExtractor.extractDataBool) + .catch((res) => this.restExtractor.handleError(res)); + } +} diff --git a/client/src/app/account/index.ts b/client/src/app/account/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..823d9fe5fd95faee4b514602d4483d22459f221a --- /dev/null +++ b/client/src/app/account/index.ts @@ -0,0 +1,3 @@ +export * from './account.component'; +export * from './account.routes'; +export * from './account.service'; diff --git a/client/src/app/admin/admin.component.ts b/client/src/app/admin/admin.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..64a7400e7e954d5c8d3660a054b7c2c06cb132e7 --- /dev/null +++ b/client/src/app/admin/admin.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + template: '<router-outlet></router-outlet>' +}) + +export class AdminComponent { +} diff --git a/client/src/app/admin/admin.routes.ts b/client/src/app/admin/admin.routes.ts new file mode 100644 index 0000000000000000000000000000000000000000..edb8ba49f8a9d1bdcb434accf773e044a8b2e412 --- /dev/null +++ b/client/src/app/admin/admin.routes.ts @@ -0,0 +1,23 @@ +import { Routes } from '@angular/router'; + +import { AdminComponent } from './admin.component'; +import { FriendsRoutes } from './friends'; +import { RequestsRoutes } from './requests'; +import { UsersRoutes } from './users'; + +export const AdminRoutes: Routes = [ + { + path: 'admin', + component: AdminComponent, + children: [ + { + path: '', + redirectTo: 'users', + pathMatch: 'full' + }, + ...FriendsRoutes, + ...RequestsRoutes, + ...UsersRoutes + ] + } +]; diff --git a/client/src/app/admin/friends/friend-add/friend-add.component.html b/client/src/app/admin/friends/friend-add/friend-add.component.html new file mode 100644 index 0000000000000000000000000000000000000000..788f3b44d168d5894aabc4316e9079fb898c13ca --- /dev/null +++ b/client/src/app/admin/friends/friend-add/friend-add.component.html @@ -0,0 +1,26 @@ +<h3>Make friends</h3> + +<div *ngIf="error" class="alert alert-danger">{{ error }}</div> + +<form (ngSubmit)="makeFriends()" [formGroup]="form"> + <div class="form-group" *ngFor="let url of urls; let id = index; trackBy:customTrackBy"> + <label for="username">Url</label> + + <div class="input-group"> + <input + type="text" class="form-control" placeholder="http://domain.com" + [id]="'url-' + id" [formControlName]="'url-' + id" + /> + <span class="input-group-btn"> + <button *ngIf="displayAddField(id)" (click)="addField()" class="btn btn-default" type="button">+</button> + <button *ngIf="displayRemoveField(id)" (click)="removeField(id)" class="btn btn-default" type="button">-</button> + </span> + </div> + + <div [hidden]="form.controls['url-' + id].valid || form.controls['url-' + id].pristine" class="alert alert-warning"> + It should be a valid url. + </div> + </div> + + <input type="submit" value="Make friends" class="btn btn-default" [disabled]="!isFormValid()"> +</form> diff --git a/client/src/app/admin/friends/friend-add/friend-add.component.scss b/client/src/app/admin/friends/friend-add/friend-add.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..5fde5163697372dcbc0622bff85f5cd74ae063aa --- /dev/null +++ b/client/src/app/admin/friends/friend-add/friend-add.component.scss @@ -0,0 +1,7 @@ +table { + margin-bottom: 40px; +} + +.input-group-btn button { + width: 35px; +} diff --git a/client/src/app/admin/friends/friend-add/friend-add.component.ts b/client/src/app/admin/friends/friend-add/friend-add.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..64165a9a5b312547f2771a639a67ffc3407f4662 --- /dev/null +++ b/client/src/app/admin/friends/friend-add/friend-add.component.ts @@ -0,0 +1,108 @@ +import { Component, OnInit } from '@angular/core'; +import { FormControl, FormGroup } from '@angular/forms'; +import { Router } from '@angular/router'; + +import { validateUrl } from '../../../shared'; +import { FriendService } from '../shared'; + +@Component({ + selector: 'my-friend-add', + templateUrl: './friend-add.component.html', + styleUrls: [ './friend-add.component.scss' ] +}) +export class FriendAddComponent implements OnInit { + form: FormGroup; + urls = [ ]; + error: string = null; + + constructor(private router: Router, private friendService: FriendService) {} + + ngOnInit() { + this.form = new FormGroup({}); + this.addField(); + } + + addField() { + this.form.addControl(`url-${this.urls.length}`, new FormControl('', [ validateUrl ])); + this.urls.push(''); + } + + customTrackBy(index: number, obj: any): any { + return index; + } + + displayAddField(index: number) { + return index === (this.urls.length - 1); + } + + displayRemoveField(index: number) { + return (index !== 0 || this.urls.length > 1) && index !== (this.urls.length - 1); + } + + isFormValid() { + // Do not check the last input + for (let i = 0; i < this.urls.length - 1; i++) { + if (!this.form.controls[`url-${i}`].valid) return false; + } + + const lastIndex = this.urls.length - 1; + // If the last input (which is not the first) is empty, it's ok + if (this.urls[lastIndex] === '' && lastIndex !== 0) { + return true; + } else { + return this.form.controls[`url-${lastIndex}`].valid; + } + } + + removeField(index: number) { + // Remove the last control + this.form.removeControl(`url-${this.urls.length - 1}`); + this.urls.splice(index, 1); + } + + makeFriends() { + this.error = ''; + + const notEmptyUrls = this.getNotEmptyUrls(); + if (notEmptyUrls.length === 0) { + this.error = 'You need to specify at less 1 url.'; + return; + } + + if (!this.isUrlsUnique(notEmptyUrls)) { + this.error = 'Urls need to be unique.'; + return; + } + + const confirmMessage = 'Are you sure to make friends with:\n - ' + notEmptyUrls.join('\n - '); + if (!confirm(confirmMessage)) return; + + this.friendService.makeFriends(notEmptyUrls).subscribe( + status => { + // TODO: extractdatastatus + // if (status === 409) { + // alert('Already made friends!'); + // } else { + alert('Make friends request sent!'); + this.router.navigate([ '/admin/friends/list' ]); + // } + }, + error => alert(error.text) + ); + } + + private getNotEmptyUrls() { + const notEmptyUrls = []; + + Object.keys(this.form.value).forEach((urlKey) => { + const url = this.form.value[urlKey]; + if (url !== '') notEmptyUrls.push(url); + }); + + return notEmptyUrls; + } + + private isUrlsUnique(urls: string[]) { + return urls.every(url => urls.indexOf(url) === urls.lastIndexOf(url)); + } +} diff --git a/client/src/app/admin/friends/friend-add/index.ts b/client/src/app/admin/friends/friend-add/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..a101b3be5c6782bbcb31c3ade07d6f90ad059d80 --- /dev/null +++ b/client/src/app/admin/friends/friend-add/index.ts @@ -0,0 +1 @@ +export * from './friend-add.component'; diff --git a/client/src/app/admin/friends/friend-list/friend-list.component.html b/client/src/app/admin/friends/friend-list/friend-list.component.html new file mode 100644 index 0000000000000000000000000000000000000000..d786a78469ee612bc3d13f9094f50b5ba44373fe --- /dev/null +++ b/client/src/app/admin/friends/friend-list/friend-list.component.html @@ -0,0 +1,29 @@ +<h3>Friends list</h3> + +<table class="table table-hover"> + <thead> + <tr> + <th class="table-column-id">ID</th> + <th>Url</th> + <th>Score</th> + <th>Created Date</th> + </tr> + </thead> + + <tbody> + <tr *ngFor="let friend of friends"> + <td>{{ friend.id }}</td> + <td>{{ friend.url }}</td> + <td>{{ friend.score }}</td> + <td>{{ friend.createdDate | date: 'medium' }}</td> + </tr> + </tbody> +</table> + +<a *ngIf="friends?.length !== 0" class="add-user btn btn-danger pull-left" (click)="quitFriends()"> + Quit friends +</a> + +<a *ngIf="friends?.length === 0" class="add-user btn btn-success pull-right" [routerLink]="['/admin/friends/add']"> + Make friends +</a> diff --git a/client/src/app/admin/friends/friend-list/friend-list.component.scss b/client/src/app/admin/friends/friend-list/friend-list.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..cb597e12b3ee3cf440d19457fd25c4c519d6c4c7 --- /dev/null +++ b/client/src/app/admin/friends/friend-list/friend-list.component.scss @@ -0,0 +1,3 @@ +table { + margin-bottom: 40px; +} diff --git a/client/src/app/admin/friends/friend-list/friend-list.component.ts b/client/src/app/admin/friends/friend-list/friend-list.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..88c4800ee74be25da096aee52e5f780c19fbef21 --- /dev/null +++ b/client/src/app/admin/friends/friend-list/friend-list.component.ts @@ -0,0 +1,38 @@ +import { Component, OnInit } from '@angular/core'; + +import { Friend, FriendService } from '../shared'; + +@Component({ + selector: 'my-friend-list', + templateUrl: './friend-list.component.html', + styleUrls: [ './friend-list.component.scss' ] +}) +export class FriendListComponent implements OnInit { + friends: Friend[]; + + constructor(private friendService: FriendService) { } + + ngOnInit() { + this.getFriends(); + } + + quitFriends() { + if (!confirm('Are you sure?')) return; + + this.friendService.quitFriends().subscribe( + status => { + alert('Quit friends!'); + this.getFriends(); + }, + error => alert(error.text) + ); + } + + private getFriends() { + this.friendService.getFriends().subscribe( + friends => this.friends = friends, + + err => alert(err.text) + ); + } +} diff --git a/client/src/app/admin/friends/friend-list/index.ts b/client/src/app/admin/friends/friend-list/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..354c978a44a9747ed9da0de6bc654c4a43b89408 --- /dev/null +++ b/client/src/app/admin/friends/friend-list/index.ts @@ -0,0 +1 @@ +export * from './friend-list.component'; diff --git a/client/src/app/admin/friends/friends.component.ts b/client/src/app/admin/friends/friends.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..bc3f54158fef093f4c7d04f14f5f96c78f8b6549 --- /dev/null +++ b/client/src/app/admin/friends/friends.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + template: '<router-outlet></router-outlet>' +}) + +export class FriendsComponent { +} diff --git a/client/src/app/admin/friends/friends.routes.ts b/client/src/app/admin/friends/friends.routes.ts new file mode 100644 index 0000000000000000000000000000000000000000..7fdef68f90585eb361f85c5882b63f3188c298b4 --- /dev/null +++ b/client/src/app/admin/friends/friends.routes.ts @@ -0,0 +1,27 @@ +import { Routes } from '@angular/router'; + +import { FriendsComponent } from './friends.component'; +import { FriendAddComponent } from './friend-add'; +import { FriendListComponent } from './friend-list'; + +export const FriendsRoutes: Routes = [ + { + path: 'friends', + component: FriendsComponent, + children: [ + { + path: '', + redirectTo: 'list', + pathMatch: 'full' + }, + { + path: 'list', + component: FriendListComponent + }, + { + path: 'add', + component: FriendAddComponent + } + ] + } +]; diff --git a/client/src/app/admin/friends/index.ts b/client/src/app/admin/friends/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..dd4df2538e4a5d305f1e87e0cd5f8181315c6b50 --- /dev/null +++ b/client/src/app/admin/friends/index.ts @@ -0,0 +1,5 @@ +export * from './friend-add'; +export * from './friend-list'; +export * from './shared'; +export * from './friends.component'; +export * from './friends.routes'; diff --git a/client/src/app/admin/friends/shared/friend.model.ts b/client/src/app/admin/friends/shared/friend.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..7cb28f44064be81db58c1a3edb323e1a4704cf68 --- /dev/null +++ b/client/src/app/admin/friends/shared/friend.model.ts @@ -0,0 +1,6 @@ +export interface Friend { + id: string; + url: string; + score: number; + createdDate: Date; +} diff --git a/client/src/app/admin/friends/shared/friend.service.ts b/client/src/app/admin/friends/shared/friend.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..75826fc17bccac2e7c2388d7997a5aa42db4e1fa --- /dev/null +++ b/client/src/app/admin/friends/shared/friend.service.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; + +import { Friend } from './friend.model'; +import { AuthHttp, RestExtractor } from '../../../shared'; + +@Injectable() +export class FriendService { + private static BASE_FRIEND_URL: string = '/api/v1/pods/'; + + constructor ( + private authHttp: AuthHttp, + private restExtractor: RestExtractor + ) {} + + getFriends(): Observable<Friend[]> { + return this.authHttp.get(FriendService.BASE_FRIEND_URL) + // Not implemented as a data list by the server yet + // .map(this.restExtractor.extractDataList) + .map((res) => res.json()) + .catch((res) => this.restExtractor.handleError(res)); + } + + makeFriends(notEmptyUrls) { + const body = { + urls: notEmptyUrls + }; + + return this.authHttp.post(FriendService.BASE_FRIEND_URL + 'makefriends', body) + .map(this.restExtractor.extractDataBool) + .catch((res) => this.restExtractor.handleError(res)); + } + + quitFriends() { + return this.authHttp.get(FriendService.BASE_FRIEND_URL + 'quitfriends') + .map(res => res.status) + .catch((res) => this.restExtractor.handleError(res)); + } +} diff --git a/client/src/app/friends/index.ts b/client/src/app/admin/friends/shared/index.ts similarity index 51% rename from client/src/app/friends/index.ts rename to client/src/app/admin/friends/shared/index.ts index 0adc256c43280b29ac261098e1d7d31a77f82dcb..0d671637d11b8cfd6cc7119080262ff6edf1db59 100644 --- a/client/src/app/friends/index.ts +++ b/client/src/app/admin/friends/shared/index.ts @@ -1 +1,2 @@ +export * from './friend.model'; export * from './friend.service'; diff --git a/client/src/app/admin/index.ts b/client/src/app/admin/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..493caed1576635d2a7937ad85d4afc349985706a --- /dev/null +++ b/client/src/app/admin/index.ts @@ -0,0 +1,6 @@ +export * from './friends'; +export * from './requests'; +export * from './users'; +export * from './admin.component'; +export * from './admin.routes'; +export * from './menu-admin.component'; diff --git a/client/src/app/admin/menu-admin.component.html b/client/src/app/admin/menu-admin.component.html new file mode 100644 index 0000000000000000000000000000000000000000..e250615aab24e2e2bc134a17ca8939abdd34ceeb --- /dev/null +++ b/client/src/app/admin/menu-admin.component.html @@ -0,0 +1,26 @@ +<menu class="col-md-2 col-sm-3 col-xs-3"> + + <div class="panel-block"> + <div id="panel-users" class="panel-button"> + <span class="hidden-xs glyphicon glyphicon-user"></span> + <a [routerLink]="['/admin/users/list']">List users</a> + </div> + + <div id="panel-friends" class="panel-button"> + <span class="hidden-xs glyphicon glyphicon-cloud"></span> + <a [routerLink]="['/admin/friends/list']">List friends</a> + </div> + + <div id="panel-request-stats" class="panel-button"> + <span class="hidden-xs glyphicon glyphicon-stats"></span> + <a [routerLink]="['/admin/requests/stats']">Request stats</a> + </div> + </div> + + <div class="panel-block"> + <div id="panel-quit-administration" class="panel-button"> + <span class="hidden-xs glyphicon glyphicon-cog"></span> + <a [routerLink]="['/videos/list']">Quit admin.</a> + </div> + </div> +</menu> diff --git a/client/src/app/admin/menu-admin.component.ts b/client/src/app/admin/menu-admin.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..59ffccf9f6c9658689e257e88bd61672fd81b5d6 --- /dev/null +++ b/client/src/app/admin/menu-admin.component.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'my-menu-admin', + templateUrl: './menu-admin.component.html' +}) +export class MenuAdminComponent { } diff --git a/client/src/app/admin/requests/index.ts b/client/src/app/admin/requests/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..236a9ee8fc4ee5bc3497bf31bb35cd51cc4a4604 --- /dev/null +++ b/client/src/app/admin/requests/index.ts @@ -0,0 +1,4 @@ +export * from './request-stats'; +export * from './shared'; +export * from './requests.component'; +export * from './requests.routes'; diff --git a/client/src/app/admin/requests/request-stats/index.ts b/client/src/app/admin/requests/request-stats/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..be3a66f77dec65f41e1cb837300bb9055ddcec98 --- /dev/null +++ b/client/src/app/admin/requests/request-stats/index.ts @@ -0,0 +1 @@ +export * from './request-stats.component'; diff --git a/client/src/app/admin/requests/request-stats/request-stats.component.html b/client/src/app/admin/requests/request-stats/request-stats.component.html new file mode 100644 index 0000000000000000000000000000000000000000..b5ac59a9aa57efed6dd33cf6f869290204f86402 --- /dev/null +++ b/client/src/app/admin/requests/request-stats/request-stats.component.html @@ -0,0 +1,23 @@ +<h3>Requests stats</h3> + +<div *ngIf="stats !== null"> + <div> + <span class="label-description">Interval seconds between requests:</span> + {{ stats.secondsInterval }} + </div> + + <div> + <span class="label-description">Remaining time before the scheduled request:</span> + {{ stats.remainingSeconds }} + </div> + + <div> + <span class="label-description">Maximum number of requests per interval:</span> + {{ stats.maxRequestsInParallel }} + </div> + + <div> + <span class="label-description">Remaining requests:</span> + {{ stats.requests.length }} + </div> +</div> diff --git a/client/src/app/admin/requests/request-stats/request-stats.component.scss b/client/src/app/admin/requests/request-stats/request-stats.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..92c28dc99521bca903cc02286a44570d471d3890 --- /dev/null +++ b/client/src/app/admin/requests/request-stats/request-stats.component.scss @@ -0,0 +1,6 @@ +.label-description { + display: inline-block; + width: 350px; + font-weight: bold; + color: black; +} diff --git a/client/src/app/admin/requests/request-stats/request-stats.component.ts b/client/src/app/admin/requests/request-stats/request-stats.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..4b0844574fd1b238ead2320e65375fab6d794b54 --- /dev/null +++ b/client/src/app/admin/requests/request-stats/request-stats.component.ts @@ -0,0 +1,51 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; + +import { RequestService, RequestStats } from '../shared'; + +@Component({ + selector: 'my-request-stats', + templateUrl: './request-stats.component.html', + styleUrls: [ './request-stats.component.scss' ] +}) +export class RequestStatsComponent implements OnInit, OnDestroy { + stats: RequestStats = null; + + private interval: NodeJS.Timer = null; + + constructor(private requestService: RequestService) { } + + ngOnInit() { + this.getStats(); + } + + ngOnDestroy() { + if (this.stats.secondsInterval !== null) { + clearInterval(this.interval); + } + } + + getStats() { + this.requestService.getStats().subscribe( + stats => { + console.log(stats); + this.stats = stats; + this.runInterval(); + }, + + err => alert(err.text) + ); + } + + private runInterval() { + this.interval = setInterval(() => { + this.stats.remainingMilliSeconds -= 1000; + + if (this.stats.remainingMilliSeconds <= 0) { + setTimeout(() => this.getStats(), this.stats.remainingMilliSeconds + 100); + clearInterval(this.interval); + } + }, 1000); + } + + +} diff --git a/client/src/app/admin/requests/requests.component.ts b/client/src/app/admin/requests/requests.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..471112b45befc19845d99c8043f438dd4f9fecd0 --- /dev/null +++ b/client/src/app/admin/requests/requests.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + template: '<router-outlet></router-outlet>' +}) + +export class RequestsComponent { +} diff --git a/client/src/app/admin/requests/requests.routes.ts b/client/src/app/admin/requests/requests.routes.ts new file mode 100644 index 0000000000000000000000000000000000000000..78221a9ff1b53b47cf3f715de1eda655bb78bb04 --- /dev/null +++ b/client/src/app/admin/requests/requests.routes.ts @@ -0,0 +1,22 @@ +import { Routes } from '@angular/router'; + +import { RequestsComponent } from './requests.component'; +import { RequestStatsComponent } from './request-stats'; + +export const RequestsRoutes: Routes = [ + { + path: 'requests', + component: RequestsComponent, + children: [ + { + path: '', + redirectTo: 'stats', + pathMatch: 'full' + }, + { + path: 'stats', + component: RequestStatsComponent + } + ] + } +]; diff --git a/client/src/app/admin/requests/shared/index.ts b/client/src/app/admin/requests/shared/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..32ab5767be6a1431a2cb0848e6238738b730ef44 --- /dev/null +++ b/client/src/app/admin/requests/shared/index.ts @@ -0,0 +1,2 @@ +export * from './request-stats.model'; +export * from './request.service'; diff --git a/client/src/app/admin/requests/shared/request-stats.model.ts b/client/src/app/admin/requests/shared/request-stats.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..766e808360294f5ac84b9d379a3bafe7705412cc --- /dev/null +++ b/client/src/app/admin/requests/shared/request-stats.model.ts @@ -0,0 +1,32 @@ +export interface Request { + request: any; + to: any; +} + +export class RequestStats { + maxRequestsInParallel: number; + milliSecondsInterval: number; + remainingMilliSeconds: number; + requests: Request[]; + + constructor(hash: { + maxRequestsInParallel: number, + milliSecondsInterval: number, + remainingMilliSeconds: number, + requests: Request[]; + }) { + this.maxRequestsInParallel = hash.maxRequestsInParallel; + this.milliSecondsInterval = hash.milliSecondsInterval; + this.remainingMilliSeconds = hash.remainingMilliSeconds; + this.requests = hash.requests; + } + + get remainingSeconds() { + return Math.floor(this.remainingMilliSeconds / 1000); + } + + get secondsInterval() { + return Math.floor(this.milliSecondsInterval / 1000); + } + +} diff --git a/client/src/app/admin/requests/shared/request.service.ts b/client/src/app/admin/requests/shared/request.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..aeec37448904b26a80a2fdcded4e005b787c453c --- /dev/null +++ b/client/src/app/admin/requests/shared/request.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; + +import { RequestStats } from './request-stats.model'; +import { AuthHttp, RestExtractor } from '../../../shared'; + +@Injectable() +export class RequestService { + private static BASE_REQUEST_URL: string = '/api/v1/requests/'; + + constructor ( + private authHttp: AuthHttp, + private restExtractor: RestExtractor + ) {} + + getStats(): Observable<RequestStats> { + return this.authHttp.get(RequestService.BASE_REQUEST_URL + 'stats') + .map(this.restExtractor.extractDataGet) + .map((data) => new RequestStats(data)) + .catch((res) => this.restExtractor.handleError(res)); + } +} diff --git a/client/src/app/admin/users/index.ts b/client/src/app/admin/users/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..e98a81f6208eb079098cff80f883ba3664718ce8 --- /dev/null +++ b/client/src/app/admin/users/index.ts @@ -0,0 +1,5 @@ +export * from './shared'; +export * from './user-add'; +export * from './user-list'; +export * from './users.component'; +export * from './users.routes'; diff --git a/client/src/app/admin/users/shared/index.ts b/client/src/app/admin/users/shared/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..e17ee5c7aca08d72e28c4ebd1fbe00d4162e069e --- /dev/null +++ b/client/src/app/admin/users/shared/index.ts @@ -0,0 +1 @@ +export * from './user.service'; diff --git a/client/src/app/admin/users/shared/user.service.ts b/client/src/app/admin/users/shared/user.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..13be553c01ce7b0db239a3339fd5c576a447b695 --- /dev/null +++ b/client/src/app/admin/users/shared/user.service.ts @@ -0,0 +1,47 @@ +import { Injectable } from '@angular/core'; + +import { AuthHttp, RestExtractor, ResultList, User } from '../../../shared'; + +@Injectable() +export class UserService { + // TODO: merge this constant with account + private static BASE_USERS_URL = '/api/v1/users/'; + + constructor( + private authHttp: AuthHttp, + private restExtractor: RestExtractor + ) {} + + addUser(username: string, password: string) { + const body = { + username, + password + }; + + return this.authHttp.post(UserService.BASE_USERS_URL, body) + .map(this.restExtractor.extractDataBool) + .catch(this.restExtractor.handleError); + } + + getUsers() { + return this.authHttp.get(UserService.BASE_USERS_URL) + .map(this.restExtractor.extractDataList) + .map(this.extractUsers) + .catch((res) => this.restExtractor.handleError(res)); + } + + removeUser(user: User) { + return this.authHttp.delete(UserService.BASE_USERS_URL + user.id); + } + + private extractUsers(result: ResultList) { + const usersJson = result.data; + const totalUsers = result.total; + const users = []; + for (const userJson of usersJson) { + users.push(new User(userJson)); + } + + return { users, totalUsers }; + } +} diff --git a/client/src/app/admin/users/user-add/index.ts b/client/src/app/admin/users/user-add/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..66d5ca04f14ce660895c8a5c677e40e4318b0098 --- /dev/null +++ b/client/src/app/admin/users/user-add/index.ts @@ -0,0 +1 @@ +export * from './user-add.component'; diff --git a/client/src/app/admin/users/user-add/user-add.component.html b/client/src/app/admin/users/user-add/user-add.component.html new file mode 100644 index 0000000000000000000000000000000000000000..9b76c7c1b95cda3e7a381aed632be6af8090b0c5 --- /dev/null +++ b/client/src/app/admin/users/user-add/user-add.component.html @@ -0,0 +1,29 @@ +<h3>Add user</h3> + +<div *ngIf="error" class="alert alert-danger">{{ error }}</div> + +<form role="form" (ngSubmit)="addUser()" [formGroup]="form"> + <div class="form-group"> + <label for="username">Username</label> + <input + type="text" class="form-control" id="username" placeholder="Username" + formControlName="username" + > + <div *ngIf="formErrors.username" class="alert alert-danger"> + {{ formErrors.username }} + </div> + </div> + + <div class="form-group"> + <label for="password">Password</label> + <input + type="password" class="form-control" id="password" placeholder="Password" + formControlName="password" + > + <div *ngIf="formErrors.password" class="alert alert-danger"> + {{ formErrors.password }} + </div> + </div> + + <input type="submit" value="Add user" class="btn btn-default" [disabled]="!form.valid"> +</form> diff --git a/client/src/app/admin/users/user-add/user-add.component.ts b/client/src/app/admin/users/user-add/user-add.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..ab96fb01dac65e3883c37e5e5d1ff11d3942a60c --- /dev/null +++ b/client/src/app/admin/users/user-add/user-add.component.ts @@ -0,0 +1,57 @@ +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Router } from '@angular/router'; + +import { UserService } from '../shared'; +import { FormReactive, USER_USERNAME, USER_PASSWORD } from '../../../shared'; + +@Component({ + selector: 'my-user-add', + templateUrl: './user-add.component.html' +}) +export class UserAddComponent extends FormReactive implements OnInit { + error: string = null; + + form: FormGroup; + formErrors = { + 'username': '', + 'password': '' + }; + validationMessages = { + 'username': USER_USERNAME.MESSAGES, + 'password': USER_PASSWORD.MESSAGES, + }; + + constructor( + private formBuilder: FormBuilder, + private router: Router, + private userService: UserService + ) { + super(); + } + + buildForm() { + this.form = this.formBuilder.group({ + username: [ '', USER_USERNAME.VALIDATORS ], + password: [ '', USER_PASSWORD.VALIDATORS ], + }); + + this.form.valueChanges.subscribe(data => this.onValueChanged(data)); + } + + ngOnInit() { + this.buildForm(); + } + + addUser() { + this.error = null; + + const { username, password } = this.form.value; + + this.userService.addUser(username, password).subscribe( + ok => this.router.navigate([ '/admin/users/list' ]), + + err => this.error = err.text + ); + } +} diff --git a/client/src/app/admin/users/user-list/index.ts b/client/src/app/admin/users/user-list/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..51fbefa8089e09aa89f05752cfbf785ebfc449f4 --- /dev/null +++ b/client/src/app/admin/users/user-list/index.ts @@ -0,0 +1 @@ +export * from './user-list.component'; diff --git a/client/src/app/admin/users/user-list/user-list.component.html b/client/src/app/admin/users/user-list/user-list.component.html new file mode 100644 index 0000000000000000000000000000000000000000..328b1be77d2b3217cbc012f7df4738e3cfba3ffc --- /dev/null +++ b/client/src/app/admin/users/user-list/user-list.component.html @@ -0,0 +1,28 @@ +<h3>Users list</h3> + +<table class="table table-hover"> + <thead> + <tr> + <th class="table-column-id">ID</th> + <th>Username</th> + <th>Created Date</th> + <th class="text-right">Remove</th> + </tr> + </thead> + + <tbody> + <tr *ngFor="let user of users"> + <td>{{ user.id }}</td> + <td>{{ user.username }}</td> + <td>{{ user.createdDate | date: 'medium' }}</td> + <td class="text-right"> + <span class="glyphicon glyphicon-remove" *ngIf="!user.isAdmin()" (click)="removeUser(user)"></span> + </td> + </tr> + </tbody> +</table> + +<a class="add-user btn btn-success pull-right" [routerLink]="['/admin/users/add']"> + <span class="glyphicon glyphicon-plus"></span> + Add user +</a> diff --git a/client/src/app/admin/users/user-list/user-list.component.scss b/client/src/app/admin/users/user-list/user-list.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..e9f61e900ad00c798d10ae1feefca0be89251302 --- /dev/null +++ b/client/src/app/admin/users/user-list/user-list.component.scss @@ -0,0 +1,7 @@ +.glyphicon-remove { + cursor: pointer; +} + +.add-user { + margin-top: 10px; +} diff --git a/client/src/app/admin/users/user-list/user-list.component.ts b/client/src/app/admin/users/user-list/user-list.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..03f4e5c0aa0f31e7f50fc0268c94e7ac0a49b2b9 --- /dev/null +++ b/client/src/app/admin/users/user-list/user-list.component.ts @@ -0,0 +1,42 @@ +import { Component, OnInit } from '@angular/core'; + +import { User } from '../../../shared'; +import { UserService } from '../shared'; + +@Component({ + selector: 'my-user-list', + templateUrl: './user-list.component.html', + styleUrls: [ './user-list.component.scss' ] +}) +export class UserListComponent implements OnInit { + totalUsers: number; + users: User[]; + + constructor(private userService: UserService) {} + + ngOnInit() { + this.getUsers(); + } + + getUsers() { + this.userService.getUsers().subscribe( + ({ users, totalUsers }) => { + this.users = users; + this.totalUsers = totalUsers; + }, + + err => alert(err.text) + ); + } + + + removeUser(user: User) { + if (confirm('Are you sure?')) { + this.userService.removeUser(user).subscribe( + () => this.getUsers(), + + err => alert(err.text) + ); + } + } +} diff --git a/client/src/app/admin/users/users.component.ts b/client/src/app/admin/users/users.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..37e3b158d1c0757447b2107843ac9316fca68829 --- /dev/null +++ b/client/src/app/admin/users/users.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + template: '<router-outlet></router-outlet>' +}) + +export class UsersComponent { +} diff --git a/client/src/app/admin/users/users.routes.ts b/client/src/app/admin/users/users.routes.ts new file mode 100644 index 0000000000000000000000000000000000000000..eb71bd0ae0b6801cb0dc55e3e8e485e5ffa21629 --- /dev/null +++ b/client/src/app/admin/users/users.routes.ts @@ -0,0 +1,27 @@ +import { Routes } from '@angular/router'; + +import { UsersComponent } from './users.component'; +import { UserAddComponent } from './user-add'; +import { UserListComponent } from './user-list'; + +export const UsersRoutes: Routes = [ + { + path: 'users', + component: UsersComponent, + children: [ + { + path: '', + redirectTo: 'list', + pathMatch: 'full' + }, + { + path: 'list', + component: UserListComponent + }, + { + path: 'add', + component: UserAddComponent + } + ] + } +]; diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html index f2acffea4a2b1520169ebe5a25ff6a0047f81d65..04c32f596344624014b008ccc615d24a1d3cff3e 100644 --- a/client/src/app/app.component.html +++ b/client/src/app/app.component.html @@ -14,48 +14,14 @@ <div class="row"> - - <menu class="col-md-2 col-sm-3 col-xs-3"> - <div class="panel-block"> - <div id="panel-user-login" class="panel-button"> - <span class="hidden-xs glyphicon glyphicon-user"></span> - <a *ngIf="!isLoggedIn" [routerLink]="['/login']">Login</a> - <a *ngIf="isLoggedIn" (click)="logout()">Logout</a> - </div> - </div> - - <div class="panel-block"> - <div id="panel-get-videos" class="panel-button"> - <span class="hidden-xs glyphicon glyphicon-list"></span> - <a [routerLink]="['/videos/list']">Get videos</a> - </div> - - <div id="panel-upload-video" class="panel-button" *ngIf="isLoggedIn"> - <span class="hidden-xs glyphicon glyphicon-cloud-upload"></span> - <a [routerLink]="['/videos/add']">Upload a video</a> - </div> - </div> - - <div class="panel-block" *ngIf="isLoggedIn"> - <div id="panel-make-friends" class="panel-button"> - <span class="hidden-xs glyphicon glyphicon-cloud"></span> - <a (click)='makeFriends()'>Make friends</a> - </div> - - <div id="panel-quit-friends" class="panel-button"> - <span class="hidden-xs glyphicon glyphicon-plane"></span> - <a (click)='quitFriends()'>Quit friends</a> - </div> - </div> - </menu> + <my-menu *ngIf="isInAdmin() === false"></my-menu> + <my-menu-admin *ngIf="isInAdmin() === true"></my-menu-admin> <div class="col-md-9 col-sm-8 col-xs-8 router-outlet-container"> <router-outlet></router-outlet> </div> - </div> - <footer> PeerTube, CopyLeft 2015-2016 </footer> diff --git a/client/src/app/app.component.scss b/client/src/app/app.component.scss index 1b02b2f5744965ba094f4cf0cfa55b46b789d752..95f306d759cd3d46975098378edc719668ecf809 100644 --- a/client/src/app/app.component.scss +++ b/client/src/app/app.component.scss @@ -12,40 +12,6 @@ header div { margin-bottom: 30px; } -menu { - @media screen and (max-width: 600px) { - margin-right: 3px !important; - padding: 3px !important; - min-height: 400px !important; - } - - min-height: 600px; - margin-right: 20px; - border-right: 1px solid rgba(0, 0, 0, 0.2); - - .panel-button { - margin: 8px; - cursor: pointer; - transition: margin 0.2s; - - &:hover { - margin-left: 15px; - } - - a { - color: #333333; - } - } - - .glyphicon { - margin: 5px; - } -} - -.panel-block:not(:last-child) { - border-bottom: 1px solid rgba(0, 0, 0, 0.1); -} - .router-outlet-container { @media screen and (max-width: 400px) { padding: 0 3px 0 3px; diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index b7a3d7c58c583e164ff1e62720b25750743a87d8..d6b83c684012153583120f0a0db2985889ad1181 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts @@ -1,73 +1,16 @@ import { Component } from '@angular/core'; -import { ActivatedRoute, Router, ROUTER_DIRECTIVES } from '@angular/router'; - -import { FriendService } from './friends'; -import { - AuthService, - AuthStatus, - SearchComponent, - SearchService -} from './shared'; -import { VideoService } from './videos'; +import { Router } from '@angular/router'; @Component({ selector: 'my-app', - template: require('./app.component.html'), - styles: [ require('./app.component.scss') ], - directives: [ ROUTER_DIRECTIVES, SearchComponent ], - providers: [ FriendService, VideoService, SearchService ] + templateUrl: './app.component.html', + styleUrls: [ './app.component.scss' ] }) export class AppComponent { - choices = []; - isLoggedIn: boolean; - - constructor( - private authService: AuthService, - private friendService: FriendService, - private route: ActivatedRoute, - private router: Router - ) { - this.isLoggedIn = this.authService.isLoggedIn(); - - this.authService.loginChangedSource.subscribe( - status => { - if (status === AuthStatus.LoggedIn) { - this.isLoggedIn = true; - console.log('Logged in.'); - } else if (status === AuthStatus.LoggedOut) { - this.isLoggedIn = false; - console.log('Logged out.'); - } else { - console.error('Unknown auth status: ' + status); - } - } - ); - } - - logout() { - this.authService.logout(); - } - - makeFriends() { - this.friendService.makeFriends().subscribe( - status => { - if (status === 409) { - alert('Already made friends!'); - } else { - alert('Made friends!'); - } - }, - error => alert(error) - ); - } + constructor(private router: Router) {} - quitFriends() { - this.friendService.quitFriends().subscribe( - status => { - alert('Quit friends!'); - }, - error => alert(error) - ); + isInAdmin() { + return this.router.url.indexOf('/admin/') !== -1; } } diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..980625f13495cd8a612f56d635c1b0334e9d2a7e --- /dev/null +++ b/client/src/app/app.module.ts @@ -0,0 +1,146 @@ +import { ApplicationRef, NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { HttpModule, RequestOptions, XHRBackend } from '@angular/http'; +import { RouterModule } from '@angular/router'; +import { removeNgStyles, createNewHosts } from '@angularclass/hmr'; + +import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe'; +import { ProgressbarModule } from 'ng2-bootstrap/components/progressbar'; +import { PaginationModule } from 'ng2-bootstrap/components/pagination'; +import { FileUploadModule } from 'ng2-file-upload/ng2-file-upload'; + +/* + * Platform and Environment providers/directives/pipes + */ +import { ENV_PROVIDERS } from './environment'; +import { routes } from './app.routes'; +// App is our top level component +import { AppComponent } from './app.component'; +import { AppState } from './app.service'; + +import { + AdminComponent, + FriendsComponent, + FriendAddComponent, + FriendListComponent, + FriendService, + MenuAdminComponent, + RequestsComponent, + RequestStatsComponent, + RequestService, + UsersComponent, + UserAddComponent, + UserListComponent, + UserService +} from './admin'; +import { AccountComponent, AccountService } from './account'; +import { LoginComponent } from './login'; +import { MenuComponent } from './menu.component'; +import { AuthService, AuthHttp, RestExtractor, RestService, SearchComponent, SearchService } from './shared'; +import { + LoaderComponent, + VideosComponent, + VideoAddComponent, + VideoListComponent, + VideoMiniatureComponent, + VideoSortComponent, + VideoWatchComponent, + VideoService, + WebTorrentService +} from './videos'; + +// Application wide providers +const APP_PROVIDERS = [ + AppState, + + { + provide: AuthHttp, + useFactory: (backend: XHRBackend, defaultOptions: RequestOptions, authService: AuthService) => { + return new AuthHttp(backend, defaultOptions, authService); + }, + deps: [ XHRBackend, RequestOptions, AuthService ] + }, + + AuthService, + RestExtractor, + RestService, + + VideoService, + SearchService, + FriendService, + RequestService, + UserService, + AccountService, + WebTorrentService +]; +/** + * `AppModule` is the main entry point into Angular2's bootstraping process + */ +@NgModule({ + bootstrap: [ AppComponent ], + declarations: [ + AccountComponent, + AdminComponent, + AppComponent, + BytesPipe, + FriendAddComponent, + FriendListComponent, + FriendsComponent, + LoaderComponent, + LoginComponent, + MenuAdminComponent, + MenuComponent, + RequestsComponent, + RequestStatsComponent, + SearchComponent, + UserAddComponent, + UserListComponent, + UsersComponent, + VideoAddComponent, + VideoListComponent, + VideoMiniatureComponent, + VideosComponent, + VideoSortComponent, + VideoWatchComponent, + ], + imports: [ // import Angular's modules + BrowserModule, + FormsModule, + ReactiveFormsModule, + HttpModule, + RouterModule.forRoot(routes), + + ProgressbarModule, + PaginationModule, + FileUploadModule + ], + providers: [ // expose our Services and Providers into Angular's dependency injection + ENV_PROVIDERS, + APP_PROVIDERS + ] +}) +export class AppModule { + constructor(public appRef: ApplicationRef, public appState: AppState) {} + hmrOnInit(store) { + if (!store || !store.state) return; + console.log('HMR store', store); + this.appState._state = store.state; + this.appRef.tick(); + delete store.state; + } + hmrOnDestroy(store) { + const cmpLocation = this.appRef.components.map(cmp => cmp.location.nativeElement); + // recreate elements + const state = this.appState._state; + store.state = state; + store.disposeOldHosts = createNewHosts(cmpLocation); + // remove styles + removeNgStyles(); + } + hmrAfterDestroy(store) { + // display new elements + store.disposeOldHosts(); + delete store.disposeOldHosts; + } +} diff --git a/client/src/app/app.routes.ts b/client/src/app/app.routes.ts index 59ef4ce55ceb4e6f6cc254b749e4142cae43aff4..03e2bce5128102984d063c4cc6c4bf4043cb7cef 100644 --- a/client/src/app/app.routes.ts +++ b/client/src/app/app.routes.ts @@ -1,15 +1,18 @@ -import { RouterConfig } from '@angular/router'; +import { Routes } from '@angular/router'; +import { AccountRoutes } from './account'; import { LoginRoutes } from './login'; +import { AdminRoutes } from './admin'; import { VideosRoutes } from './videos'; -export const routes: RouterConfig = [ +export const routes: Routes = [ { path: '', redirectTo: '/videos/list', pathMatch: 'full' }, - + ...AdminRoutes, + ...AccountRoutes, ...LoginRoutes, ...VideosRoutes ]; diff --git a/client/src/app/app.service.ts b/client/src/app/app.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..033c21900affe152a7221f5edd8428ec51aee103 --- /dev/null +++ b/client/src/app/app.service.ts @@ -0,0 +1,36 @@ + +import { Injectable } from '@angular/core'; + +@Injectable() +export class AppState { + _state = { }; + + constructor() { ; } + + // already return a clone of the current state + get state() { + return this._state = this._clone(this._state); + } + // never allow mutation + set state(value) { + throw new Error('do not mutate the `.state` directly'); + } + + + get(prop?: any) { + // use our state getter for the clone + const state = this.state; + return state.hasOwnProperty(prop) ? state[prop] : state; + } + + set(prop: string, value: any) { + // internally mutate our state + return this._state[prop] = value; + } + + + _clone(object) { + // simple object clone + return JSON.parse(JSON.stringify( object )); + } +} diff --git a/client/src/app/environment.ts b/client/src/app/environment.ts new file mode 100644 index 0000000000000000000000000000000000000000..8bba89c4ecb139ea78fb7e25973b8a68f1414655 --- /dev/null +++ b/client/src/app/environment.ts @@ -0,0 +1,50 @@ + +// Angular 2 +// rc2 workaround +import { enableDebugTools, disableDebugTools } from '@angular/platform-browser'; +import { enableProdMode, ApplicationRef } from '@angular/core'; +// Environment Providers +let PROVIDERS = [ + // common env directives +]; + +// Angular debug tools in the dev console +// https://github.com/angular/angular/blob/86405345b781a9dc2438c0fbe3e9409245647019/TOOLS_JS.md +let _decorateModuleRef = function identity(value) { return value; }; + +if ('production' === ENV) { + // Production + disableDebugTools(); + enableProdMode(); + + PROVIDERS = [ + ...PROVIDERS, + // custom providers in production + ]; + +} else { + + _decorateModuleRef = (modRef: any) => { + const appRef = modRef.injector.get(ApplicationRef); + const cmpRef = appRef.components[0]; + + let _ng = (<any>window).ng; + enableDebugTools(cmpRef); + (<any>window).ng.probe = _ng.probe; + (<any>window).ng.coreTokens = _ng.coreTokens; + return modRef; + }; + + // Development + PROVIDERS = [ + ...PROVIDERS, + // custom providers in development + ]; + +} + +export const decorateModuleRef = _decorateModuleRef; + +export const ENV_PROVIDERS = [ + ...PROVIDERS +]; diff --git a/client/src/app/friends/friend.service.ts b/client/src/app/friends/friend.service.ts deleted file mode 100644 index 7710464846633a0448d7586594b49c0c54062c7e..0000000000000000000000000000000000000000 --- a/client/src/app/friends/friend.service.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Injectable } from '@angular/core'; -import { Response } from '@angular/http'; -import { Observable } from 'rxjs/Observable'; - -import { AuthHttp, AuthService } from '../shared'; - -@Injectable() -export class FriendService { - private static BASE_FRIEND_URL: string = '/api/v1/pods/'; - - constructor (private authHttp: AuthHttp, private authService: AuthService) {} - - makeFriends() { - return this.authHttp.get(FriendService.BASE_FRIEND_URL + 'makefriends') - .map(res => res.status) - .catch(this.handleError); - } - - quitFriends() { - return this.authHttp.get(FriendService.BASE_FRIEND_URL + 'quitfriends') - .map(res => res.status) - .catch(this.handleError); - } - - private handleError (error: Response): Observable<number> { - console.error(error); - return Observable.throw(error.json().error || 'Server error'); - } -} diff --git a/client/src/app/index.ts b/client/src/app/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..da53f6aef4c17e3718a460f30273191ddd57b686 --- /dev/null +++ b/client/src/app/index.ts @@ -0,0 +1 @@ +export * from './app.module'; diff --git a/client/src/app/login/login.component.html b/client/src/app/login/login.component.html index 5848fcba30d55f18f8e6f83867002af296c52768..94a405405baac501c98b536a8422702e826e2e88 100644 --- a/client/src/app/login/login.component.html +++ b/client/src/app/login/login.component.html @@ -1,17 +1,16 @@ <h3>Login</h3> - <div *ngIf="error" class="alert alert-danger">{{ error }}</div> -<form role="form" (ngSubmit)="login(username.value, password.value)" #loginForm="ngForm"> +<form role="form" (ngSubmit)="login()" [formGroup]="form"> <div class="form-group"> <label for="username">Username</label> <input - type="text" class="form-control" name="username" id="username" placeholder="Username" required - ngControl="username" #username="ngForm" + type="text" class="form-control" id="username" placeholder="Username" required + formControlName="username" > - <div [hidden]="username.valid || username.pristine" class="alert alert-danger"> - Username is required + <div *ngIf="formErrors.username" class="alert alert-danger"> + {{ formErrors.username }} </div> </div> @@ -19,12 +18,12 @@ <label for="password">Password</label> <input type="password" class="form-control" name="password" id="password" placeholder="Password" required - ngControl="password" #password="ngForm" + formControlName="password" > - <div [hidden]="password.valid || password.pristine" class="alert alert-danger"> - Password is required + <div *ngIf="formErrors.password" class="alert alert-danger"> + {{ formErrors.password }} </div> </div> - <input type="submit" value="Login" class="btn btn-default" [disabled]="!loginForm.form.valid"> + <input type="submit" value="Login" class="btn btn-default" [disabled]="!form.valid"> </form> diff --git a/client/src/app/login/login.component.ts b/client/src/app/login/login.component.ts index ddd62462ebed68cbf100f00138c8f0d1e2cb6db1..c4ff7050bf1f422301180e31d1e645421844ac91 100644 --- a/client/src/app/login/login.component.ts +++ b/client/src/app/login/login.component.ts @@ -1,35 +1,67 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; -import { AuthService } from '../shared'; +import { AuthService, FormReactive } from '../shared'; @Component({ selector: 'my-login', - template: require('./login.component.html') + templateUrl: './login.component.html' }) -export class LoginComponent { +export class LoginComponent extends FormReactive implements OnInit { error: string = null; + form: FormGroup; + formErrors = { + 'username': '', + 'password': '' + }; + validationMessages = { + 'username': { + 'required': 'Username is required.', + }, + 'password': { + 'required': 'Password is required.' + } + }; + constructor( private authService: AuthService, + private formBuilder: FormBuilder, private router: Router - ) {} + ) { + super(); + } + + buildForm() { + this.form = this.formBuilder.group({ + username: [ '', Validators.required ], + password: [ '', Validators.required ], + }); + + this.form.valueChanges.subscribe(data => this.onValueChanged(data)); + } + + ngOnInit() { + this.buildForm(); + } + + login() { + this.error = null; + + const { username, password } = this.form.value; - login(username: string, password: string) { this.authService.login(username, password).subscribe( - result => { - this.error = null; + result => this.router.navigate(['/videos/list']), - this.router.navigate(['/videos/list']); - }, error => { - console.error(error); + console.error(error.json); - if (error.error === 'invalid_grant') { + if (error.json.error === 'invalid_grant') { this.error = 'Credentials are invalid.'; } else { - this.error = `${error.error}: ${error.error_description}`; + this.error = `${error.json.error}: ${error.json.error_description}`; } } ); diff --git a/client/src/app/menu.component.html b/client/src/app/menu.component.html new file mode 100644 index 0000000000000000000000000000000000000000..29ef7f9cf43b330ff36cbea87bb5c065be28e35d --- /dev/null +++ b/client/src/app/menu.component.html @@ -0,0 +1,39 @@ +<menu class="col-md-2 col-sm-3 col-xs-3"> + <div class="panel-block"> + <div id="panel-user-login" class="panel-button"> + <span *ngIf="!isLoggedIn" > + <span class="hidden-xs glyphicon glyphicon-log-in"></span> + <a [routerLink]="['/login']">Login</a> + </span> + + <span *ngIf="isLoggedIn"> + <span class="hidden-xs glyphicon glyphicon-log-out"></span> + <a *ngIf="isLoggedIn" (click)="logout()">Logout</a> + </span> + </div> + + <div *ngIf="isLoggedIn" id="panel-user-account" class="panel-button"> + <span class="hidden-xs glyphicon glyphicon-user"></span> + <a [routerLink]="['/account']">My account</a> + </div> + </div> + + <div class="panel-block"> + <div id="panel-get-videos" class="panel-button"> + <span class="hidden-xs glyphicon glyphicon-list"></span> + <a [routerLink]="['/videos/list']">Get videos</a> + </div> + + <div id="panel-upload-video" class="panel-button" *ngIf="isLoggedIn"> + <span class="hidden-xs glyphicon glyphicon-cloud-upload"></span> + <a [routerLink]="['/videos/add']">Upload a video</a> + </div> + </div> + + <div class="panel-block" *ngIf="isUserAdmin()"> + <div id="panel-get-videos" class="panel-button"> + <span class="hidden-xs glyphicon glyphicon-cog"></span> + <a [routerLink]="['/admin']">Administration</a> + </div> + </div> +</menu> diff --git a/client/src/app/menu.component.ts b/client/src/app/menu.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..6cfc854dde757801c4fe165a0305c56e4edc391b --- /dev/null +++ b/client/src/app/menu.component.ts @@ -0,0 +1,45 @@ +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; + +import { AuthService, AuthStatus } from './shared'; + +@Component({ + selector: 'my-menu', + templateUrl: './menu.component.html' +}) +export class MenuComponent implements OnInit { + isLoggedIn: boolean; + + constructor ( + private authService: AuthService, + private router: Router + ) {} + + ngOnInit() { + this.isLoggedIn = this.authService.isLoggedIn(); + + this.authService.loginChangedSource.subscribe( + status => { + if (status === AuthStatus.LoggedIn) { + this.isLoggedIn = true; + console.log('Logged in.'); + } else if (status === AuthStatus.LoggedOut) { + this.isLoggedIn = false; + console.log('Logged out.'); + } else { + console.error('Unknown auth status: ' + status); + } + } + ); + } + + isUserAdmin() { + return this.authService.isAdmin(); + } + + logout() { + this.authService.logout(); + // Redirect to home page + this.router.navigate(['/videos/list']); + } +} diff --git a/client/src/app/shared/auth/auth-http.service.ts b/client/src/app/shared/auth/auth-http.service.ts index 9c7ef4389d7395349fd73c3bcc5bb006baf69b93..2392898cae43521a56bcb348af5ea84d5c09ec09 100644 --- a/client/src/app/shared/auth/auth-http.service.ts +++ b/client/src/app/shared/auth/auth-http.service.ts @@ -28,7 +28,7 @@ export class AuthHttp extends Http { return super.request(url, options) .catch((err) => { if (err.status === 401) { - return this.handleTokenExpired(err, url, options); + return this.handleTokenExpired(url, options); } return Observable.throw(err); @@ -49,26 +49,29 @@ export class AuthHttp extends Http { return this.request(url, options); } - post(url: string, options?: RequestOptionsArgs): Observable<Response> { + post(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> { if (!options) options = {}; options.method = RequestMethod.Post; + options.body = body; return this.request(url, options); } - put(url: string, options?: RequestOptionsArgs): Observable<Response> { + put(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> { if (!options) options = {}; options.method = RequestMethod.Put; + options.body = body; return this.request(url, options); } - private handleTokenExpired(err: Response, url: string | Request, options: RequestOptionsArgs) { - return this.authService.refreshAccessToken().flatMap(() => { - this.setAuthorizationHeader(options.headers); + private handleTokenExpired(url: string | Request, options: RequestOptionsArgs) { + return this.authService.refreshAccessToken() + .flatMap(() => { + this.setAuthorizationHeader(options.headers); - return super.request(url, options); - }); + return super.request(url, options); + }); } private setAuthorizationHeader(headers: Headers) { diff --git a/client/src/app/shared/auth/user.model.ts b/client/src/app/shared/auth/auth-user.model.ts similarity index 72% rename from client/src/app/shared/auth/user.model.ts rename to client/src/app/shared/auth/auth-user.model.ts index 98852f8355c834dfda754ef45591afa07365038b..bdd5ea5a9e30df298a0a80319278cd9e6e59f0e4 100644 --- a/client/src/app/shared/auth/user.model.ts +++ b/client/src/app/shared/auth/auth-user.model.ts @@ -1,15 +1,28 @@ -export class User { +import { User } from '../users'; + +export class AuthUser extends User { private static KEYS = { + ID: 'id', + ROLE: 'role', USERNAME: 'username' }; + id: string; + role: string; username: string; tokens: Tokens; static load() { const usernameLocalStorage = localStorage.getItem(this.KEYS.USERNAME); if (usernameLocalStorage) { - return new User(localStorage.getItem(this.KEYS.USERNAME), Tokens.load()); + return new AuthUser( + { + id: localStorage.getItem(this.KEYS.ID), + username: localStorage.getItem(this.KEYS.USERNAME), + role: localStorage.getItem(this.KEYS.ROLE) + }, + Tokens.load() + ); } return null; @@ -17,12 +30,14 @@ export class User { static flush() { localStorage.removeItem(this.KEYS.USERNAME); + localStorage.removeItem(this.KEYS.ID); + localStorage.removeItem(this.KEYS.ROLE); Tokens.flush(); } - constructor(username: string, hash_tokens: any) { - this.username = username; - this.tokens = new Tokens(hash_tokens); + constructor(userHash: { id: string, username: string, role: string }, hashTokens: any) { + super(userHash); + this.tokens = new Tokens(hashTokens); } getAccessToken() { @@ -43,12 +58,14 @@ export class User { } save() { - localStorage.setItem('username', this.username); + localStorage.setItem(AuthUser.KEYS.ID, this.id); + localStorage.setItem(AuthUser.KEYS.USERNAME, this.username); + localStorage.setItem(AuthUser.KEYS.ROLE, this.role); this.tokens.save(); } } -// Private class used only by User +// Private class only used by User class Tokens { private static KEYS = { ACCESS_TOKEN: 'access_token', diff --git a/client/src/app/shared/auth/auth.service.ts b/client/src/app/shared/auth/auth.service.ts index 584298fff585605b69f11dc9ebb9ad85273a12b7..a30c79c8685edafb59aa2bbdc6ba2d64afd17789 100644 --- a/client/src/app/shared/auth/auth.service.ts +++ b/client/src/app/shared/auth/auth.service.ts @@ -1,32 +1,39 @@ import { Injectable } from '@angular/core'; import { Headers, Http, Response, URLSearchParams } from '@angular/http'; +import { Router } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import { Subject } from 'rxjs/Subject'; import { AuthStatus } from './auth-status.model'; -import { User } from './user.model'; +import { AuthUser } from './auth-user.model'; +import { RestExtractor } from '../rest'; @Injectable() export class AuthService { - private static BASE_CLIENT_URL = '/api/v1/users/client'; + private static BASE_CLIENT_URL = '/api/v1/clients/local'; private static BASE_TOKEN_URL = '/api/v1/users/token'; + private static BASE_USER_INFORMATIONS_URL = '/api/v1/users/me'; loginChangedSource: Observable<AuthStatus>; private clientId: string; private clientSecret: string; private loginChanged: Subject<AuthStatus>; - private user: User = null; + private user: AuthUser = null; - constructor(private http: Http) { + constructor( + private http: Http, + private restExtractor: RestExtractor, + private router: Router + ) { this.loginChanged = new Subject<AuthStatus>(); this.loginChangedSource = this.loginChanged.asObservable(); // Fetch the client_id/client_secret // FIXME: save in local storage? this.http.get(AuthService.BASE_CLIENT_URL) - .map(res => res.json()) - .catch(this.handleError) + .map(this.restExtractor.extractDataGet) + .catch((res) => this.restExtractor.handleError(res)) .subscribe( result => { this.clientId = result.client_id; @@ -34,12 +41,15 @@ export class AuthService { console.log('Client credentials loaded.'); }, error => { - alert(error); + alert( + `Cannot retrieve OAuth Client credentials: ${error.text}. \n` + + 'Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.' + ); } ); // Return null if there is nothing to load - this.user = User.load(); + this.user = AuthUser.load(); } getRefreshToken() { @@ -64,10 +74,16 @@ export class AuthService { return this.user.getTokenType(); } - getUser(): User { + getUser(): AuthUser { return this.user; } + isAdmin() { + if (this.user === null) return false; + + return this.user.isAdmin(); + } + isLoggedIn() { if (this.getAccessToken()) { return true; @@ -94,21 +110,23 @@ export class AuthService { }; return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options) - .map(res => res.json()) + .map(this.restExtractor.extractDataGet) .map(res => { res.username = username; return res; }) + .flatMap(res => this.fetchUserInformations(res)) .map(res => this.handleLogin(res)) - .catch(this.handleError); + .catch((res) => this.restExtractor.handleError(res)); } logout() { // TODO: make an HTTP request to revoke the tokens this.user = null; - User.flush(); - this.setStatus(AuthStatus.LoggedIn); + AuthUser.flush(); + + this.setStatus(AuthStatus.LoggedOut); } refreshAccessToken() { @@ -131,36 +149,64 @@ export class AuthService { }; return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options) - .map(res => res.json()) + .map(this.restExtractor.extractDataGet) .map(res => this.handleRefreshToken(res)) - .catch(this.handleError); + .catch((res: Response) => { + // The refresh token is invalid? + if (res.status === 400 && res.json() && res.json().error === 'invalid_grant') { + console.error('Cannot refresh token -> logout...'); + this.logout(); + this.router.navigate(['/login']); + + return Observable.throw({ + json: '', + text: 'You need to reconnect.' + }); + } + + return this.restExtractor.handleError(res); + }); } - private setStatus(status: AuthStatus) { - this.loginChanged.next(status); + private fetchUserInformations (obj: any) { + // Do not call authHttp here to avoid circular dependencies headaches + + const headers = new Headers(); + headers.set('Authorization', `Bearer ${obj.access_token}`); + + return this.http.get(AuthService.BASE_USER_INFORMATIONS_URL, { headers }) + .map(res => res.json()) + .map(res => { + obj.id = res.id; + obj.role = res.role; + return obj; + } + ); } private handleLogin (obj: any) { + const id = obj.id; const username = obj.username; - const hash_tokens = { + const role = obj.role; + const hashTokens = { access_token: obj.access_token, token_type: obj.token_type, refresh_token: obj.refresh_token }; - this.user = new User(username, hash_tokens); + this.user = new AuthUser({ id, username, role }, hashTokens); this.user.save(); this.setStatus(AuthStatus.LoggedIn); } - private handleError (error: Response) { - console.error(error); - return Observable.throw(error.json() || { error: 'Server error' }); - } - private handleRefreshToken (obj: any) { this.user.refreshTokens(obj.access_token, obj.refresh_token); this.user.save(); } + + private setStatus(status: AuthStatus) { + this.loginChanged.next(status); + } + } diff --git a/client/src/app/shared/auth/index.ts b/client/src/app/shared/auth/index.ts index aafaacbf1ff35296c279d8c3e970ef6aee2866c8..ebd9e14cd48e20895b2361dddccab49d40213154 100644 --- a/client/src/app/shared/auth/index.ts +++ b/client/src/app/shared/auth/index.ts @@ -1,4 +1,4 @@ export * from './auth-http.service'; export * from './auth-status.model'; export * from './auth.service'; -export * from './user.model'; +export * from './auth-user.model'; diff --git a/client/src/app/shared/forms/form-reactive.ts b/client/src/app/shared/forms/form-reactive.ts new file mode 100644 index 0000000000000000000000000000000000000000..1e8a69771c5ffe37d563a665cb8fa026e6c38185 --- /dev/null +++ b/client/src/app/shared/forms/form-reactive.ts @@ -0,0 +1,24 @@ +import { FormGroup } from '@angular/forms'; + +export abstract class FormReactive { + abstract form: FormGroup; + abstract formErrors: Object; + abstract validationMessages: Object; + + abstract buildForm(): void; + + protected onValueChanged(data?: any) { + for (const field in this.formErrors) { + // clear previous error message (if any) + this.formErrors[field] = ''; + const control = this.form.get(field); + + if (control && control.dirty && !control.valid) { + const messages = this.validationMessages[field]; + for (const key in control.errors) { + this.formErrors[field] += messages[key] + ' '; + } + } + } + } +} diff --git a/client/src/app/shared/forms/form-validators/index.ts b/client/src/app/shared/forms/form-validators/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..1d2ae6f689756265e40684d7d5fbd237abc88ea2 --- /dev/null +++ b/client/src/app/shared/forms/form-validators/index.ts @@ -0,0 +1,3 @@ +export * from './url.validator'; +export * from './user'; +export * from './video'; diff --git a/client/src/app/shared/forms/form-validators/url.validator.ts b/client/src/app/shared/forms/form-validators/url.validator.ts new file mode 100644 index 0000000000000000000000000000000000000000..67163b4e972cec7e9bc37feb49f6d3dd9bcd89e7 --- /dev/null +++ b/client/src/app/shared/forms/form-validators/url.validator.ts @@ -0,0 +1,11 @@ +import { FormControl } from '@angular/forms'; + +export function validateUrl(c: FormControl) { + let URL_REGEXP = new RegExp('^https?://(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$'); + + return URL_REGEXP.test(c.value) ? null : { + validateUrl: { + valid: false + } + }; +} diff --git a/client/src/app/shared/forms/form-validators/user.ts b/client/src/app/shared/forms/form-validators/user.ts new file mode 100644 index 0000000000000000000000000000000000000000..5b11ff265cb195e8c6cf3a08713f24b3da092636 --- /dev/null +++ b/client/src/app/shared/forms/form-validators/user.ts @@ -0,0 +1,17 @@ +import { Validators } from '@angular/forms'; + +export const USER_USERNAME = { + VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(20) ], + MESSAGES: { + 'required': 'Username is required.', + 'minlength': 'Username must be at least 3 characters long.', + 'maxlength': 'Username cannot be more than 20 characters long.' + } +}; +export const USER_PASSWORD = { + VALIDATORS: [ Validators.required, Validators.minLength(6) ], + MESSAGES: { + 'required': 'Password is required.', + 'minlength': 'Password must be at least 6 characters long.', + } +}; diff --git a/client/src/app/shared/forms/form-validators/video.ts b/client/src/app/shared/forms/form-validators/video.ts new file mode 100644 index 0000000000000000000000000000000000000000..3766d4018b4df670a4a2541139c41836b03a9d66 --- /dev/null +++ b/client/src/app/shared/forms/form-validators/video.ts @@ -0,0 +1,25 @@ +import { Validators } from '@angular/forms'; + +export const VIDEO_NAME = { + VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(50) ], + MESSAGES: { + 'required': 'Video name is required.', + 'minlength': 'Video name must be at least 3 characters long.', + 'maxlength': 'Video name cannot be more than 50 characters long.' + } +}; +export const VIDEO_DESCRIPTION = { + VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(250) ], + MESSAGES: { + 'required': 'Video description is required.', + 'minlength': 'Video description must be at least 3 characters long.', + 'maxlength': 'Video description cannot be more than 250 characters long.' + } +}; + +export const VIDEO_TAGS = { + VALIDATORS: [ Validators.pattern('^[a-zA-Z0-9]{2,10}$') ], + MESSAGES: { + 'pattern': 'A tag should be between 2 and 10 alphanumeric characters long.' + } +}; diff --git a/client/src/app/shared/forms/index.ts b/client/src/app/shared/forms/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..588ebb4be4f97b707f788f2996ac2e2cf88252b3 --- /dev/null +++ b/client/src/app/shared/forms/index.ts @@ -0,0 +1,2 @@ +export * from './form-validators'; +export * from './form-reactive'; diff --git a/client/src/app/shared/index.ts b/client/src/app/shared/index.ts index dfea4c67cdb959ed561a9dfc2d01774d0e1efca2..af34b4b64da57cad4e724a699e887ef7c7257d1b 100644 --- a/client/src/app/shared/index.ts +++ b/client/src/app/shared/index.ts @@ -1,2 +1,5 @@ export * from './auth'; +export * from './forms'; +export * from './rest'; export * from './search'; +export * from './users'; diff --git a/client/src/app/shared/rest/index.ts b/client/src/app/shared/rest/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..3c9509dc7be6a59f95e2f989f69052caa5724d1a --- /dev/null +++ b/client/src/app/shared/rest/index.ts @@ -0,0 +1,3 @@ +export * from './rest-extractor.service'; +export * from './rest-pagination'; +export * from './rest.service'; diff --git a/client/src/app/shared/rest/rest-extractor.service.ts b/client/src/app/shared/rest/rest-extractor.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..fcb1598f4393385a78521c7e8225cd83c73e5abc --- /dev/null +++ b/client/src/app/shared/rest/rest-extractor.service.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@angular/core'; +import { Response } from '@angular/http'; +import { Observable } from 'rxjs/Observable'; + +export interface ResultList { + data: any[]; + total: number; +} + +@Injectable() +export class RestExtractor { + + constructor () { ; } + + extractDataBool(res: Response) { + return true; + } + + extractDataList(res: Response) { + const body = res.json(); + + const ret: ResultList = { + data: body.data, + total: body.total + }; + + return ret; + } + + extractDataGet(res: Response) { + return res.json(); + } + + handleError(res: Response) { + let text = 'Server error: '; + text += res.text(); + let json = ''; + + try { + json = res.json(); + } catch (err) { ; } + + const error = { + json, + text + }; + + console.error(error); + + return Observable.throw(error); + } +} diff --git a/client/src/app/videos/shared/pagination.model.ts b/client/src/app/shared/rest/rest-pagination.ts similarity index 65% rename from client/src/app/videos/shared/pagination.model.ts rename to client/src/app/shared/rest/rest-pagination.ts index eda44ebfbae5a378bb56af33e2b2d8deee926953..0cfa4f4681aee7bcb4102d057d9da20f203e7c6e 100644 --- a/client/src/app/videos/shared/pagination.model.ts +++ b/client/src/app/shared/rest/rest-pagination.ts @@ -1,5 +1,5 @@ -export interface Pagination { +export interface RestPagination { currentPage: number; itemsPerPage: number; totalItems: number; -} +}; diff --git a/client/src/app/shared/rest/rest.service.ts b/client/src/app/shared/rest/rest.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..16b47e9574d771d2b55dc7dee89033f24582bcb7 --- /dev/null +++ b/client/src/app/shared/rest/rest.service.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@angular/core'; +import { URLSearchParams } from '@angular/http'; + +import { RestPagination } from './rest-pagination'; + +@Injectable() +export class RestService { + + buildRestGetParams(pagination?: RestPagination, sort?: string) { + const params = new URLSearchParams(); + + if (pagination) { + const start: number = (pagination.currentPage - 1) * pagination.itemsPerPage; + const count: number = pagination.itemsPerPage; + + params.set('start', start.toString()); + params.set('count', count.toString()); + } + + if (sort) { + params.set('sort', sort); + } + + return params; + } + +} diff --git a/client/src/app/shared/search/search.component.ts b/client/src/app/shared/search/search.component.ts index e864fbc172350776b38ae8784cae22e4959721e3..b6237469b0efd2ead2440c0350abc77d1787be9c 100644 --- a/client/src/app/shared/search/search.component.ts +++ b/client/src/app/shared/search/search.component.ts @@ -1,15 +1,13 @@ import { Component, OnInit } from '@angular/core'; - -import { DROPDOWN_DIRECTIVES} from 'ng2-bootstrap/components/dropdown'; +import { Router } from '@angular/router'; import { Search } from './search.model'; import { SearchField } from './search-field.type'; import { SearchService } from './search.service'; @Component({ - selector: 'my-search', - template: require('./search.component.html'), - directives: [ DROPDOWN_DIRECTIVES ] + selector: 'my-search', + templateUrl: './search.component.html' }) export class SearchComponent implements OnInit { @@ -25,10 +23,10 @@ export class SearchComponent implements OnInit { value: '' }; - constructor(private searchService: SearchService) {} + constructor(private searchService: SearchService, private router: Router) {} ngOnInit() { - // Subscribe is the search changed + // Subscribe if the search changed // Usually changed by videos list component this.searchService.updateSearch.subscribe( newSearchCriterias => { @@ -58,6 +56,10 @@ export class SearchComponent implements OnInit { } doSearch() { + if (this.router.url.indexOf('/videos/list') === -1) { + this.router.navigate([ '/videos/list' ]); + } + this.searchService.searchUpdated.next(this.searchCriterias); } diff --git a/client/src/app/shared/search/search.service.ts b/client/src/app/shared/search/search.service.ts index c7993db3dd05df9ba299dbb372e3300a0d398f42..717a7fa500e044fecc9bdce2d0aa0440196220eb 100644 --- a/client/src/app/shared/search/search.service.ts +++ b/client/src/app/shared/search/search.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@angular/core'; import { Subject } from 'rxjs/Subject'; +import { ReplaySubject } from 'rxjs/ReplaySubject'; import { Search } from './search.model'; @@ -12,6 +13,6 @@ export class SearchService { constructor() { this.updateSearch = new Subject<Search>(); - this.searchUpdated = new Subject<Search>(); + this.searchUpdated = new ReplaySubject<Search>(1); } } diff --git a/client/src/app/shared/users/index.ts b/client/src/app/shared/users/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..5a670ce8f9c25e52f39be4335866407fadd8bda2 --- /dev/null +++ b/client/src/app/shared/users/index.ts @@ -0,0 +1 @@ +export * from './user.model'; diff --git a/client/src/app/shared/users/user.model.ts b/client/src/app/shared/users/user.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..726495d1182721d095bcca3e11ee72da5b782cb5 --- /dev/null +++ b/client/src/app/shared/users/user.model.ts @@ -0,0 +1,20 @@ +export class User { + id: string; + username: string; + role: string; + createdDate: Date; + + constructor(hash: { id: string, username: string, role: string, createdDate?: Date }) { + this.id = hash.id; + this.username = hash.username; + this.role = hash.role; + + if (hash.createdDate) { + this.createdDate = hash.createdDate; + } + } + + isAdmin() { + return this.role === 'admin'; + } +} diff --git a/client/src/app/videos/shared/index.ts b/client/src/app/videos/shared/index.ts index a54120f5d5d2c69a8a1e8f7a4523e138bf6e1e66..67d16ead155876bf31f47d54dc653fc3faa3cab6 100644 --- a/client/src/app/videos/shared/index.ts +++ b/client/src/app/videos/shared/index.ts @@ -1,5 +1,4 @@ export * from './loader'; -export * from './pagination.model'; export * from './sort-field.type'; export * from './video.model'; export * from './video.service'; diff --git a/client/src/app/videos/shared/loader/loader.component.ts b/client/src/app/videos/shared/loader/loader.component.ts index cdd07d1b4e6a565b7607ff6d9939ef4e97332d02..e72d2f3f3d605bd0d5f0fcdb87f3a41e976229f9 100644 --- a/client/src/app/videos/shared/loader/loader.component.ts +++ b/client/src/app/videos/shared/loader/loader.component.ts @@ -2,8 +2,8 @@ import { Component, Input } from '@angular/core'; @Component({ selector: 'my-loader', - styles: [ require('./loader.component.scss') ], - template: require('./loader.component.html') + styleUrls: [ './loader.component.scss' ], + templateUrl: './loader.component.html' }) export class LoaderComponent { diff --git a/client/src/app/videos/shared/video.service.ts b/client/src/app/videos/shared/video.service.ts index b4396f76794abe1eebc2265e66959f3f6ce9f08d..ad855753344a2f289a2713bdda9f46c8b5b62062 100644 --- a/client/src/app/videos/shared/video.service.ts +++ b/client/src/app/videos/shared/video.service.ts @@ -1,11 +1,10 @@ import { Injectable } from '@angular/core'; -import { Http, Response, URLSearchParams } from '@angular/http'; +import { Http } from '@angular/http'; import { Observable } from 'rxjs/Observable'; -import { Pagination } from './pagination.model'; import { Search } from '../../shared'; import { SortField } from './sort-field.type'; -import { AuthHttp, AuthService } from '../../shared'; +import { AuthHttp, AuthService, RestExtractor, RestPagination, RestService, ResultList } from '../../shared'; import { Video } from './video.model'; @Injectable() @@ -15,68 +14,51 @@ export class VideoService { constructor( private authService: AuthService, private authHttp: AuthHttp, - private http: Http + private http: Http, + private restExtractor: RestExtractor, + private restService: RestService ) {} - getVideo(id: string) { + getVideo(id: string): Observable<Video> { return this.http.get(VideoService.BASE_VIDEO_URL + id) - .map(res => <Video> res.json()) - .catch(this.handleError); + .map(this.restExtractor.extractDataGet) + .catch((res) => this.restExtractor.handleError(res)); } - getVideos(pagination: Pagination, sort: SortField) { - const params = this.createPaginationParams(pagination); - - if (sort) params.set('sort', sort); + getVideos(pagination: RestPagination, sort: SortField) { + const params = this.restService.buildRestGetParams(pagination, sort); return this.http.get(VideoService.BASE_VIDEO_URL, { search: params }) .map(res => res.json()) .map(this.extractVideos) - .catch(this.handleError); + .catch((res) => this.restExtractor.handleError(res)); } removeVideo(id: string) { return this.authHttp.delete(VideoService.BASE_VIDEO_URL + id) - .map(res => <number> res.status) - .catch(this.handleError); + .map(this.restExtractor.extractDataBool) + .catch((res) => this.restExtractor.handleError(res)); } - searchVideos(search: Search, pagination: Pagination, sort: SortField) { - const params = this.createPaginationParams(pagination); + searchVideos(search: Search, pagination: RestPagination, sort: SortField) { + const params = this.restService.buildRestGetParams(pagination, sort); if (search.field) params.set('field', search.field); - if (sort) params.set('sort', sort); return this.http.get(VideoService.BASE_VIDEO_URL + 'search/' + encodeURIComponent(search.value), { search: params }) - .map(res => res.json()) + .map(this.restExtractor.extractDataList) .map(this.extractVideos) - .catch(this.handleError); - } - - private createPaginationParams(pagination: Pagination) { - const params = new URLSearchParams(); - const start: number = (pagination.currentPage - 1) * pagination.itemsPerPage; - const count: number = pagination.itemsPerPage; - - params.set('start', start.toString()); - params.set('count', count.toString()); - - return params; + .catch((res) => this.restExtractor.handleError(res)); } - private extractVideos(body: any) { - const videos_json = body.data; - const totalVideos = body.total; + private extractVideos(result: ResultList) { + const videosJson = result.data; + const totalVideos = result.total; const videos = []; - for (const video_json of videos_json) { - videos.push(new Video(video_json)); + for (const videoJson of videosJson) { + videos.push(new Video(videoJson)); } return { videos, totalVideos }; } - - private handleError(error: Response) { - console.error(error); - return Observable.throw(error.json().error || 'Server error'); - } } diff --git a/client/src/app/videos/video-add/video-add.component.html b/client/src/app/videos/video-add/video-add.component.html index bcd78c7cb5dc96a1e47adddd3a1281e335267cb8..64320cae7dc5885ef7a9831ba34a54c2beed2ace 100644 --- a/client/src/app/videos/video-add/video-add.component.html +++ b/client/src/app/videos/video-add/video-add.component.html @@ -2,31 +2,31 @@ <div *ngIf="error" class="alert alert-danger">{{ error }}</div> -<form novalidate (ngSubmit)="upload()" [ngFormModel]="videoForm"> +<form novalidate (ngSubmit)="upload()" [formGroup]="form"> <div class="form-group"> <label for="name">Name</label> <input - type="text" class="form-control" name="name" id="name" - ngControl="name" #name="ngForm" [(ngModel)]="video.name" + type="text" class="form-control" id="name" + formControlName="name" > - <div [hidden]="name.valid || name.pristine" class="alert alert-warning"> - A name is required and should be between 3 and 50 characters long + <div *ngIf="formErrors.name" class="alert alert-danger"> + {{ formErrors.name }} </div> </div> <div class="form-group"> <label for="tags">Tags</label> <input - type="text" class="form-control" name="tags" id="tags" - ngControl="tags" #tags="ngForm" [disabled]="isTagsInputDisabled" (keyup)="onTagKeyPress($event)" [(ngModel)]="currentTag" + type="text" class="form-control" id="currentTag" + formControlName="currentTag" (keyup)="onTagKeyPress($event)" > - <div [hidden]="tags.valid || tags.pristine" class="alert alert-warning"> - A tag should be between 2 and 10 characters (alphanumeric) long + <div *ngIf="formErrors.currentTag" class="alert alert-danger"> + {{ formErrors.currentTag }} </div> </div> <div class="tags"> - <div class="label label-primary tag" *ngFor="let tag of video.tags"> + <div class="label label-primary tag" *ngFor="let tag of tags"> {{ tag }} <span class="remove" (click)="removeTag(tag)">x</span> </div> @@ -53,12 +53,12 @@ <div class="form-group"> <label for="description">Description</label> <textarea - name="description" id="description" class="form-control" placeholder="Description..." - ngControl="description" #description="ngForm" [(ngModel)]="video.description" + id="description" class="form-control" placeholder="Description..." + formControlName="description" > </textarea> - <div [hidden]="description.valid || description.pristine" class="alert alert-warning"> - A description is required and should be between 3 and 250 characters long + <div *ngIf="formErrors.description" class="alert alert-danger"> + {{ formErrors.description }} </div> </div> @@ -69,7 +69,7 @@ <div class="form-group"> <input type="submit" value="Upload" class="btn btn-default form-control" [title]="getInvalidFieldsTitle()" - [disabled]="!videoForm.valid || video.tags.length === 0 || filename === null" + [disabled]="!form.valid || tags.length === 0 || filename === null" > </div> </form> diff --git a/client/src/app/videos/video-add/video-add.component.ts b/client/src/app/videos/video-add/video-add.component.ts index c0f8cb9c437921f63bdbb22e8b409bb9dd18ce1d..d12a7d572481365dc311818bde864f91426d7a05 100644 --- a/client/src/app/videos/video-add/video-add.component.ts +++ b/client/src/app/videos/video-add/video-add.component.ts @@ -1,37 +1,42 @@ -import { Control, ControlGroup, Validators } from '@angular/common'; import { Component, ElementRef, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; import { Router } from '@angular/router'; -import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe'; -import { PROGRESSBAR_DIRECTIVES } from 'ng2-bootstrap/components/progressbar'; -import { FileSelectDirective, FileUploader } from 'ng2-file-upload/ng2-file-upload'; +import { FileUploader } from 'ng2-file-upload/ng2-file-upload'; -import { AuthService } from '../../shared'; +import { AuthService, FormReactive, VIDEO_NAME, VIDEO_DESCRIPTION, VIDEO_TAGS } from '../../shared'; @Component({ selector: 'my-videos-add', - styles: [ require('./video-add.component.scss') ], - template: require('./video-add.component.html'), - directives: [ FileSelectDirective, PROGRESSBAR_DIRECTIVES ], - pipes: [ BytesPipe ] + styleUrls: [ './video-add.component.scss' ], + templateUrl: './video-add.component.html' }) -export class VideoAddComponent implements OnInit { - currentTag: string; // Tag the user is writing in the input - error: string = null; - videoForm: ControlGroup; +export class VideoAddComponent extends FormReactive implements OnInit { + tags: string[] = []; uploader: FileUploader; - video = { + + error: string = null; + form: FormGroup; + formErrors = { name: '', - tags: [], - description: '' + description: '', + currentTag: '' + }; + validationMessages = { + name: VIDEO_NAME.MESSAGES, + description: VIDEO_DESCRIPTION.MESSAGES, + currentTag: VIDEO_TAGS.MESSAGES }; constructor( private authService: AuthService, private elementRef: ElementRef, + private formBuilder: FormBuilder, private router: Router - ) {} + ) { + super(); + } get filename() { if (this.uploader.queue.length === 0) { @@ -41,20 +46,26 @@ export class VideoAddComponent implements OnInit { return this.uploader.queue[0].file.name; } - get isTagsInputDisabled () { - return this.video.tags.length >= 3; + buildForm() { + this.form = this.formBuilder.group({ + name: [ '', VIDEO_NAME.VALIDATORS ], + description: [ '', VIDEO_DESCRIPTION.VALIDATORS ], + currentTag: [ '', VIDEO_TAGS.VALIDATORS ] + }); + + this.form.valueChanges.subscribe(data => this.onValueChanged(data)); } getInvalidFieldsTitle() { let title = ''; - const nameControl = this.videoForm.controls['name']; - const descriptionControl = this.videoForm.controls['description']; + const nameControl = this.form.controls['name']; + const descriptionControl = this.form.controls['description']; if (!nameControl.valid) { title += 'A name is required\n'; } - if (this.video.tags.length === 0) { + if (this.tags.length === 0) { title += 'At least one tag is required\n'; } @@ -70,13 +81,6 @@ export class VideoAddComponent implements OnInit { } ngOnInit() { - this.videoForm = new ControlGroup({ - name: new Control('', Validators.compose([ Validators.required, Validators.minLength(3), Validators.maxLength(50) ])), - description: new Control('', Validators.compose([ Validators.required, Validators.minLength(3), Validators.maxLength(250) ])), - tags: new Control('', Validators.pattern('^[a-zA-Z0-9]{2,10}$')) - }); - - this.uploader = new FileUploader({ authToken: this.authService.getRequestHeaderValue(), queueLimit: 1, @@ -85,26 +89,37 @@ export class VideoAddComponent implements OnInit { }); this.uploader.onBuildItemForm = (item, form) => { - form.append('name', this.video.name); - form.append('description', this.video.description); + const name = this.form.value['name']; + const description = this.form.value['description']; + + form.append('name', name); + form.append('description', description); - for (let i = 0; i < this.video.tags.length; i++) { - form.append(`tags[${i}]`, this.video.tags[i]); + for (let i = 0; i < this.tags.length; i++) { + form.append(`tags[${i}]`, this.tags[i]); } }; + + this.buildForm(); } onTagKeyPress(event: KeyboardEvent) { + const currentTag = this.form.value['currentTag']; + // Enter press if (event.keyCode === 13) { // Check if the tag is valid and does not already exist if ( - this.currentTag !== '' && - this.videoForm.controls['tags'].valid && - this.video.tags.indexOf(this.currentTag) === -1 + currentTag !== '' && + this.form.controls['currentTag'].valid && + this.tags.indexOf(currentTag) === -1 ) { - this.video.tags.push(this.currentTag); - this.currentTag = ''; + this.tags.push(currentTag); + this.form.patchValue({ currentTag: '' }); + + if (this.tags.length >= 3) { + this.form.get('currentTag').disable(); + } } } } @@ -114,7 +129,7 @@ export class VideoAddComponent implements OnInit { } removeTag(tag: string) { - this.video.tags.splice(this.video.tags.indexOf(tag), 1); + this.tags.splice(this.tags.indexOf(tag), 1); } upload() { diff --git a/client/src/app/videos/video-list/video-list.component.ts b/client/src/app/videos/video-list/video-list.component.ts index 5691d684eee9cb5ab6ebaff98f5810d7ab150893..6b086e93880b34cee8c2fba58f4878406b215d7a 100644 --- a/client/src/app/videos/video-list/video-list.component.ts +++ b/client/src/app/videos/video-list/video-list.component.ts @@ -1,39 +1,30 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; -import { AsyncPipe } from '@angular/common'; -import { ActivatedRoute, Router, ROUTER_DIRECTIVES } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { BehaviorSubject } from 'rxjs/BehaviorSubject'; -import { PAGINATION_DIRECTIVES } from 'ng2-bootstrap/components/pagination'; - import { - LoaderComponent, - Pagination, SortField, Video, VideoService } from '../shared'; -import { AuthService, Search, SearchField, User } from '../../shared'; -import { VideoMiniatureComponent } from './video-miniature.component'; -import { VideoSortComponent } from './video-sort.component'; +import { AuthService, AuthUser, RestPagination, Search, SearchField } from '../../shared'; import { SearchService } from '../../shared'; @Component({ selector: 'my-videos-list', - styles: [ require('./video-list.component.scss') ], - pipes: [ AsyncPipe ], - template: require('./video-list.component.html'), - directives: [ LoaderComponent, PAGINATION_DIRECTIVES, ROUTER_DIRECTIVES, VideoMiniatureComponent, VideoSortComponent ] + styleUrls: [ './video-list.component.scss' ], + templateUrl: './video-list.component.html' }) export class VideoListComponent implements OnInit, OnDestroy { loading: BehaviorSubject<boolean> = new BehaviorSubject(false); - pagination: Pagination = { + pagination: RestPagination = { currentPage: 1, itemsPerPage: 9, totalItems: null }; sort: SortField; - user: User = null; + user: AuthUser = null; videos: Video[] = []; private search: Search; @@ -51,7 +42,7 @@ export class VideoListComponent implements OnInit, OnDestroy { ngOnInit() { if (this.authService.isLoggedIn()) { - this.user = User.load(); + this.user = AuthUser.load(); } // Subscribe to route changes @@ -66,6 +57,8 @@ export class VideoListComponent implements OnInit, OnDestroy { // Subscribe to search changes this.subSearch = this.searchService.searchUpdated.subscribe(search => { this.search = search; + // Reset pagination + this.pagination.currentPage = 1; this.navigateToNewParams(); }); @@ -76,7 +69,7 @@ export class VideoListComponent implements OnInit, OnDestroy { this.subSearch.unsubscribe(); } - getVideos(detectChanges = true) { + getVideos() { this.loading.next(true); this.videos = []; @@ -97,7 +90,7 @@ export class VideoListComponent implements OnInit, OnDestroy { this.loading.next(false); }, - error => alert(error) + error => alert(error.text) ); } @@ -153,7 +146,11 @@ export class VideoListComponent implements OnInit, OnDestroy { this.sort = <SortField>routeParams['sort'] || '-createdDate'; - this.pagination.currentPage = parseInt(routeParams['page']) || 1; + if (routeParams['page'] !== undefined) { + this.pagination.currentPage = parseInt(routeParams['page']); + } else { + this.pagination.currentPage = 1; + } this.changeDetector.detectChanges(); } diff --git a/client/src/app/videos/video-list/video-miniature.component.ts b/client/src/app/videos/video-list/video-miniature.component.ts index 84bab950e450d295b69dd74ca3728fcd3076a9ad..398d2db7507a2d4f4f2f6df78e9e99ca5b0488e8 100644 --- a/client/src/app/videos/video-list/video-miniature.component.ts +++ b/client/src/app/videos/video-list/video-miniature.component.ts @@ -1,16 +1,12 @@ -import { DatePipe } from '@angular/common'; import { Component, Input, Output, EventEmitter } from '@angular/core'; -import { ROUTER_DIRECTIVES } from '@angular/router'; import { SortField, Video, VideoService } from '../shared'; import { User } from '../../shared'; @Component({ selector: 'my-video-miniature', - styles: [ require('./video-miniature.component.scss') ], - template: require('./video-miniature.component.html'), - directives: [ ROUTER_DIRECTIVES ], - pipes: [ DatePipe ] + styleUrls: [ './video-miniature.component.scss' ], + templateUrl: './video-miniature.component.html' }) export class VideoMiniatureComponent { @@ -40,7 +36,7 @@ export class VideoMiniatureComponent { if (confirm('Do you really want to remove this video?')) { this.videoService.removeVideo(id).subscribe( status => this.removed.emit(true), - error => alert(error) + error => alert(error.text) ); } } diff --git a/client/src/app/videos/video-list/video-sort.component.ts b/client/src/app/videos/video-list/video-sort.component.ts index 0d76b54b7a7bd2202368af7afadfbbca83ab6bb8..ca94b07c2e7aa190ee061eb68c0493825883917c 100644 --- a/client/src/app/videos/video-list/video-sort.component.ts +++ b/client/src/app/videos/video-list/video-sort.component.ts @@ -4,7 +4,7 @@ import { SortField } from '../shared'; @Component({ selector: 'my-video-sort', - template: require('./video-sort.component.html') + templateUrl: './video-sort.component.html' }) export class VideoSortComponent { diff --git a/client/src/app/videos/video-watch/video-watch.component.ts b/client/src/app/videos/video-watch/video-watch.component.ts index 3aaed0487e53ba9748eee3fa9854d593e4a73e58..239e24c990e8c82613faa92b4abdeb628c905305 100644 --- a/client/src/app/videos/video-watch/video-watch.component.ts +++ b/client/src/app/videos/video-watch/video-watch.component.ts @@ -1,18 +1,13 @@ -import { Component, ElementRef, OnDestroy, OnInit } from '@angular/core'; +import { Component, ElementRef, NgZone, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe'; - -import { LoaderComponent, Video, VideoService } from '../shared'; +import { Video, VideoService } from '../shared'; import { WebTorrentService } from './webtorrent.service'; @Component({ selector: 'my-video-watch', - template: require('./video-watch.component.html'), - styles: [ require('./video-watch.component.scss') ], - providers: [ WebTorrentService ], - directives: [ LoaderComponent ], - pipes: [ BytesPipe ] + templateUrl: './video-watch.component.html', + styleUrls: [ './video-watch.component.scss' ] }) export class VideoWatchComponent implements OnInit, OnDestroy { @@ -31,6 +26,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { constructor( private elementRef: ElementRef, + private ngZone: NgZone, private route: ActivatedRoute, private videoService: VideoService, private webTorrentService: WebTorrentService @@ -65,12 +61,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { } }); - // Refresh each second - this.torrentInfosInterval = setInterval(() => { - this.downloadSpeed = torrent.downloadSpeed; - this.numPeers = torrent.numPeers; - this.uploadSpeed = torrent.uploadSpeed; - }, 1000); + this.runInProgress(torrent); }); } @@ -91,7 +82,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { this.video = video; this.loadVideo(); }, - error => alert(error) + error => alert(error.text) ); }); } @@ -100,4 +91,15 @@ export class VideoWatchComponent implements OnInit, OnDestroy { this.error = true; console.error('The video load seems to be abnormally long.'); } + + private runInProgress(torrent: any) { + // Refresh each second + this.torrentInfosInterval = setInterval(() => { + this.ngZone.run(() => { + this.downloadSpeed = torrent.downloadSpeed; + this.numPeers = torrent.numPeers; + this.uploadSpeed = torrent.uploadSpeed; + }); + }, 1000); + } } diff --git a/client/src/app/videos/videos.component.ts b/client/src/app/videos/videos.component.ts index 76252afbb25b75d6683f9118de23b31a278cac40..591e7523d1db690d18530044b9eed7090d8597e1 100644 --- a/client/src/app/videos/videos.component.ts +++ b/client/src/app/videos/videos.component.ts @@ -1,9 +1,7 @@ import { Component } from '@angular/core'; -import { ROUTER_DIRECTIVES } from '@angular/router'; @Component({ - template: '<router-outlet></router-outlet>', - directives: [ ROUTER_DIRECTIVES ] + template: '<router-outlet></router-outlet>' }) export class VideosComponent { diff --git a/client/src/app/videos/videos.routes.ts b/client/src/app/videos/videos.routes.ts index 1f088b376b1bb81e85f59c31831725cc43a845bf..074f96596e1a81e5feeb806e98877fc244cecfee 100644 --- a/client/src/app/videos/videos.routes.ts +++ b/client/src/app/videos/videos.routes.ts @@ -1,11 +1,11 @@ -import { RouterConfig } from '@angular/router'; +import { Routes } from '@angular/router'; import { VideoAddComponent } from './video-add'; import { VideoListComponent } from './video-list'; import { VideosComponent } from './videos.component'; import { VideoWatchComponent } from './video-watch'; -export const VideosRoutes: RouterConfig = [ +export const VideosRoutes: Routes = [ { path: 'videos', component: VideosComponent, diff --git a/client/src/custom-typings.d.ts b/client/src/custom-typings.d.ts index 14c7d8aded9c4f0c5cebfbb015c7e01692be1d7a..95787181f4cb5e03a2e377c39dab9283d2f191e0 100644 --- a/client/src/custom-typings.d.ts +++ b/client/src/custom-typings.d.ts @@ -1,15 +1,27 @@ /* * Custom Type Definitions * When including 3rd party modules you also need to include the type definition for the module - * if they don't provide one within the module. You can try to install it with typings + * if they don't provide one within the module. You can try to install it with @types -typings install node --save +npm install @types/node +npm install @types/lodash - * If you can't find the type definition in the registry we can make an ambient definition in + * If you can't find the type definition in the registry we can make an ambient/global definition in * this file for now. For example -declare module "my-module" { - export function doesSomething(value: string): string; +declare module 'my-module' { + export function doesSomething(value: string): string; +} + + * If you are using a CommonJS module that is using module.exports then you will have to write your + * types using export = yourObjectOrFunction with a namespace above it + * notice how we have to create a namespace that is equal to the function we're + * assigning the export to + +declare module 'jwt-decode' { + function jwtDecode(token: string): any; + namespace jwtDecode {} + export = jwtDecode; } * @@ -17,33 +29,65 @@ declare module "my-module" { * declare var assert: any; +declare var _: any; +declare var $: any; * * If you're importing a module that uses Node.js modules which are CommonJS you need to import as + * in the files such as main.browser.ts or any file within app/ * import * as _ from 'lodash' - * You can include your type definitions in this file until you create one for the typings registry - * see https://github.com/typings/registry + * You can include your type definitions in this file until you create one for the @types * */ +// support NodeJS modules without type definitions +declare module '*'; // Extra variables that live on Global that will be replaced by webpack DefinePlugin declare var ENV: string; declare var HMR: boolean; +declare var System: SystemJS; + +interface SystemJS { + import: (path?: string) => Promise<any>; +} + interface GlobalEnvironment { ENV; HMR; + SystemJS: SystemJS; + System: SystemJS; } +interface Es6PromiseLoader { + (id: string): (exportName?: string) => Promise<any>; +} + +type FactoryEs6PromiseLoader = () => Es6PromiseLoader; +type FactoryPromise = () => Promise<any>; + +type AsyncRoutes = { + [component: string]: Es6PromiseLoader | + Function | + FactoryEs6PromiseLoader | + FactoryPromise +}; + + +type IdleCallbacks = Es6PromiseLoader | + Function | + FactoryEs6PromiseLoader | + FactoryPromise ; + interface WebpackModule { hot: { data?: any, idle: any, accept(dependencies?: string | string[], callback?: (updatedDependencies?: any) => void): void; - decline(dependencies?: string | string[]): void; + decline(deps?: any | string | string[]): void; dispose(callback?: (data?: any) => void): void; addDisposeHandler(callback?: (data?: any) => void): void; removeDisposeHandler(callback?: (data?: any) => void): void; @@ -54,66 +98,26 @@ interface WebpackModule { }; } + interface WebpackRequire { - context(file: string, flag?: boolean, exp?: RegExp): any; + (id: string): any; + (paths: string[], callback: (...modules: any[]) => void): void; + ensure(ids: string[], callback: (req: WebpackRequire) => void, chunkName?: string): void; + context(directory: string, useSubDirectories?: boolean, regExp?: RegExp): WebpackContext; } +interface WebpackContext extends WebpackRequire { + keys(): string[]; +} interface ErrorStackTraceLimit { stackTraceLimit: number; } - // Extend typings interface NodeRequire extends WebpackRequire {} interface ErrorConstructor extends ErrorStackTraceLimit {} +interface NodeRequireFunction extends Es6PromiseLoader {} interface NodeModule extends WebpackModule {} interface Global extends GlobalEnvironment {} - - -declare namespace Reflect { - function decorate(decorators: ClassDecorator[], target: Function): Function; - function decorate( - decorators: (PropertyDecorator | MethodDecorator)[], - target: Object, - targetKey: string | symbol, - descriptor?: PropertyDescriptor): PropertyDescriptor; - - function metadata(metadataKey: any, metadataValue: any): { - (target: Function): void; - (target: Object, propertyKey: string | symbol): void; - }; - function defineMetadata(metadataKey: any, metadataValue: any, target: Object): void; - function defineMetadata( - metadataKey: any, - metadataValue: any, - target: Object, - targetKey: string | symbol): void; - function hasMetadata(metadataKey: any, target: Object): boolean; - function hasMetadata(metadataKey: any, target: Object, targetKey: string | symbol): boolean; - function hasOwnMetadata(metadataKey: any, target: Object): boolean; - function hasOwnMetadata(metadataKey: any, target: Object, targetKey: string | symbol): boolean; - function getMetadata(metadataKey: any, target: Object): any; - function getMetadata(metadataKey: any, target: Object, targetKey: string | symbol): any; - function getOwnMetadata(metadataKey: any, target: Object): any; - function getOwnMetadata(metadataKey: any, target: Object, targetKey: string | symbol): any; - function getMetadataKeys(target: Object): any[]; - function getMetadataKeys(target: Object, targetKey: string | symbol): any[]; - function getOwnMetadataKeys(target: Object): any[]; - function getOwnMetadataKeys(target: Object, targetKey: string | symbol): any[]; - function deleteMetadata(metadataKey: any, target: Object): boolean; - function deleteMetadata(metadataKey: any, target: Object, targetKey: string | symbol): boolean; -} - - -// We need this here since there is a problem with Zone.js typings -interface Thenable<T> { - then<U>( - onFulfilled?: (value: T) => U | Thenable<U>, - onRejected?: (error: any) => U | Thenable<U>): Thenable<U>; - then<U>( - onFulfilled?: (value: T) => U | Thenable<U>, - onRejected?: (error: any) => void): Thenable<U>; - catch<U>(onRejected?: (error: any) => U | Thenable<U>): Thenable<U>; -} diff --git a/client/src/index.html b/client/src/index.html index 5cf491221b54c5645135b2c1535a90355ad52add..f39d8d2cfc1a637247f0338b644e23d4f8959628 100644 --- a/client/src/index.html +++ b/client/src/index.html @@ -1,3 +1,4 @@ +<!DOCTYPE html> <html> <head> <base href="/"> diff --git a/client/src/main.ts b/client/src/main.ts index a78d275ad4029191b3d1db1c460f4f3aba4b4ea5..70bf4853782ed6ac515d453e34b36ea955e0300a 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -1,28 +1,20 @@ -import { enableProdMode, provide } from '@angular/core'; -import { - HTTP_PROVIDERS, - RequestOptions, - XHRBackend -} from '@angular/http'; -import { bootstrap } from '@angular/platform-browser-dynamic'; -import { provideRouter } from '@angular/router'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { decorateModuleRef } from './app/environment'; +import { bootloader } from '@angularclass/hmr'; +/* + * App Module + * our top level module that holds all of our components + */ +import { AppModule } from './app'; -import { AppComponent } from './app/app.component'; -import { routes } from './app/app.routes'; -import { AuthHttp, AuthService } from './app/shared'; - -if (process.env.ENV === 'production') { - enableProdMode(); +/* + * Bootstrap our Angular app with a top level NgModule + */ +export function main(): Promise<any> { + return platformBrowserDynamic() + .bootstrapModule(AppModule) + .then(decorateModuleRef) + .catch(err => console.error(err)); } -bootstrap(AppComponent, [ - HTTP_PROVIDERS, - provide(AuthHttp, { - useFactory: (backend: XHRBackend, defaultOptions: RequestOptions, authService: AuthService) => { - return new AuthHttp(backend, defaultOptions, authService); - }, - deps: [ XHRBackend, RequestOptions, AuthService ] - }), - AuthService, - provideRouter(routes) -]); +bootloader(main); diff --git a/client/src/polyfills.ts b/client/src/polyfills.ts index 740a563bb96bf5b4197ce5cfad0d29ca4c38a21c..65e211459ba76a3dae8adfc8e350061e0020073a 100644 --- a/client/src/polyfills.ts +++ b/client/src/polyfills.ts @@ -6,9 +6,28 @@ require('intl/locale-data/jsonp/en.js'); import 'ie-shim'; // Internet Explorer // Prefer CoreJS over the polyfills above -import 'core-js/es6'; +import 'core-js/es6/symbol'; +import 'core-js/es6/object'; +import 'core-js/es6/function'; +import 'core-js/es6/parse-int'; +import 'core-js/es6/parse-float'; +import 'core-js/es6/number'; +import 'core-js/es6/math'; +import 'core-js/es6/string'; +import 'core-js/es6/date'; +import 'core-js/es6/array'; +import 'core-js/es6/regexp'; +import 'core-js/es6/map'; +import 'core-js/es6/set'; +import 'core-js/es6/weak-map'; +import 'core-js/es6/weak-set'; +import 'core-js/es6/typed'; +import 'core-js/es6/reflect'; +// see issue https://github.com/AngularClass/angular2-webpack-starter/issues/709 +// import 'core-js/es6/promise'; + import 'core-js/es7/reflect'; -require('zone.js/dist/zone'); +import 'zone.js/dist/zone'; // Typescript emit helpers polyfill import 'ts-helpers'; diff --git a/client/src/sass/application.scss b/client/src/sass/application.scss index 9c48b4627bd2adf4e72abd6c3eb1433f029ad2c8..b3bdffe5055150f15954aafc95fa4e2edf9df0ec 100644 --- a/client/src/sass/application.scss +++ b/client/src/sass/application.scss @@ -6,6 +6,45 @@ body { } } +menu { + @media screen and (max-width: 600px) { + margin-right: 3px !important; + padding: 3px !important; + min-height: 400px !important; + } + + min-height: 600px; + margin-right: 20px; + border-right: 1px solid rgba(0, 0, 0, 0.2); + + .panel-block:not(:last-child) { + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + } + + .panel-button { + margin: 8px; + cursor: pointer; + transition: margin 0.2s; + + &:hover { + margin-left: 15px; + } + + a { + color: #333333; + } + } + + .glyphicon { + margin: 5px; + } +} + +.table-column-id { + width: 200px; +} + + footer { border-top: 1px solid rgba(0, 0, 0, 0.2); padding-top: 10px; diff --git a/client/src/vendor.ts b/client/src/vendor.ts index 8f029191a1ff0c1c3b64065d2338ca037a2c7748..95356d9d0a01172b1bc9ee41e57d24045210612e 100644 --- a/client/src/vendor.ts +++ b/client/src/vendor.ts @@ -8,13 +8,17 @@ import '@angular/platform-browser'; import '@angular/platform-browser-dynamic'; import '@angular/core'; import '@angular/common'; +import '@angular/forms'; import '@angular/http'; import '@angular/router'; +import '@angularclass/hmr'; + // RxJS import 'rxjs/Observable'; import 'rxjs/Subject'; import 'rxjs/add/operator/catch'; +import 'rxjs/add/operator/mergeMap'; import 'rxjs/add/operator/map'; import 'rxjs/add/observable/throw'; diff --git a/client/tsconfig.json b/client/tsconfig.json index 67d1fb4f179665382c89d2e336bf97b263f1c50e..10573b8ee4e1c188b8d90ba3ec8dfad03dcc3410 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -3,74 +3,35 @@ "target": "es5", "module": "commonjs", "moduleResolution": "node", - "sourceMap": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, - "noImplicitAny": false, - "noEmitHelpers": true + "allowSyntheticDefaultImports": true, + "sourceMap": true, + "noEmitHelpers": true, + "strictNullChecks": false, + "baseUrl": "./src", + "paths": [ + ], + "lib": [ + "dom", + "es6" + ], + "types": [ + "node", + "source-map", + "uglify-js", + "webpack" + ] }, + "exclude": [ + "node_modules", + "dist" + ], "awesomeTypescriptLoaderOptions": { - "forkChecker": true + "forkChecker": true, + "useWebpackText": true }, "compileOnSave": false, "buildOnSave": false, - "atom": { - "rewriteTsconfig": true - }, - "filesGlob": [ - "**/*.ts", - "!node_modules/**" - ], - "exclude": [ - "node_modules", - "typings/main", - "typings/main.d.ts" - ], - "files": [ - "src/app/app.component.ts", - "src/app/app.routes.ts", - "src/app/friends/friend.service.ts", - "src/app/friends/index.ts", - "src/app/login/index.ts", - "src/app/login/login.component.ts", - "src/app/login/login.routes.ts", - "src/app/shared/auth/auth-http.service.ts", - "src/app/shared/auth/auth-status.model.ts", - "src/app/shared/auth/auth.service.ts", - "src/app/shared/auth/index.ts", - "src/app/shared/auth/user.model.ts", - "src/app/shared/index.ts", - "src/app/shared/search/index.ts", - "src/app/shared/search/search-field.type.ts", - "src/app/shared/search/search.component.ts", - "src/app/shared/search/search.model.ts", - "src/app/shared/search/search.service.ts", - "src/app/videos/index.ts", - "src/app/videos/shared/index.ts", - "src/app/videos/shared/loader/index.ts", - "src/app/videos/shared/loader/loader.component.ts", - "src/app/videos/shared/pagination.model.ts", - "src/app/videos/shared/sort-field.type.ts", - "src/app/videos/shared/video.model.ts", - "src/app/videos/shared/video.service.ts", - "src/app/videos/video-add/index.ts", - "src/app/videos/video-add/video-add.component.ts", - "src/app/videos/video-list/index.ts", - "src/app/videos/video-list/video-list.component.ts", - "src/app/videos/video-list/video-miniature.component.ts", - "src/app/videos/video-list/video-sort.component.ts", - "src/app/videos/video-watch/index.ts", - "src/app/videos/video-watch/video-watch.component.ts", - "src/app/videos/video-watch/webtorrent.service.ts", - "src/app/videos/videos.component.ts", - "src/app/videos/videos.routes.ts", - "src/custom-typings.d.ts", - "src/main.ts", - "src/polyfills.ts", - "src/vendor.ts", - "typings/globals/es6-shim/index.d.ts", - "typings/globals/jasmine/index.d.ts", - "typings/globals/node/index.d.ts", - "typings/index.d.ts" - ] + "atom": { "rewriteTsconfig": false } } diff --git a/client/typings.json b/client/typings.json deleted file mode 100644 index 9a8891f25f86fc1378759c0a99de59272edd6f3f..0000000000000000000000000000000000000000 --- a/client/typings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "globalDependencies": { - "es6-shim": "registry:dt/es6-shim#0.31.2+20160317120654", - "jasmine": "registry:dt/jasmine#2.2.0+20160412134438", - "node": "registry:dt/node#4.0.0+20160509154515" - } -} diff --git a/client/webpack.config.js b/client/webpack.config.js index 8f54d88e5957384f4097c7a8ae32ca2a72cef41c..3d3af91ad10072b54857cbbd815ccdca5334688d 100644 --- a/client/webpack.config.js +++ b/client/webpack.config.js @@ -1,16 +1,16 @@ switch (process.env.NODE_ENV) { case 'prod': case 'production': - module.exports = require('./config/webpack.prod') + module.exports = require('./config/webpack.prod')({env: 'production'}) break case 'test': case 'testing': - module.exports = require('./config/webpack.test') + module.exports = require('./config/webpack.test')({env: 'test'}) break case 'dev': case 'development': default: - module.exports = require('./config/webpack.dev') + module.exports = require('./config/webpack.dev')({env: 'development'}) } diff --git a/config/default.yaml b/config/default.yaml index 9a8a57879340980f4db1477cd910ec8bfa88fb8d..b44be31b0ab2ad92fd0295a161e976e5032901fc 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -19,8 +19,5 @@ storage: thumbnails: 'thumbnails/' torrents: 'torrents/' -network: - friends: [] - electron: debug: false diff --git a/config/production.yaml.example b/config/production.yaml.example index 6dc79d73fd5a120003103bb2325f52c8f20c20ec..8bc63ee92461967b65a65e29c15a4c7bdd382ce8 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example @@ -1,4 +1,8 @@ +# Correspond to your reverse proxy "listen" configuration webserver: https: false host: 'example.com' port: 80 + +database: + suffix: '-prod' diff --git a/config/test-1.yaml b/config/test-1.yaml index 0998eaea1ae59221d589ee067822683f0e62dc01..a59566cc4f3362154565ec7817ac611b2835ade4 100644 --- a/config/test-1.yaml +++ b/config/test-1.yaml @@ -15,7 +15,3 @@ storage: logs: 'test1/logs/' thumbnails: 'test1/thumbnails/' torrents: 'test1/torrents/' - -network: - friends: - - 'http://localhost:9002' diff --git a/config/test-2.yaml b/config/test-2.yaml index ec2cff811b357ff43cfee9d01f4a5a826f4511df..1b937898feac428179aee63d1b0f7e5eb87b8112 100644 --- a/config/test-2.yaml +++ b/config/test-2.yaml @@ -15,7 +15,3 @@ storage: logs: 'test2/logs/' thumbnails: 'test2/thumbnails/' torrents: 'test2/torrents/' - -network: - friends: - - 'http://localhost:9003' diff --git a/config/test-3.yaml b/config/test-3.yaml index 24f5533e0a48f0bc77ee607a94ce694ce51a65e4..e522c13e7fcaa25d1c10b89172d0ce5bed350ea5 100644 --- a/config/test-3.yaml +++ b/config/test-3.yaml @@ -15,7 +15,3 @@ storage: logs: 'test3/logs/' thumbnails: 'test3/thumbnails/' torrents: 'test3/torrents/' - -network: - friends: - - 'http://localhost:9001' diff --git a/config/test-4.yaml b/config/test-4.yaml index 1f884dbf2114b338ebd261e34113eabcffdeee36..e30cd79789490fba263be80f8f70ba9b263a6618 100644 --- a/config/test-4.yaml +++ b/config/test-4.yaml @@ -15,7 +15,3 @@ storage: logs: 'test4/logs/' thumbnails: 'test4/thumbnails/' torrents: 'test4/torrents/' - -network: - friends: - - 'http://localhost:9002' diff --git a/config/test-5.yaml b/config/test-5.yaml index 08ed9f06870023cd28e0dc4699f2ed82f7765879..3a54599f5555761d5a1a506e91a0d09b93be3692 100644 --- a/config/test-5.yaml +++ b/config/test-5.yaml @@ -15,8 +15,3 @@ storage: logs: 'test5/logs/' thumbnails: 'test5/thumbnails/' torrents: 'test5/torrents/' - -network: - friends: - - 'http://localhost:9001' - - 'http://localhost:9004' diff --git a/config/test-6.yaml b/config/test-6.yaml index a57784cca484d6282d29faa5723ab1253bee2edf..31608add2ac32fd42bede89980bb00ee25f55d33 100644 --- a/config/test-6.yaml +++ b/config/test-6.yaml @@ -15,9 +15,3 @@ storage: logs: 'test6/logs/' thumbnails: 'test6/thumbnails/' torrents: 'test6/torrents/' - -network: - friends: - - 'http://localhost:9001' - - 'http://localhost:9002' - - 'http://localhost:9003' diff --git a/package.json b/package.json index 63d01437621dab774d44561ec27a5948c6b3408a..59c7a4332cae749448fe5e328458dbcb65d2f4ef 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ }, "dependencies": { "async": "^2.0.0", + "bcrypt": "^0.8.7", "bittorrent-tracker": "^8.0.0", "body-parser": "^1.12.4", "concurrently": "^2.0.0", @@ -59,7 +60,6 @@ "request": "^2.57.0", "request-replay": "^1.0.2", "scripty": "^1.5.0", - "segfault-handler": "^1.0.0", "ursa": "^0.9.1", "winston": "^2.1.1", "ws": "^1.1.1" @@ -67,9 +67,9 @@ "devDependencies": { "chai": "^3.3.0", "commander": "^2.9.0", - "mocha": "^2.3.3", - "standard": "^7.0.1", - "supertest": "^1.1.0" + "mocha": "^3.0.1", + "standard": "^8.0.0", + "supertest": "^2.0.0" }, "standard": { "ignore": [ diff --git a/scripts/build/client/prod.sh b/scripts/build/client/prod.sh index 478cae99ed615b73cb4cd32c732decd21e704387..e090e8082ecd3cd30ddb15cee169f01d45d38281 100755 --- a/scripts/build/client/prod.sh +++ b/scripts/build/client/prod.sh @@ -2,4 +2,4 @@ cd client || exit -1 -npm run webpack -- --config config/webpack.prod.js --progress --profile --colors --display-error-details --display-cached --bail +npm run webpack -- --config config/webpack.prod.js --progress --profile --bail diff --git a/server.js b/server.js index 0033ed1dbb70e25d10cb3a0957373d6da2063788..5feb214764f183f10cd7f8b58e5407ff3ce0a98c 100644 --- a/server.js +++ b/server.js @@ -32,6 +32,7 @@ if (miss.length !== 0) { // ----------- PeerTube modules ----------- const customValidators = require('./server/helpers/custom-validators') const installer = require('./server/initializers/installer') +const migrator = require('./server/initializers/migrator') const mongoose = require('mongoose') const routes = require('./server/controllers') const Request = mongoose.model('Request') @@ -46,18 +47,21 @@ const port = config.get('listen.port') // For the logger app.use(morgan('combined', { stream: logger.stream })) // For body requests -app.use(bodyParser.json()) +app.use(bodyParser.json({ limit: '500kb' })) app.use(bodyParser.urlencoded({ extended: false })) // Validate some params for the API app.use(expressValidator({ - customValidators: customValidators + customValidators: Object.assign( + {}, + customValidators.misc, + customValidators.pods, + customValidators.users, + customValidators.videos + ) })) // ----------- Views, routes and static files ----------- -// Catch sefaults -require('segfault-handler').registerHandler() - // API routes const apiRoute = '/api/' + constants.API_VERSION app.use(apiRoute, routes.api) @@ -125,14 +129,19 @@ app.use(function (err, req, res, next) { installer.installApplication(function (err) { if (err) throw err - // ----------- Make the server listening ----------- - server.listen(port, function () { - // Activate the pool requests - Request.activate() + // Run the migration scripts if needed + migrator.migrate(function (err) { + if (err) throw err + + // ----------- Make the server listening ----------- + server.listen(port, function () { + // Activate the pool requests + Request.activate() - logger.info('Seeded all the videos') - logger.info('Server listening on port %d', port) - app.emit('ready') + logger.info('Seeded all the videos') + logger.info('Server listening on port %d', port) + app.emit('ready') + }) }) }) diff --git a/server/controllers/api/v1/clients.js b/server/controllers/api/v1/clients.js new file mode 100644 index 0000000000000000000000000000000000000000..5b460db2edeb00c6780cad24d4bff41c5fee791a --- /dev/null +++ b/server/controllers/api/v1/clients.js @@ -0,0 +1,41 @@ +'use strict' + +const express = require('express') +const mongoose = require('mongoose') + +const constants = require('../../../initializers/constants') + +const Client = mongoose.model('OAuthClient') + +const router = express.Router() + +router.get('/local', getLocalClient) + +// Get the client credentials for the PeerTube front end +function getLocalClient (req, res, next) { + const serverHost = constants.CONFIG.WEBSERVER.HOST + const serverPort = constants.CONFIG.WEBSERVER.PORT + let headerHostShouldBe = serverHost + if (serverPort !== 80 && serverPort !== 443) { + headerHostShouldBe += ':' + serverPort + } + + // Don't make this check if this is a test instance + if (process.env.NODE_ENV !== 'test' && req.get('host') !== headerHostShouldBe) { + return res.type('json').status(403).end() + } + + Client.loadFirstClient(function (err, client) { + if (err) return next(err) + if (!client) return next(new Error('No client available.')) + + res.json({ + client_id: client._id, + client_secret: client.clientSecret + }) + }) +} + +// --------------------------------------------------------------------------- + +module.exports = router diff --git a/server/controllers/api/v1/index.js b/server/controllers/api/v1/index.js index e0c29a8a22845b424f99a08474dbcd30e5b892d7..2e4fb2dab5bebff8f09ce0b94a408c517341d6e0 100644 --- a/server/controllers/api/v1/index.js +++ b/server/controllers/api/v1/index.js @@ -4,13 +4,17 @@ const express = require('express') const router = express.Router() +const clientsController = require('./clients') const podsController = require('./pods') const remoteController = require('./remote') +const requestsController = require('./requests') const usersController = require('./users') const videosController = require('./videos') +router.use('/clients', clientsController) router.use('/pods', podsController) router.use('/remote', remoteController) +router.use('/requests', requestsController) router.use('/users', usersController) router.use('/videos', videosController) router.use('/*', badRequest) diff --git a/server/controllers/api/v1/pods.js b/server/controllers/api/v1/pods.js index 2bc761fef4348c93673c4b4b903e3564ca9c7779..8ffade578505a269ea64701b69a6156e03ced539 100644 --- a/server/controllers/api/v1/pods.js +++ b/server/controllers/api/v1/pods.js @@ -8,7 +8,10 @@ const waterfall = require('async/waterfall') const logger = require('../../../helpers/logger') const friends = require('../../../lib/friends') const middlewares = require('../../../middlewares') +const admin = middlewares.admin const oAuth = middlewares.oauth +const podsMiddleware = middlewares.pods +const checkSignature = middlewares.secure.checkSignature const validators = middlewares.validators.pods const signatureValidator = middlewares.validators.remote.signature @@ -16,12 +19,30 @@ const router = express.Router() const Pod = mongoose.model('Pod') const Video = mongoose.model('Video') -router.get('/', listPodsUrl) -router.post('/', validators.podsAdd, addPods) -router.get('/makefriends', oAuth.authenticate, validators.makeFriends, makeFriends) -router.get('/quitfriends', oAuth.authenticate, quitFriends) +router.get('/', listPods) +router.post('/', + validators.podsAdd, + podsMiddleware.setBodyUrlPort, + addPods +) +router.post('/makefriends', + oAuth.authenticate, + admin.ensureIsAdmin, + validators.makeFriends, + podsMiddleware.setBodyUrlsPort, + makeFriends +) +router.get('/quitfriends', + oAuth.authenticate, + admin.ensureIsAdmin, + quitFriends +) // Post because this is a secured request -router.post('/remove', signatureValidator, removePods) +router.post('/remove', + signatureValidator, + checkSignature, + removePods +) // --------------------------------------------------------------------------- @@ -64,20 +85,27 @@ function addPods (req, res, next) { }) } -function listPodsUrl (req, res, next) { - Pod.listOnlyUrls(function (err, podsUrlList) { +function listPods (req, res, next) { + Pod.list(function (err, podsUrlList) { if (err) return next(err) - res.json(podsUrlList) + res.json(getFormatedPods(podsUrlList)) }) } function makeFriends (req, res, next) { - friends.makeFriends(function (err) { - if (err) return next(err) + const urls = req.body.urls - res.type('json').status(204).end() + friends.makeFriends(urls, function (err) { + if (err) { + logger.error('Could not make friends.', { error: err }) + return + } + + logger.info('Made friends!') }) + + res.type('json').status(204).end() } function removePods (req, res, next) { @@ -125,3 +153,15 @@ function quitFriends (req, res, next) { res.type('json').status(204).end() }) } + +// --------------------------------------------------------------------------- + +function getFormatedPods (pods) { + const formatedPods = [] + + pods.forEach(function (pod) { + formatedPods.push(pod.toFormatedJSON()) + }) + + return formatedPods +} diff --git a/server/controllers/api/v1/remote.js b/server/controllers/api/v1/remote.js index f452986b8e951aa0498fb5b35b7a4ffca0f6dde3..a22c5d1515b2255033330701a71ba1145a9965e4 100644 --- a/server/controllers/api/v1/remote.js +++ b/server/controllers/api/v1/remote.js @@ -16,6 +16,7 @@ const Video = mongoose.model('Video') router.post('/videos', validators.signature, validators.dataToDecrypt, + secureMiddleware.checkSignature, secureMiddleware.decryptBody, validators.remoteVideos, remoteVideos diff --git a/server/controllers/api/v1/requests.js b/server/controllers/api/v1/requests.js new file mode 100644 index 0000000000000000000000000000000000000000..97616424d0e24d87996be0c0f3c30e417fd82709 --- /dev/null +++ b/server/controllers/api/v1/requests.js @@ -0,0 +1,38 @@ +'use strict' + +const express = require('express') +const mongoose = require('mongoose') + +const constants = require('../../../initializers/constants') +const middlewares = require('../../../middlewares') +const admin = middlewares.admin +const oAuth = middlewares.oauth + +const Request = mongoose.model('Request') + +const router = express.Router() + +router.get('/stats', + oAuth.authenticate, + admin.ensureIsAdmin, + getStatsRequests +) + +// --------------------------------------------------------------------------- + +module.exports = router + +// --------------------------------------------------------------------------- + +function getStatsRequests (req, res, next) { + Request.list(function (err, requests) { + if (err) return next(err) + + return res.json({ + requests: requests, + maxRequestsInParallel: constants.REQUESTS_IN_PARALLEL, + remainingMilliSeconds: Request.remainingMilliSeconds(), + milliSecondsInterval: constants.REQUESTS_INTERVAL + }) + }) +} diff --git a/server/controllers/api/v1/users.js b/server/controllers/api/v1/users.js index fbbe6e4726276140409dee8b7247659034e0aa4a..975e25e68f5ea1979a501a7050d723a4bcb80e95 100644 --- a/server/controllers/api/v1/users.js +++ b/server/controllers/api/v1/users.js @@ -1,18 +1,59 @@ 'use strict' -const config = require('config') -const mongoose = require('mongoose') +const each = require('async/each') const express = require('express') +const mongoose = require('mongoose') +const waterfall = require('async/waterfall') -const oAuth = require('../../../middlewares').oauth +const constants = require('../../../initializers/constants') +const friends = require('../../../lib/friends') +const logger = require('../../../helpers/logger') +const middlewares = require('../../../middlewares') +const admin = middlewares.admin +const oAuth = middlewares.oauth +const pagination = middlewares.pagination +const sort = middlewares.sort +const validatorsPagination = middlewares.validators.pagination +const validatorsSort = middlewares.validators.sort +const validatorsUsers = middlewares.validators.users -const Client = mongoose.model('OAuthClient') +const User = mongoose.model('User') +const Video = mongoose.model('Video') const router = express.Router() -router.get('/client', getAngularClient) +router.get('/me', oAuth.authenticate, getUserInformation) + +router.get('/', + validatorsPagination.pagination, + validatorsSort.usersSort, + sort.setUsersSort, + pagination.setPagination, + listUsers +) + +router.post('/', + oAuth.authenticate, + admin.ensureIsAdmin, + validatorsUsers.usersAdd, + createUser +) + +router.put('/:id', + oAuth.authenticate, + validatorsUsers.usersUpdate, + updateUser +) + +router.delete('/:id', + oAuth.authenticate, + admin.ensureIsAdmin, + validatorsUsers.usersRemove, + removeUser +) + router.post('/token', oAuth.token, success) -// TODO: Once https://github.com/oauthjs/node-oauth2-server/pull/289 is merged,, implement revoke token route +// TODO: Once https://github.com/oauthjs/node-oauth2-server/pull/289 is merged, implement revoke token route // --------------------------------------------------------------------------- @@ -20,26 +61,91 @@ module.exports = router // --------------------------------------------------------------------------- -function getAngularClient (req, res, next) { - const serverHost = config.get('webserver.host') - const serverPort = config.get('webserver.port') - let headerHostShouldBe = serverHost - if (serverPort !== 80 && serverPort !== 443) { - headerHostShouldBe += ':' + serverPort - } +function createUser (req, res, next) { + const user = new User({ + username: req.body.username, + password: req.body.password, + role: constants.USER_ROLES.USER + }) - // Don't make this check if this is a test instance - if (process.env.NODE_ENV !== 'test' && req.get('host') !== headerHostShouldBe) { - return res.type('json').status(403).end() - } + user.save(function (err, createdUser) { + if (err) return next(err) - Client.loadFirstClient(function (err, client) { + return res.type('json').status(204).end() + }) +} + +function getUserInformation (req, res, next) { + User.loadByUsername(res.locals.oauth.token.user.username, function (err, user) { if (err) return next(err) - if (!client) return next(new Error('No client available.')) - res.json({ - client_id: client._id, - client_secret: client.clientSecret + return res.json(user.toFormatedJSON()) + }) +} + +function listUsers (req, res, next) { + User.listForApi(req.query.start, req.query.count, req.query.sort, function (err, usersList, usersTotal) { + if (err) return next(err) + + res.json(getFormatedUsers(usersList, usersTotal)) + }) +} + +function removeUser (req, res, next) { + waterfall([ + function getUser (callback) { + User.loadById(req.params.id, callback) + }, + + function getVideos (user, callback) { + Video.listOwnedByAuthor(user.username, function (err, videos) { + return callback(err, user, videos) + }) + }, + + function removeVideosFromDB (user, videos, callback) { + each(videos, function (video, callbackEach) { + video.remove(callbackEach) + }, function (err) { + return callback(err, user, videos) + }) + }, + + function sendInformationToFriends (user, videos, callback) { + videos.forEach(function (video) { + const params = { + name: video.name, + magnetUri: video.magnetUri + } + + friends.removeVideoToFriends(params) + }) + + return callback(null, user) + }, + + function removeUserFromDB (user, callback) { + user.remove(callback) + } + ], function andFinally (err) { + if (err) { + logger.error('Errors when removed the user.', { error: err }) + return next(err) + } + + return res.sendStatus(204) + }) +} + +function updateUser (req, res, next) { + User.loadByUsername(res.locals.oauth.token.user.username, function (err, user) { + if (err) return next(err) + + user.password = req.body.password + user.save(function (err) { + if (err) return next(err) + + return res.sendStatus(204) }) }) } @@ -47,3 +153,18 @@ function getAngularClient (req, res, next) { function success (req, res, next) { res.end() } + +// --------------------------------------------------------------------------- + +function getFormatedUsers (users, usersTotal) { + const formatedUsers = [] + + users.forEach(function (user) { + formatedUsers.push(user.toFormatedJSON()) + }) + + return { + total: usersTotal, + data: formatedUsers + } +} diff --git a/server/controllers/api/v1/videos.js b/server/controllers/api/v1/videos.js index 1f939b077f1f80711c3560142a9c7f5708360319..70d22f139ba35dba729928509ec36f134f6a9e38 100644 --- a/server/controllers/api/v1/videos.js +++ b/server/controllers/api/v1/videos.js @@ -1,11 +1,11 @@ 'use strict' -const config = require('config') const express = require('express') const mongoose = require('mongoose') const multer = require('multer') const waterfall = require('async/waterfall') +const constants = require('../../../initializers/constants') const logger = require('../../../helpers/logger') const friends = require('../../../lib/friends') const middlewares = require('../../../middlewares') @@ -20,13 +20,12 @@ const sort = middlewares.sort const utils = require('../../../helpers/utils') const router = express.Router() -const uploads = config.get('storage.uploads') const Video = mongoose.model('Video') // multer configuration const storage = multer.diskStorage({ destination: function (req, file, cb) { - cb(null, uploads) + cb(null, constants.CONFIG.STORAGE.UPLOAD_DIR) }, filename: function (req, file, cb) { @@ -142,7 +141,7 @@ function getVideo (req, res, next) { } function listVideos (req, res, next) { - Video.list(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) { + Video.listForApi(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) { if (err) return next(err) res.json(getFormatedVideos(videosList, videosTotal)) diff --git a/server/helpers/custom-validators/index.js b/server/helpers/custom-validators/index.js new file mode 100644 index 0000000000000000000000000000000000000000..96b5b20b91ab50196f864dc310d11b73669341bb --- /dev/null +++ b/server/helpers/custom-validators/index.js @@ -0,0 +1,17 @@ +'use strict' + +const miscValidators = require('./misc') +const podsValidators = require('./pods') +const usersValidators = require('./users') +const videosValidators = require('./videos') + +const validators = { + misc: miscValidators, + pods: podsValidators, + users: usersValidators, + videos: videosValidators +} + +// --------------------------------------------------------------------------- + +module.exports = validators diff --git a/server/helpers/custom-validators/misc.js b/server/helpers/custom-validators/misc.js new file mode 100644 index 0000000000000000000000000000000000000000..0527262419e2a1ef04584dc656fa6ae2ee08aba5 --- /dev/null +++ b/server/helpers/custom-validators/misc.js @@ -0,0 +1,18 @@ +'use strict' + +const miscValidators = { + exists, + isArray +} + +function exists (value) { + return value !== undefined && value !== null +} + +function isArray (value) { + return Array.isArray(value) +} + +// --------------------------------------------------------------------------- + +module.exports = miscValidators diff --git a/server/helpers/custom-validators/pods.js b/server/helpers/custom-validators/pods.js new file mode 100644 index 0000000000000000000000000000000000000000..40f8b5d0b895f4ad8819a0625f14c3727d98113d --- /dev/null +++ b/server/helpers/custom-validators/pods.js @@ -0,0 +1,21 @@ +'use strict' + +const validator = require('express-validator').validator + +const miscValidators = require('./misc') + +const podsValidators = { + isEachUniqueUrlValid +} + +function isEachUniqueUrlValid (urls) { + return miscValidators.isArray(urls) && + urls.length !== 0 && + urls.every(function (url) { + return validator.isURL(url) && urls.indexOf(url) === urls.lastIndexOf(url) + }) +} + +// --------------------------------------------------------------------------- + +module.exports = podsValidators diff --git a/server/helpers/custom-validators/users.js b/server/helpers/custom-validators/users.js new file mode 100644 index 0000000000000000000000000000000000000000..88fa1592e858e25ab421b58672734fb0130b5b79 --- /dev/null +++ b/server/helpers/custom-validators/users.js @@ -0,0 +1,31 @@ +'use strict' + +const validator = require('express-validator').validator +const values = require('lodash/values') + +const constants = require('../../initializers/constants') +const USERS_CONSTRAINTS_FIELDS = constants.CONSTRAINTS_FIELDS.USERS + +const usersValidators = { + isUserPasswordValid, + isUserRoleValid, + isUserUsernameValid +} + +function isUserPasswordValid (value) { + return validator.isLength(value, USERS_CONSTRAINTS_FIELDS.PASSWORD) +} + +function isUserRoleValid (value) { + return values(constants.USER_ROLES).indexOf(value) !== -1 +} + +function isUserUsernameValid (value) { + const max = USERS_CONSTRAINTS_FIELDS.USERNAME.max + const min = USERS_CONSTRAINTS_FIELDS.USERNAME.min + return validator.matches(value, new RegExp(`^[a-zA-Z0-9._]{${min},${max}}$`)) +} + +// --------------------------------------------------------------------------- + +module.exports = usersValidators diff --git a/server/helpers/custom-validators.js b/server/helpers/custom-validators/videos.js similarity index 50% rename from server/helpers/custom-validators.js rename to server/helpers/custom-validators/videos.js index b666644c0167c81bc500570b5c746e7fe7197405..a507ff68617a7e46925e31356843dc0585e8d7ec 100644 --- a/server/helpers/custom-validators.js +++ b/server/helpers/custom-validators/videos.js @@ -2,66 +2,51 @@ const validator = require('express-validator').validator -const constants = require('../initializers/constants') -const VIDEOS_CONSTRAINTS_FIELDS = constants.VIDEOS_CONSTRAINTS_FIELDS - -const customValidators = { - exists: exists, - isEachRemoteVideosValid: isEachRemoteVideosValid, - isArray: isArray, - isVideoAuthorValid: isVideoAuthorValid, - isVideoDateValid: isVideoDateValid, - isVideoDescriptionValid: isVideoDescriptionValid, - isVideoDurationValid: isVideoDurationValid, - isVideoMagnetUriValid: isVideoMagnetUriValid, - isVideoNameValid: isVideoNameValid, - isVideoPodUrlValid: isVideoPodUrlValid, - isVideoTagsValid: isVideoTagsValid, - isVideoThumbnailValid: isVideoThumbnailValid, - isVideoThumbnail64Valid: isVideoThumbnail64Valid -} - -function exists (value) { - return value !== undefined && value !== null +const constants = require('../../initializers/constants') +const usersValidators = require('./users') +const miscValidators = require('./misc') +const VIDEOS_CONSTRAINTS_FIELDS = constants.CONSTRAINTS_FIELDS.VIDEOS + +const videosValidators = { + isEachRemoteVideosValid, + isVideoAuthorValid, + isVideoDateValid, + isVideoDescriptionValid, + isVideoDurationValid, + isVideoMagnetUriValid, + isVideoNameValid, + isVideoPodUrlValid, + isVideoTagsValid, + isVideoThumbnailValid, + isVideoThumbnail64Valid } function isEachRemoteVideosValid (requests) { - return requests.every(function (request) { - const video = request.data - return ( - isRequestTypeAddValid(request.type) && - isVideoAuthorValid(video.author) && - isVideoDateValid(video.createdDate) && - isVideoDescriptionValid(video.description) && - isVideoDurationValid(video.duration) && - isVideoMagnetUriValid(video.magnetUri) && - isVideoNameValid(video.name) && - isVideoPodUrlValid(video.podUrl) && - isVideoTagsValid(video.tags) && - isVideoThumbnail64Valid(video.thumbnailBase64) - ) || - ( - isRequestTypeRemoveValid(request.type) && - isVideoNameValid(video.name) && - isVideoMagnetUriValid(video.magnetUri) - ) - }) -} - -function isArray (value) { - return Array.isArray(value) -} - -function isRequestTypeAddValid (value) { - return value === 'add' -} - -function isRequestTypeRemoveValid (value) { - return value === 'remove' + return miscValidators.isArray(requests) && + requests.every(function (request) { + const video = request.data + return ( + isRequestTypeAddValid(request.type) && + isVideoAuthorValid(video.author) && + isVideoDateValid(video.createdDate) && + isVideoDescriptionValid(video.description) && + isVideoDurationValid(video.duration) && + isVideoMagnetUriValid(video.magnetUri) && + isVideoNameValid(video.name) && + isVideoPodUrlValid(video.podUrl) && + isVideoTagsValid(video.tags) && + isVideoThumbnail64Valid(video.thumbnailBase64) + ) || + ( + isRequestTypeRemoveValid(request.type) && + isVideoNameValid(video.name) && + isVideoMagnetUriValid(video.magnetUri) + ) + }) } function isVideoAuthorValid (value) { - return validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.AUTHOR) + return usersValidators.isUserUsernameValid(value) } function isVideoDateValid (value) { @@ -90,7 +75,7 @@ function isVideoPodUrlValid (value) { } function isVideoTagsValid (tags) { - return isArray(tags) && + return miscValidators.isArray(tags) && validator.isInt(tags.length, VIDEOS_CONSTRAINTS_FIELDS.TAGS) && tags.every(function (tag) { return validator.isAlphanumeric(tag) && @@ -109,6 +94,14 @@ function isVideoThumbnail64Valid (value) { // --------------------------------------------------------------------------- -module.exports = customValidators +module.exports = videosValidators // --------------------------------------------------------------------------- + +function isRequestTypeAddValid (value) { + return value === 'add' +} + +function isRequestTypeRemoveValid (value) { + return value === 'remove' +} diff --git a/server/helpers/logger.js b/server/helpers/logger.js index 8ae90a4b2c80a935bec95170787fe74a090356a0..590ceaeb6c5be0ef69761166f497ed8a7d8b54ff 100644 --- a/server/helpers/logger.js +++ b/server/helpers/logger.js @@ -1,23 +1,23 @@ // Thanks http://tostring.it/2014/06/23/advanced-logging-with-nodejs/ 'use strict' -const config = require('config') const mkdirp = require('mkdirp') const path = require('path') const winston = require('winston') winston.emitErrs = true -const logDir = path.join(__dirname, '..', '..', config.get('storage.logs')) -const label = config.get('webserver.host') + ':' + config.get('webserver.port') +const constants = require('../initializers/constants') + +const label = constants.CONFIG.WEBSERVER.HOST + ':' + constants.CONFIG.WEBSERVER.PORT // Create the directory if it does not exist -mkdirp.sync(logDir) +mkdirp.sync(constants.CONFIG.STORAGE.LOG_DIR) const logger = new winston.Logger({ transports: [ new winston.transports.File({ level: 'debug', - filename: path.join(logDir, 'all-logs.log'), + filename: path.join(constants.CONFIG.STORAGE.LOG_DIR, 'all-logs.log'), handleExceptions: true, json: true, maxsize: 5242880, diff --git a/server/helpers/peertube-crypto.js b/server/helpers/peertube-crypto.js index 46dff8d034df69df5ce7e751cabc7d713d2f973a..1ff638b04ecce144a71698c9eaffea2efcef8391 100644 --- a/server/helpers/peertube-crypto.js +++ b/server/helpers/peertube-crypto.js @@ -1,24 +1,24 @@ 'use strict' -const config = require('config') +const bcrypt = require('bcrypt') const crypto = require('crypto') const fs = require('fs') const openssl = require('openssl-wrapper') -const path = require('path') const ursa = require('ursa') +const constants = require('../initializers/constants') const logger = require('./logger') -const certDir = path.join(__dirname, '..', '..', config.get('storage.certs')) const algorithm = 'aes-256-ctr' const peertubeCrypto = { - checkSignature: checkSignature, - createCertsIfNotExist: createCertsIfNotExist, - decrypt: decrypt, - encrypt: encrypt, - getCertDir: getCertDir, - sign: sign + checkSignature, + comparePassword, + createCertsIfNotExist, + cryptPassword, + decrypt, + encrypt, + sign } function checkSignature (publicKey, rawData, hexSignature) { @@ -27,6 +27,14 @@ function checkSignature (publicKey, rawData, hexSignature) { return isValid } +function comparePassword (plainPassword, hashPassword, callback) { + bcrypt.compare(plainPassword, hashPassword, function (err, isPasswordMatch) { + if (err) return callback(err) + + return callback(null, isPasswordMatch) + }) +} + function createCertsIfNotExist (callback) { certsExist(function (exist) { if (exist === true) { @@ -39,8 +47,18 @@ function createCertsIfNotExist (callback) { }) } +function cryptPassword (password, callback) { + bcrypt.genSalt(constants.BCRYPT_SALT_SIZE, function (err, salt) { + if (err) return callback(err) + + bcrypt.hash(password, salt, function (err, hash) { + return callback(err, hash) + }) + }) +} + function decrypt (key, data, callback) { - fs.readFile(getCertDir() + 'peertube.key.pem', function (err, file) { + fs.readFile(constants.CONFIG.STORAGE.CERT_DIR + 'peertube.key.pem', function (err, file) { if (err) return callback(err) const myPrivateKey = ursa.createPrivateKey(file) @@ -67,12 +85,8 @@ function encrypt (publicKey, data, callback) { }) } -function getCertDir () { - return certDir -} - function sign (data) { - const myKey = ursa.createPrivateKey(fs.readFileSync(certDir + 'peertube.key.pem')) + const myKey = ursa.createPrivateKey(fs.readFileSync(constants.CONFIG.STORAGE.CERT_DIR + 'peertube.key.pem')) const signature = myKey.hashAndSign('sha256', data, 'utf8', 'hex') return signature @@ -85,7 +99,7 @@ module.exports = peertubeCrypto // --------------------------------------------------------------------------- function certsExist (callback) { - fs.exists(certDir + 'peertube.key.pem', function (exists) { + fs.exists(constants.CONFIG.STORAGE.CERT_DIR + 'peertube.key.pem', function (exists) { return callback(exists) }) } @@ -99,15 +113,25 @@ function createCerts (callback) { } logger.info('Generating a RSA key...') - openssl.exec('genrsa', { 'out': certDir + 'peertube.key.pem', '2048': false }, function (err) { + + let options = { + 'out': constants.CONFIG.STORAGE.CERT_DIR + 'peertube.key.pem', + '2048': false + } + openssl.exec('genrsa', options, function (err) { if (err) { logger.error('Cannot create private key on this pod.') return callback(err) } logger.info('RSA key generated.') + options = { + 'in': constants.CONFIG.STORAGE.CERT_DIR + 'peertube.key.pem', + 'pubout': true, + 'out': constants.CONFIG.STORAGE.CERT_DIR + 'peertube.pub' + } logger.info('Manage public key...') - openssl.exec('rsa', { 'in': certDir + 'peertube.key.pem', 'pubout': true, 'out': certDir + 'peertube.pub' }, function (err) { + openssl.exec('rsa', options, function (err) { if (err) { logger.error('Cannot create public key on this pod.') return callback(err) diff --git a/server/helpers/requests.js b/server/helpers/requests.js index 547230adc973a0958ce672d26363c89588554612..95775c981c1a5545d0e82bd45bc4bb48b88770f0 100644 --- a/server/helpers/requests.js +++ b/server/helpers/requests.js @@ -1,19 +1,14 @@ 'use strict' -const config = require('config') const replay = require('request-replay') const request = require('request') const constants = require('../initializers/constants') const peertubeCrypto = require('./peertube-crypto') -const http = config.get('webserver.https') ? 'https' : 'http' -const host = config.get('webserver.host') -const port = config.get('webserver.port') - const requests = { - makeRetryRequest: makeRetryRequest, - makeSecureRequest: makeSecureRequest + makeRetryRequest, + makeSecureRequest } function makeRetryRequest (params, callback) { @@ -29,8 +24,6 @@ function makeRetryRequest (params, callback) { } function makeSecureRequest (params, callback) { - const myUrl = http + '://' + host + ':' + port - const requestParams = { url: params.toPod.url + params.path } @@ -42,8 +35,8 @@ function makeSecureRequest (params, callback) { // Add signature if it is specified in the params if (params.sign === true) { requestParams.json.signature = { - url: myUrl, - signature: peertubeCrypto.sign(myUrl) + url: constants.CONFIG.WEBSERVER.URL, + signature: peertubeCrypto.sign(constants.CONFIG.WEBSERVER.URL) } } diff --git a/server/helpers/utils.js b/server/helpers/utils.js index a77116e0833f44335848d3a4c8ccf0dec1af7bb9..9c2d402e3105047615d26bfe4ebd016deed2810d 100644 --- a/server/helpers/utils.js +++ b/server/helpers/utils.js @@ -5,8 +5,8 @@ const crypto = require('crypto') const logger = require('./logger') const utils = { - cleanForExit: cleanForExit, - generateRandomString: generateRandomString + cleanForExit, + generateRandomString } function generateRandomString (size, callback) { diff --git a/server/initializers/checker.js b/server/initializers/checker.js index 3831efb8d5eccffe3d75a464e4270181d6bb4917..91fbcfaf933ffe2885c491f44dc2e6de7c398f98 100644 --- a/server/initializers/checker.js +++ b/server/initializers/checker.js @@ -7,9 +7,9 @@ const Client = mongoose.model('OAuthClient') const User = mongoose.model('User') const checker = { - checkConfig: checkConfig, - clientsExist: clientsExist, - usersExist: usersExist + checkConfig, + clientsExist, + usersExist } // Check the config files @@ -17,8 +17,8 @@ function checkConfig () { const required = [ 'listen.port', 'webserver.https', 'webserver.host', 'webserver.port', 'database.host', 'database.port', 'database.suffix', - 'storage.certs', 'storage.uploads', 'storage.logs', - 'network.friends', 'electron.debug' ] + 'storage.certs', 'storage.uploads', 'storage.logs', 'storage.thumbnails', + 'electron.debug' ] const miss = [] for (const key of required) { @@ -39,10 +39,10 @@ function clientsExist (callback) { } function usersExist (callback) { - User.list(function (err, users) { + User.countTotal(function (err, totalUsers) { if (err) return callback(err) - return callback(null, users.length !== 0) + return callback(null, totalUsers !== 0) }) } diff --git a/server/initializers/constants.js b/server/initializers/constants.js index e0ea188af77ca549bac07b09d06fbecb642390ab..be2e3e9439bcf9bc6d8354a8ded910968b21814d 100644 --- a/server/initializers/constants.js +++ b/server/initializers/constants.js @@ -1,24 +1,103 @@ 'use strict' -// API version of our pod +const config = require('config') +const path = require('path') + +// --------------------------------------------------------------------------- + +// API version const API_VERSION = 'v1' -// Score a pod has when we create it as a friend -const FRIEND_SCORE = { - BASE: 100, - MAX: 1000 +// Number of results by default for the pagination +const PAGINATION_COUNT_DEFAULT = 15 + +// Sortable columns per schema +const SEARCHABLE_COLUMNS = { + VIDEOS: [ 'name', 'magnetUri', 'podUrl', 'author', 'tags' ] } -// Time to wait between requests to the friends (10 min) -let INTERVAL = 600000 +// Sortable columns per schema +const SORTABLE_COLUMNS = { + USERS: [ 'username', '-username', 'createdDate', '-createdDate' ], + VIDEOS: [ 'name', '-name', 'duration', '-duration', 'createdDate', '-createdDate' ] +} const OAUTH_LIFETIME = { ACCESS_TOKEN: 3600 * 4, // 4 hours REFRESH_TOKEN: 1209600 // 2 weeks } -// Number of results by default for the pagination -const PAGINATION_COUNT_DEFAULT = 15 +// --------------------------------------------------------------------------- + +const CONFIG = { + DATABASE: { + DBNAME: 'peertube' + config.get('database.suffix'), + HOST: config.get('database.host'), + PORT: config.get('database.port') + }, + ELECTRON: { + DEBUG: config.get('electron.debug') + }, + STORAGE: { + CERT_DIR: path.join(__dirname, '..', '..', config.get('storage.certs')), + LOG_DIR: path.join(__dirname, '..', '..', config.get('storage.logs')), + UPLOAD_DIR: path.join(__dirname, '..', '..', config.get('storage.uploads')), + THUMBNAILS_DIR: path.join(__dirname, '..', '..', config.get('storage.thumbnails')) + }, + WEBSERVER: { + SCHEME: config.get('webserver.https') === true ? 'https' : 'http', + HOST: config.get('webserver.host'), + PORT: config.get('webserver.port') + } +} +CONFIG.WEBSERVER.URL = CONFIG.WEBSERVER.SCHEME + '://' + CONFIG.WEBSERVER.HOST + ':' + CONFIG.WEBSERVER.PORT + +// --------------------------------------------------------------------------- + +const CONSTRAINTS_FIELDS = { + USERS: { + USERNAME: { min: 3, max: 20 }, // Length + PASSWORD: { min: 6, max: 255 } // Length + }, + VIDEOS: { + NAME: { min: 3, max: 50 }, // Length + DESCRIPTION: { min: 3, max: 250 }, // Length + MAGNET_URI: { min: 10 }, // Length + DURATION: { min: 1, max: 7200 }, // Number + TAGS: { min: 1, max: 3 }, // Number of total tags + TAG: { min: 2, max: 10 }, // Length + THUMBNAIL: { min: 2, max: 30 }, + THUMBNAIL64: { min: 0, max: 20000 } // Bytes + } +} + +// --------------------------------------------------------------------------- + +// Score a pod has when we create it as a friend +const FRIEND_SCORE = { + BASE: 100, + MAX: 1000 +} + +// --------------------------------------------------------------------------- + +const MONGO_MIGRATION_SCRIPTS = [ + { + script: '0005-create-application', + version: 5 + }, + { + script: '0010-users-password', + version: 10 + }, + { + script: '0015-admin-role', + version: 15 + } +] +const LAST_MONGO_SCHEMA_VERSION = 15 + +// --------------------------------------------------------------------------- // Number of points we add/remove from a friend after a successful/bad request const PODS_SCORE = { @@ -26,28 +105,22 @@ const PODS_SCORE = { BONUS: 10 } +// Time to wait between requests to the friends (10 min) +let REQUESTS_INTERVAL = 600000 + // Number of requests in parallel we can make const REQUESTS_IN_PARALLEL = 10 -// How many requests we put in request (request scheduler) +// How many requests we put in request const REQUESTS_LIMIT = 10 // Number of requests to retry for replay requests module const RETRY_REQUESTS = 5 -// Sortable columns per schema -const SEARCHABLE_COLUMNS = { - VIDEOS: [ 'name', 'magnetUri', 'podUrl', 'author', 'tags' ] -} - -// Seeds in parallel we send to electron when "seed all" -// Once a video is in seeding state we seed another video etc -const SEEDS_IN_PARALLEL = 3 +// --------------------------------------------------------------------------- -// Sortable columns per schema -const SORTABLE_COLUMNS = { - VIDEOS: [ 'name', '-name', 'duration', '-duration', 'createdDate', '-createdDate' ] -} +// Password encryption +const BCRYPT_SALT_SIZE = 10 // Express static paths (router) const STATIC_PATHS = { @@ -59,43 +132,47 @@ const STATIC_PATHS = { // Videos thumbnail size const THUMBNAILS_SIZE = '200x110' -const VIDEOS_CONSTRAINTS_FIELDS = { - NAME: { min: 3, max: 50 }, // Length - DESCRIPTION: { min: 3, max: 250 }, // Length - MAGNET_URI: { min: 10 }, // Length - DURATION: { min: 1, max: 7200 }, // Number - AUTHOR: { min: 3, max: 20 }, // Length - TAGS: { min: 1, max: 3 }, // Number of total tags - TAG: { min: 2, max: 10 }, // Length - THUMBNAIL: { min: 2, max: 30 }, - THUMBNAIL64: { min: 0, max: 20000 } // Bytes +const USER_ROLES = { + ADMIN: 'admin', + USER: 'user' } +// Seeds in parallel we send to electron when "seed all" +// Once a video is in seeding state we seed another video etc +const SEEDS_IN_PARALLEL = 3 + +// --------------------------------------------------------------------------- + // Special constants for a test instance if (isTestInstance() === true) { + CONSTRAINTS_FIELDS.VIDEOS.DURATION.max = 14 FRIEND_SCORE.BASE = 20 - INTERVAL = 10000 - VIDEOS_CONSTRAINTS_FIELDS.DURATION.max = 14 + REQUESTS_INTERVAL = 10000 } // --------------------------------------------------------------------------- module.exports = { - API_VERSION: API_VERSION, - FRIEND_SCORE: FRIEND_SCORE, - INTERVAL: INTERVAL, - OAUTH_LIFETIME: OAUTH_LIFETIME, - PAGINATION_COUNT_DEFAULT: PAGINATION_COUNT_DEFAULT, - PODS_SCORE: PODS_SCORE, - REQUESTS_IN_PARALLEL: REQUESTS_IN_PARALLEL, - REQUESTS_LIMIT: REQUESTS_LIMIT, - RETRY_REQUESTS: RETRY_REQUESTS, - SEARCHABLE_COLUMNS: SEARCHABLE_COLUMNS, - SEEDS_IN_PARALLEL: SEEDS_IN_PARALLEL, - SORTABLE_COLUMNS: SORTABLE_COLUMNS, - STATIC_PATHS: STATIC_PATHS, - THUMBNAILS_SIZE: THUMBNAILS_SIZE, - VIDEOS_CONSTRAINTS_FIELDS: VIDEOS_CONSTRAINTS_FIELDS + API_VERSION, + BCRYPT_SALT_SIZE, + CONFIG, + CONSTRAINTS_FIELDS, + FRIEND_SCORE, + LAST_MONGO_SCHEMA_VERSION, + MONGO_MIGRATION_SCRIPTS, + OAUTH_LIFETIME, + PAGINATION_COUNT_DEFAULT, + PODS_SCORE, + REQUESTS_IN_PARALLEL, + REQUESTS_INTERVAL, + REQUESTS_LIMIT, + RETRY_REQUESTS, + SEARCHABLE_COLUMNS, + SEEDS_IN_PARALLEL, + SORTABLE_COLUMNS, + STATIC_PATHS, + THUMBNAILS_SIZE, + USER_ROLES } // --------------------------------------------------------------------------- diff --git a/server/initializers/database.js b/server/initializers/database.js index 8626895ee6fd4cd60d8bfac71065d900399f2c84..45c8a240d79414ceefc71ee54ce494d8fb2ad6a6 100644 --- a/server/initializers/database.js +++ b/server/initializers/database.js @@ -1,30 +1,27 @@ 'use strict' -const config = require('config') const mongoose = require('mongoose') +const constants = require('../initializers/constants') const logger = require('../helpers/logger') // Bootstrap models +require('../models/application') +require('../models/oauth-token') require('../models/user') require('../models/oauth-client') -require('../models/oauth-token') require('../models/pods') require('../models/video') // Request model needs Video model require('../models/request') -const dbname = 'peertube' + config.get('database.suffix') -const host = config.get('database.host') -const port = config.get('database.port') - const database = { connect: connect } function connect () { mongoose.Promise = global.Promise - mongoose.connect('mongodb://' + host + ':' + port + '/' + dbname) + mongoose.connect('mongodb://' + constants.CONFIG.DATABASE.HOST + ':' + constants.CONFIG.DATABASE.PORT + '/' + constants.CONFIG.DATABASE.DBNAME) mongoose.connection.on('error', function () { throw new Error('Mongodb connection error.') }) diff --git a/server/initializers/installer.js b/server/initializers/installer.js index 32830d4dab5d3aa811428074296d4d7b56578a38..1df300ba8d467fb76d3a7b5647fe1efc76559294 100644 --- a/server/initializers/installer.js +++ b/server/initializers/installer.js @@ -9,14 +9,16 @@ const path = require('path') const series = require('async/series') const checker = require('./checker') +const constants = require('./constants') const logger = require('../helpers/logger') const peertubeCrypto = require('../helpers/peertube-crypto') +const Application = mongoose.model('Application') const Client = mongoose.model('OAuthClient') const User = mongoose.model('User') const installer = { - installApplication: installApplication + installApplication } function installApplication (callback) { @@ -34,7 +36,7 @@ function installApplication (callback) { }, function createOAuthUser (callbackAsync) { - createOAuthUserIfNotExist(callbackAsync) + createOAuthAdminIfNotExist(callbackAsync) } ], callback) } @@ -80,7 +82,7 @@ function createOAuthClientIfNotExist (callback) { }) } -function createOAuthUserIfNotExist (callback) { +function createOAuthAdminIfNotExist (callback) { checker.usersExist(function (err, exist) { if (err) return callback(err) @@ -90,6 +92,7 @@ function createOAuthUserIfNotExist (callback) { logger.info('Creating the administrator.') const username = 'root' + const role = constants.USER_ROLES.ADMIN let password = '' // Do not generate a random password for tests @@ -104,17 +107,20 @@ function createOAuthUserIfNotExist (callback) { } const user = new User({ - username: username, - password: password + username, + password, + role }) user.save(function (err, createdUser) { if (err) return callback(err) - logger.info('Username: ' + createdUser.username) - logger.info('User password: ' + createdUser.password) + logger.info('Username: ' + username) + logger.info('User password: ' + password) - return callback(null) + logger.info('Creating Application collection.') + const application = new Application({ mongoSchemaVersion: constants.LAST_MONGO_SCHEMA_VERSION }) + application.save(callback) }) }) } diff --git a/server/initializers/migrations/0005-create-application.js b/server/initializers/migrations/0005-create-application.js new file mode 100644 index 0000000000000000000000000000000000000000..e99dec019c9239bbaad4cb2ec8b6961031b97725 --- /dev/null +++ b/server/initializers/migrations/0005-create-application.js @@ -0,0 +1,17 @@ +/* + Create the application collection in MongoDB. + Used to store the actual MongoDB scheme version. +*/ + +const mongoose = require('mongoose') + +const Application = mongoose.model('Application') + +exports.up = function (callback) { + const application = new Application() + application.save(callback) +} + +exports.down = function (callback) { + throw new Error('Not implemented.') +} diff --git a/server/initializers/migrations/0010-users-password.js b/server/initializers/migrations/0010-users-password.js new file mode 100644 index 0000000000000000000000000000000000000000..a0616a269c8d162fed6611c2560895137a3730ff --- /dev/null +++ b/server/initializers/migrations/0010-users-password.js @@ -0,0 +1,22 @@ +/* + Convert plain user password to encrypted user password. +*/ + +const eachSeries = require('async/eachSeries') +const mongoose = require('mongoose') + +const User = mongoose.model('User') + +exports.up = function (callback) { + User.list(function (err, users) { + if (err) return callback(err) + + eachSeries(users, function (user, callbackEach) { + user.save(callbackEach) + }, callback) + }) +} + +exports.down = function (callback) { + throw new Error('Not implemented.') +} diff --git a/server/initializers/migrations/0015-admin-role.js b/server/initializers/migrations/0015-admin-role.js new file mode 100644 index 0000000000000000000000000000000000000000..af06dca9e751dba55fe3081fb346a008356f4064 --- /dev/null +++ b/server/initializers/migrations/0015-admin-role.js @@ -0,0 +1,16 @@ +/* + Set the admin role to the root user. +*/ + +const constants = require('../constants') +const mongoose = require('mongoose') + +const User = mongoose.model('User') + +exports.up = function (callback) { + User.update({ username: 'root' }, { role: constants.USER_ROLES.ADMIN }, callback) +} + +exports.down = function (callback) { + throw new Error('Not implemented.') +} diff --git a/server/initializers/migrator.js b/server/initializers/migrator.js new file mode 100644 index 0000000000000000000000000000000000000000..6b31d994f55fd7d25ebc6d25dbf2f01ae2c6cf57 --- /dev/null +++ b/server/initializers/migrator.js @@ -0,0 +1,56 @@ +'use strict' + +const eachSeries = require('async/eachSeries') +const mongoose = require('mongoose') +const path = require('path') + +const constants = require('./constants') +const logger = require('../helpers/logger') + +const Application = mongoose.model('Application') + +const migrator = { + migrate: migrate +} + +function migrate (callback) { + Application.loadMongoSchemaVersion(function (err, actualVersion) { + if (err) return callback(err) + + // If there are a new mongo schemas + if (!actualVersion || actualVersion < constants.LAST_MONGO_SCHEMA_VERSION) { + logger.info('Begin migrations.') + + eachSeries(constants.MONGO_MIGRATION_SCRIPTS, function (entity, callbackEach) { + const versionScript = entity.version + + // Do not execute old migration scripts + if (versionScript <= actualVersion) return callbackEach(null) + + // Load the migration module and run it + const migrationScriptName = entity.script + logger.info('Executing %s migration script.', migrationScriptName) + + const migrationScript = require(path.join(__dirname, 'migrations', migrationScriptName)) + migrationScript.up(function (err) { + if (err) return callbackEach(err) + + // Update the new mongo version schema + Application.updateMongoSchemaVersion(versionScript, callbackEach) + }) + }, function (err) { + if (err) return callback(err) + + logger.info('Migrations finished. New mongo version schema: %s', constants.LAST_MONGO_SCHEMA_VERSION) + return callback(null) + }) + } else { + return callback(null) + } + }) +} + +// --------------------------------------------------------------------------- + +module.exports = migrator + diff --git a/server/lib/friends.js b/server/lib/friends.js index 6e1516b94a14ae09d07ea387f53e9264c6e82263..556d2e7733e934f45d34a3d3098991ff56631c16 100644 --- a/server/lib/friends.js +++ b/server/lib/friends.js @@ -1,6 +1,5 @@ 'use strict' -const config = require('config') const each = require('async/each') const eachLimit = require('async/eachLimit') const eachSeries = require('async/eachSeries') @@ -11,24 +10,20 @@ const waterfall = require('async/waterfall') const constants = require('../initializers/constants') const logger = require('../helpers/logger') -const peertubeCrypto = require('../helpers/peertube-crypto') const requests = require('../helpers/requests') -const http = config.get('webserver.https') ? 'https' : 'http' -const host = config.get('webserver.host') -const port = config.get('webserver.port') const Pod = mongoose.model('Pod') const Request = mongoose.model('Request') const Video = mongoose.model('Video') const friends = { - addVideoToFriends: addVideoToFriends, - hasFriends: hasFriends, - getMyCertificate: getMyCertificate, - makeFriends: makeFriends, - quitFriends: quitFriends, - removeVideoToFriends: removeVideoToFriends, - sendOwnedVideosToPod: sendOwnedVideosToPod + addVideoToFriends, + hasFriends, + getMyCertificate, + makeFriends, + quitFriends, + removeVideoToFriends, + sendOwnedVideosToPod } function addVideoToFriends (video) { @@ -45,10 +40,10 @@ function hasFriends (callback) { } function getMyCertificate (callback) { - fs.readFile(peertubeCrypto.getCertDir() + 'peertube.pub', 'utf8', callback) + fs.readFile(constants.CONFIG.STORAGE.CERT_DIR + 'peertube.pub', 'utf8', callback) } -function makeFriends (callback) { +function makeFriends (urls, callback) { const podsScore = {} logger.info('Make friends!') @@ -58,8 +53,6 @@ function makeFriends (callback) { return callback(err) } - const urls = config.get('network.friends') - eachSeries(urls, function (url, callbackEach) { computeForeignPodsList(url, podsScore, callbackEach) }, function (err) { @@ -205,7 +198,12 @@ function getForeignPodsList (url, callback) { request.get(url + path, function (err, response, body) { if (err) return callback(err) - callback(null, JSON.parse(body)) + try { + const json = JSON.parse(body) + return callback(null, json) + } catch (err) { + return callback(err) + } }) } @@ -220,7 +218,7 @@ function makeRequestsToWinningPods (cert, podsList, callback) { url: pod.url + '/api/' + constants.API_VERSION + '/pods/', method: 'POST', json: { - url: http + '://' + host + ':' + port, + url: constants.CONFIG.WEBSERVER.URL, publicKey: cert } } diff --git a/server/lib/oauth-model.js b/server/lib/oauth-model.js index d9f8b175a6df62698fd810ec4c5ec85e3dda618b..45f796796ca85880f67183a4af5853649a62008b 100644 --- a/server/lib/oauth-model.js +++ b/server/lib/oauth-model.js @@ -8,12 +8,12 @@ const User = mongoose.model('User') // See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications const OAuthModel = { - getAccessToken: getAccessToken, - getClient: getClient, - getRefreshToken: getRefreshToken, - getUser: getUser, - revokeToken: revokeToken, - saveToken: saveToken + getAccessToken, + getClient, + getRefreshToken, + getUser, + revokeToken, + saveToken } // --------------------------------------------------------------------------- @@ -41,7 +41,22 @@ function getRefreshToken (refreshToken, callback) { function getUser (username, password) { logger.debug('Getting User (username: ' + username + ', password: ' + password + ').') - return User.getByUsernameAndPassword(username, password) + return User.getByUsername(username).then(function (user) { + if (!user) return null + + // We need to return a promise + return new Promise(function (resolve, reject) { + return user.isPasswordMatch(password, function (err, isPasswordMatch) { + if (err) return reject(err) + + if (isPasswordMatch === true) { + return resolve(user) + } + + return resolve(null) + }) + }) + }) } function revokeToken (token) { diff --git a/server/middlewares/admin.js b/server/middlewares/admin.js new file mode 100644 index 0000000000000000000000000000000000000000..e6d9dc8878a7a4a3725e9f575121c4bef6a44b7a --- /dev/null +++ b/server/middlewares/admin.js @@ -0,0 +1,22 @@ +'use strict' + +const constants = require('../initializers/constants') +const logger = require('../helpers/logger') + +const adminMiddleware = { + ensureIsAdmin +} + +function ensureIsAdmin (req, res, next) { + const user = res.locals.oauth.token.user + if (user.role !== constants.USER_ROLES.ADMIN) { + logger.info('A non admin user is trying to access to an admin content.') + return res.sendStatus(403) + } + + return next() +} + +// --------------------------------------------------------------------------- + +module.exports = adminMiddleware diff --git a/server/middlewares/index.js b/server/middlewares/index.js index 0a233e70106f52cb89b521102973816b1dddd190..3f253e31bec9d3a94d67df5f55125db463884d8c 100644 --- a/server/middlewares/index.js +++ b/server/middlewares/index.js @@ -1,19 +1,23 @@ 'use strict' -const oauth = require('./oauth') -const pagination = require('./pagination') +const adminMiddleware = require('./admin') +const oauthMiddleware = require('./oauth') +const paginationMiddleware = require('./pagination') +const podsMiddleware = require('./pods') const validatorsMiddleware = require('./validators') -const search = require('./search') -const sort = require('./sort') +const searchMiddleware = require('./search') +const sortMiddleware = require('./sort') const secureMiddleware = require('./secure') const middlewares = { - oauth: oauth, - pagination: pagination, - validators: validatorsMiddleware, - search: search, - sort: sort, - secure: secureMiddleware + admin: adminMiddleware, + oauth: oauthMiddleware, + pagination: paginationMiddleware, + pods: podsMiddleware, + search: searchMiddleware, + secure: secureMiddleware, + sort: sortMiddleware, + validators: validatorsMiddleware } // --------------------------------------------------------------------------- diff --git a/server/middlewares/oauth.js b/server/middlewares/oauth.js index 91a99050913eead153e1c281bd5b86c524fc5993..3a02b9b4863c1e16b06502ae781368f13a48b7cb 100644 --- a/server/middlewares/oauth.js +++ b/server/middlewares/oauth.js @@ -12,8 +12,8 @@ const oAuthServer = new OAuthServer({ }) const oAuth = { - authenticate: authenticate, - token: token + authenticate, + token } function authenticate (req, res, next) { @@ -23,7 +23,7 @@ function authenticate (req, res, next) { return res.sendStatus(500) } - if (res.statusCode === 401 || res.statusCode === 400) return res.end() + if (res.statusCode === 401 || res.statusCode === 400 || res.statusCode === 503) return res.end() return next() }) diff --git a/server/middlewares/pagination.js b/server/middlewares/pagination.js index a571e51f6c75ec3a6e7a28e3365998884adb0300..a90f60aab22dc684cd23e1078f6ff6dedea45fe7 100644 --- a/server/middlewares/pagination.js +++ b/server/middlewares/pagination.js @@ -3,7 +3,7 @@ const constants = require('../initializers/constants') const paginationMiddleware = { - setPagination: setPagination + setPagination } function setPagination (req, res, next) { diff --git a/server/middlewares/pods.js b/server/middlewares/pods.js new file mode 100644 index 0000000000000000000000000000000000000000..6e0874a7699833c0d8ed7804c90926c8c1aa61f9 --- /dev/null +++ b/server/middlewares/pods.js @@ -0,0 +1,62 @@ +'use strict' + +const urlModule = require('url') + +const logger = require('../helpers/logger') + +const podsMiddleware = { + setBodyUrlsPort, + setBodyUrlPort +} + +function setBodyUrlsPort (req, res, next) { + for (let i = 0; i < req.body.urls.length; i++) { + const urlWithPort = getUrlWithPort(req.body.urls[i]) + + // Problem with the url parsing? + if (urlWithPort === null) { + return res.sendStatus(500) + } + + req.body.urls[i] = urlWithPort + } + + return next() +} + +function setBodyUrlPort (req, res, next) { + const urlWithPort = getUrlWithPort(req.body.url) + + // Problem with the url parsing? + if (urlWithPort === null) { + return res.sendStatus(500) + } + + req.body.url = urlWithPort + + return next() +} + +// --------------------------------------------------------------------------- + +module.exports = podsMiddleware + +// --------------------------------------------------------------------------- + +function getUrlWithPort (url) { + const urlObj = urlModule.parse(url) + + // Add the port if it is not specified + if (urlObj.port === null) { + if (urlObj.protocol === 'http:') { + return url + ':80' + } else if (urlObj.protocol === 'https:') { + return url + ':443' + } else { + logger.error('Unknown url protocol: ' + urlObj.protocol) + return null + } + } + + return url +} diff --git a/server/middlewares/search.js b/server/middlewares/search.js index 89302a5643fc7d68a14d347a0373b9c7b824c3d9..bb88faf545b23d5585d517aa52ff22cf0702be24 100644 --- a/server/middlewares/search.js +++ b/server/middlewares/search.js @@ -1,7 +1,7 @@ 'use strict' const searchMiddleware = { - setVideosSearch: setVideosSearch + setVideosSearch } function setVideosSearch (req, res, next) { diff --git a/server/middlewares/secure.js b/server/middlewares/secure.js index 9779c14ac2a4e30d53b50bfcccbb26b9192096ff..58f824d14cbb4561f00d42c42d2c214e9640ea03 100644 --- a/server/middlewares/secure.js +++ b/server/middlewares/secure.js @@ -7,10 +7,11 @@ const peertubeCrypto = require('../helpers/peertube-crypto') const Pod = mongoose.model('Pod') const secureMiddleware = { - decryptBody: decryptBody + checkSignature, + decryptBody } -function decryptBody (req, res, next) { +function checkSignature (req, res, next) { const url = req.body.signature.url Pod.loadByUrl(url, function (err, pod) { if (err) { @@ -28,21 +29,30 @@ function decryptBody (req, res, next) { const signatureOk = peertubeCrypto.checkSignature(pod.publicKey, url, req.body.signature.signature) if (signatureOk === true) { - peertubeCrypto.decrypt(req.body.key, req.body.data, function (err, decrypted) { - if (err) { - logger.error('Cannot decrypt data.', { error: err }) - return res.sendStatus(500) - } - - req.body.data = JSON.parse(decrypted) - delete req.body.key - - next() - }) - } else { - logger.error('Signature is not okay in decryptBody for %s.', req.body.signature.url) - return res.sendStatus(403) + return next() + } + + logger.error('Signature is not okay in decryptBody for %s.', req.body.signature.url) + return res.sendStatus(403) + }) +} + +function decryptBody (req, res, next) { + peertubeCrypto.decrypt(req.body.key, req.body.data, function (err, decrypted) { + if (err) { + logger.error('Cannot decrypt data.', { error: err }) + return res.sendStatus(500) } + + try { + req.body.data = JSON.parse(decrypted) + delete req.body.key + } catch (err) { + logger.error('Error in JSON.parse', { error: err }) + return res.sendStatus(500) + } + + next() }) } diff --git a/server/middlewares/sort.js b/server/middlewares/sort.js index 9f52290a6446a1f85dae27da13374082e9963c0f..f0b7274eba11ea244a600f6e0037bef71e0793e6 100644 --- a/server/middlewares/sort.js +++ b/server/middlewares/sort.js @@ -1,7 +1,14 @@ 'use strict' const sortMiddleware = { - setVideosSort: setVideosSort + setUsersSort, + setVideosSort +} + +function setUsersSort (req, res, next) { + if (!req.query.sort) req.query.sort = '-createdDate' + + return next() } function setVideosSort (req, res, next) { diff --git a/server/middlewares/validators/index.js b/server/middlewares/validators/index.js index 0471b3f92c618b803206c8a0c88b445b0599feec..6c3a9c2b4d032c197c09652d64a8f9947c4e0184 100644 --- a/server/middlewares/validators/index.js +++ b/server/middlewares/validators/index.js @@ -4,6 +4,7 @@ const paginationValidators = require('./pagination') const podsValidators = require('./pods') const remoteValidators = require('./remote') const sortValidators = require('./sort') +const usersValidators = require('./users') const videosValidators = require('./videos') const validators = { @@ -11,6 +12,7 @@ const validators = { pods: podsValidators, remote: remoteValidators, sort: sortValidators, + users: usersValidators, videos: videosValidators } diff --git a/server/middlewares/validators/pagination.js b/server/middlewares/validators/pagination.js index 8e9a010536fb7cfd1f2a6d62e98c8e1c888f3845..16682696e5a49cafc9e00c0a74a9c9eea0fdc45b 100644 --- a/server/middlewares/validators/pagination.js +++ b/server/middlewares/validators/pagination.js @@ -4,7 +4,7 @@ const checkErrors = require('./utils').checkErrors const logger = require('../../helpers/logger') const validatorsPagination = { - pagination: pagination + pagination } function pagination (req, res, next) { diff --git a/server/middlewares/validators/pods.js b/server/middlewares/validators/pods.js index fda2e865f58157ebc6a852aa8e130ce0fa8d5a9c..fd3d1e2f243375ab5a2b52ccf07c330614b45d28 100644 --- a/server/middlewares/validators/pods.js +++ b/server/middlewares/validators/pods.js @@ -5,23 +5,29 @@ const friends = require('../../lib/friends') const logger = require('../../helpers/logger') const validatorsPod = { - makeFriends: makeFriends, - podsAdd: podsAdd + makeFriends, + podsAdd } function makeFriends (req, res, next) { - friends.hasFriends(function (err, hasFriends) { - if (err) { - logger.error('Cannot know if we have friends.', { error: err }) - res.sendStatus(500) - } - - if (hasFriends === true) { - // We need to quit our friends before make new ones - res.sendStatus(409) - } else { - return next() - } + req.checkBody('urls', 'Should have an array of unique urls').isEachUniqueUrlValid() + + logger.debug('Checking makeFriends parameters', { parameters: req.body }) + + checkErrors(req, res, function () { + friends.hasFriends(function (err, hasFriends) { + if (err) { + logger.error('Cannot know if we have friends.', { error: err }) + res.sendStatus(500) + } + + if (hasFriends === true) { + // We need to quit our friends before make new ones + res.sendStatus(409) + } else { + return next() + } + }) }) } diff --git a/server/middlewares/validators/remote.js b/server/middlewares/validators/remote.js index 1be119458dd2c37235c2ffb31a561e0d81150fd8..8c29ef8ca40c63407c4353f1a0a3a1f610a435a9 100644 --- a/server/middlewares/validators/remote.js +++ b/server/middlewares/validators/remote.js @@ -4,9 +4,9 @@ const checkErrors = require('./utils').checkErrors const logger = require('../../helpers/logger') const validatorsRemote = { - dataToDecrypt: dataToDecrypt, - remoteVideos: remoteVideos, - signature: signature + dataToDecrypt, + remoteVideos, + signature } function dataToDecrypt (req, res, next) { @@ -19,7 +19,6 @@ function dataToDecrypt (req, res, next) { } function remoteVideos (req, res, next) { - req.checkBody('data').isArray() req.checkBody('data').isEachRemoteVideosValid() logger.debug('Checking remoteVideos parameters', { parameters: req.body }) diff --git a/server/middlewares/validators/sort.js b/server/middlewares/validators/sort.js index 56b63cc8b9ddaffd5c5e1304ec4c0c38683382da..431d3fffd1e8b3062e46bd2eea7e0e3f68a54c0d 100644 --- a/server/middlewares/validators/sort.js +++ b/server/middlewares/validators/sort.js @@ -5,7 +5,18 @@ const constants = require('../../initializers/constants') const logger = require('../../helpers/logger') const validatorsSort = { - videosSort: videosSort + usersSort, + videosSort +} + +function usersSort (req, res, next) { + const sortableColumns = constants.SORTABLE_COLUMNS.USERS + + req.checkQuery('sort', 'Should have correct sortable column').optional().isIn(sortableColumns) + + logger.debug('Checking sort parameters', { parameters: req.query }) + + checkErrors(req, res, next) } function videosSort (req, res, next) { diff --git a/server/middlewares/validators/users.js b/server/middlewares/validators/users.js new file mode 100644 index 0000000000000000000000000000000000000000..d541e91247c0c1125761dff8f2b951836d90d501 --- /dev/null +++ b/server/middlewares/validators/users.js @@ -0,0 +1,67 @@ +'use strict' + +const mongoose = require('mongoose') + +const checkErrors = require('./utils').checkErrors +const logger = require('../../helpers/logger') + +const User = mongoose.model('User') + +const validatorsUsers = { + usersAdd, + usersRemove, + usersUpdate +} + +function usersAdd (req, res, next) { + req.checkBody('username', 'Should have a valid username').isUserUsernameValid() + req.checkBody('password', 'Should have a valid password').isUserPasswordValid() + + logger.debug('Checking usersAdd parameters', { parameters: req.body }) + + checkErrors(req, res, function () { + User.loadByUsername(req.body.username, function (err, user) { + if (err) { + logger.error('Error in usersAdd request validator.', { error: err }) + return res.sendStatus(500) + } + + if (user) return res.status(409).send('User already exists.') + + next() + }) + }) +} + +function usersRemove (req, res, next) { + req.checkParams('id', 'Should have a valid id').notEmpty().isMongoId() + + logger.debug('Checking usersRemove parameters', { parameters: req.params }) + + checkErrors(req, res, function () { + User.loadById(req.params.id, function (err, user) { + if (err) { + logger.error('Error in usersRemove request validator.', { error: err }) + return res.sendStatus(500) + } + + if (!user) return res.status(404).send('User not found') + + next() + }) + }) +} + +function usersUpdate (req, res, next) { + req.checkParams('id', 'Should have a valid id').notEmpty().isMongoId() + // Add old password verification + req.checkBody('password', 'Should have a valid password').isUserPasswordValid() + + logger.debug('Checking usersUpdate parameters', { parameters: req.body }) + + checkErrors(req, res, next) +} + +// --------------------------------------------------------------------------- + +module.exports = validatorsUsers diff --git a/server/middlewares/validators/utils.js b/server/middlewares/validators/utils.js index f6e5b2b38206973cc9d2b26db940f4483f1ca0d1..3741b84c65c4c98ef7e26db287d9b25987704cf5 100644 --- a/server/middlewares/validators/utils.js +++ b/server/middlewares/validators/utils.js @@ -5,7 +5,7 @@ const util = require('util') const logger = require('../../helpers/logger') const validatorsUtils = { - checkErrors: checkErrors + checkErrors } function checkErrors (req, res, next, statusCode) { diff --git a/server/middlewares/validators/videos.js b/server/middlewares/validators/videos.js index 3e2af06fb0c253e45c6746f041764e16bbf212fb..76e943e77b2ed363a1385b8176d4475592a6c992 100644 --- a/server/middlewares/validators/videos.js +++ b/server/middlewares/validators/videos.js @@ -4,20 +4,21 @@ const mongoose = require('mongoose') const checkErrors = require('./utils').checkErrors const constants = require('../../initializers/constants') -const customValidators = require('../../helpers/custom-validators') +const customVideosValidators = require('../../helpers/custom-validators').videos const logger = require('../../helpers/logger') const Video = mongoose.model('Video') const validatorsVideos = { - videosAdd: videosAdd, - videosGet: videosGet, - videosRemove: videosRemove, - videosSearch: videosSearch + videosAdd, + videosGet, + videosRemove, + videosSearch } function videosAdd (req, res, next) { req.checkFiles('videofile[0].originalname', 'Should have an input video').notEmpty() + // TODO: move to constants and function req.checkFiles('videofile[0].mimetype', 'Should have a correct mime type').matches(/video\/(webm)|(mp4)|(ogg)/i) req.checkBody('name', 'Should have a valid name').isVideoNameValid() req.checkBody('description', 'Should have a valid description').isVideoDescriptionValid() @@ -33,8 +34,8 @@ function videosAdd (req, res, next) { return res.status(400).send('Cannot retrieve metadata of the file.') } - if (!customValidators.isVideoDurationValid(duration)) { - return res.status(400).send('Duration of the video file is too big (max: ' + constants.VIDEOS_CONSTRAINTS_FIELDS.DURATION.max + 's).') + if (!customVideosValidators.isVideoDurationValid(duration)) { + return res.status(400).send('Duration of the video file is too big (max: ' + constants.CONSTRAINTS_FIELDS.VIDEOS.DURATION.max + 's).') } videoFile.duration = duration @@ -76,6 +77,7 @@ function videosRemove (req, res, next) { if (!video) return res.status(404).send('Video not found') else if (video.isOwned() === false) return res.status(403).send('Cannot remove video of another pod') + else if (video.author !== res.locals.oauth.token.user.username) return res.status(403).send('Cannot remove video of another user') next() }) diff --git a/server/models/application.js b/server/models/application.js new file mode 100644 index 0000000000000000000000000000000000000000..452ac4283ba71efa0b5942269682ead45f30bed8 --- /dev/null +++ b/server/models/application.js @@ -0,0 +1,31 @@ +const mongoose = require('mongoose') + +// --------------------------------------------------------------------------- + +const ApplicationSchema = mongoose.Schema({ + mongoSchemaVersion: { + type: Number, + default: 0 + } +}) + +ApplicationSchema.statics = { + loadMongoSchemaVersion, + updateMongoSchemaVersion +} + +mongoose.model('Application', ApplicationSchema) + +// --------------------------------------------------------------------------- + +function loadMongoSchemaVersion (callback) { + return this.findOne({}, { mongoSchemaVersion: 1 }, function (err, data) { + const version = data ? data.mongoSchemaVersion : 0 + + return callback(err, version) + }) +} + +function updateMongoSchemaVersion (newVersion, callback) { + return this.update({}, { mongoSchemaVersion: newVersion }, callback) +} diff --git a/server/models/oauth-client.js b/server/models/oauth-client.js index 45834c5a5374328d238f20a78f68a534d36e9cfa..a1aefa985283ad279a6e925e3a918784b4c8e244 100644 --- a/server/models/oauth-client.js +++ b/server/models/oauth-client.js @@ -11,9 +11,9 @@ const OAuthClientSchema = mongoose.Schema({ OAuthClientSchema.path('clientSecret').required(true) OAuthClientSchema.statics = { - getByIdAndSecret: getByIdAndSecret, - list: list, - loadFirstClient: loadFirstClient + getByIdAndSecret, + list, + loadFirstClient } mongoose.model('OAuthClient', OAuthClientSchema) diff --git a/server/models/oauth-token.js b/server/models/oauth-token.js index f6a814c36fc37989c714c4076ed809b1e6e0048e..5beb47bedb29ab230a1f93b87839764acdad4d23 100644 --- a/server/models/oauth-token.js +++ b/server/models/oauth-token.js @@ -18,9 +18,10 @@ OAuthTokenSchema.path('client').required(true) OAuthTokenSchema.path('user').required(true) OAuthTokenSchema.statics = { - getByRefreshTokenAndPopulateClient: getByRefreshTokenAndPopulateClient, - getByTokenAndPopulateUser: getByTokenAndPopulateUser, - getByRefreshToken: getByRefreshToken + getByRefreshTokenAndPopulateClient, + getByTokenAndPopulateUser, + getByRefreshToken, + removeByUserId } mongoose.model('OAuthToken', OAuthTokenSchema) @@ -53,3 +54,7 @@ function getByTokenAndPopulateUser (bearerToken) { function getByRefreshToken (refreshToken) { return this.findOne({ refreshToken: refreshToken }).exec() } + +function removeByUserId (userId, callback) { + return this.remove({ user: userId }, callback) +} diff --git a/server/models/pods.js b/server/models/pods.js index bf43d7b25c14315672fe3c27031791d6763b4d57..4020a96034bed386feaa9723f74eb336ef685e9f 100644 --- a/server/models/pods.js +++ b/server/models/pods.js @@ -11,7 +11,11 @@ const constants = require('../initializers/constants') const PodSchema = mongoose.Schema({ url: String, publicKey: String, - score: { type: Number, max: constants.FRIEND_SCORE.MAX } + score: { type: Number, max: constants.FRIEND_SCORE.MAX }, + createdDate: { + type: Date, + default: Date.now + } }) // TODO: set options (TLD...) @@ -19,16 +23,19 @@ PodSchema.path('url').validate(validator.isURL) PodSchema.path('publicKey').required(true) PodSchema.path('score').validate(function (value) { return !isNaN(value) }) +PodSchema.methods = { + toFormatedJSON +} + PodSchema.statics = { - countAll: countAll, - incrementScores: incrementScores, - list: list, - listAllIds: listAllIds, - listOnlyUrls: listOnlyUrls, - listBadPods: listBadPods, - load: load, - loadByUrl: loadByUrl, - removeAll: removeAll + countAll, + incrementScores, + list, + listAllIds, + listBadPods, + load, + loadByUrl, + removeAll } PodSchema.pre('save', function (next) { @@ -46,6 +53,19 @@ PodSchema.pre('save', function (next) { const Pod = mongoose.model('Pod', PodSchema) +// ------------------------------ METHODS ------------------------------ + +function toFormatedJSON () { + const json = { + id: this._id, + url: this.url, + score: this.score, + createdDate: this.createdDate + } + + return json +} + // ------------------------------ Statics ------------------------------ function countAll (callback) { @@ -69,10 +89,6 @@ function listAllIds (callback) { }) } -function listOnlyUrls (callback) { - return this.find({}, { _id: 0, url: 1 }, callback) -} - function listBadPods (callback) { return this.find({ score: 0 }, callback) } diff --git a/server/models/request.js b/server/models/request.js index 4d521919a11185fd4cb3c522bd1f99be1e6061f3..2d1c5af15802804aa2743970a7e0f3cd0459ad84 100644 --- a/server/models/request.js +++ b/server/models/request.js @@ -14,19 +14,22 @@ const Pod = mongoose.model('Pod') const Video = mongoose.model('Video') let timer = null +let lastRequestTimestamp = 0 // --------------------------------------------------------------------------- const RequestSchema = mongoose.Schema({ request: mongoose.Schema.Types.Mixed, - to: [ { type: mongoose.Schema.Types.ObjectId, ref: 'users' } ] + to: [ { type: mongoose.Schema.Types.ObjectId, ref: 'Pod' } ] }) RequestSchema.statics = { activate, deactivate, flush, - forceSend + forceSend, + list, + remainingMilliSeconds } RequestSchema.pre('save', function (next) { @@ -53,12 +56,19 @@ mongoose.model('Request', RequestSchema) function activate () { logger.info('Requests scheduler activated.') - timer = setInterval(makeRequests.bind(this), constants.INTERVAL) + lastRequestTimestamp = Date.now() + + const self = this + timer = setInterval(function () { + lastRequestTimestamp = Date.now() + makeRequests.call(self) + }, constants.REQUESTS_INTERVAL) } function deactivate () { logger.info('Requests scheduler deactivated.') clearInterval(timer) + timer = null } function flush () { @@ -72,6 +82,16 @@ function forceSend () { makeRequests.call(this) } +function list (callback) { + this.find({ }, callback) +} + +function remainingMilliSeconds () { + if (timer === null) return -1 + + return constants.REQUESTS_INTERVAL - (Date.now() - lastRequestTimestamp) +} + // --------------------------------------------------------------------------- // Make a requests to friends of a certain type @@ -91,7 +111,13 @@ function makeRequest (toPod, requestsToMake, callback) { // The function fire some useful callbacks requests.makeSecureRequest(params, function (err, res) { if (err || (res.statusCode !== 200 && res.statusCode !== 201 && res.statusCode !== 204)) { - logger.error('Error sending secure request to %s pod.', toPod.url, { error: err || new Error('Status code not 20x') }) + logger.error( + 'Error sending secure request to %s pod.', + toPod.url, + { + error: err || new Error('Status code not 20x : ' + res.statusCode) + } + ) return callback(false) } @@ -148,19 +174,14 @@ function makeRequests () { return callbackEach() } - // Maybe the pod is not our friend anymore so simply remove them + // Maybe the pod is not our friend anymore so simply remove it if (!toPod) { + logger.info('Removing %d requests of unexisting pod %s.', requestToMake.ids.length, toPodId) removePodOf.call(self, requestToMake.ids, toPodId) return callbackEach() } makeRequest(toPod, requestToMake.datas, function (success) { - if (err) { - logger.error('Errors when sent request to %s.', toPod.url, { error: err }) - // Do not stop the process just for one error - return callbackEach() - } - if (success === true) { logger.debug('Removing requests for %s pod.', toPodId, { requestsIds: requestToMake.ids }) diff --git a/server/models/user.js b/server/models/user.js index 14ffecbff1ca89ae5139a26946d1b54344fc5e12..a19de7072cd77ae5e881297e775f25c829ab45aa 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -1,28 +1,98 @@ const mongoose = require('mongoose') +const customUsersValidators = require('../helpers/custom-validators').users +const modelUtils = require('./utils') +const peertubeCrypto = require('../helpers/peertube-crypto') + +const OAuthToken = mongoose.model('OAuthToken') + // --------------------------------------------------------------------------- const UserSchema = mongoose.Schema({ + createdDate: { + type: Date, + default: Date.now + }, password: String, - username: String + username: String, + role: String }) -UserSchema.path('password').required(true) -UserSchema.path('username').required(true) +UserSchema.path('password').required(customUsersValidators.isUserPasswordValid) +UserSchema.path('username').required(customUsersValidators.isUserUsernameValid) +UserSchema.path('role').validate(customUsersValidators.isUserRoleValid) + +UserSchema.methods = { + isPasswordMatch, + toFormatedJSON +} UserSchema.statics = { - getByUsernameAndPassword: getByUsernameAndPassword, - list: list + countTotal, + getByUsername, + list, + listForApi, + loadById, + loadByUsername } +UserSchema.pre('save', function (next) { + const user = this + + peertubeCrypto.cryptPassword(this.password, function (err, hash) { + if (err) return next(err) + + user.password = hash + + return next() + }) +}) + +UserSchema.pre('remove', function (next) { + const user = this + + OAuthToken.removeByUserId(user._id, next) +}) + mongoose.model('User', UserSchema) -// --------------------------------------------------------------------------- +// ------------------------------ METHODS ------------------------------ + +function isPasswordMatch (password, callback) { + return peertubeCrypto.comparePassword(password, this.password, callback) +} + +function toFormatedJSON () { + return { + id: this._id, + username: this.username, + role: this.role, + createdDate: this.createdDate + } +} +// ------------------------------ STATICS ------------------------------ + +function countTotal (callback) { + return this.count(callback) +} + +function getByUsername (username) { + return this.findOne({ username: username }) +} function list (callback) { return this.find(callback) } -function getByUsernameAndPassword (username, password) { - return this.findOne({ username: username, password: password }) +function listForApi (start, count, sort, callback) { + const query = {} + return modelUtils.listForApiWithCount.call(this, query, start, count, sort, callback) +} + +function loadById (id, callback) { + return this.findById(id, callback) +} + +function loadByUsername (username, callback) { + return this.findOne({ username: username }, callback) } diff --git a/server/models/utils.js b/server/models/utils.js new file mode 100644 index 0000000000000000000000000000000000000000..e798aabe67a0f67a2648f450f6c0f26879bc1191 --- /dev/null +++ b/server/models/utils.js @@ -0,0 +1,30 @@ +'use strict' + +const parallel = require('async/parallel') + +const utils = { + listForApiWithCount +} + +function listForApiWithCount (query, start, count, sort, callback) { + const self = this + + parallel([ + function (asyncCallback) { + self.find(query).skip(start).limit(count).sort(sort).exec(asyncCallback) + }, + function (asyncCallback) { + self.count(query, asyncCallback) + } + ], function (err, results) { + if (err) return callback(err) + + const data = results[0] + const total = results[1] + return callback(null, data, total) + }) +} + +// --------------------------------------------------------------------------- + +module.exports = utils diff --git a/server/models/video.js b/server/models/video.js index 14e0df6f26cb96873264c276ffe9781782992a04..7d073cffa97cb33b43b03246909f7a6fe4e58b53 100644 --- a/server/models/video.js +++ b/server/models/video.js @@ -11,8 +11,9 @@ const magnet = require('magnet-uri') const mongoose = require('mongoose') const constants = require('../initializers/constants') -const customValidators = require('../helpers/custom-validators') +const customVideosValidators = require('../helpers/custom-validators').videos const logger = require('../helpers/logger') +const modelUtils = require('./utils') const utils = require('../helpers/utils') const http = config.get('webserver.https') === true ? 'https' : 'http' @@ -42,34 +43,35 @@ const VideoSchema = mongoose.Schema({ } }) -VideoSchema.path('name').validate(customValidators.isVideoNameValid) -VideoSchema.path('description').validate(customValidators.isVideoDescriptionValid) -VideoSchema.path('magnetUri').validate(customValidators.isVideoMagnetUriValid) -VideoSchema.path('podUrl').validate(customValidators.isVideoPodUrlValid) -VideoSchema.path('author').validate(customValidators.isVideoAuthorValid) -VideoSchema.path('duration').validate(customValidators.isVideoDurationValid) +VideoSchema.path('name').validate(customVideosValidators.isVideoNameValid) +VideoSchema.path('description').validate(customVideosValidators.isVideoDescriptionValid) +VideoSchema.path('magnetUri').validate(customVideosValidators.isVideoMagnetUriValid) +VideoSchema.path('podUrl').validate(customVideosValidators.isVideoPodUrlValid) +VideoSchema.path('author').validate(customVideosValidators.isVideoAuthorValid) +VideoSchema.path('duration').validate(customVideosValidators.isVideoDurationValid) // The tumbnail can be the path or the data in base 64 // The pre save hook will convert the base 64 data in a file on disk and replace the thumbnail key by the filename VideoSchema.path('thumbnail').validate(function (value) { - return customValidators.isVideoThumbnailValid(value) || customValidators.isVideoThumbnail64Valid(value) + return customVideosValidators.isVideoThumbnailValid(value) || customVideosValidators.isVideoThumbnail64Valid(value) }) -VideoSchema.path('tags').validate(customValidators.isVideoTagsValid) +VideoSchema.path('tags').validate(customVideosValidators.isVideoTagsValid) VideoSchema.methods = { - isOwned: isOwned, - toFormatedJSON: toFormatedJSON, - toRemoteJSON: toRemoteJSON + isOwned, + toFormatedJSON, + toRemoteJSON } VideoSchema.statics = { - getDurationFromFile: getDurationFromFile, - list: list, - listByUrlAndMagnet: listByUrlAndMagnet, - listByUrls: listByUrls, - listOwned: listOwned, - listRemotes: listRemotes, - load: load, - search: search + getDurationFromFile, + listForApi, + listByUrlAndMagnet, + listByUrls, + listOwned, + listOwnedByAuthor, + listRemotes, + load, + search } VideoSchema.pre('remove', function (next) { @@ -101,8 +103,8 @@ VideoSchema.pre('save', function (next) { const tasks = [] if (video.isOwned()) { - const videoPath = pathUtils.join(uploadsDir, video.filename) - this.podUrl = http + '://' + host + ':' + port + const videoPath = pathUtils.join(constants.CONFIG.STORAGE.UPLOAD_DIR, video.filename) + this.podUrl = constants.CONFIG.WEBSERVER.URL tasks.push( // TODO: refractoring @@ -174,7 +176,7 @@ function toRemoteJSON (callback) { const self = this // Convert thumbnail to base64 - fs.readFile(pathUtils.join(thumbnailsDir, this.thumbnail), function (err, thumbnailData) { + fs.readFile(pathUtils.join(constants.CONFIG.STORAGE.THUMBNAILS_DIR, this.thumbnail), function (err, thumbnailData) { if (err) { logger.error('Cannot read the thumbnail of the video') return callback(err) @@ -207,9 +209,9 @@ function getDurationFromFile (videoPath, callback) { }) } -function list (start, count, sort, callback) { +function listForApi (start, count, sort, callback) { const query = {} - return findWithCount.call(this, query, start, count, sort, callback) + return modelUtils.listForApiWithCount.call(this, query, start, count, sort, callback) } function listByUrlAndMagnet (fromUrl, magnetUri, callback) { @@ -225,6 +227,10 @@ function listOwned (callback) { this.find({ filename: { $ne: null } }, callback) } +function listOwnedByAuthor (author, callback) { + this.find({ filename: { $ne: null }, author: author }, callback) +} + function listRemotes (callback) { this.find({ filename: null }, callback) } @@ -242,36 +248,17 @@ function search (value, field, start, count, sort, callback) { query[field] = new RegExp(value) } - findWithCount.call(this, query, start, count, sort, callback) + modelUtils.listForApiWithCount.call(this, query, start, count, sort, callback) } // --------------------------------------------------------------------------- -function findWithCount (query, start, count, sort, callback) { - const self = this - - parallel([ - function (asyncCallback) { - self.find(query).skip(start).limit(count).sort(sort).exec(asyncCallback) - }, - function (asyncCallback) { - self.count(query, asyncCallback) - } - ], function (err, results) { - if (err) return callback(err) - - const videos = results[0] - const totalVideos = results[1] - return callback(null, videos, totalVideos) - }) -} - function removeThumbnail (video, callback) { - fs.unlink(thumbnailsDir + video.thumbnail, callback) + fs.unlink(constants.CONFIG.STORAGE.THUMBNAILS_DIR + video.thumbnail, callback) } function removeFile (video, callback) { - fs.unlink(uploadsDir + video.filename, callback) + fs.unlink(constants.CONFIG.STORAGE.UPLOAD_DIR + video.filename, callback) } // Maybe the torrent is not seeded, but we catch the error to don't stop the removing process @@ -288,7 +275,7 @@ function createThumbnail (videoPath, callback) { }) .thumbnail({ count: 1, - folder: thumbnailsDir, + folder: constants.CONFIG.STORAGE.THUMBNAILS_DIR, size: constants.THUMBNAILS_SIZE, filename: filename }) @@ -300,7 +287,7 @@ function generateThumbnailFromBase64 (data, callback) { if (err) return callback(err) const thumbnailName = randomString + '.jpg' - fs.writeFile(thumbnailsDir + thumbnailName, data, { encoding: 'base64' }, function (err) { + fs.writeFile(constants.CONFIG.STORAGE.THUMBNAILS_DIR + thumbnailName, data, { encoding: 'base64' }, function (err) { if (err) return callback(err) return callback(null, thumbnailName) diff --git a/server/tests/api/check-params.js b/server/tests/api/check-params.js new file mode 100644 index 0000000000000000000000000000000000000000..57b5ca024d797685ab333f21a602343030b9f3c5 --- /dev/null +++ b/server/tests/api/check-params.js @@ -0,0 +1,746 @@ +'use strict' + +const chai = require('chai') +const expect = chai.expect +const pathUtils = require('path') +const request = require('supertest') +const series = require('async/series') + +const loginUtils = require('../utils/login') +const requestsUtils = require('../utils/requests') +const serversUtils = require('../utils/servers') +const usersUtils = require('../utils/users') + +describe('Test parameters validator', function () { + let server = null + let userAccessToken = null + + // --------------------------------------------------------------- + + before(function (done) { + this.timeout(20000) + + series([ + function (next) { + serversUtils.flushTests(next) + }, + function (next) { + serversUtils.runServer(1, function (server1) { + server = server1 + + next() + }) + }, + function (next) { + loginUtils.loginAndGetAccessToken(server, function (err, token) { + if (err) throw err + server.accessToken = token + + next() + }) + } + ], done) + }) + + describe('Of the pods API', function () { + const path = '/api/v1/pods/' + + describe('When making friends', function () { + let userAccessToken = null + + before(function (done) { + usersUtils.createUser(server.url, server.accessToken, 'user1', 'password', function () { + server.user = { + username: 'user1', + password: 'password' + } + + loginUtils.loginAndGetAccessToken(server, function (err, accessToken) { + if (err) throw err + + userAccessToken = accessToken + + done() + }) + }) + }) + + describe('When making friends', function () { + const body = { + urls: [ 'http://localhost:9002' ] + } + + it('Should fail without urls', function (done) { + request(server.url) + .post(path + '/makefriends') + .set('Authorization', 'Bearer ' + server.accessToken) + .set('Accept', 'application/json') + .expect(400, done) + }) + + it('Should fail with urls is not an array', function (done) { + request(server.url) + .post(path + '/makefriends') + .send({ urls: 'http://localhost:9002' }) + .set('Authorization', 'Bearer ' + server.accessToken) + .set('Accept', 'application/json') + .expect(400, done) + }) + + it('Should fail if the array is not composed by urls', function (done) { + request(server.url) + .post(path + '/makefriends') + .send({ urls: [ 'http://localhost:9002', 'localhost:coucou' ] }) + .set('Authorization', 'Bearer ' + server.accessToken) + .set('Accept', 'application/json') + .expect(400, done) + }) + + it('Should fail if urls are not unique', function (done) { + request(server.url) + .post(path + '/makefriends') + .send({ urls: [ 'http://localhost:9002', 'http://localhost:9002' ] }) + .set('Authorization', 'Bearer ' + server.accessToken) + .set('Accept', 'application/json') + .expect(400, done) + }) + + it('Should fail with a invalid token', function (done) { + request(server.url) + .post(path + '/makefriends') + .send(body) + .set('Authorization', 'Bearer faketoken') + .set('Accept', 'application/json') + .expect(401, done) + }) + + it('Should fail if the user is not an administrator', function (done) { + request(server.url) + .post(path + '/makefriends') + .send(body) + .set('Authorization', 'Bearer ' + userAccessToken) + .set('Accept', 'application/json') + .expect(403, done) + }) + }) + + describe('When quitting friends', function () { + it('Should fail with a invalid token', function (done) { + request(server.url) + .get(path + '/quitfriends') + .query({ start: 'hello' }) + .set('Authorization', 'Bearer faketoken') + .set('Accept', 'application/json') + .expect(401, done) + }) + + it('Should fail if the user is not an administrator', function (done) { + request(server.url) + .get(path + '/quitfriends') + .query({ start: 'hello' }) + .set('Authorization', 'Bearer ' + userAccessToken) + .set('Accept', 'application/json') + .expect(403, done) + }) + }) + }) + + describe('When adding a pod', function () { + it('Should fail with nothing', function (done) { + const data = {} + requestsUtils.makePostBodyRequest(server.url, path, null, data, done) + }) + + it('Should fail without public key', function (done) { + const data = { + url: 'http://coucou.com' + } + requestsUtils.makePostBodyRequest(server.url, path, null, data, done) + }) + + it('Should fail without an url', function (done) { + const data = { + publicKey: 'mysuperpublickey' + } + requestsUtils.makePostBodyRequest(server.url, path, null, data, done) + }) + + it('Should fail with an incorrect url', function (done) { + const data = { + url: 'coucou.com', + publicKey: 'mysuperpublickey' + } + requestsUtils.makePostBodyRequest(server.url, path, null, data, function () { + data.url = 'http://coucou' + requestsUtils.makePostBodyRequest(server.url, path, null, data, function () { + data.url = 'coucou' + requestsUtils.makePostBodyRequest(server.url, path, null, data, done) + }) + }) + }) + + it('Should succeed with the correct parameters', function (done) { + const data = { + url: 'http://coucou.com', + publicKey: 'mysuperpublickey' + } + requestsUtils.makePostBodyRequest(server.url, path, null, data, done, 200) + }) + }) + }) + + describe('Of the videos API', function () { + const path = '/api/v1/videos/' + + describe('When listing a video', function () { + it('Should fail with a bad start pagination', function (done) { + request(server.url) + .get(path) + .query({ start: 'hello' }) + .set('Accept', 'application/json') + .expect(400, done) + }) + + it('Should fail with a bad count pagination', function (done) { + request(server.url) + .get(path) + .query({ count: 'hello' }) + .set('Accept', 'application/json') + .expect(400, done) + }) + + it('Should fail with an incorrect sort', function (done) { + request(server.url) + .get(path) + .query({ sort: 'hello' }) + .set('Accept', 'application/json') + .expect(400, done) + }) + }) + + describe('When searching a video', function () { + it('Should fail with nothing', function (done) { + request(server.url) + .get(pathUtils.join(path, 'search')) + .set('Accept', 'application/json') + .expect(400, done) + }) + + it('Should fail with a bad start pagination', function (done) { + request(server.url) + .get(pathUtils.join(path, 'search', 'test')) + .query({ start: 'hello' }) + .set('Accept', 'application/json') + .expect(400, done) + }) + + it('Should fail with a bad count pagination', function (done) { + request(server.url) + .get(pathUtils.join(path, 'search', 'test')) + .query({ count: 'hello' }) + .set('Accept', 'application/json') + .expect(400, done) + }) + + it('Should fail with an incorrect sort', function (done) { + request(server.url) + .get(pathUtils.join(path, 'search', 'test')) + .query({ sort: 'hello' }) + .set('Accept', 'application/json') + .expect(400, done) + }) + }) + + describe('When adding a video', function () { + it('Should fail with nothing', function (done) { + const data = {} + const attach = {} + requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done) + }) + + it('Should fail without name', function (done) { + const data = { + description: 'my super description', + tags: [ 'tag1', 'tag2' ] + } + const attach = { + 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm') + } + requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done) + }) + + it('Should fail with a long name', function (done) { + const data = { + name: 'My very very very very very very very very very very very very very very very very long name', + description: 'my super description', + tags: [ 'tag1', 'tag2' ] + } + const attach = { + 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm') + } + requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done) + }) + + it('Should fail without description', function (done) { + const data = { + name: 'my super name', + tags: [ 'tag1', 'tag2' ] + } + const attach = { + 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm') + } + requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done) + }) + + it('Should fail with a long description', function (done) { + const data = { + name: 'my super name', + description: 'my super description which is very very very very very very very very very very very very very very' + + 'very very very very very very very very very very very very very very very very very very very very very' + + 'very very very very very very very very very very very very very very very long', + tags: [ 'tag1', 'tag2' ] + } + const attach = { + 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm') + } + requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done) + }) + + it('Should fail without tags', function (done) { + const data = { + name: 'my super name', + description: 'my super description' + } + const attach = { + 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm') + } + requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done) + }) + + it('Should fail with too many tags', function (done) { + const data = { + name: 'my super name', + description: 'my super description', + tags: [ 'tag1', 'tag2', 'tag3', 'tag4' ] + } + const attach = { + 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm') + } + requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done) + }) + + it('Should fail with not enough tags', function (done) { + const data = { + name: 'my super name', + description: 'my super description', + tags: [ ] + } + const attach = { + 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm') + } + requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done) + }) + + it('Should fail with a tag length too low', function (done) { + const data = { + name: 'my super name', + description: 'my super description', + tags: [ 'tag1', 't' ] + } + const attach = { + 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm') + } + requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done) + }) + + it('Should fail with a tag length too big', function (done) { + const data = { + name: 'my super name', + description: 'my super description', + tags: [ 'mysupertagtoolong', 'tag1' ] + } + const attach = { + 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm') + } + requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done) + }) + + it('Should fail with malformed tags', function (done) { + const data = { + name: 'my super name', + description: 'my super description', + tags: [ 'my tag' ] + } + const attach = { + 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm') + } + requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done) + }) + + it('Should fail without an input file', function (done) { + const data = { + name: 'my super name', + description: 'my super description', + tags: [ 'tag1', 'tag2' ] + } + const attach = {} + requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done) + }) + + it('Should fail without an incorrect input file', function (done) { + const data = { + name: 'my super name', + description: 'my super description', + tags: [ 'tag1', 'tag2' ] + } + const attach = { + 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short_fake.webm') + } + requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done) + }) + + it('Should fail with a too big duration', function (done) { + const data = { + name: 'my super name', + description: 'my super description', + tags: [ 'tag1', 'tag2' ] + } + const attach = { + 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_too_long.webm') + } + requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done) + }) + + it('Should succeed with the correct parameters', function (done) { + const data = { + name: 'my super name', + description: 'my super description', + tags: [ 'tag1', 'tag2' ] + } + const attach = { + 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm') + } + requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, function () { + attach.videofile = pathUtils.join(__dirname, 'fixtures', 'video_short.mp4') + requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, function () { + attach.videofile = pathUtils.join(__dirname, 'fixtures', 'video_short.ogv') + requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done, 204) + }, false) + }, false) + }) + }) + + describe('When getting a video', function () { + it('Should return the list of the videos with nothing', function (done) { + request(server.url) + .get(path) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) throw err + + expect(res.body.data).to.be.an('array') + expect(res.body.data.length).to.equal(3) + + done() + }) + }) + + it('Should fail without a mongodb id', function (done) { + request(server.url) + .get(path + 'coucou') + .set('Accept', 'application/json') + .expect(400, done) + }) + + it('Should return 404 with an incorrect video', function (done) { + request(server.url) + .get(path + '123456789012345678901234') + .set('Accept', 'application/json') + .expect(404, done) + }) + + it('Should succeed with the correct parameters') + }) + + describe('When removing a video', function () { + it('Should have 404 with nothing', function (done) { + request(server.url) + .delete(path) + .set('Authorization', 'Bearer ' + server.accessToken) + .expect(400, done) + }) + + it('Should fail without a mongodb id', function (done) { + request(server.url) + .delete(path + 'hello') + .set('Authorization', 'Bearer ' + server.accessToken) + .expect(400, done) + }) + + it('Should fail with a video which does not exist', function (done) { + request(server.url) + .delete(path + '123456789012345678901234') + .set('Authorization', 'Bearer ' + server.accessToken) + .expect(404, done) + }) + + it('Should fail with a video of another user') + + it('Should fail with a video of another pod') + + it('Should succeed with the correct parameters') + }) + }) + + describe('Of the users API', function () { + const path = '/api/v1/users/' + let userId = null + + describe('When listing users', function () { + it('Should fail with a bad start pagination', function (done) { + request(server.url) + .get(path) + .query({ start: 'hello' }) + .set('Accept', 'application/json') + .expect(400, done) + }) + + it('Should fail with a bad count pagination', function (done) { + request(server.url) + .get(path) + .query({ count: 'hello' }) + .set('Accept', 'application/json') + .expect(400, done) + }) + + it('Should fail with an incorrect sort', function (done) { + request(server.url) + .get(path) + .query({ sort: 'hello' }) + .set('Accept', 'application/json') + .expect(400, done) + }) + }) + + describe('When adding a new user', function () { + it('Should fail with a too small username', function (done) { + const data = { + username: 'ji', + password: 'mysuperpassword' + } + + requestsUtils.makePostBodyRequest(server.url, path, server.accessToken, data, done) + }) + + it('Should fail with a too long username', function (done) { + const data = { + username: 'mysuperusernamewhichisverylong', + password: 'mysuperpassword' + } + + requestsUtils.makePostBodyRequest(server.url, path, server.accessToken, data, done) + }) + + it('Should fail with an incorrect username', function (done) { + const data = { + username: 'my username', + password: 'mysuperpassword' + } + + requestsUtils.makePostBodyRequest(server.url, path, server.accessToken, data, done) + }) + + it('Should fail with a too small password', function (done) { + const data = { + username: 'myusername', + password: 'bla' + } + + requestsUtils.makePostBodyRequest(server.url, path, server.accessToken, data, done) + }) + + it('Should fail with a too long password', function (done) { + const data = { + username: 'myusername', + password: 'my super long password which is very very very very very very very very very very very very very very' + + 'very very very very very very very very very very very very very very very veryv very very very very' + + 'very very very very very very very very very very very very very very very very very very very very long' + } + + requestsUtils.makePostBodyRequest(server.url, path, server.accessToken, data, done) + }) + + it('Should fail with an non authenticated user', function (done) { + const data = { + username: 'myusername', + password: 'my super password' + } + + requestsUtils.makePostBodyRequest(server.url, path, 'super token', data, done, 401) + }) + + it('Should fail if we add a user with the same username', function (done) { + const data = { + username: 'user1', + password: 'my super password' + } + + requestsUtils.makePostBodyRequest(server.url, path, server.accessToken, data, done, 409) + }) + + it('Should succeed with the correct params', function (done) { + const data = { + username: 'user2', + password: 'my super password' + } + + requestsUtils.makePostBodyRequest(server.url, path, server.accessToken, data, done, 204) + }) + + it('Should fail with a non admin user', function (done) { + server.user = { + username: 'user1', + password: 'password' + } + + loginUtils.loginAndGetAccessToken(server, function (err, accessToken) { + if (err) throw err + + userAccessToken = accessToken + + const data = { + username: 'user3', + password: 'my super password' + } + + requestsUtils.makePostBodyRequest(server.url, path, userAccessToken, data, done, 403) + }) + }) + }) + + describe('When updating a user', function () { + before(function (done) { + usersUtils.getUsersList(server.url, function (err, res) { + if (err) throw err + + userId = res.body.data[1].id + done() + }) + }) + + it('Should fail with a too small password', function (done) { + const data = { + password: 'bla' + } + + requestsUtils.makePutBodyRequest(server.url, path + userId, userAccessToken, data, done) + }) + + it('Should fail with a too long password', function (done) { + const data = { + password: 'my super long password which is very very very very very very very very very very very very very very' + + 'very very very very very very very very very very very very very very very veryv very very very very' + + 'very very very very very very very very very very very very very very very very very very very very long' + } + + requestsUtils.makePutBodyRequest(server.url, path + userId, userAccessToken, data, done) + }) + + it('Should fail with an non authenticated user', function (done) { + const data = { + password: 'my super password' + } + + requestsUtils.makePutBodyRequest(server.url, path + userId, 'super token', data, done, 401) + }) + + it('Should succeed with the correct params', function (done) { + const data = { + password: 'my super password' + } + + requestsUtils.makePutBodyRequest(server.url, path + userId, userAccessToken, data, done, 204) + }) + }) + + describe('When getting my information', function () { + it('Should fail with a non authenticated user', function (done) { + request(server.url) + .get(path + 'me') + .set('Authorization', 'Bearer faketoken') + .set('Accept', 'application/json') + .expect(401, done) + }) + + it('Should success with the correct parameters', function (done) { + request(server.url) + .get(path + 'me') + .set('Authorization', 'Bearer ' + userAccessToken) + .set('Accept', 'application/json') + .expect(200, done) + }) + }) + + describe('When removing an user', function () { + it('Should fail with an incorrect id', function (done) { + request(server.url) + .delete(path + 'bla-bla') + .set('Authorization', 'Bearer ' + server.accessToken) + .expect(400, done) + }) + + it('Should return 404 with a non existing id', function (done) { + request(server.url) + .delete(path + '579f982228c99c221d8092b8') + .set('Authorization', 'Bearer ' + server.accessToken) + .expect(404, done) + }) + }) + }) + + describe('Of the remote videos API', function () { + describe('When making a secure request', function () { + it('Should check a secure request') + }) + + describe('When adding a video', function () { + it('Should check when adding a video') + }) + + describe('When removing a video', function () { + it('Should check when removing a video') + }) + }) + + describe('Of the requests API', function () { + const path = '/api/v1/requests/stats' + + it('Should fail with an non authenticated user', function (done) { + request(server.url) + .get(path) + .set('Accept', 'application/json') + .expect(401, done) + }) + + it('Should fail with a non admin user', function (done) { + request(server.url) + .get(path) + .set('Authorization', 'Bearer ' + userAccessToken) + .set('Accept', 'application/json') + .expect(403, done) + }) + }) + + after(function (done) { + process.kill(-server.app.pid) + + // Keep the logs if the test failed + if (this.ok) { + serversUtils.flushTests(done) + } else { + done() + } + }) +}) diff --git a/server/tests/api/checkParams.js b/server/tests/api/checkParams.js deleted file mode 100644 index c1ba9c2c09d899de0c71297f7a1bdfc268905022..0000000000000000000000000000000000000000 --- a/server/tests/api/checkParams.js +++ /dev/null @@ -1,456 +0,0 @@ -'use strict' - -const chai = require('chai') -const expect = chai.expect -const pathUtils = require('path') -const request = require('supertest') -const series = require('async/series') - -const utils = require('./utils') - -describe('Test parameters validator', function () { - let server = null - - function makePostRequest (path, token, fields, attaches, done, fail) { - let statusCode = 400 - if (fail !== undefined && fail === false) statusCode = 204 - - const req = request(server.url) - .post(path) - .set('Accept', 'application/json') - - if (token) req.set('Authorization', 'Bearer ' + token) - - Object.keys(fields).forEach(function (field) { - const value = fields[field] - - if (Array.isArray(value)) { - for (let i = 0; i < value.length; i++) { - req.field(field + '[' + i + ']', value[i]) - } - } else { - req.field(field, value) - } - }) - - Object.keys(attaches).forEach(function (attach) { - const value = attaches[attach] - req.attach(attach, value) - }) - - req.expect(statusCode, done) - } - - function makePostBodyRequest (path, fields, done, fail) { - let statusCode = 400 - if (fail !== undefined && fail === false) statusCode = 200 - - request(server.url) - .post(path) - .set('Accept', 'application/json') - .send(fields) - .expect(statusCode, done) - } - - // --------------------------------------------------------------- - - before(function (done) { - this.timeout(20000) - - series([ - function (next) { - utils.flushTests(next) - }, - function (next) { - utils.runServer(1, function (server1) { - server = server1 - - next() - }) - }, - function (next) { - utils.loginAndGetAccessToken(server, function (err, token) { - if (err) throw err - server.accessToken = token - - next() - }) - } - ], done) - }) - - describe('Of the pods API', function () { - const path = '/api/v1/pods/' - - describe('When adding a pod', function () { - it('Should fail with nothing', function (done) { - const data = {} - makePostBodyRequest(path, data, done) - }) - - it('Should fail without public key', function (done) { - const data = { - url: 'http://coucou.com' - } - makePostBodyRequest(path, data, done) - }) - - it('Should fail without an url', function (done) { - const data = { - publicKey: 'mysuperpublickey' - } - makePostBodyRequest(path, data, done) - }) - - it('Should fail with an incorrect url', function (done) { - const data = { - url: 'coucou.com', - publicKey: 'mysuperpublickey' - } - makePostBodyRequest(path, data, function () { - data.url = 'http://coucou' - makePostBodyRequest(path, data, function () { - data.url = 'coucou' - makePostBodyRequest(path, data, done) - }) - }) - }) - - it('Should succeed with the correct parameters', function (done) { - const data = { - url: 'http://coucou.com', - publicKey: 'mysuperpublickey' - } - makePostBodyRequest(path, data, done, false) - }) - }) - }) - - describe('Of the videos API', function () { - const path = '/api/v1/videos/' - - describe('When listing a video', function () { - it('Should fail with a bad start pagination', function (done) { - request(server.url) - .get(path) - .query({ start: 'hello' }) - .set('Accept', 'application/json') - .expect(400, done) - }) - - it('Should fail with a bad count pagination', function (done) { - request(server.url) - .get(path) - .query({ count: 'hello' }) - .set('Accept', 'application/json') - .expect(400, done) - }) - - it('Should fail with an incorrect sort', function (done) { - request(server.url) - .get(path) - .query({ sort: 'hello' }) - .set('Accept', 'application/json') - .expect(400, done) - }) - }) - - describe('When searching a video', function () { - it('Should fail with nothing', function (done) { - request(server.url) - .get(pathUtils.join(path, 'search')) - .set('Accept', 'application/json') - .expect(400, done) - }) - - it('Should fail with a bad start pagination', function (done) { - request(server.url) - .get(pathUtils.join(path, 'search', 'test')) - .query({ start: 'hello' }) - .set('Accept', 'application/json') - .expect(400, done) - }) - - it('Should fail with a bad count pagination', function (done) { - request(server.url) - .get(pathUtils.join(path, 'search', 'test')) - .query({ count: 'hello' }) - .set('Accept', 'application/json') - .expect(400, done) - }) - - it('Should fail with an incorrect sort', function (done) { - request(server.url) - .get(pathUtils.join(path, 'search', 'test')) - .query({ sort: 'hello' }) - .set('Accept', 'application/json') - .expect(400, done) - }) - }) - - describe('When adding a video', function () { - it('Should fail with nothing', function (done) { - const data = {} - const attach = {} - makePostRequest(path, server.accessToken, data, attach, done) - }) - - it('Should fail without name', function (done) { - const data = { - description: 'my super description', - tags: [ 'tag1', 'tag2' ] - } - const attach = { - 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm') - } - makePostRequest(path, server.accessToken, data, attach, done) - }) - - it('Should fail with a long name', function (done) { - const data = { - name: 'My very very very very very very very very very very very very very very very very long name', - description: 'my super description', - tags: [ 'tag1', 'tag2' ] - } - const attach = { - 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm') - } - makePostRequest(path, server.accessToken, data, attach, done) - }) - - it('Should fail without description', function (done) { - const data = { - name: 'my super name', - tags: [ 'tag1', 'tag2' ] - } - const attach = { - 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm') - } - makePostRequest(path, server.accessToken, data, attach, done) - }) - - it('Should fail with a long description', function (done) { - const data = { - name: 'my super name', - description: 'my super description which is very very very very very very very very very very very very very very' + - 'very very very very very very very very very very very very very very very very very very very very very' + - 'very very very very very very very very very very very very very very very long', - tags: [ 'tag1', 'tag2' ] - } - const attach = { - 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm') - } - makePostRequest(path, server.accessToken, data, attach, done) - }) - - it('Should fail without tags', function (done) { - const data = { - name: 'my super name', - description: 'my super description' - } - const attach = { - 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm') - } - makePostRequest(path, server.accessToken, data, attach, done) - }) - - it('Should fail with too many tags', function (done) { - const data = { - name: 'my super name', - description: 'my super description', - tags: [ 'tag1', 'tag2', 'tag3', 'tag4' ] - } - const attach = { - 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm') - } - makePostRequest(path, server.accessToken, data, attach, done) - }) - - it('Should fail with not enough tags', function (done) { - const data = { - name: 'my super name', - description: 'my super description', - tags: [ ] - } - const attach = { - 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm') - } - makePostRequest(path, server.accessToken, data, attach, done) - }) - - it('Should fail with a tag length too low', function (done) { - const data = { - name: 'my super name', - description: 'my super description', - tags: [ 'tag1', 't' ] - } - const attach = { - 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm') - } - makePostRequest(path, server.accessToken, data, attach, done) - }) - - it('Should fail with a tag length too big', function (done) { - const data = { - name: 'my super name', - description: 'my super description', - tags: [ 'mysupertagtoolong', 'tag1' ] - } - const attach = { - 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm') - } - makePostRequest(path, server.accessToken, data, attach, done) - }) - - it('Should fail with malformed tags', function (done) { - const data = { - name: 'my super name', - description: 'my super description', - tags: [ 'my tag' ] - } - const attach = { - 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm') - } - makePostRequest(path, server.accessToken, data, attach, done) - }) - - it('Should fail without an input file', function (done) { - const data = { - name: 'my super name', - description: 'my super description', - tags: [ 'tag1', 'tag2' ] - } - const attach = {} - makePostRequest(path, server.accessToken, data, attach, done) - }) - - it('Should fail without an incorrect input file', function (done) { - const data = { - name: 'my super name', - description: 'my super description', - tags: [ 'tag1', 'tag2' ] - } - const attach = { - 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short_fake.webm') - } - makePostRequest(path, server.accessToken, data, attach, done) - }) - - it('Should fail with a too big duration', function (done) { - const data = { - name: 'my super name', - description: 'my super description', - tags: [ 'tag1', 'tag2' ] - } - const attach = { - 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_too_long.webm') - } - makePostRequest(path, server.accessToken, data, attach, done) - }) - - it('Should succeed with the correct parameters', function (done) { - const data = { - name: 'my super name', - description: 'my super description', - tags: [ 'tag1', 'tag2' ] - } - const attach = { - 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm') - } - makePostRequest(path, server.accessToken, data, attach, function () { - attach.videofile = pathUtils.join(__dirname, 'fixtures', 'video_short.mp4') - makePostRequest(path, server.accessToken, data, attach, function () { - attach.videofile = pathUtils.join(__dirname, 'fixtures', 'video_short.ogv') - makePostRequest(path, server.accessToken, data, attach, done, false) - }, false) - }, false) - }) - }) - - describe('When getting a video', function () { - it('Should return the list of the videos with nothing', function (done) { - request(server.url) - .get(path) - .set('Accept', 'application/json') - .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) throw err - - expect(res.body.data).to.be.an('array') - expect(res.body.data.length).to.equal(3) - - done() - }) - }) - - it('Should fail without a mongodb id', function (done) { - request(server.url) - .get(path + 'coucou') - .set('Accept', 'application/json') - .expect(400, done) - }) - - it('Should return 404 with an incorrect video', function (done) { - request(server.url) - .get(path + '123456789012345678901234') - .set('Accept', 'application/json') - .expect(404, done) - }) - - it('Should succeed with the correct parameters') - }) - - describe('When removing a video', function () { - it('Should have 404 with nothing', function (done) { - request(server.url) - .delete(path) - .set('Authorization', 'Bearer ' + server.accessToken) - .expect(400, done) - }) - - it('Should fail without a mongodb id', function (done) { - request(server.url) - .delete(path + 'hello') - .set('Authorization', 'Bearer ' + server.accessToken) - .expect(400, done) - }) - - it('Should fail with a video which does not exist', function (done) { - request(server.url) - .delete(path + '123456789012345678901234') - .set('Authorization', 'Bearer ' + server.accessToken) - .expect(404, done) - }) - - it('Should fail with a video of another pod') - - it('Should succeed with the correct parameters') - }) - }) - - describe('Of the remote videos API', function () { - describe('When making a secure request', function () { - it('Should check a secure request') - }) - - describe('When adding a video', function () { - it('Should check when adding a video') - }) - - describe('When removing a video', function () { - it('Should check when removing a video') - }) - }) - - after(function (done) { - process.kill(-server.app.pid) - - // Keep the logs if the test failed - if (this.ok) { - utils.flushTests(done) - } else { - done() - } - }) -}) diff --git a/server/tests/api/friendsAdvanced.js b/server/tests/api/friends-advanced.js similarity index 88% rename from server/tests/api/friendsAdvanced.js rename to server/tests/api/friends-advanced.js index 603fbc16bec368b4d94c30f8b8af24a8a8fe8414..0d24481ef64c79676fbbbb45307bab08a5544796 100644 --- a/server/tests/api/friendsAdvanced.js +++ b/server/tests/api/friends-advanced.js @@ -5,24 +5,27 @@ const each = require('async/each') const expect = chai.expect const series = require('async/series') -const utils = require('./utils') +const loginUtils = require('../utils/login') +const podsUtils = require('../utils/pods') +const serversUtils = require('../utils/servers') +const videosUtils = require('../utils/videos') describe('Test advanced friends', function () { let servers = [] function makeFriends (podNumber, callback) { const server = servers[podNumber - 1] - return utils.makeFriends(server.url, server.accessToken, callback) + return podsUtils.makeFriends(server.url, server.accessToken, callback) } function quitFriends (podNumber, callback) { const server = servers[podNumber - 1] - return utils.quitFriends(server.url, server.accessToken, callback) + return podsUtils.quitFriends(server.url, server.accessToken, callback) } function getFriendsList (podNumber, end) { const server = servers[podNumber - 1] - return utils.getFriendsList(server.url, end) + return podsUtils.getFriendsList(server.url, end) } function uploadVideo (podNumber, callback) { @@ -32,22 +35,22 @@ describe('Test advanced friends', function () { const fixture = 'video_short.webm' const server = servers[podNumber - 1] - return utils.uploadVideo(server.url, server.accessToken, name, description, tags, fixture, callback) + return videosUtils.uploadVideo(server.url, server.accessToken, name, description, tags, fixture, callback) } function getVideos (podNumber, callback) { - return utils.getVideosList(servers[podNumber - 1].url, callback) + return videosUtils.getVideosList(servers[podNumber - 1].url, callback) } // --------------------------------------------------------------- before(function (done) { this.timeout(30000) - utils.flushAndRunMultipleServers(6, function (serversRun, urlsRun) { + serversUtils.flushAndRunMultipleServers(6, function (serversRun, urlsRun) { servers = serversRun each(servers, function (server, callbackEach) { - utils.loginAndGetAccessToken(server, function (err, accessToken) { + loginUtils.loginAndGetAccessToken(server, function (err, accessToken) { if (err) return callbackEach(err) server.accessToken = accessToken @@ -169,7 +172,7 @@ describe('Test advanced friends', function () { }, // Rerun server 4 function (next) { - utils.runServer(4, function (server) { + serversUtils.runServer(4, function (server) { servers[3].app = server.app next() }) @@ -273,7 +276,7 @@ describe('Test advanced friends', function () { }) if (this.ok) { - utils.flushTests(done) + serversUtils.flushTests(done) } else { done() } diff --git a/server/tests/api/friendsBasic.js b/server/tests/api/friends-basic.js similarity index 70% rename from server/tests/api/friendsBasic.js rename to server/tests/api/friends-basic.js index c74a7f2249154c47e5a142102a4f19176cb7166d..f1393b5ec78dac8d71b4382660e1837a3ce74e58 100644 --- a/server/tests/api/friendsBasic.js +++ b/server/tests/api/friends-basic.js @@ -5,14 +5,17 @@ const each = require('async/each') const expect = chai.expect const series = require('async/series') -const utils = require('./utils') +const loginUtils = require('../utils/login') +const miscsUtils = require('../utils/miscs') +const podsUtils = require('../utils/pods') +const serversUtils = require('../utils/servers') describe('Test basic friends', function () { let servers = [] function makeFriends (podNumber, callback) { const server = servers[podNumber - 1] - return utils.makeFriends(server.url, server.accessToken, callback) + return podsUtils.makeFriends(server.url, server.accessToken, callback) } function testMadeFriends (servers, serverToTest, callback) { @@ -22,7 +25,7 @@ describe('Test basic friends', function () { friends.push(servers[i].url) } - utils.getFriendsList(serverToTest.url, function (err, res) { + podsUtils.getFriendsList(serverToTest.url, function (err, res) { if (err) throw err const result = res.body @@ -43,11 +46,11 @@ describe('Test basic friends', function () { before(function (done) { this.timeout(20000) - utils.flushAndRunMultipleServers(3, function (serversRun, urlsRun) { + serversUtils.flushAndRunMultipleServers(3, function (serversRun, urlsRun) { servers = serversRun each(servers, function (server, callbackEach) { - utils.loginAndGetAccessToken(server, function (err, accessToken) { + loginUtils.loginAndGetAccessToken(server, function (err, accessToken) { if (err) return callbackEach(err) server.accessToken = accessToken @@ -59,7 +62,7 @@ describe('Test basic friends', function () { it('Should not have friends', function (done) { each(servers, function (server, callback) { - utils.getFriendsList(server.url, function (err, res) { + podsUtils.getFriendsList(server.url, function (err, res) { if (err) throw err const result = res.body @@ -71,7 +74,7 @@ describe('Test basic friends', function () { }) it('Should make friends', function (done) { - this.timeout(10000) + this.timeout(40000) series([ // The second pod make friend with the third @@ -80,30 +83,38 @@ describe('Test basic friends', function () { }, // Wait for the request between pods function (next) { - setTimeout(next, 1000) + setTimeout(next, 11000) }, // The second pod should have the third as a friend function (next) { - utils.getFriendsList(servers[1].url, function (err, res) { + podsUtils.getFriendsList(servers[1].url, function (err, res) { if (err) throw err const result = res.body expect(result).to.be.an('array') expect(result.length).to.equal(1) - expect(result[0].url).to.be.equal(servers[2].url) + + const pod = result[0] + expect(pod.url).to.equal(servers[2].url) + expect(pod.score).to.equal(20) + expect(miscsUtils.dateIsValid(pod.createdDate)).to.be.true next() }) }, // Same here, the third pod should have the second pod as a friend function (next) { - utils.getFriendsList(servers[2].url, function (err, res) { + podsUtils.getFriendsList(servers[2].url, function (err, res) { if (err) throw err const result = res.body expect(result).to.be.an('array') expect(result.length).to.equal(1) - expect(result[0].url).to.be.equal(servers[1].url) + + const pod = result[0] + expect(pod.url).to.equal(servers[1].url) + expect(pod.score).to.equal(20) + expect(miscsUtils.dateIsValid(pod.createdDate)).to.be.true next() }) @@ -114,7 +125,7 @@ describe('Test basic friends', function () { }, // Wait for the request between pods function (next) { - setTimeout(next, 1000) + setTimeout(next, 11000) } ], // Now each pod should be friend with the other ones @@ -128,7 +139,7 @@ describe('Test basic friends', function () { it('Should not be allowed to make friend again', function (done) { const server = servers[1] - utils.makeFriends(server.url, server.accessToken, 409, done) + podsUtils.makeFriends(server.url, server.accessToken, 409, done) }) it('Should quit friends of pod 2', function (done) { @@ -136,11 +147,11 @@ describe('Test basic friends', function () { // Pod 1 quit friends function (next) { const server = servers[1] - utils.quitFriends(server.url, server.accessToken, next) + podsUtils.quitFriends(server.url, server.accessToken, next) }, // Pod 1 should not have friends anymore function (next) { - utils.getFriendsList(servers[1].url, function (err, res) { + podsUtils.getFriendsList(servers[1].url, function (err, res) { if (err) throw err const result = res.body @@ -153,7 +164,7 @@ describe('Test basic friends', function () { // Other pods shouldn't have pod 1 too function (next) { each([ servers[0].url, servers[2].url ], function (url, callback) { - utils.getFriendsList(url, function (err, res) { + podsUtils.getFriendsList(url, function (err, res) { if (err) throw err const result = res.body @@ -168,11 +179,15 @@ describe('Test basic friends', function () { }) it('Should allow pod 2 to make friend again', function (done) { + this.timeout(20000) + const server = servers[1] - utils.makeFriends(server.url, server.accessToken, function () { - each(servers, function (server, callback) { - testMadeFriends(servers, server, callback) - }, done) + podsUtils.makeFriends(server.url, server.accessToken, function () { + setTimeout(function () { + each(servers, function (server, callback) { + testMadeFriends(servers, server, callback) + }, done) + }, 11000) }) }) @@ -182,7 +197,7 @@ describe('Test basic friends', function () { }) if (this.ok) { - utils.flushTests(done) + serversUtils.flushTests(done) } else { done() } diff --git a/server/tests/api/index.js b/server/tests/api/index.js index 61c9a7aca4a980d48b652482090f6de1961b150a..11f49e1e2cc3486811c9dfaa56f8ebf49f9ffc36 100644 --- a/server/tests/api/index.js +++ b/server/tests/api/index.js @@ -1,9 +1,9 @@ 'use strict' // Order of the tests we want to execute -require('./checkParams') -require('./friendsBasic') +require('./check-params') +require('./friends-basic') require('./users') -require('./singlePod') -require('./multiplePods') -require('./friendsAdvanced') +require('./single-pod') +require('./multiple-pods') +require('./friends-advanced') diff --git a/server/tests/api/multiplePods.js b/server/tests/api/multiple-pods.js similarity index 82% rename from server/tests/api/multiplePods.js rename to server/tests/api/multiple-pods.js index ac140f6bb4aef7ffb013ad37dead888857c36c07..b86f88c22643965bd17bc8208072f41f982583af 100644 --- a/server/tests/api/multiplePods.js +++ b/server/tests/api/multiple-pods.js @@ -6,7 +6,11 @@ const expect = chai.expect const pathUtils = require('path') const series = require('async/series') -const utils = require('./utils') +const loginUtils = require('../utils/login') +const miscsUtils = require('../utils/miscs') +const podsUtils = require('../utils/pods') +const serversUtils = require('../utils/servers') +const videosUtils = require('../utils/videos') const webtorrent = require(pathUtils.join(__dirname, '../../lib/webtorrent')) webtorrent.silent = true @@ -20,7 +24,7 @@ describe('Test multiple pods', function () { series([ // Run servers function (next) { - utils.flushAndRunMultipleServers(3, function (serversRun) { + serversUtils.flushAndRunMultipleServers(3, function (serversRun) { servers = serversRun next() }) @@ -28,7 +32,7 @@ describe('Test multiple pods', function () { // Get the access tokens function (next) { each(servers, function (server, callbackEach) { - utils.loginAndGetAccessToken(server, function (err, accessToken) { + loginUtils.loginAndGetAccessToken(server, function (err, accessToken) { if (err) return callbackEach(err) server.accessToken = accessToken @@ -39,7 +43,7 @@ describe('Test multiple pods', function () { // The second pod make friend with the third function (next) { const server = servers[1] - utils.makeFriends(server.url, server.accessToken, next) + podsUtils.makeFriends(server.url, server.accessToken, next) }, // Wait for the request between pods function (next) { @@ -48,7 +52,7 @@ describe('Test multiple pods', function () { // Pod 1 make friends too function (next) { const server = servers[0] - utils.makeFriends(server.url, server.accessToken, next) + podsUtils.makeFriends(server.url, server.accessToken, next) }, function (next) { webtorrent.create({ host: 'client', port: '1' }, next) @@ -58,7 +62,7 @@ describe('Test multiple pods', function () { it('Should not have videos for all pods', function (done) { each(servers, function (server, callback) { - utils.getVideosList(server.url, function (err, res) { + videosUtils.getVideosList(server.url, function (err, res) { if (err) throw err const videos = res.body.data @@ -80,7 +84,7 @@ describe('Test multiple pods', function () { const description = 'my super description for pod 1' const tags = [ 'tag1p1', 'tag2p1' ] const file = 'video_short1.webm' - utils.uploadVideo(servers[0].url, servers[0].accessToken, name, description, tags, file, next) + videosUtils.uploadVideo(servers[0].url, servers[0].accessToken, name, description, tags, file, next) }, function (next) { setTimeout(next, 11000) @@ -92,7 +96,7 @@ describe('Test multiple pods', function () { each(servers, function (server, callback) { let baseMagnet = null - utils.getVideosList(server.url, function (err, res) { + videosUtils.getVideosList(server.url, function (err, res) { if (err) throw err const videos = res.body.data @@ -105,7 +109,7 @@ describe('Test multiple pods', function () { expect(video.magnetUri).to.exist expect(video.duration).to.equal(10) expect(video.tags).to.deep.equal([ 'tag1p1', 'tag2p1' ]) - expect(utils.dateIsValid(video.createdDate)).to.be.true + expect(miscsUtils.dateIsValid(video.createdDate)).to.be.true expect(video.author).to.equal('root') if (server.url !== 'http://localhost:9001') { @@ -121,7 +125,7 @@ describe('Test multiple pods', function () { expect(video.magnetUri).to.equal.magnetUri } - utils.testImage(server.url, 'video_short1.webm', video.thumbnailPath, function (err, test) { + videosUtils.testVideoImage(server.url, 'video_short1.webm', video.thumbnailPath, function (err, test) { if (err) throw err expect(test).to.equal(true) @@ -142,7 +146,7 @@ describe('Test multiple pods', function () { const description = 'my super description for pod 2' const tags = [ 'tag1p2', 'tag2p2', 'tag3p2' ] const file = 'video_short2.webm' - utils.uploadVideo(servers[1].url, servers[1].accessToken, name, description, tags, file, next) + videosUtils.uploadVideo(servers[1].url, servers[1].accessToken, name, description, tags, file, next) }, function (next) { setTimeout(next, 11000) @@ -154,7 +158,7 @@ describe('Test multiple pods', function () { each(servers, function (server, callback) { let baseMagnet = null - utils.getVideosList(server.url, function (err, res) { + videosUtils.getVideosList(server.url, function (err, res) { if (err) throw err const videos = res.body.data @@ -167,7 +171,7 @@ describe('Test multiple pods', function () { expect(video.magnetUri).to.exist expect(video.duration).to.equal(5) expect(video.tags).to.deep.equal([ 'tag1p2', 'tag2p2', 'tag3p2' ]) - expect(utils.dateIsValid(video.createdDate)).to.be.true + expect(miscsUtils.dateIsValid(video.createdDate)).to.be.true expect(video.author).to.equal('root') if (server.url !== 'http://localhost:9002') { @@ -183,7 +187,7 @@ describe('Test multiple pods', function () { expect(video.magnetUri).to.equal.magnetUri } - utils.testImage(server.url, 'video_short2.webm', video.thumbnailPath, function (err, test) { + videosUtils.testVideoImage(server.url, 'video_short2.webm', video.thumbnailPath, function (err, test) { if (err) throw err expect(test).to.equal(true) @@ -204,14 +208,14 @@ describe('Test multiple pods', function () { const description = 'my super description for pod 3' const tags = [ 'tag1p3' ] const file = 'video_short3.webm' - utils.uploadVideo(servers[2].url, servers[2].accessToken, name, description, tags, file, next) + videosUtils.uploadVideo(servers[2].url, servers[2].accessToken, name, description, tags, file, next) }, function (next) { const name = 'my super name for pod 3-2' const description = 'my super description for pod 3-2' const tags = [ 'tag2p3', 'tag3p3', 'tag4p3' ] const file = 'video_short.webm' - utils.uploadVideo(servers[2].url, servers[2].accessToken, name, description, tags, file, next) + videosUtils.uploadVideo(servers[2].url, servers[2].accessToken, name, description, tags, file, next) }, function (next) { setTimeout(next, 22000) @@ -222,7 +226,7 @@ describe('Test multiple pods', function () { let baseMagnet = null // All pods should have this video each(servers, function (server, callback) { - utils.getVideosList(server.url, function (err, res) { + videosUtils.getVideosList(server.url, function (err, res) { if (err) throw err const videos = res.body.data @@ -247,7 +251,7 @@ describe('Test multiple pods', function () { expect(video1.duration).to.equal(5) expect(video1.tags).to.deep.equal([ 'tag1p3' ]) expect(video1.author).to.equal('root') - expect(utils.dateIsValid(video1.createdDate)).to.be.true + expect(miscsUtils.dateIsValid(video1.createdDate)).to.be.true expect(video2.name).to.equal('my super name for pod 3-2') expect(video2.description).to.equal('my super description for pod 3-2') @@ -256,7 +260,7 @@ describe('Test multiple pods', function () { expect(video2.duration).to.equal(5) expect(video2.tags).to.deep.equal([ 'tag2p3', 'tag3p3', 'tag4p3' ]) expect(video2.author).to.equal('root') - expect(utils.dateIsValid(video2.createdDate)).to.be.true + expect(miscsUtils.dateIsValid(video2.createdDate)).to.be.true if (server.url !== 'http://localhost:9003') { expect(video1.isLocal).to.be.false @@ -273,11 +277,11 @@ describe('Test multiple pods', function () { expect(video2.magnetUri).to.equal.magnetUri } - utils.testImage(server.url, 'video_short3.webm', video1.thumbnailPath, function (err, test) { + videosUtils.testVideoImage(server.url, 'video_short3.webm', video1.thumbnailPath, function (err, test) { if (err) throw err expect(test).to.equal(true) - utils.testImage(server.url, 'video_short.webm', video2.thumbnailPath, function (err, test) { + videosUtils.testVideoImage(server.url, 'video_short.webm', video2.thumbnailPath, function (err, test) { if (err) throw err expect(test).to.equal(true) @@ -296,7 +300,7 @@ describe('Test multiple pods', function () { // Yes, this could be long this.timeout(200000) - utils.getVideosList(servers[2].url, function (err, res) { + videosUtils.getVideosList(servers[2].url, function (err, res) { if (err) throw err const video = res.body.data[0] @@ -317,7 +321,7 @@ describe('Test multiple pods', function () { // Yes, this could be long this.timeout(200000) - utils.getVideosList(servers[0].url, function (err, res) { + videosUtils.getVideosList(servers[0].url, function (err, res) { if (err) throw err const video = res.body.data[1] @@ -336,7 +340,7 @@ describe('Test multiple pods', function () { // Yes, this could be long this.timeout(200000) - utils.getVideosList(servers[1].url, function (err, res) { + videosUtils.getVideosList(servers[1].url, function (err, res) { if (err) throw err const video = res.body.data[2] @@ -355,7 +359,7 @@ describe('Test multiple pods', function () { // Yes, this could be long this.timeout(200000) - utils.getVideosList(servers[0].url, function (err, res) { + videosUtils.getVideosList(servers[0].url, function (err, res) { if (err) throw err const video = res.body.data[3] @@ -375,10 +379,10 @@ describe('Test multiple pods', function () { series([ function (next) { - utils.removeVideo(servers[2].url, servers[2].accessToken, toRemove[0], next) + videosUtils.removeVideo(servers[2].url, servers[2].accessToken, toRemove[0], next) }, function (next) { - utils.removeVideo(servers[2].url, servers[2].accessToken, toRemove[1], next) + videosUtils.removeVideo(servers[2].url, servers[2].accessToken, toRemove[1], next) }], function (err) { if (err) throw err @@ -389,7 +393,7 @@ describe('Test multiple pods', function () { it('Should have videos 1 and 3 on each pod', function (done) { each(servers, function (server, callback) { - utils.getVideosList(server.url, function (err, res) { + videosUtils.getVideosList(server.url, function (err, res) { if (err) throw err const videos = res.body.data @@ -415,7 +419,7 @@ describe('Test multiple pods', function () { // Keep the logs if the test failed if (this.ok) { - utils.flushTests(done) + serversUtils.flushTests(done) } else { done() } diff --git a/server/tests/api/requests.js b/server/tests/api/requests.js new file mode 100644 index 0000000000000000000000000000000000000000..af36f6e349b540275858cbf0cbfd35c5251cbf89 --- /dev/null +++ b/server/tests/api/requests.js @@ -0,0 +1,128 @@ +'use strict' + +const chai = require('chai') +const each = require('async/each') +const expect = chai.expect +const request = require('supertest') + +const loginUtils = require('../utils/login') +const podsUtils = require('../utils/pods') +const serversUtils = require('../utils/servers') +const videosUtils = require('../utils/videos') + +describe('Test requests stats', function () { + const path = '/api/v1/requests/stats' + let servers = [] + + function uploadVideo (server, callback) { + const name = 'my super video' + const description = 'my super description' + const tags = [ 'tag1', 'tag2' ] + const fixture = 'video_short.webm' + + videosUtils.uploadVideo(server.url, server.accessToken, name, description, tags, fixture, callback) + } + + function getRequestsStats (server, callback) { + request(server.url) + .get(path) + .set('Accept', 'application/json') + .set('Authorization', 'Bearer ' + server.accessToken) + .expect(200) + .end(callback) + } + + // --------------------------------------------------------------- + + before(function (done) { + this.timeout(20000) + serversUtils.flushAndRunMultipleServers(2, function (serversRun, urlsRun) { + servers = serversRun + + each(servers, function (server, callbackEach) { + loginUtils.loginAndGetAccessToken(server, function (err, accessToken) { + if (err) return callbackEach(err) + + server.accessToken = accessToken + callbackEach() + }) + }, function (err) { + if (err) throw err + + const server1 = servers[0] + podsUtils.makeFriends(server1.url, server1.accessToken, done) + }) + }) + }) + + it('Should have a correct timer', function (done) { + const server = servers[0] + + getRequestsStats(server, function (err, res) { + if (err) throw err + + const body = res.body + expect(body.remainingMilliSeconds).to.be.at.least(0) + expect(body.remainingMilliSeconds).to.be.at.most(10000) + + done() + }) + }) + + it('Should have the correct request', function (done) { + this.timeout(15000) + + const server = servers[0] + // Ensure the requests of pod 1 won't be made + servers[1].app.kill() + + uploadVideo(server, function (err) { + if (err) throw err + + getRequestsStats(server, function (err, res) { + if (err) throw err + + const body = res.body + expect(body.requests).to.have.lengthOf(1) + + const request = body.requests[0] + expect(request.to).to.have.lengthOf(1) + expect(request.request.type).to.equal('add') + + // Wait one cycle + setTimeout(done, 10000) + }) + }) + }) + + it('Should have the correct requests', function (done) { + const server = servers[0] + + uploadVideo(server, function (err) { + if (err) throw err + + getRequestsStats(server, function (err, res) { + if (err) throw err + + const body = res.body + expect(body.requests).to.have.lengthOf(2) + + const request = body.requests[1] + expect(request.to).to.have.lengthOf(1) + expect(request.request.type).to.equal('add') + + done() + }) + }) + }) + + after(function (done) { + process.kill(-servers[0].app.pid) + + if (this.ok) { + serversUtils.flushTests(done) + } else { + done() + } + }) +}) diff --git a/server/tests/api/singlePod.js b/server/tests/api/single-pod.js similarity index 76% rename from server/tests/api/singlePod.js rename to server/tests/api/single-pod.js index 6ed719f871528909ca47b0ca9042f85e5c7f1e3f..bdaaee46c9c439e02aaae807bcacb28f6e549a7f 100644 --- a/server/tests/api/singlePod.js +++ b/server/tests/api/single-pod.js @@ -8,11 +8,13 @@ const keyBy = require('lodash/keyBy') const pathUtils = require('path') const series = require('async/series') +const loginUtils = require('../utils/login') +const miscsUtils = require('../utils/miscs') +const serversUtils = require('../utils/servers') +const videosUtils = require('../utils/videos') const webtorrent = require(pathUtils.join(__dirname, '../../lib/webtorrent')) webtorrent.silent = true -const utils = require('./utils') - describe('Test a single pod', function () { let server = null let videoId = -1 @@ -23,16 +25,16 @@ describe('Test a single pod', function () { series([ function (next) { - utils.flushTests(next) + serversUtils.flushTests(next) }, function (next) { - utils.runServer(1, function (server1) { + serversUtils.runServer(1, function (server1) { server = server1 next() }) }, function (next) { - utils.loginAndGetAccessToken(server, function (err, token) { + loginUtils.loginAndGetAccessToken(server, function (err, token) { if (err) throw err server.accessToken = token next() @@ -45,7 +47,7 @@ describe('Test a single pod', function () { }) it('Should not have videos', function (done) { - utils.getVideosList(server.url, function (err, res) { + videosUtils.getVideosList(server.url, function (err, res) { if (err) throw err expect(res.body.total).to.equal(0) @@ -62,14 +64,14 @@ describe('Test a single pod', function () { const description = 'my super description' const tags = [ 'tag1', 'tag2', 'tag3' ] const file = 'video_short.webm' - utils.uploadVideo(server.url, server.accessToken, name, description, tags, file, done) + videosUtils.uploadVideo(server.url, server.accessToken, name, description, tags, file, done) }) it('Should seed the uploaded video', function (done) { // Yes, this could be long this.timeout(60000) - utils.getVideosList(server.url, function (err, res) { + videosUtils.getVideosList(server.url, function (err, res) { if (err) throw err expect(res.body.total).to.equal(1) @@ -84,9 +86,9 @@ describe('Test a single pod', function () { expect(video.author).to.equal('root') expect(video.isLocal).to.be.true expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ]) - expect(utils.dateIsValid(video.createdDate)).to.be.true + expect(miscsUtils.dateIsValid(video.createdDate)).to.be.true - utils.testImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) { + videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) { if (err) throw err expect(test).to.equal(true) @@ -97,8 +99,7 @@ describe('Test a single pod', function () { expect(torrent.files.length).to.equal(1) expect(torrent.files[0].path).to.exist.and.to.not.equal('') - // We remove it because we'll add it again - webtorrent.remove(video.magnetUri, done) + done() }) }) }) @@ -108,7 +109,7 @@ describe('Test a single pod', function () { // Yes, this could be long this.timeout(60000) - utils.getVideo(server.url, videoId, function (err, res) { + videosUtils.getVideo(server.url, videoId, function (err, res) { if (err) throw err const video = res.body @@ -119,25 +120,19 @@ describe('Test a single pod', function () { expect(video.author).to.equal('root') expect(video.isLocal).to.be.true expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ]) - expect(utils.dateIsValid(video.createdDate)).to.be.true + expect(miscsUtils.dateIsValid(video.createdDate)).to.be.true - utils.testImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) { + videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) { if (err) throw err expect(test).to.equal(true) - webtorrent.add(video.magnetUri, function (torrent) { - expect(torrent.files).to.exist - expect(torrent.files.length).to.equal(1) - expect(torrent.files[0].path).to.exist.and.to.not.equal('') - - done() - }) + done() }) }) }) it('Should search the video by name by default', function (done) { - utils.searchVideo(server.url, 'my', function (err, res) { + videosUtils.searchVideo(server.url, 'my', function (err, res) { if (err) throw err expect(res.body.total).to.equal(1) @@ -151,9 +146,9 @@ describe('Test a single pod', function () { expect(video.author).to.equal('root') expect(video.isLocal).to.be.true expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ]) - expect(utils.dateIsValid(video.createdDate)).to.be.true + expect(miscsUtils.dateIsValid(video.createdDate)).to.be.true - utils.testImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) { + videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) { if (err) throw err expect(test).to.equal(true) @@ -163,7 +158,7 @@ describe('Test a single pod', function () { }) it('Should search the video by podUrl', function (done) { - utils.searchVideo(server.url, '9001', 'podUrl', function (err, res) { + videosUtils.searchVideo(server.url, '9001', 'podUrl', function (err, res) { if (err) throw err expect(res.body.total).to.equal(1) @@ -177,9 +172,9 @@ describe('Test a single pod', function () { expect(video.author).to.equal('root') expect(video.isLocal).to.be.true expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ]) - expect(utils.dateIsValid(video.createdDate)).to.be.true + expect(miscsUtils.dateIsValid(video.createdDate)).to.be.true - utils.testImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) { + videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) { if (err) throw err expect(test).to.equal(true) @@ -189,7 +184,7 @@ describe('Test a single pod', function () { }) it('Should search the video by tag', function (done) { - utils.searchVideo(server.url, 'tag1', 'tags', function (err, res) { + videosUtils.searchVideo(server.url, 'tag1', 'tags', function (err, res) { if (err) throw err expect(res.body.total).to.equal(1) @@ -203,9 +198,9 @@ describe('Test a single pod', function () { expect(video.author).to.equal('root') expect(video.isLocal).to.be.true expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ]) - expect(utils.dateIsValid(video.createdDate)).to.be.true + expect(miscsUtils.dateIsValid(video.createdDate)).to.be.true - utils.testImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) { + videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) { if (err) throw err expect(test).to.equal(true) @@ -215,7 +210,7 @@ describe('Test a single pod', function () { }) it('Should not find a search by name by default', function (done) { - utils.searchVideo(server.url, 'hello', function (err, res) { + videosUtils.searchVideo(server.url, 'hello', function (err, res) { if (err) throw err expect(res.body.total).to.equal(0) @@ -227,7 +222,7 @@ describe('Test a single pod', function () { }) it('Should not find a search by author', function (done) { - utils.searchVideo(server.url, 'hello', 'author', function (err, res) { + videosUtils.searchVideo(server.url, 'hello', 'author', function (err, res) { if (err) throw err expect(res.body.total).to.equal(0) @@ -239,7 +234,7 @@ describe('Test a single pod', function () { }) it('Should not find a search by tag', function (done) { - utils.searchVideo(server.url, 'tag', 'tags', function (err, res) { + videosUtils.searchVideo(server.url, 'tag', 'tags', function (err, res) { if (err) throw err expect(res.body.total).to.equal(0) @@ -251,7 +246,7 @@ describe('Test a single pod', function () { }) it('Should remove the video', function (done) { - utils.removeVideo(server.url, server.accessToken, videoId, function (err) { + videosUtils.removeVideo(server.url, server.accessToken, videoId, function (err) { if (err) throw err fs.readdir(pathUtils.join(__dirname, '../../../test1/uploads/'), function (err, files) { @@ -264,7 +259,7 @@ describe('Test a single pod', function () { }) it('Should not have videos', function (done) { - utils.getVideosList(server.url, function (err, res) { + videosUtils.getVideosList(server.url, function (err, res) { if (err) throw err expect(res.body.total).to.equal(0) @@ -286,12 +281,12 @@ describe('Test a single pod', function () { const description = video + ' description' const tags = [ 'tag1', 'tag2', 'tag3' ] - utils.uploadVideo(server.url, server.accessToken, name, description, tags, video, callbackEach) + videosUtils.uploadVideo(server.url, server.accessToken, name, description, tags, video, callbackEach) }, done) }) it('Should have the correct durations', function (done) { - utils.getVideosList(server.url, function (err, res) { + videosUtils.getVideosList(server.url, function (err, res) { if (err) throw err expect(res.body.total).to.equal(6) @@ -312,7 +307,7 @@ describe('Test a single pod', function () { }) it('Should have the correct thumbnails', function (done) { - utils.getVideosList(server.url, function (err, res) { + videosUtils.getVideosList(server.url, function (err, res) { if (err) throw err const videos = res.body.data @@ -323,7 +318,7 @@ describe('Test a single pod', function () { if (err) throw err const videoName = video.name.replace(' name', '') - utils.testImage(server.url, videoName, video.thumbnailPath, function (err, test) { + videosUtils.testVideoImage(server.url, videoName, video.thumbnailPath, function (err, test) { if (err) throw err expect(test).to.equal(true) @@ -334,7 +329,7 @@ describe('Test a single pod', function () { }) it('Should list only the two first videos', function (done) { - utils.getVideosListPagination(server.url, 0, 2, function (err, res) { + videosUtils.getVideosListPagination(server.url, 0, 2, function (err, res) { if (err) throw err const videos = res.body.data @@ -348,7 +343,7 @@ describe('Test a single pod', function () { }) it('Should list only the next three videos', function (done) { - utils.getVideosListPagination(server.url, 2, 3, function (err, res) { + videosUtils.getVideosListPagination(server.url, 2, 3, function (err, res) { if (err) throw err const videos = res.body.data @@ -363,7 +358,7 @@ describe('Test a single pod', function () { }) it('Should list the last video', function (done) { - utils.getVideosListPagination(server.url, 5, 6, function (err, res) { + videosUtils.getVideosListPagination(server.url, 5, 6, function (err, res) { if (err) throw err const videos = res.body.data @@ -376,7 +371,7 @@ describe('Test a single pod', function () { }) it('Should search the first video', function (done) { - utils.searchVideoWithPagination(server.url, 'webm', 'name', 0, 1, function (err, res) { + videosUtils.searchVideoWithPagination(server.url, 'webm', 'name', 0, 1, function (err, res) { if (err) throw err const videos = res.body.data @@ -389,7 +384,7 @@ describe('Test a single pod', function () { }) it('Should search the last two videos', function (done) { - utils.searchVideoWithPagination(server.url, 'webm', 'name', 2, 2, function (err, res) { + videosUtils.searchVideoWithPagination(server.url, 'webm', 'name', 2, 2, function (err, res) { if (err) throw err const videos = res.body.data @@ -403,7 +398,7 @@ describe('Test a single pod', function () { }) it('Should search all the webm videos', function (done) { - utils.searchVideoWithPagination(server.url, 'webm', 'name', 0, 15, function (err, res) { + videosUtils.searchVideoWithPagination(server.url, 'webm', 'name', 0, 15, function (err, res) { if (err) throw err const videos = res.body.data @@ -415,7 +410,7 @@ describe('Test a single pod', function () { }) it('Should search all the root author videos', function (done) { - utils.searchVideoWithPagination(server.url, 'root', 'author', 0, 15, function (err, res) { + videosUtils.searchVideoWithPagination(server.url, 'root', 'author', 0, 15, function (err, res) { if (err) throw err const videos = res.body.data @@ -427,7 +422,7 @@ describe('Test a single pod', function () { }) it('Should search all the 9001 port videos', function (done) { - utils.searchVideoWithPagination(server.url, '9001', 'podUrl', 0, 15, function (err, res) { + videosUtils.searchVideoWithPagination(server.url, '9001', 'podUrl', 0, 15, function (err, res) { if (err) throw err const videos = res.body.data @@ -439,7 +434,7 @@ describe('Test a single pod', function () { }) it('Should search all the localhost videos', function (done) { - utils.searchVideoWithPagination(server.url, 'localhost', 'podUrl', 0, 15, function (err, res) { + videosUtils.searchVideoWithPagination(server.url, 'localhost', 'podUrl', 0, 15, function (err, res) { if (err) throw err const videos = res.body.data @@ -452,7 +447,7 @@ describe('Test a single pod', function () { it('Should search the good magnetUri video', function (done) { const video = videosListBase[0] - utils.searchVideoWithPagination(server.url, encodeURIComponent(video.magnetUri), 'magnetUri', 0, 15, function (err, res) { + videosUtils.searchVideoWithPagination(server.url, encodeURIComponent(video.magnetUri), 'magnetUri', 0, 15, function (err, res) { if (err) throw err const videos = res.body.data @@ -465,7 +460,7 @@ describe('Test a single pod', function () { }) it('Should list and sort by name in descending order', function (done) { - utils.getVideosListSort(server.url, '-name', function (err, res) { + videosUtils.getVideosListSort(server.url, '-name', function (err, res) { if (err) throw err const videos = res.body.data @@ -483,7 +478,7 @@ describe('Test a single pod', function () { }) it('Should search and sort by name in ascending order', function (done) { - utils.searchVideoWithSort(server.url, 'webm', 'name', function (err, res) { + videosUtils.searchVideoWithSort(server.url, 'webm', 'name', function (err, res) { if (err) throw err const videos = res.body.data @@ -505,7 +500,7 @@ describe('Test a single pod', function () { // Keep the logs if the test failed if (this.ok) { - utils.flushTests(done) + serversUtils.flushTests(done) } else { done() } diff --git a/server/tests/api/users.js b/server/tests/api/users.js index 68ba9de336d088e07542772d921d74898c0a896c..c6c892bf279c1a264197bfb1392d0c07db55e908 100644 --- a/server/tests/api/users.js +++ b/server/tests/api/users.js @@ -5,25 +5,30 @@ const expect = chai.expect const pathUtils = require('path') const series = require('async/series') +const loginUtils = require('../utils/login') +const podsUtils = require('../utils/pods') +const serversUtils = require('../utils/servers') +const usersUtils = require('../utils/users') +const videosUtils = require('../utils/videos') const webtorrent = require(pathUtils.join(__dirname, '../../lib/webtorrent')) webtorrent.silent = true -const utils = require('./utils') - describe('Test users', function () { let server = null let accessToken = null - let videoId + let accessTokenUser = null + let videoId = null + let userId = null before(function (done) { this.timeout(20000) series([ function (next) { - utils.flushTests(next) + serversUtils.flushTests(next) }, function (next) { - utils.runServer(1, function (server1) { + serversUtils.runServer(1, function (server1) { server = server1 next() }) @@ -39,7 +44,7 @@ describe('Test users', function () { it('Should not login with an invalid client id', function (done) { const client = { id: 'client', password: server.client.secret } - utils.login(server.url, client, server.user, 400, function (err, res) { + loginUtils.login(server.url, client, server.user, 400, function (err, res) { if (err) throw err expect(res.body.error).to.equal('invalid_client') @@ -49,7 +54,7 @@ describe('Test users', function () { it('Should not login with an invalid client password', function (done) { const client = { id: server.client.id, password: 'coucou' } - utils.login(server.url, client, server.user, 400, function (err, res) { + loginUtils.login(server.url, client, server.user, 400, function (err, res) { if (err) throw err expect(res.body.error).to.equal('invalid_client') @@ -59,7 +64,7 @@ describe('Test users', function () { it('Should not login with an invalid username', function (done) { const user = { username: 'captain crochet', password: server.user.password } - utils.login(server.url, server.client, user, 400, function (err, res) { + loginUtils.login(server.url, server.client, user, 400, function (err, res) { if (err) throw err expect(res.body.error).to.equal('invalid_grant') @@ -69,7 +74,7 @@ describe('Test users', function () { it('Should not login with an invalid password', function (done) { const user = { username: server.user.username, password: 'mewthree' } - utils.login(server.url, server.client, user, 400, function (err, res) { + loginUtils.login(server.url, server.client, user, 400, function (err, res) { if (err) throw err expect(res.body.error).to.equal('invalid_grant') @@ -84,21 +89,21 @@ describe('Test users', function () { const description = 'my super description' const tags = [ 'tag1', 'tag2' ] const video = 'video_short.webm' - utils.uploadVideo(server.url, accessToken, name, description, tags, video, 401, done) + videosUtils.uploadVideo(server.url, accessToken, name, description, tags, video, 401, done) }) it('Should not be able to make friends', function (done) { accessToken = 'mysupertoken' - utils.makeFriends(server.url, accessToken, 401, done) + podsUtils.makeFriends(server.url, accessToken, 401, done) }) it('Should not be able to quit friends', function (done) { accessToken = 'mysupertoken' - utils.quitFriends(server.url, accessToken, 401, done) + podsUtils.quitFriends(server.url, accessToken, 401, done) }) it('Should be able to login', function (done) { - utils.login(server.url, server.client, server.user, 200, function (err, res) { + loginUtils.login(server.url, server.client, server.user, 200, function (err, res) { if (err) throw err accessToken = res.body.access_token @@ -111,10 +116,10 @@ describe('Test users', function () { const description = 'my super description' const tags = [ 'tag1', 'tag2' ] const video = 'video_short.webm' - utils.uploadVideo(server.url, accessToken, name, description, tags, video, 204, function (err, res) { + videosUtils.uploadVideo(server.url, accessToken, name, description, tags, video, 204, function (err, res) { if (err) throw err - utils.getVideosList(server.url, function (err, res) { + videosUtils.getVideosList(server.url, function (err, res) { if (err) throw err const video = res.body.data[0] @@ -131,17 +136,17 @@ describe('Test users', function () { const description = 'my super description 2' const tags = [ 'tag1' ] const video = 'video_short.webm' - utils.uploadVideo(server.url, accessToken, name, description, tags, video, 204, done) + videosUtils.uploadVideo(server.url, accessToken, name, description, tags, video, 204, done) }) it('Should not be able to remove the video with an incorrect token', function (done) { - utils.removeVideo(server.url, 'bad_token', videoId, 401, done) + videosUtils.removeVideo(server.url, 'bad_token', videoId, 401, done) }) it('Should not be able to remove the video with the token of another account') it('Should be able to remove the video with the correct token', function (done) { - utils.removeVideo(server.url, accessToken, videoId, done) + videosUtils.removeVideo(server.url, accessToken, videoId, done) }) it('Should logout (revoke token)') @@ -158,12 +163,179 @@ describe('Test users', function () { it('Should be able to upload a video again') + it('Should be able to create a new user', function (done) { + usersUtils.createUser(server.url, accessToken, 'user_1', 'super password', done) + }) + + it('Should be able to login with this user', function (done) { + server.user = { + username: 'user_1', + password: 'super password' + } + + loginUtils.loginAndGetAccessToken(server, function (err, token) { + if (err) throw err + + accessTokenUser = token + + done() + }) + }) + + it('Should be able to get the user informations', function (done) { + usersUtils.getUserInformation(server.url, accessTokenUser, function (err, res) { + if (err) throw err + + const user = res.body + + expect(user.username).to.equal('user_1') + expect(user.id).to.exist + + done() + }) + }) + + it('Should be able to upload a video with this user', function (done) { + this.timeout(5000) + + const name = 'my super name' + const description = 'my super description' + const tags = [ 'tag1', 'tag2', 'tag3' ] + const file = 'video_short.webm' + videosUtils.uploadVideo(server.url, accessTokenUser, name, description, tags, file, done) + }) + + it('Should list all the users', function (done) { + usersUtils.getUsersList(server.url, function (err, res) { + if (err) throw err + + const result = res.body + const total = result.total + const users = result.data + + expect(total).to.equal(2) + expect(users).to.be.an('array') + expect(users.length).to.equal(2) + + const user = users[0] + expect(user.username).to.equal('user_1') + + const rootUser = users[1] + expect(rootUser.username).to.equal('root') + userId = user.id + + done() + }) + }) + + it('Should list only the first user by username asc', function (done) { + usersUtils.getUsersListPaginationAndSort(server.url, 0, 1, 'username', function (err, res) { + if (err) throw err + + const result = res.body + const total = result.total + const users = result.data + + expect(total).to.equal(2) + expect(users.length).to.equal(1) + + const user = users[0] + expect(user.username).to.equal('root') + + done() + }) + }) + + it('Should list only the first user by username desc', function (done) { + usersUtils.getUsersListPaginationAndSort(server.url, 0, 1, '-username', function (err, res) { + if (err) throw err + + const result = res.body + const total = result.total + const users = result.data + + expect(total).to.equal(2) + expect(users.length).to.equal(1) + + const user = users[0] + expect(user.username).to.equal('user_1') + + done() + }) + }) + + it('Should list only the second user by createdDate desc', function (done) { + usersUtils.getUsersListPaginationAndSort(server.url, 0, 1, '-createdDate', function (err, res) { + if (err) throw err + + const result = res.body + const total = result.total + const users = result.data + + expect(total).to.equal(2) + expect(users.length).to.equal(1) + + const user = users[0] + expect(user.username).to.equal('user_1') + + done() + }) + }) + + it('Should list all the users by createdDate asc', function (done) { + usersUtils.getUsersListPaginationAndSort(server.url, 0, 2, 'createdDate', function (err, res) { + if (err) throw err + + const result = res.body + const total = result.total + const users = result.data + + expect(total).to.equal(2) + expect(users.length).to.equal(2) + + expect(users[0].username).to.equal('root') + expect(users[1].username).to.equal('user_1') + + done() + }) + }) + + it('Should update the user password', function (done) { + usersUtils.updateUser(server.url, userId, accessTokenUser, 'new password', function (err, res) { + if (err) throw err + + server.user.password = 'new password' + loginUtils.login(server.url, server.client, server.user, 200, done) + }) + }) + + it('Should be able to remove this user', function (done) { + usersUtils.removeUser(server.url, userId, accessToken, done) + }) + + it('Should not be able to login with this user', function (done) { + // server.user is already set to user 1 + loginUtils.login(server.url, server.client, server.user, 400, done) + }) + + it('Should not have videos of this user', function (done) { + videosUtils.getVideosList(server.url, function (err, res) { + if (err) throw err + + expect(res.body.total).to.equal(1) + const video = res.body.data[0] + expect(video.author).to.equal('root') + + done() + }) + }) + after(function (done) { process.kill(-server.app.pid) // Keep the logs if the test failed if (this.ok) { - utils.flushTests(done) + serversUtils.flushTests(done) } else { done() } diff --git a/server/tests/api/utils.js b/server/tests/api/utils.js deleted file mode 100644 index 3cc769f265a26066ba85dd668700073d1955767c..0000000000000000000000000000000000000000 --- a/server/tests/api/utils.js +++ /dev/null @@ -1,419 +0,0 @@ -'use strict' - -const childProcess = require('child_process') -const exec = childProcess.exec -const fork = childProcess.fork -const fs = require('fs') -const pathUtils = require('path') -const request = require('supertest') - -const testUtils = { - dateIsValid: dateIsValid, - flushTests: flushTests, - getAllVideosListBy: getAllVideosListBy, - getClient: getClient, - getFriendsList: getFriendsList, - getVideo: getVideo, - getVideosList: getVideosList, - getVideosListPagination: getVideosListPagination, - getVideosListSort: getVideosListSort, - login: login, - loginAndGetAccessToken: loginAndGetAccessToken, - makeFriends: makeFriends, - quitFriends: quitFriends, - removeVideo: removeVideo, - flushAndRunMultipleServers: flushAndRunMultipleServers, - runServer: runServer, - searchVideo: searchVideo, - searchVideoWithPagination: searchVideoWithPagination, - searchVideoWithSort: searchVideoWithSort, - testImage: testImage, - uploadVideo: uploadVideo -} - -// ---------------------- Export functions -------------------- - -function dateIsValid (dateString) { - const dateToCheck = new Date(dateString) - const now = new Date() - - // Check if the interval is more than 2 minutes - if (now - dateToCheck > 120000) return false - - return true -} - -function flushTests (callback) { - exec('npm run clean:server:test', callback) -} - -function getAllVideosListBy (url, end) { - const path = '/api/v1/videos' - - request(url) - .get(path) - .query({ sort: 'createdDate' }) - .query({ start: 0 }) - .query({ count: 10000 }) - .set('Accept', 'application/json') - .expect(200) - .expect('Content-Type', /json/) - .end(end) -} - -function getClient (url, end) { - const path = '/api/v1/users/client' - - request(url) - .get(path) - .set('Accept', 'application/json') - .expect(200) - .expect('Content-Type', /json/) - .end(end) -} - -function getFriendsList (url, end) { - const path = '/api/v1/pods/' - - request(url) - .get(path) - .set('Accept', 'application/json') - .expect(200) - .expect('Content-Type', /json/) - .end(end) -} - -function getVideo (url, id, end) { - const path = '/api/v1/videos/' + id - - request(url) - .get(path) - .set('Accept', 'application/json') - .expect(200) - .expect('Content-Type', /json/) - .end(end) -} - -function getVideosList (url, end) { - const path = '/api/v1/videos' - - request(url) - .get(path) - .query({ sort: 'name' }) - .set('Accept', 'application/json') - .expect(200) - .expect('Content-Type', /json/) - .end(end) -} - -function getVideosListPagination (url, start, count, end) { - const path = '/api/v1/videos' - - request(url) - .get(path) - .query({ start: start }) - .query({ count: count }) - .set('Accept', 'application/json') - .expect(200) - .expect('Content-Type', /json/) - .end(end) -} - -function getVideosListSort (url, sort, end) { - const path = '/api/v1/videos' - - request(url) - .get(path) - .query({ sort: sort }) - .set('Accept', 'application/json') - .expect(200) - .expect('Content-Type', /json/) - .end(end) -} - -function login (url, client, user, expectedStatus, end) { - if (!end) { - end = expectedStatus - expectedStatus = 200 - } - - const path = '/api/v1/users/token' - - const body = { - client_id: client.id, - client_secret: client.secret, - username: user.username, - password: user.password, - response_type: 'code', - grant_type: 'password', - scope: 'upload' - } - - request(url) - .post(path) - .type('form') - .send(body) - .expect(expectedStatus) - .end(end) -} - -function loginAndGetAccessToken (server, callback) { - login(server.url, server.client, server.user, 200, function (err, res) { - if (err) return callback(err) - - return callback(null, res.body.access_token) - }) -} - -function makeFriends (url, accessToken, expectedStatus, callback) { - if (!callback) { - callback = expectedStatus - expectedStatus = 204 - } - - const path = '/api/v1/pods/makefriends' - - // The first pod make friend with the third - request(url) - .get(path) - .set('Accept', 'application/json') - .set('Authorization', 'Bearer ' + accessToken) - .expect(expectedStatus) - .end(function (err, res) { - if (err) throw err - - // Wait for the request between pods - setTimeout(callback, 1000) - }) -} - -function quitFriends (url, accessToken, expectedStatus, callback) { - if (!callback) { - callback = expectedStatus - expectedStatus = 204 - } - - const path = '/api/v1/pods/quitfriends' - - // The first pod make friend with the third - request(url) - .get(path) - .set('Accept', 'application/json') - .set('Authorization', 'Bearer ' + accessToken) - .expect(expectedStatus) - .end(function (err, res) { - if (err) throw err - - // Wait for the request between pods - setTimeout(callback, 1000) - }) -} - -function removeVideo (url, token, id, expectedStatus, end) { - if (!end) { - end = expectedStatus - expectedStatus = 204 - } - - const path = '/api/v1/videos' - - request(url) - .delete(path + '/' + id) - .set('Accept', 'application/json') - .set('Authorization', 'Bearer ' + token) - .expect(expectedStatus) - .end(end) -} - -function flushAndRunMultipleServers (totalServers, serversRun) { - let apps = [] - let urls = [] - let i = 0 - - function anotherServerDone (number, app, url) { - apps[number - 1] = app - urls[number - 1] = url - i++ - if (i === totalServers) { - serversRun(apps, urls) - } - } - - flushTests(function () { - for (let j = 1; j <= totalServers; j++) { - // For the virtual buffer - setTimeout(function () { - runServer(j, function (app, url) { - anotherServerDone(j, app, url) - }) - }, 1000 * j) - } - }) -} - -function runServer (number, callback) { - const server = { - app: null, - url: `http://localhost:${9000 + number}`, - client: { - id: null, - secret: null - }, - user: { - username: null, - password: null - } - } - - // These actions are async so we need to be sure that they have both been done - const serverRunString = { - 'Connected to mongodb': false, - 'Server listening on port': false - } - - const regexps = { - client_id: 'Client id: ([a-f0-9]+)', - client_secret: 'Client secret: (.+)', - user_username: 'Username: (.+)', - user_password: 'User password: (.+)' - } - - // Share the environment - const env = Object.create(process.env) - env.NODE_ENV = 'test' - env.NODE_APP_INSTANCE = number - const options = { - silent: true, - env: env, - detached: true - } - - server.app = fork(pathUtils.join(__dirname, '../../../server.js'), [], options) - server.app.stdout.on('data', function onStdout (data) { - let dontContinue = false - - // Capture things if we want to - for (const key of Object.keys(regexps)) { - const regexp = regexps[key] - const matches = data.toString().match(regexp) - if (matches !== null) { - if (key === 'client_id') server.client.id = matches[1] - else if (key === 'client_secret') server.client.secret = matches[1] - else if (key === 'user_username') server.user.username = matches[1] - else if (key === 'user_password') server.user.password = matches[1] - } - } - - // Check if all required sentences are here - for (const key of Object.keys(serverRunString)) { - if (data.toString().indexOf(key) !== -1) serverRunString[key] = true - if (serverRunString[key] === false) dontContinue = true - } - - // If no, there is maybe one thing not already initialized (mongodb...) - if (dontContinue === true) return - - server.app.stdout.removeListener('data', onStdout) - callback(server) - }) -} - -function searchVideo (url, search, field, end) { - if (!end) { - end = field - field = null - } - - const path = '/api/v1/videos' - const req = request(url) - .get(path + '/search/' + search) - .set('Accept', 'application/json') - - if (field) req.query({ field: field }) - req.expect(200) - .expect('Content-Type', /json/) - .end(end) -} - -function searchVideoWithPagination (url, search, field, start, count, end) { - const path = '/api/v1/videos' - - request(url) - .get(path + '/search/' + search) - .query({ start: start }) - .query({ count: count }) - .query({ field: field }) - .set('Accept', 'application/json') - .expect(200) - .expect('Content-Type', /json/) - .end(end) -} - -function searchVideoWithSort (url, search, sort, end) { - const path = '/api/v1/videos' - - request(url) - .get(path + '/search/' + search) - .query({ sort: sort }) - .set('Accept', 'application/json') - .expect(200) - .expect('Content-Type', /json/) - .end(end) -} - -function testImage (url, videoName, imagePath, callback) { - // Don't test images if the node env is not set - // Because we need a special ffmpeg version for this test - if (process.env.NODE_TEST_IMAGE) { - request(url) - .get(imagePath) - .expect(200) - .end(function (err, res) { - if (err) return callback(err) - - fs.readFile(pathUtils.join(__dirname, 'fixtures', videoName + '.jpg'), function (err, data) { - if (err) return callback(err) - - callback(null, data.equals(res.body)) - }) - }) - } else { - console.log('Do not test images. Enable it by setting NODE_TEST_IMAGE env variable.') - callback(null, true) - } -} - -function uploadVideo (url, accessToken, name, description, tags, fixture, specialStatus, end) { - if (!end) { - end = specialStatus - specialStatus = 204 - } - - const path = '/api/v1/videos' - - const req = request(url) - .post(path) - .set('Accept', 'application/json') - .set('Authorization', 'Bearer ' + accessToken) - .field('name', name) - .field('description', description) - - for (let i = 0; i < tags.length; i++) { - req.field('tags[' + i + ']', tags[i]) - } - - let filepath = '' - if (pathUtils.isAbsolute(fixture)) { - filepath = fixture - } else { - filepath = pathUtils.join(__dirname, 'fixtures', fixture) - } - - req.attach('videofile', filepath) - .expect(specialStatus) - .end(end) -} - -// --------------------------------------------------------------------------- - -module.exports = testUtils diff --git a/server/tests/real-world/real-world.js b/server/tests/real-world/real-world.js index b28796852911087a16c5c725df67e28511ca88c3..dba1970c58f9169e39cae38497b3b388bbfc02eb 100644 --- a/server/tests/real-world/real-world.js +++ b/server/tests/real-world/real-world.js @@ -1,6 +1,6 @@ 'use strict' -const each = require('each') +const each = require('async/each') const isEqual = require('lodash/isEqual') const program = require('commander') const series = require('async/series') @@ -8,7 +8,10 @@ const series = require('async/series') process.env.NODE_ENV = 'test' const constants = require('../../initializers/constants') -const utils = require('../api/utils') +const loginUtils = require('../utils/login') +const podsUtils = require('../utils/pods') +const serversUtils = require('../utils/servers') +const videosUtils = require('../utils/videos') program .option('-c, --create [weight]', 'Weight for creating videos') @@ -97,7 +100,7 @@ function runServers (numberOfPods, callback) { series([ // Run servers function (next) { - utils.flushAndRunMultipleServers(numberOfPods, function (serversRun) { + serversUtils.flushAndRunMultipleServers(numberOfPods, function (serversRun) { servers = serversRun next() }) @@ -105,7 +108,7 @@ function runServers (numberOfPods, callback) { // Get the access tokens function (next) { each(servers, function (server, callbackEach) { - utils.loginAndGetAccessToken(server, function (err, accessToken) { + loginUtils.loginAndGetAccessToken(server, function (err, accessToken) { if (err) return callbackEach(err) server.accessToken = accessToken @@ -115,26 +118,26 @@ function runServers (numberOfPods, callback) { }, function (next) { const server = servers[1] - utils.makeFriends(server.url, server.accessToken, next) + podsUtils.makeFriends(server.url, server.accessToken, next) }, function (next) { const server = servers[0] - utils.makeFriends(server.url, server.accessToken, next) + podsUtils.makeFriends(server.url, server.accessToken, next) }, function (next) { setTimeout(next, 1000) }, function (next) { const server = servers[3] - utils.makeFriends(server.url, server.accessToken, next) + podsUtils.makeFriends(server.url, server.accessToken, next) }, function (next) { const server = servers[5] - utils.makeFriends(server.url, server.accessToken, next) + podsUtils.makeFriends(server.url, server.accessToken, next) }, function (next) { const server = servers[4] - utils.makeFriends(server.url, server.accessToken, next) + podsUtils.makeFriends(server.url, server.accessToken, next) }, function (next) { setTimeout(next, 1000) @@ -151,7 +154,7 @@ function exitServers (servers, callback) { if (server.app) process.kill(-server.app.pid) }) - if (flushAtExit) utils.flushTests(callback) + if (flushAtExit) serversUtils.flushTests(callback) } function upload (servers, numServer, callback) { @@ -164,13 +167,13 @@ function upload (servers, numServer, callback) { console.log('Upload video to server ' + numServer) - utils.uploadVideo(servers[numServer].url, servers[numServer].accessToken, name, description, tags, file, callback) + videosUtils.uploadVideo(servers[numServer].url, servers[numServer].accessToken, name, description, tags, file, callback) } function remove (servers, numServer, callback) { if (!callback) callback = function () {} - utils.getVideosList(servers[numServer].url, function (err, res) { + videosUtils.getVideosList(servers[numServer].url, function (err, res) { if (err) throw err const videos = res.body.data @@ -179,14 +182,14 @@ function remove (servers, numServer, callback) { const toRemove = videos[getRandomInt(0, videos.length)].id console.log('Removing video from server ' + numServer) - utils.removeVideo(servers[numServer].url, servers[numServer].accessToken, toRemove, callback) + videosUtils.removeVideo(servers[numServer].url, servers[numServer].accessToken, toRemove, callback) }) } function checkIntegrity (servers, callback) { const videos = [] each(servers, function (server, callback) { - utils.getAllVideosListBy(server.url, function (err, res) { + videosUtils.getAllVideosListBy(server.url, function (err, res) { if (err) throw err const serverVideos = res.body.data for (const serverVideo of serverVideos) { diff --git a/server/tests/utils/clients.js b/server/tests/utils/clients.js new file mode 100644 index 0000000000000000000000000000000000000000..e3ded493e69cf050f2d4d8b9cf3abe140d24a352 --- /dev/null +++ b/server/tests/utils/clients.js @@ -0,0 +1,24 @@ +'use strict' + +const request = require('supertest') + +const clientsUtils = { + getClient: getClient +} + +// ---------------------- Export functions -------------------- + +function getClient (url, end) { + const path = '/api/v1/users/client' + + request(url) + .get(path) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .end(end) +} + +// --------------------------------------------------------------------------- + +module.exports = clientsUtils diff --git a/server/tests/utils/login.js b/server/tests/utils/login.js new file mode 100644 index 0000000000000000000000000000000000000000..465564e141b96eb2b8caf84730e2506c3bd1b43a --- /dev/null +++ b/server/tests/utils/login.js @@ -0,0 +1,48 @@ +'use strict' + +const request = require('supertest') + +const loginUtils = { + login, + loginAndGetAccessToken +} + +// ---------------------- Export functions -------------------- + +function login (url, client, user, expectedStatus, end) { + if (!end) { + end = expectedStatus + expectedStatus = 200 + } + + const path = '/api/v1/users/token' + + const body = { + client_id: client.id, + client_secret: client.secret, + username: user.username, + password: user.password, + response_type: 'code', + grant_type: 'password', + scope: 'upload' + } + + request(url) + .post(path) + .type('form') + .send(body) + .expect(expectedStatus) + .end(end) +} + +function loginAndGetAccessToken (server, callback) { + login(server.url, server.client, server.user, 200, function (err, res) { + if (err) return callback(err) + + return callback(null, res.body.access_token) + }) +} + +// --------------------------------------------------------------------------- + +module.exports = loginUtils diff --git a/server/tests/utils/miscs.js b/server/tests/utils/miscs.js new file mode 100644 index 0000000000000000000000000000000000000000..4ceff65df5cb09e4bf316979bbc04ac6a1114cae --- /dev/null +++ b/server/tests/utils/miscs.js @@ -0,0 +1,21 @@ +'use strict' + +const miscsUtils = { + dateIsValid +} + +// ---------------------- Export functions -------------------- + +function dateIsValid (dateString) { + const dateToCheck = new Date(dateString) + const now = new Date() + + // Check if the interval is more than 2 minutes + if (now - dateToCheck > 120000) return false + + return true +} + +// --------------------------------------------------------------------------- + +module.exports = miscsUtils diff --git a/server/tests/utils/pods.js b/server/tests/utils/pods.js new file mode 100644 index 0000000000000000000000000000000000000000..a8551a49dc108358c9b4da3650d3921e81d4f259 --- /dev/null +++ b/server/tests/utils/pods.js @@ -0,0 +1,95 @@ +'use strict' + +const request = require('supertest') + +const podsUtils = { + getFriendsList, + makeFriends, + quitFriends +} + +// ---------------------- Export functions -------------------- + +function getFriendsList (url, end) { + const path = '/api/v1/pods/' + + request(url) + .get(path) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .end(end) +} + +function makeFriends (url, accessToken, expectedStatus, end) { + if (!end) { + end = expectedStatus + expectedStatus = 204 + } + + // Which pod makes friends with which pod + const friendsMatrix = { + 'http://localhost:9001': [ + 'http://localhost:9002' + ], + 'http://localhost:9002': [ + 'http://localhost:9003' + ], + 'http://localhost:9003': [ + 'http://localhost:9001' + ], + 'http://localhost:9004': [ + 'http://localhost:9002' + ], + 'http://localhost:9005': [ + 'http://localhost:9001', + 'http://localhost:9004' + ], + 'http://localhost:9006': [ + 'http://localhost:9001', + 'http://localhost:9002', + 'http://localhost:9003' + ] + } + const path = '/api/v1/pods/makefriends' + + // The first pod make friend with the third + request(url) + .post(path) + .set('Accept', 'application/json') + .set('Authorization', 'Bearer ' + accessToken) + .send({ 'urls': friendsMatrix[url] }) + .expect(expectedStatus) + .end(function (err, res) { + if (err) throw err + + // Wait for the request between pods + setTimeout(end, 1000) + }) +} + +function quitFriends (url, accessToken, expectedStatus, end) { + if (!end) { + end = expectedStatus + expectedStatus = 204 + } + + const path = '/api/v1/pods/quitfriends' + + // The first pod make friend with the third + request(url) + .get(path) + .set('Accept', 'application/json') + .set('Authorization', 'Bearer ' + accessToken) + .expect(expectedStatus) + .end(function (err, res) { + if (err) throw err + + // Wait for the request between pods + setTimeout(end, 1000) + }) +} + +// --------------------------------------------------------------------------- + +module.exports = podsUtils diff --git a/server/tests/utils/requests.js b/server/tests/utils/requests.js new file mode 100644 index 0000000000000000000000000000000000000000..b1470814d4729d25199a81d242cb415d70a24dbf --- /dev/null +++ b/server/tests/utils/requests.js @@ -0,0 +1,68 @@ +'use strict' + +const request = require('supertest') + +const requestsUtils = { + makePostUploadRequest, + makePostBodyRequest, + makePutBodyRequest +} + +// ---------------------- Export functions -------------------- + +function makePostUploadRequest (url, path, token, fields, attaches, done, statusCodeExpected) { + if (!statusCodeExpected) statusCodeExpected = 400 + + const req = request(url) + .post(path) + .set('Accept', 'application/json') + + if (token) req.set('Authorization', 'Bearer ' + token) + + Object.keys(fields).forEach(function (field) { + const value = fields[field] + + if (Array.isArray(value)) { + for (let i = 0; i < value.length; i++) { + req.field(field + '[' + i + ']', value[i]) + } + } else { + req.field(field, value) + } + }) + + Object.keys(attaches).forEach(function (attach) { + const value = attaches[attach] + req.attach(attach, value) + }) + + req.expect(statusCodeExpected, done) +} + +function makePostBodyRequest (url, path, token, fields, done, statusCodeExpected) { + if (!statusCodeExpected) statusCodeExpected = 400 + + const req = request(url) + .post(path) + .set('Accept', 'application/json') + + if (token) req.set('Authorization', 'Bearer ' + token) + + req.send(fields).expect(statusCodeExpected, done) +} + +function makePutBodyRequest (url, path, token, fields, done, statusCodeExpected) { + if (!statusCodeExpected) statusCodeExpected = 400 + + const req = request(url) + .put(path) + .set('Accept', 'application/json') + + if (token) req.set('Authorization', 'Bearer ' + token) + + req.send(fields).expect(statusCodeExpected, done) +} + +// --------------------------------------------------------------------------- + +module.exports = requestsUtils diff --git a/server/tests/utils/servers.js b/server/tests/utils/servers.js new file mode 100644 index 0000000000000000000000000000000000000000..d62838bc71529eb468bb8e54a76530b4a7142b5f --- /dev/null +++ b/server/tests/utils/servers.js @@ -0,0 +1,115 @@ +'use strict' + +const childProcess = require('child_process') +const exec = childProcess.exec +const fork = childProcess.fork +const pathUtils = require('path') + +const serversUtils = { + flushAndRunMultipleServers, + flushTests, + runServer +} + +// ---------------------- Export functions -------------------- + +function flushAndRunMultipleServers (totalServers, serversRun) { + let apps = [] + let urls = [] + let i = 0 + + function anotherServerDone (number, app, url) { + apps[number - 1] = app + urls[number - 1] = url + i++ + if (i === totalServers) { + serversRun(apps, urls) + } + } + + flushTests(function () { + for (let j = 1; j <= totalServers; j++) { + // For the virtual buffer + setTimeout(function () { + runServer(j, function (app, url) { + anotherServerDone(j, app, url) + }) + }, 1000 * j) + } + }) +} + +function flushTests (callback) { + exec('npm run clean:server:test', callback) +} + +function runServer (number, callback) { + const server = { + app: null, + url: `http://localhost:${9000 + number}`, + client: { + id: null, + secret: null + }, + user: { + username: null, + password: null + } + } + + // These actions are async so we need to be sure that they have both been done + const serverRunString = { + 'Connected to mongodb': false, + 'Server listening on port': false + } + + const regexps = { + client_id: 'Client id: ([a-f0-9]+)', + client_secret: 'Client secret: (.+)', + user_username: 'Username: (.+)', + user_password: 'User password: (.+)' + } + + // Share the environment + const env = Object.create(process.env) + env.NODE_ENV = 'test' + env.NODE_APP_INSTANCE = number + const options = { + silent: true, + env: env, + detached: true + } + + server.app = fork(pathUtils.join(__dirname, '../../../server.js'), [], options) + server.app.stdout.on('data', function onStdout (data) { + let dontContinue = false + + // Capture things if we want to + for (const key of Object.keys(regexps)) { + const regexp = regexps[key] + const matches = data.toString().match(regexp) + if (matches !== null) { + if (key === 'client_id') server.client.id = matches[1] + else if (key === 'client_secret') server.client.secret = matches[1] + else if (key === 'user_username') server.user.username = matches[1] + else if (key === 'user_password') server.user.password = matches[1] + } + } + + // Check if all required sentences are here + for (const key of Object.keys(serverRunString)) { + if (data.toString().indexOf(key) !== -1) serverRunString[key] = true + if (serverRunString[key] === false) dontContinue = true + } + + // If no, there is maybe one thing not already initialized (mongodb...) + if (dontContinue === true) return + + server.app.stdout.removeListener('data', onStdout) + callback(server) + }) +} + +// --------------------------------------------------------------------------- + +module.exports = serversUtils diff --git a/server/tests/utils/users.js b/server/tests/utils/users.js new file mode 100644 index 0000000000000000000000000000000000000000..2bf9c6e3e13f2e7f29359fe130d7c9e62c763291 --- /dev/null +++ b/server/tests/utils/users.js @@ -0,0 +1,100 @@ +'use strict' + +const request = require('supertest') + +const usersUtils = { + createUser, + getUserInformation, + getUsersList, + getUsersListPaginationAndSort, + removeUser, + updateUser +} + +// ---------------------- Export functions -------------------- + +function createUser (url, accessToken, username, password, specialStatus, end) { + if (!end) { + end = specialStatus + specialStatus = 204 + } + + const path = '/api/v1/users' + + request(url) + .post(path) + .set('Accept', 'application/json') + .set('Authorization', 'Bearer ' + accessToken) + .send({ username: username, password: password }) + .expect(specialStatus) + .end(end) +} + +function getUserInformation (url, accessToken, end) { + const path = '/api/v1/users/me' + + request(url) + .get(path) + .set('Accept', 'application/json') + .set('Authorization', 'Bearer ' + accessToken) + .expect(200) + .expect('Content-Type', /json/) + .end(end) +} + +function getUsersList (url, end) { + const path = '/api/v1/users' + + request(url) + .get(path) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .end(end) +} + +function getUsersListPaginationAndSort (url, start, count, sort, end) { + const path = '/api/v1/users' + + request(url) + .get(path) + .query({ start: start }) + .query({ count: count }) + .query({ sort: sort }) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .end(end) +} + +function removeUser (url, userId, accessToken, expectedStatus, end) { + if (!end) { + end = expectedStatus + expectedStatus = 204 + } + + const path = '/api/v1/users' + + request(url) + .delete(path + '/' + userId) + .set('Accept', 'application/json') + .set('Authorization', 'Bearer ' + accessToken) + .expect(expectedStatus) + .end(end) +} + +function updateUser (url, userId, accessToken, newPassword, end) { + const path = '/api/v1/users/' + userId + + request(url) + .put(path) + .set('Accept', 'application/json') + .set('Authorization', 'Bearer ' + accessToken) + .send({ password: newPassword }) + .expect(204) + .end(end) +} + +// --------------------------------------------------------------------------- + +module.exports = usersUtils diff --git a/server/tests/utils/videos.js b/server/tests/utils/videos.js new file mode 100644 index 0000000000000000000000000000000000000000..536093db13998c0cf700753d00402924a3c6f7c8 --- /dev/null +++ b/server/tests/utils/videos.js @@ -0,0 +1,199 @@ +'use strict' + +const fs = require('fs') +const pathUtils = require('path') +const request = require('supertest') + +const videosUtils = { + getAllVideosListBy, + getVideo, + getVideosList, + getVideosListPagination, + getVideosListSort, + removeVideo, + searchVideo, + searchVideoWithPagination, + searchVideoWithSort, + testVideoImage, + uploadVideo +} + +// ---------------------- Export functions -------------------- + +function getAllVideosListBy (url, end) { + const path = '/api/v1/videos' + + request(url) + .get(path) + .query({ sort: 'createdDate' }) + .query({ start: 0 }) + .query({ count: 10000 }) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .end(end) +} + +function getVideo (url, id, end) { + const path = '/api/v1/videos/' + id + + request(url) + .get(path) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .end(end) +} + +function getVideosList (url, end) { + const path = '/api/v1/videos' + + request(url) + .get(path) + .query({ sort: 'name' }) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .end(end) +} + +function getVideosListPagination (url, start, count, end) { + const path = '/api/v1/videos' + + request(url) + .get(path) + .query({ start: start }) + .query({ count: count }) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .end(end) +} + +function getVideosListSort (url, sort, end) { + const path = '/api/v1/videos' + + request(url) + .get(path) + .query({ sort: sort }) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .end(end) +} + +function removeVideo (url, token, id, expectedStatus, end) { + if (!end) { + end = expectedStatus + expectedStatus = 204 + } + + const path = '/api/v1/videos' + + request(url) + .delete(path + '/' + id) + .set('Accept', 'application/json') + .set('Authorization', 'Bearer ' + token) + .expect(expectedStatus) + .end(end) +} + +function searchVideo (url, search, field, end) { + if (!end) { + end = field + field = null + } + + const path = '/api/v1/videos' + const req = request(url) + .get(path + '/search/' + search) + .set('Accept', 'application/json') + + if (field) req.query({ field: field }) + req.expect(200) + .expect('Content-Type', /json/) + .end(end) +} + +function searchVideoWithPagination (url, search, field, start, count, end) { + const path = '/api/v1/videos' + + request(url) + .get(path + '/search/' + search) + .query({ start: start }) + .query({ count: count }) + .query({ field: field }) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .end(end) +} + +function searchVideoWithSort (url, search, sort, end) { + const path = '/api/v1/videos' + + request(url) + .get(path + '/search/' + search) + .query({ sort: sort }) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .end(end) +} + +function testVideoImage (url, videoName, imagePath, callback) { + // Don't test images if the node env is not set + // Because we need a special ffmpeg version for this test + if (process.env.NODE_TEST_IMAGE) { + request(url) + .get(imagePath) + .expect(200) + .end(function (err, res) { + if (err) return callback(err) + + fs.readFile(pathUtils.join(__dirname, '..', 'api', 'fixtures', videoName + '.jpg'), function (err, data) { + if (err) return callback(err) + + callback(null, data.equals(res.body)) + }) + }) + } else { + console.log('Do not test images. Enable it by setting NODE_TEST_IMAGE env variable.') + callback(null, true) + } +} + +function uploadVideo (url, accessToken, name, description, tags, fixture, specialStatus, end) { + if (!end) { + end = specialStatus + specialStatus = 204 + } + + const path = '/api/v1/videos' + + const req = request(url) + .post(path) + .set('Accept', 'application/json') + .set('Authorization', 'Bearer ' + accessToken) + .field('name', name) + .field('description', description) + + for (let i = 0; i < tags.length; i++) { + req.field('tags[' + i + ']', tags[i]) + } + + let filepath = '' + if (pathUtils.isAbsolute(fixture)) { + filepath = fixture + } else { + filepath = pathUtils.join(__dirname, '..', 'api', 'fixtures', fixture) + } + + req.attach('videofile', filepath) + .expect(specialStatus) + .end(end) +} + +// --------------------------------------------------------------------------- + +module.exports = videosUtils