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 }}
+
+
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 }}
+
+
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
+
+
+
+
+ ID |
+ Url |
+ Score |
+ Created 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 @@
+
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 }}
+
+
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
+
+
+
+
+ ID |
+ Username |
+ Created Date |
+ Remove |
+
+
+
+
+
+ {{ 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 @@
-
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 }}
-