diff --git a/.gitignore b/.gitignore index ec5b4b2f0..7ca89dca8 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 e6a92d883..7b025f0b9 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 1a3470711..777df6d7d 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 24d7dae9f..6268d2628 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 2ff3a1506..882013a9e 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' - }, - - /* - * Options affecting the resolving of modules. - * - * See: http://webpack.github.io/docs/configuration.html#resolve - */ - resolve: { + return { /* - * An array of extensions that should be used to resolve modules. + * Static metadata for index.html * - * See: http://webpack.github.io/docs/configuration.html#resolve-extensions + * See: (custom attribute) */ - extensions: [ '', '.ts', '.js', '.scss' ], + metadata: METADATA, - // Make sure root is src - root: helpers.root('src'), - - // remove other default values - modulesDirectories: [ 'node_modules' ], - - packageAlias: 'browser' - - }, - - 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. + * 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#module-preloaders-module-postloaders + * See: http://webpack.github.io/docs/configuration.html#cache */ - preLoaders: [ + // 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' + }, + + /* + * 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' ], + + // Make sure root is src + root: helpers.root('src'), + + // remove other default values + modulesDirectories: [ 'node_modules' ] + }, + + 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. + * + * See: http://webpack.github.io/docs/configuration.html#module-preloaders-module-postloaders + */ + 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')] + } + ], /* - * Tslint loader support for *.ts files + * An array of automatically applied loaders. * - * See: https://github.com/wbuchwalter/tslint-loader + * 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: http://webpack.github.io/docs/configuration.html#module-loaders */ - // { test: /\.ts$/, loader: 'tslint-loader', exclude: [ helpers.root('node_modules') ] }, + 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 + }, + + /* + * Add additional plugins to the compiler. + * + * See: http://webpack.github.io/docs/configuration.html#plugins + */ + plugins: [ + new AssetsPlugin({ + path: helpers.root('dist'), + filename: 'webpack-assets.json', + prettyPrint: true + }), /* - * Source map loader support for *.js files - * Extracts SourceMaps for source files that as added as sourceMappingURL comment. + * Plugin: ForkCheckerPlugin + * Description: Do type checking in a separate process, so webpack don't need to wait. * - * See: https://github.com/webpack/source-map-loader + * See: https://github.com/s-panferov/awesome-typescript-loader#forkchecker-boolean-defaultfalse */ - { - test: /\.js$/, - loader: 'source-map-loader', - exclude: [ - // these packages have problems with their sourcemaps - helpers.root('node_modules/rxjs'), - helpers.root('node_modules/@angular') - ] - } + new ForkCheckerPlugin(), + /* + * 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: 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 + ), + + /* + * 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' + } + ]), + + /* + * 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' + }), + + new WebpackNotifierPlugin({ alwaysNotify: true }) ], /* - * An array of automatically applied loaders. + * Include polyfills or mocks for various node stuff + * Description: Node configuration * - * 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: http://webpack.github.io/docs/configuration.html#module-loaders + * See: https://webpack.github.io/docs/configuration.html#node */ - 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$/, - loader: 'awesome-typescript-loader', - exclude: [/\.(spec|e2e)\.ts$/] - }, - - /* - * Json loader support for *.json files. - * - * See: https://github.com/webpack/json-loader - */ - { - test: /\.json$/, - loader: 'json-loader' - }, - - { - test: /\.scss$/, - exclude: /node_modules/, - loaders: [ 'raw-loader', 'sass-loader' ] - }, - - { - test: /\.(woff2?|ttf|eot|svg)$/, - loader: 'url?limit=10000&name=assets/fonts/[hash].[ext]' - }, - - /* 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 - }, - - /* - * 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: 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' - } - ]), - - /* - * 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' - }), - - 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 50193bf58..0b6c00cbd 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', - - /** 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' - - }, - - externals: { - webtorrent: 'WebTorrent' - }, - - plugins: [ + devtool: 'cheap-module-source-map', /** - * Plugin: DefinePlugin - * Description: Define free variables. - * Useful for having development builds with debug logging or adding global constants. + * Options affecting the output of the compilation. * - * Environment helpers - * - * See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin + * See: http://webpack.github.io/docs/configuration.html#output */ - // 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': { + 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), - 'NODE_ENV': JSON.stringify(METADATA.ENV), - 'HMR': METADATA.HMR - } - }) - ], + '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' - }, + new NamedModulesPlugin() + ], - /* - * 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 - } + /** + * 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' + }, -}) + 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/configuration.html#node + */ + 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 7ce5727d3..46db54482 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' + plugins: [ - }, + /** + * Plugin: WebpackMd5Hash + * Description: Plugin to replace a standard webpack chunkhash with md5. + * + * See: https://www.npmjs.com/package/webpack-md5-hash + */ + new WebpackMd5Hash(), - externals: { - webtorrent: 'WebTorrent' - }, + /** + * 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(), - /** - * 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(), - - /** - * 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': { + /** + * 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), - 'NODE_ENV': JSON.stringify(METADATA.ENV), - 'HMR': METADATA.HMR - } - }), + '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 + /** + * 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 + beautify: false, // prod + mangle: { screw_ie8: true, keep_fnames: true }, // prod + compress: { screw_ie8: true }, // prod + comments: false // prod + }), - mangle: { - screw_ie8: true, - keep_fnames: true - }, // prod + new NormalModuleReplacementPlugin( + /angular2-hmr/, + helpers.root('config/modules/angular2-hmr-prod.js') + ) - compress: { - screw_ie8: true - }, // prod + /** + * 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 + // }) - comments: false // prod - }), - - /** - * 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 - }) - - ], - - /** - * 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 - } + /** + * 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 + } + + }) +} diff --git a/client/package.json b/client/package.json index a5c5d092b..cc116f3e5 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 000000000..5a8847acd --- /dev/null +++ b/client/src/app/account/account.component.html @@ -0,0 +1,27 @@ +

Account

+ +
{{ information }}
+
{{ error }}
+ +
+
+ + +
+ {{ formErrors['new-password'] }} +
+
+ +
+ + +
+ + +
diff --git a/client/src/app/account/account.component.ts b/client/src/app/account/account.component.ts new file mode 100644 index 000000000..851eaf198 --- /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 000000000..e348c6ebe --- /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 000000000..355bcef74 --- /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 000000000..823d9fe5f --- /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 000000000..64a7400e7 --- /dev/null +++ b/client/src/app/admin/admin.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + template: '' +}) + +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 000000000..edb8ba49f --- /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 000000000..788f3b44d --- /dev/null +++ b/client/src/app/admin/friends/friend-add/friend-add.component.html @@ -0,0 +1,26 @@ +

Make friends

+ +
{{ error }}
+ +
+
+ + +
+ + + + + +
+ +
+ It should be a valid url. +
+
+ + +
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 000000000..5fde51636 --- /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 000000000..64165a9a5 --- /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 000000000..a101b3be5 --- /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 000000000..d786a7846 --- /dev/null +++ b/client/src/app/admin/friends/friend-list/friend-list.component.html @@ -0,0 +1,29 @@ +

Friends list

+ + + + + + + + + + + + + + + + + + + +
IDUrlScoreCreated Date
{{ friend.id }}{{ friend.url }}{{ friend.score }}{{ friend.createdDate | date: 'medium' }}
+ + + Quit friends + + + + Make friends + 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 000000000..cb597e12b --- /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 000000000..88c4800ee --- /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 000000000..354c978a4 --- /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 000000000..bc3f54158 --- /dev/null +++ b/client/src/app/admin/friends/friends.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + template: '' +}) + +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 000000000..7fdef68f9 --- /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 000000000..dd4df2538 --- /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 000000000..7cb28f440 --- /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 000000000..75826fc17 --- /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 { + 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 0adc256c4..0d671637d 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 000000000..493caed15 --- /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 000000000..e250615aa --- /dev/null +++ b/client/src/app/admin/menu-admin.component.html @@ -0,0 +1,26 @@ + + +
+
+ + List users +
+ +
+ + List friends +
+ + +
+ +
+
+ + Quit admin. +
+
+
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 000000000..59ffccf9f --- /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 000000000..236a9ee8f --- /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 000000000..be3a66f77 --- /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 000000000..b5ac59a9a --- /dev/null +++ b/client/src/app/admin/requests/request-stats/request-stats.component.html @@ -0,0 +1,23 @@ +

Requests stats

+ +
+
+ Interval seconds between requests: + {{ stats.secondsInterval }} +
+ +
+ Remaining time before the scheduled request: + {{ stats.remainingSeconds }} +
+ +
+ Maximum number of requests per interval: + {{ stats.maxRequestsInParallel }} +
+ +
+ Remaining requests: + {{ stats.requests.length }} +
+
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 000000000..92c28dc99 --- /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 000000000..4b0844574 --- /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 000000000..471112b45 --- /dev/null +++ b/client/src/app/admin/requests/requests.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + template: '' +}) + +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 000000000..78221a9ff --- /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 000000000..32ab5767b --- /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 000000000..766e80836 --- /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 000000000..aeec37448 --- /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 { + 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 000000000..e98a81f62 --- /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 000000000..e17ee5c7a --- /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 000000000..13be553c0 --- /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 000000000..66d5ca04f --- /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 000000000..9b76c7c1b --- /dev/null +++ b/client/src/app/admin/users/user-add/user-add.component.html @@ -0,0 +1,29 @@ +

Add user

+ +
{{ error }}
+ +
+
+ + +
+ {{ formErrors.username }} +
+
+ +
+ + +
+ {{ formErrors.password }} +
+
+ + +
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 000000000..ab96fb01d --- /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 000000000..51fbefa80 --- /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 000000000..328b1be77 --- /dev/null +++ b/client/src/app/admin/users/user-list/user-list.component.html @@ -0,0 +1,28 @@ +

Users list

+ + + + + + + + + + + + + + + + + + + +
IDUsernameCreated DateRemove
{{ user.id }}{{ user.username }}{{ user.createdDate | date: 'medium' }} + +
+ + + + Add user + 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 000000000..e9f61e900 --- /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 000000000..03f4e5c0a --- /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 000000000..37e3b158d --- /dev/null +++ b/client/src/app/admin/users/users.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + template: '' +}) + +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 000000000..eb71bd0ae --- /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 f2acffea4..04c32f596 100644 --- a/client/src/app/app.component.html +++ b/client/src/app/app.component.html @@ -14,48 +14,14 @@
- - -
-
- - Login - Logout -
-
- -
-
- - Get videos -
- - -
- -
-
- - Make friends -
- -
- - Quit friends -
-
-
+ +
-
-
PeerTube, CopyLeft 2015-2016
diff --git a/client/src/app/app.component.scss b/client/src/app/app.component.scss index 1b02b2f57..95f306d75 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 b7a3d7c58..d6b83c684 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 router: Router) {} - 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) - ); - } - - 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 000000000..980625f13 --- /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 59ef4ce55..03e2bce51 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 000000000..033c21900 --- /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 000000000..8bba89c4e --- /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 = (window).ng; + enableDebugTools(cmpRef); + (window).ng.probe = _ng.probe; + (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 771046484..000000000 --- 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 { - 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 000000000..da53f6aef --- /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 5848fcba3..94a405405 100644 --- a/client/src/app/login/login.component.html +++ b/client/src/app/login/login.component.html @@ -1,17 +1,16 @@

Login

-
{{ error }}
-
+
-
- Username is required +
+ {{ formErrors.username }}
@@ -19,12 +18,12 @@ -
- Password is required +
+ {{ formErrors.password }}
- + diff --git a/client/src/app/login/login.component.ts b/client/src/app/login/login.component.ts index ddd62462e..c4ff7050b 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 000000000..29ef7f9cf --- /dev/null +++ b/client/src/app/menu.component.html @@ -0,0 +1,39 @@ + +
+
+ + + Login + + + + + Logout + +
+ +
+ + My account +
+
+ +
+
+ + Get videos +
+ + +
+ +
+ +
+
diff --git a/client/src/app/menu.component.ts b/client/src/app/menu.component.ts new file mode 100644 index 000000000..6cfc854dd --- /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 9c7ef4389..2392898ca 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 { + post(url: string, body: any, options?: RequestOptionsArgs): Observable { if (!options) options = {}; options.method = RequestMethod.Post; + options.body = body; return this.request(url, options); } - put(url: string, options?: RequestOptionsArgs): Observable { + put(url: string, body: any, options?: RequestOptionsArgs): Observable { 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 98852f835..bdd5ea5a9 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 584298fff..a30c79c86 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; private clientId: string; private clientSecret: string; private loginChanged: Subject; - 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(); 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 aafaacbf1..ebd9e14cd 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 000000000..1e8a69771 --- /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 000000000..1d2ae6f68 --- /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 000000000..67163b4e9 --- /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 000000000..5b11ff265 --- /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 000000000..3766d4018 --- /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 000000000..588ebb4be --- /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 dfea4c67c..af34b4b64 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 000000000..3c9509dc7 --- /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 000000000..fcb1598f4 --- /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 eda44ebfb..0cfa4f468 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 000000000..16b47e957 --- /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 e864fbc17..b6237469b 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 c7993db3d..717a7fa50 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(); - this.searchUpdated = new Subject(); + this.searchUpdated = new ReplaySubject(1); } } diff --git a/client/src/app/shared/users/index.ts b/client/src/app/shared/users/index.ts new file mode 100644 index 000000000..5a670ce8f --- /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 000000000..726495d11 --- /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 a54120f5d..67d16ead1 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 cdd07d1b4..e72d2f3f3 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 b4396f767..ad8557533 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