]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Merge branch 'master' into webseed-merged
authorChocobozzz <florian.bigard@gmail.com>
Sun, 2 Oct 2016 13:39:09 +0000 (15:39 +0200)
committerChocobozzz <florian.bigard@gmail.com>
Sun, 2 Oct 2016 13:39:09 +0000 (15:39 +0200)
186 files changed:
.gitignore
.travis.yml
README.md
client/config/helpers.js
client/config/webpack.common.js
client/config/webpack.dev.js
client/config/webpack.prod.js
client/package.json
client/src/app/account/account.component.html [new file with mode: 0644]
client/src/app/account/account.component.ts [new file with mode: 0644]
client/src/app/account/account.routes.ts [new file with mode: 0644]
client/src/app/account/account.service.ts [new file with mode: 0644]
client/src/app/account/index.ts [new file with mode: 0644]
client/src/app/admin/admin.component.ts [new file with mode: 0644]
client/src/app/admin/admin.routes.ts [new file with mode: 0644]
client/src/app/admin/friends/friend-add/friend-add.component.html [new file with mode: 0644]
client/src/app/admin/friends/friend-add/friend-add.component.scss [new file with mode: 0644]
client/src/app/admin/friends/friend-add/friend-add.component.ts [new file with mode: 0644]
client/src/app/admin/friends/friend-add/index.ts [new file with mode: 0644]
client/src/app/admin/friends/friend-list/friend-list.component.html [new file with mode: 0644]
client/src/app/admin/friends/friend-list/friend-list.component.scss [new file with mode: 0644]
client/src/app/admin/friends/friend-list/friend-list.component.ts [new file with mode: 0644]
client/src/app/admin/friends/friend-list/index.ts [new file with mode: 0644]
client/src/app/admin/friends/friends.component.ts [new file with mode: 0644]
client/src/app/admin/friends/friends.routes.ts [new file with mode: 0644]
client/src/app/admin/friends/index.ts [new file with mode: 0644]
client/src/app/admin/friends/shared/friend.model.ts [new file with mode: 0644]
client/src/app/admin/friends/shared/friend.service.ts [new file with mode: 0644]
client/src/app/admin/friends/shared/index.ts [moved from client/src/app/friends/index.ts with 51% similarity]
client/src/app/admin/index.ts [new file with mode: 0644]
client/src/app/admin/menu-admin.component.html [new file with mode: 0644]
client/src/app/admin/menu-admin.component.ts [new file with mode: 0644]
client/src/app/admin/requests/index.ts [new file with mode: 0644]
client/src/app/admin/requests/request-stats/index.ts [new file with mode: 0644]
client/src/app/admin/requests/request-stats/request-stats.component.html [new file with mode: 0644]
client/src/app/admin/requests/request-stats/request-stats.component.scss [new file with mode: 0644]
client/src/app/admin/requests/request-stats/request-stats.component.ts [new file with mode: 0644]
client/src/app/admin/requests/requests.component.ts [new file with mode: 0644]
client/src/app/admin/requests/requests.routes.ts [new file with mode: 0644]
client/src/app/admin/requests/shared/index.ts [new file with mode: 0644]
client/src/app/admin/requests/shared/request-stats.model.ts [new file with mode: 0644]
client/src/app/admin/requests/shared/request.service.ts [new file with mode: 0644]
client/src/app/admin/users/index.ts [new file with mode: 0644]
client/src/app/admin/users/shared/index.ts [new file with mode: 0644]
client/src/app/admin/users/shared/user.service.ts [new file with mode: 0644]
client/src/app/admin/users/user-add/index.ts [new file with mode: 0644]
client/src/app/admin/users/user-add/user-add.component.html [new file with mode: 0644]
client/src/app/admin/users/user-add/user-add.component.ts [new file with mode: 0644]
client/src/app/admin/users/user-list/index.ts [new file with mode: 0644]
client/src/app/admin/users/user-list/user-list.component.html [new file with mode: 0644]
client/src/app/admin/users/user-list/user-list.component.scss [new file with mode: 0644]
client/src/app/admin/users/user-list/user-list.component.ts [new file with mode: 0644]
client/src/app/admin/users/users.component.ts [new file with mode: 0644]
client/src/app/admin/users/users.routes.ts [new file with mode: 0644]
client/src/app/app.component.html
client/src/app/app.component.scss
client/src/app/app.component.ts
client/src/app/app.module.ts [new file with mode: 0644]
client/src/app/app.routes.ts
client/src/app/app.service.ts [new file with mode: 0644]
client/src/app/environment.ts [new file with mode: 0644]
client/src/app/friends/friend.service.ts [deleted file]
client/src/app/index.ts [new file with mode: 0644]
client/src/app/login/login.component.html
client/src/app/login/login.component.ts
client/src/app/menu.component.html [new file with mode: 0644]
client/src/app/menu.component.ts [new file with mode: 0644]
client/src/app/shared/auth/auth-http.service.ts
client/src/app/shared/auth/auth-user.model.ts [moved from client/src/app/shared/auth/user.model.ts with 72% similarity]
client/src/app/shared/auth/auth.service.ts
client/src/app/shared/auth/index.ts
client/src/app/shared/forms/form-reactive.ts [new file with mode: 0644]
client/src/app/shared/forms/form-validators/index.ts [new file with mode: 0644]
client/src/app/shared/forms/form-validators/url.validator.ts [new file with mode: 0644]
client/src/app/shared/forms/form-validators/user.ts [new file with mode: 0644]
client/src/app/shared/forms/form-validators/video.ts [new file with mode: 0644]
client/src/app/shared/forms/index.ts [new file with mode: 0644]
client/src/app/shared/index.ts
client/src/app/shared/rest/index.ts [new file with mode: 0644]
client/src/app/shared/rest/rest-extractor.service.ts [new file with mode: 0644]
client/src/app/shared/rest/rest-pagination.ts [moved from client/src/app/videos/shared/pagination.model.ts with 65% similarity]
client/src/app/shared/rest/rest.service.ts [new file with mode: 0644]
client/src/app/shared/search/search.component.ts
client/src/app/shared/search/search.service.ts
client/src/app/shared/users/index.ts [new file with mode: 0644]
client/src/app/shared/users/user.model.ts [new file with mode: 0644]
client/src/app/videos/shared/index.ts
client/src/app/videos/shared/loader/loader.component.ts
client/src/app/videos/shared/video.service.ts
client/src/app/videos/video-add/video-add.component.html
client/src/app/videos/video-add/video-add.component.ts
client/src/app/videos/video-list/video-list.component.ts
client/src/app/videos/video-list/video-miniature.component.ts
client/src/app/videos/video-list/video-sort.component.ts
client/src/app/videos/video-watch/video-watch.component.ts
client/src/app/videos/videos.component.ts
client/src/app/videos/videos.routes.ts
client/src/custom-typings.d.ts
client/src/index.html
client/src/main.ts
client/src/polyfills.ts
client/src/sass/application.scss
client/src/vendor.ts
client/tsconfig.json
client/typings.json [deleted file]
client/webpack.config.js
config/default.yaml
config/production.yaml.example
config/test-1.yaml
config/test-2.yaml
config/test-3.yaml
config/test-4.yaml
config/test-5.yaml
config/test-6.yaml
package.json
scripts/build/client/prod.sh
server.js
server/controllers/api/v1/clients.js [new file with mode: 0644]
server/controllers/api/v1/index.js
server/controllers/api/v1/pods.js
server/controllers/api/v1/remote.js
server/controllers/api/v1/requests.js [new file with mode: 0644]
server/controllers/api/v1/users.js
server/controllers/api/v1/videos.js
server/helpers/custom-validators/index.js [new file with mode: 0644]
server/helpers/custom-validators/misc.js [new file with mode: 0644]
server/helpers/custom-validators/pods.js [new file with mode: 0644]
server/helpers/custom-validators/users.js [new file with mode: 0644]
server/helpers/custom-validators/videos.js [moved from server/helpers/custom-validators.js with 50% similarity]
server/helpers/logger.js
server/helpers/peertube-crypto.js
server/helpers/requests.js
server/helpers/utils.js
server/initializers/checker.js
server/initializers/constants.js
server/initializers/database.js
server/initializers/installer.js
server/initializers/migrations/0005-create-application.js [new file with mode: 0644]
server/initializers/migrations/0010-users-password.js [new file with mode: 0644]
server/initializers/migrations/0015-admin-role.js [new file with mode: 0644]
server/initializers/migrator.js [new file with mode: 0644]
server/lib/friends.js
server/lib/oauth-model.js
server/middlewares/admin.js [new file with mode: 0644]
server/middlewares/index.js
server/middlewares/oauth.js
server/middlewares/pagination.js
server/middlewares/pods.js [new file with mode: 0644]
server/middlewares/search.js
server/middlewares/secure.js
server/middlewares/sort.js
server/middlewares/validators/index.js
server/middlewares/validators/pagination.js
server/middlewares/validators/pods.js
server/middlewares/validators/remote.js
server/middlewares/validators/sort.js
server/middlewares/validators/users.js [new file with mode: 0644]
server/middlewares/validators/utils.js
server/middlewares/validators/videos.js
server/models/application.js [new file with mode: 0644]
server/models/oauth-client.js
server/models/oauth-token.js
server/models/pods.js
server/models/request.js
server/models/user.js
server/models/utils.js [new file with mode: 0644]
server/models/video.js
server/tests/api/check-params.js [new file with mode: 0644]
server/tests/api/checkParams.js [deleted file]
server/tests/api/friends-advanced.js [moved from server/tests/api/friendsAdvanced.js with 88% similarity]
server/tests/api/friends-basic.js [moved from server/tests/api/friendsBasic.js with 70% similarity]
server/tests/api/index.js
server/tests/api/multiple-pods.js [moved from server/tests/api/multiplePods.js with 82% similarity]
server/tests/api/requests.js [new file with mode: 0644]
server/tests/api/single-pod.js [moved from server/tests/api/singlePod.js with 76% similarity]
server/tests/api/users.js
server/tests/api/utils.js [deleted file]
server/tests/real-world/real-world.js
server/tests/utils/clients.js [new file with mode: 0644]
server/tests/utils/login.js [new file with mode: 0644]
server/tests/utils/miscs.js [new file with mode: 0644]
server/tests/utils/pods.js [new file with mode: 0644]
server/tests/utils/requests.js [new file with mode: 0644]
server/tests/utils/servers.js [new file with mode: 0644]
server/tests/utils/users.js [new file with mode: 0644]
server/tests/utils/videos.js [new file with mode: 0644]

index ec5b4b2f03e602cd8f4fe84d055fa86263d7c1d4..7ca89dca8532625749c8e09c466040913187a17b 100644 (file)
@@ -14,4 +14,11 @@ uploads
 thumbnails
 config/production.yaml
 ffmpeg
+<<<<<<< HEAD
 torrents
+=======
+.tags
+*.sublime-project
+*.sublime-workspace
+torrents/
+>>>>>>> master
index e6a92d883134ef4b0d9d577c96a0464b982fbe45..7b025f0b98964c96760179cc147514d65f8b2b68 100644 (file)
@@ -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
index 1a3470711be0f82feeca1548fbd036eecce22383..777df6d7d54f448fbfa7f7689c2c21b024d7833b 100644 (file)
--- 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:
index 24d7dae9f406b7e8c4f7fc603c62c8ef96ce6cdb..6268d2628fbfdf067e1e7a773e6f6c83f29a4216 100644 (file)
@@ -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
index 2ff3a1506497687a4ba6ec3e3f0500c52fab5ceb..882013a9e1b6f7c481e2d35d23ab3529fd23e89e 100644 (file)
@@ -5,9 +5,11 @@ const helpers = require('./helpers')
  * Webpack Plugins
  */
 
-var CopyWebpackPlugin = (CopyWebpackPlugin = require('copy-webpack-plugin'), CopyWebpackPlugin.default || CopyWebpackPlugin)
+const CopyWebpackPlugin = require('copy-webpack-plugin')
 const HtmlWebpackPlugin = require('html-webpack-plugin')
 const ForkCheckerPlugin = require('awesome-typescript-loader').ForkCheckerPlugin
+const AssetsPlugin = require('assets-webpack-plugin')
+const ContextReplacementPlugin = require('webpack/lib/ContextReplacementPlugin')
 const WebpackNotifierPlugin = require('webpack-notifier')
 
 /*
@@ -15,7 +17,8 @@ const WebpackNotifierPlugin = require('webpack-notifier')
  */
 const METADATA = {
   title: 'PeerTube',
-  baseUrl: '/'
+  baseUrl: '/',
+  isDevServer: helpers.isWebpackDevServer()
 }
 
 /*
@@ -23,247 +26,241 @@ const METADATA = {
  *
  * See: http://webpack.github.io/docs/configuration.html#cli
  */
-module.exports = {
-  /*
-   * Static metadata for index.html
-   *
-   * See: (custom attribute)
-   */
-  metadata: METADATA,
+module.exports = function (options) {
+  var isProd = options.env === 'production'
 
-  /*
-   * Cache generated modules and chunks to improve performance for multiple incremental builds.
-   * This is enabled by default in watch mode.
-   * You can pass false to disable it.
-   *
-   * See: http://webpack.github.io/docs/configuration.html#cache
-   */
-  // cache: false,
-
-  /*
-   * The entry point for the bundle
-   * Our Angular.js app
-   *
-   * See: http://webpack.github.io/docs/configuration.html#entry
-   */
-  entry: {
-    'polyfills': './src/polyfills.ts',
-    'vendor': './src/vendor.ts',
-    'main': './src/main.ts'
-  },
+  return {
+    /*
+     * Static metadata for index.html
+     *
+     * See: (custom attribute)
+     */
+    metadata: METADATA,
 
-  /*
-   * Options affecting the resolving of modules.
-   *
-   * See: http://webpack.github.io/docs/configuration.html#resolve
-   */
-  resolve: {
     /*
-     * An array of extensions that should be used to resolve modules.
+     * Cache generated modules and chunks to improve performance for multiple incremental builds.
+     * This is enabled by default in watch mode.
+     * You can pass false to disable it.
      *
-     * See: http://webpack.github.io/docs/configuration.html#resolve-extensions
+     * See: http://webpack.github.io/docs/configuration.html#cache
      */
-    extensions: [ '', '.ts', '.js', '.scss' ],
+    // cache: false,
 
-    // Make sure root is src
-    root: helpers.root('src'),
+    /*
+     * The entry point for the bundle
+     * Our Angular.js app
+     *
+     * See: http://webpack.github.io/docs/configuration.html#entry
+     */
+    entry: {
+      'polyfills': './src/polyfills.ts',
+      'vendor': './src/vendor.ts',
+      'main': './src/main.ts'
+    },
 
-    // remove other default values
-    modulesDirectories: [ 'node_modules' ],
+    /*
+     * Options affecting the resolving of modules.
+     *
+     * See: http://webpack.github.io/docs/configuration.html#resolve
+     */
+    resolve: {
+      /*
+       * An array of extensions that should be used to resolve modules.
+       *
+       * See: http://webpack.github.io/docs/configuration.html#resolve-extensions
+       */
+      extensions: [ '', '.ts', '.js', '.scss' ],
 
-    packageAlias: 'browser'
+      // Make sure root is src
+      root: helpers.root('src'),
 
-  },
+      // remove other default values
+      modulesDirectories: [ 'node_modules' ]
+    },
 
-  output: {
-    publicPath: '/client/'
-  },
+    output: {
+      publicPath: '/client/'
+    },
 
-  /*
-   * Options affecting the normal modules.
-   *
-   * See: http://webpack.github.io/docs/configuration.html#module
-   */
-  module: {
     /*
-     * An array of applied pre and post loaders.
+     * Options affecting the normal modules.
      *
-     * See: http://webpack.github.io/docs/configuration.html#module-preloaders-module-postloaders
+     * See: http://webpack.github.io/docs/configuration.html#module
      */
-    preLoaders: [
-
+    module: {
       /*
-       * Tslint loader support for *.ts files
+       * An array of applied pre and post loaders.
        *
-       * See: https://github.com/wbuchwalter/tslint-loader
+       * See: http://webpack.github.io/docs/configuration.html#module-preloaders-module-postloaders
        */
-      // { test: /\.ts$/, loader: 'tslint-loader', exclude: [ helpers.root('node_modules') ] },
+      preLoaders: [
+        {
+          test: /\.ts$/,
+          loader: 'string-replace-loader',
+          query: {
+            search: '(System|SystemJS)(.*[\\n\\r]\\s*\\.|\\.)import\\((.+)\\)',
+            replace: '$1.import($3).then(mod => (mod.__esModule && mod.default) ? mod.default : mod)',
+            flags: 'g'
+          },
+          include: [helpers.root('src')]
+        }
+      ],
 
       /*
-       * Source map loader support for *.js files
-       * Extracts SourceMaps for source files that as added as sourceMappingURL comment.
+       * An array of automatically applied loaders.
+       *
+       * IMPORTANT: The loaders here are resolved relative to the resource which they are applied to.
+       * This means they are not resolved relative to the configuration file.
        *
-       * See: https://github.com/webpack/source-map-loader
+       * See: http://webpack.github.io/docs/configuration.html#module-loaders
        */
-      {
-        test: /\.js$/,
-        loader: 'source-map-loader',
-        exclude: [
-          // these packages have problems with their sourcemaps
-          helpers.root('node_modules/rxjs'),
-          helpers.root('node_modules/@angular')
-        ]
-      }
-
-    ],
+      loaders: [
+
+        /*
+         * Typescript loader support for .ts and Angular 2 async routes via .async.ts
+         *
+         * See: https://github.com/s-panferov/awesome-typescript-loader
+         */
+        {
+          test: /\.ts$/,
+          loaders: [
+            '@angularclass/hmr-loader?pretty=' + !isProd + '&prod=' + isProd,
+            'awesome-typescript-loader',
+            'angular2-template-loader'
+          ],
+          exclude: [/\.(spec|e2e)\.ts$/]
+        },
+
+        /*
+         * Json loader support for *.json files.
+         *
+         * See: https://github.com/webpack/json-loader
+         */
+        {
+          test: /\.json$/,
+          loader: 'json-loader'
+        },
+
+        {
+          test: /\.(sass|scss)$/,
+          loaders: ['css-to-string-loader', 'css-loader?sourceMap', 'resolve-url', 'sass-loader?sourceMap']
+        },
+        { test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'url?limit=10000&minetype=application/font-woff' },
+        { test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'file' },
+
+        /* Raw loader support for *.html
+         * Returns file content as string
+         *
+         * See: https://github.com/webpack/raw-loader
+         */
+        {
+          test: /\.html$/,
+          loader: 'raw-loader',
+          exclude: [ helpers.root('src/index.html') ]
+        }
+
+      ]
+
+    },
+
+    sassLoader: {
+      precision: 10
+    },
 
     /*
-     * An array of automatically applied loaders.
-     *
-     * IMPORTANT: The loaders here are resolved relative to the resource which they are applied to.
-     * This means they are not resolved relative to the configuration file.
+     * Add additional plugins to the compiler.
      *
-     * See: http://webpack.github.io/docs/configuration.html#module-loaders
+     * See: http://webpack.github.io/docs/configuration.html#plugins
      */
-    loaders: [
+    plugins: [
+      new AssetsPlugin({
+        path: helpers.root('dist'),
+        filename: 'webpack-assets.json',
+        prettyPrint: true
+      }),
 
       /*
-       * Typescript loader support for .ts and Angular 2 async routes via .async.ts
+       * Plugin: ForkCheckerPlugin
+       * Description: Do type checking in a separate process, so webpack don't need to wait.
        *
-       * See: https://github.com/s-panferov/awesome-typescript-loader
+       * See: https://github.com/s-panferov/awesome-typescript-loader#forkchecker-boolean-defaultfalse
        */
-      {
-        test: /\.ts$/,
-        loader: 'awesome-typescript-loader',
-        exclude: [/\.(spec|e2e)\.ts$/]
-      },
+      new ForkCheckerPlugin(),
 
       /*
-       * Json loader support for *.json files.
+       * Plugin: CommonsChunkPlugin
+       * Description: Shares common code between the pages.
+       * It identifies common modules and put them into a commons chunk.
        *
-       * See: https://github.com/webpack/json-loader
+       * See: https://webpack.github.io/docs/list-of-plugins.html#commonschunkplugin
+       * See: https://github.com/webpack/docs/wiki/optimization#multi-page-app
        */
-      {
-        test: /\.json$/,
-        loader: 'json-loader'
-      },
+      new webpack.optimize.CommonsChunkPlugin({
+        name: [ 'polyfills', 'vendor' ].reverse()
+      }),
 
-      {
-        test: /\.scss$/,
-        exclude: /node_modules/,
-        loaders: [ 'raw-loader', 'sass-loader' ]
-      },
-
-      {
-        test: /\.(woff2?|ttf|eot|svg)$/,
-        loader: 'url?limit=10000&name=assets/fonts/[hash].[ext]'
-      },
+      /**
+       * Plugin: ContextReplacementPlugin
+       * Description: Provides context to Angular's use of System.import
+       *
+       * See: https://webpack.github.io/docs/list-of-plugins.html#contextreplacementplugin
+       * See: https://github.com/angular/angular/issues/11580
+      */
+      new ContextReplacementPlugin(
+        // The (\\|\/) piece accounts for path separators in *nix and Windows
+        /angular(\\|\/)core(\\|\/)(esm(\\|\/)src|src)(\\|\/)linker/,
+        helpers.root('src') // location of your src
+      ),
 
-      /* Raw loader support for *.html
-       * Returns file content as string
+      /*
+       * Plugin: CopyWebpackPlugin
+       * Description: Copy files and directories in webpack.
        *
-       * See: https://github.com/webpack/raw-loader
+       * Copies project static assets.
+       *
+       * See: https://www.npmjs.com/package/copy-webpack-plugin
        */
-      {
-        test: /\.html$/,
-        loader: 'raw-loader',
-        exclude: [ helpers.root('src/index.html') ]
-      }
-
-    ]
-
-  },
+      new CopyWebpackPlugin([
+        {
+          from: 'src/assets',
+          to: 'assets'
+        },
+        {
+          from: 'node_modules/webtorrent/webtorrent.min.js',
+          to: 'assets/webtorrent'
+        }
+      ]),
 
-  sassLoader: {
-    precision: 10
-  },
-
-  /*
-   * Add additional plugins to the compiler.
-   *
-   * See: http://webpack.github.io/docs/configuration.html#plugins
-   */
-  plugins: [
-
-    /*
-     * Plugin: ForkCheckerPlugin
-     * Description: Do type checking in a separate process, so webpack don't need to wait.
-     *
-     * See: https://github.com/s-panferov/awesome-typescript-loader#forkchecker-boolean-defaultfalse
-     */
-    new ForkCheckerPlugin(),
-
-    /*
-     * Plugin: OccurenceOrderPlugin
-     * Description: Varies the distribution of the ids to get the smallest id length
-     * for often used ids.
-     *
-     * See: https://webpack.github.io/docs/list-of-plugins.html#occurrenceorderplugin
-     * See: https://github.com/webpack/docs/wiki/optimization#minimize
-     */
-    new webpack.optimize.OccurenceOrderPlugin(true),
-
-    /*
-     * Plugin: CommonsChunkPlugin
-     * Description: Shares common code between the pages.
-     * It identifies common modules and put them into a commons chunk.
-     *
-     * See: https://webpack.github.io/docs/list-of-plugins.html#commonschunkplugin
-     * See: https://github.com/webpack/docs/wiki/optimization#multi-page-app
-     */
-    new webpack.optimize.CommonsChunkPlugin({
-      name: [ 'polyfills', 'vendor' ].reverse()
-    }),
+      /*
+       * Plugin: HtmlWebpackPlugin
+       * Description: Simplifies creation of HTML files to serve your webpack bundles.
+       * This is especially useful for webpack bundles that include a hash in the filename
+       * which changes every compilation.
+       *
+       * See: https://github.com/ampedandwired/html-webpack-plugin
+       */
+      new HtmlWebpackPlugin({
+        template: 'src/index.html',
+        chunksSortMode: 'dependency'
+      }),
 
-    /*
-     * Plugin: CopyWebpackPlugin
-     * Description: Copy files and directories in webpack.
-     *
-     * Copies project static assets.
-     *
-     * See: https://www.npmjs.com/package/copy-webpack-plugin
-     */
-    new CopyWebpackPlugin([
-      {
-        from: 'src/assets',
-        to: 'assets'
-      },
-      {
-        from: 'node_modules/webtorrent/webtorrent.min.js',
-        to: 'assets/webtorrent'
-      }
-    ]),
+      new WebpackNotifierPlugin({ alwaysNotify: true })
+    ],
 
     /*
-     * Plugin: HtmlWebpackPlugin
-     * Description: Simplifies creation of HTML files to serve your webpack bundles.
-     * This is especially useful for webpack bundles that include a hash in the filename
-     * which changes every compilation.
+     * Include polyfills or mocks for various node stuff
+     * Description: Node configuration
      *
-     * See: https://github.com/ampedandwired/html-webpack-plugin
+     * See: https://webpack.github.io/docs/configuration.html#node
      */
-    new HtmlWebpackPlugin({
-      template: 'src/index.html',
-      chunksSortMode: 'dependency'
-    }),
-
-    new WebpackNotifierPlugin({ alwaysNotify: true })
-  ],
-
-  /*
-   * Include polyfills or mocks for various node stuff
-   * Description: Node configuration
-   *
-   * See: https://webpack.github.io/docs/configuration.html#node
-   */
-  node: {
-    global: 'window',
-    crypto: 'empty',
-    fs: 'empty',
-    events: true,
-    module: false,
-    clearImmediate: false,
-    setImmediate: false
+    node: {
+      global: 'window',
+      crypto: 'empty',
+      fs: 'empty',
+      events: true,
+      module: false,
+      clearImmediate: false,
+      setImmediate: false
+    }
   }
-
 }
index 50193bf58f18c7f2e61e4eb48c271083853c5f27..0b6c00cbde54eca23acc9718fbbe97134e858a4c 100644 (file)
@@ -6,15 +6,18 @@ const commonConfig = require('./webpack.common.js') // the settings that are com
  * Webpack Plugins
  */
 const DefinePlugin = require('webpack/lib/DefinePlugin')
+const NamedModulesPlugin = require('webpack/lib/NamedModulesPlugin')
 
 /**
  * Webpack Constants
  */
 const ENV = process.env.ENV = process.env.NODE_ENV = 'development'
+const HOST = process.env.HOST || 'localhost'
+const PORT = process.env.PORT || 3000
 const HMR = helpers.hasProcessFlag('hot')
-const METADATA = webpackMerge(commonConfig.metadata, {
-  host: 'localhost',
-  port: 3000,
+const METADATA = webpackMerge(commonConfig({env: ENV}).metadata, {
+  host: HOST,
+  port: PORT,
   ENV: ENV,
   HMR: HMR
 })
@@ -24,119 +27,136 @@ const METADATA = webpackMerge(commonConfig.metadata, {
  *
  * See: http://webpack.github.io/docs/configuration.html#cli
  */
-module.exports = webpackMerge(commonConfig, {
-  /**
-   * Merged metadata from webpack.common.js for index.html
-   *
-   * See: (custom attribute)
-   */
-  metadata: METADATA,
-
-  /**
-   * Switch loaders to debug mode.
-   *
-   * See: http://webpack.github.io/docs/configuration.html#debug
-   */
-  debug: true,
-
-  /**
-   * Developer tool to enhance debugging
-   *
-   * See: http://webpack.github.io/docs/configuration.html#devtool
-   * See: https://github.com/webpack/docs/wiki/build-performance#sourcemaps
-   */
-  devtool: 'cheap-module-source-map',
-
-  /**
-   * Options affecting the output of the compilation.
-   *
-   * See: http://webpack.github.io/docs/configuration.html#output
-   */
-  output: {
+module.exports = function (env) {
+  return webpackMerge(commonConfig({env: ENV}), {
     /**
-     * The output directory as absolute path (required).
+     * Merged metadata from webpack.common.js for index.html
      *
-     * See: http://webpack.github.io/docs/configuration.html#output-path
+     * See: (custom attribute)
      */
-    path: helpers.root('dist'),
+    metadata: METADATA,
 
     /**
-     * Specifies the name of each output file on disk.
-     * IMPORTANT: You must not specify an absolute path here!
+     * Switch loaders to debug mode.
      *
-     * See: http://webpack.github.io/docs/configuration.html#output-filename
+     * See: http://webpack.github.io/docs/configuration.html#debug
      */
-    filename: '[name].bundle.js',
+    debug: true,
 
     /**
-     * The filename of the SourceMaps for the JavaScript files.
-     * They are inside the output.path directory.
+     * Developer tool to enhance debugging
      *
-     * See: http://webpack.github.io/docs/configuration.html#output-sourcemapfilename
+     * See: http://webpack.github.io/docs/configuration.html#devtool
+     * See: https://github.com/webpack/docs/wiki/build-performance#sourcemaps
      */
-    sourceMapFilename: '[name].map',
+    devtool: 'cheap-module-source-map',
 
-    /** The filename of non-entry chunks as relative path
-     * inside the output.path directory.
+    /**
+     * Options affecting the output of the compilation.
      *
-     * See: http://webpack.github.io/docs/configuration.html#output-chunkfilename
+     * See: http://webpack.github.io/docs/configuration.html#output
      */
-    chunkFilename: '[id].chunk.js'
-
-  },
-
-  externals: {
-    webtorrent: 'WebTorrent'
-  },
+    output: {
+      /**
+       * The output directory as absolute path (required).
+       *
+       * See: http://webpack.github.io/docs/configuration.html#output-path
+       */
+      path: helpers.root('dist'),
+
+      /**
+       * Specifies the name of each output file on disk.
+       * IMPORTANT: You must not specify an absolute path here!
+       *
+       * See: http://webpack.github.io/docs/configuration.html#output-filename
+       */
+      filename: '[name].bundle.js',
+
+      /**
+       * The filename of the SourceMaps for the JavaScript files.
+       * They are inside the output.path directory.
+       *
+       * See: http://webpack.github.io/docs/configuration.html#output-sourcemapfilename
+       */
+      sourceMapFilename: '[name].map',
+
+      /** The filename of non-entry chunks as relative path
+       * inside the output.path directory.
+       *
+       * See: http://webpack.github.io/docs/configuration.html#output-chunkfilename
+       */
+      chunkFilename: '[id].chunk.js',
+
+      library: 'ac_[name]',
+      libraryTarget: 'var'
+
+    },
+
+    externals: {
+      webtorrent: 'WebTorrent'
+    },
+
+    plugins: [
+
+      /**
+       * Plugin: DefinePlugin
+       * Description: Define free variables.
+       * Useful for having development builds with debug logging or adding global constants.
+       *
+       * Environment helpers
+       *
+       * See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin
+       */
+      // NOTE: when adding more properties, make sure you include them in custom-typings.d.ts
+      new DefinePlugin({
+        'ENV': JSON.stringify(METADATA.ENV),
+        'HMR': METADATA.HMR,
+        'process.env': {
+          'ENV': JSON.stringify(METADATA.ENV),
+          'NODE_ENV': JSON.stringify(METADATA.ENV),
+          'HMR': METADATA.HMR
+        }
+      }),
 
-  plugins: [
+      new NamedModulesPlugin()
+    ],
 
     /**
-     * Plugin: DefinePlugin
-     * Description: Define free variables.
-     * Useful for having development builds with debug logging or adding global constants.
+     * Static analysis linter for TypeScript advanced options configuration
+     * Description: An extensible linter for the TypeScript language.
      *
-     * Environment helpers
+     * See: https://github.com/wbuchwalter/tslint-loader
+     */
+    tslint: {
+      emitErrors: false,
+      failOnHint: false,
+      resourcePath: 'src'
+    },
+
+    devServer: {
+      port: METADATA.port,
+      host: METADATA.host,
+      historyApiFallback: true,
+      watchOptions: {
+        aggregateTimeout: 300,
+        poll: 1000
+      },
+      outputPath: helpers.root('dist')
+    },
+
+    /*
+     * Include polyfills or mocks for various node stuff
+     * Description: Node configuration
      *
-     * See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin
+     * See: https://webpack.github.io/docs/configuration.html#node
      */
-    // NOTE: when adding more properties, make sure you include them in custom-typings.d.ts
-    new DefinePlugin({
-      'ENV': JSON.stringify(METADATA.ENV),
-      'HMR': METADATA.HMR,
-      'process.env': {
-        'ENV': JSON.stringify(METADATA.ENV),
-        'NODE_ENV': JSON.stringify(METADATA.ENV),
-        'HMR': METADATA.HMR
-      }
-    })
-  ],
-
-  /**
-   * Static analysis linter for TypeScript advanced options configuration
-   * Description: An extensible linter for the TypeScript language.
-   *
-   * See: https://github.com/wbuchwalter/tslint-loader
-   */
-  tslint: {
-    emitErrors: false,
-    failOnHint: false,
-    resourcePath: 'src'
-  },
-
-  /*
-   * Include polyfills or mocks for various node stuff
-   * Description: Node configuration
-   *
-   * See: https://webpack.github.io/docs/configuration.html#node
-   */
-  node: {
-    global: 'window',
-    crypto: 'empty',
-    process: true,
-    module: false,
-    clearImmediate: false,
-    setImmediate: false
-  }
-
-})
+    node: {
+      global: 'window',
+      crypto: 'empty',
+      process: true,
+      module: false,
+      clearImmediate: false,
+      setImmediate: false
+    }
+  })
+}
index 7ce5727d32bbfed1d6c7bad41c71c5035d39fb9e..46db5448216381cc24c09b7b053797d0b7daadee 100644 (file)
@@ -9,10 +9,12 @@ const commonConfig = require('./webpack.common.js') // the settings that are com
 /**
  * Webpack Plugins
  */
+// const ProvidePlugin = require('webpack/lib/ProvidePlugin')
 const DefinePlugin = require('webpack/lib/DefinePlugin')
-const DedupePlugin = require('webpack/lib/optimize/DedupePlugin')
+const NormalModuleReplacementPlugin = require('webpack/lib/NormalModuleReplacementPlugin')
+// const IgnorePlugin = require('webpack/lib/IgnorePlugin')
+// const DedupePlugin = require('webpack/lib/optimize/DedupePlugin')
 const UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin')
-const CompressionPlugin = require('compression-webpack-plugin')
 const WebpackMd5Hash = require('webpack-md5-hash')
 
 /**
@@ -21,211 +23,210 @@ const WebpackMd5Hash = require('webpack-md5-hash')
 const ENV = process.env.NODE_ENV = process.env.ENV = 'production'
 const HOST = process.env.HOST || 'localhost'
 const PORT = process.env.PORT || 8080
-const METADATA = webpackMerge(commonConfig.metadata, {
+const METADATA = webpackMerge(commonConfig({env: ENV}).metadata, {
   host: HOST,
   port: PORT,
   ENV: ENV,
   HMR: false
 })
 
-module.exports = webpackMerge(commonConfig, {
-  /**
-   * Switch loaders to debug mode.
-   *
-   * See: http://webpack.github.io/docs/configuration.html#debug
-   */
-  debug: false,
-
-  /**
-   * Developer tool to enhance debugging
-   *
-   * See: http://webpack.github.io/docs/configuration.html#devtool
-   * See: https://github.com/webpack/docs/wiki/build-performance#sourcemaps
-   */
-  devtool: 'source-map',
-
-  /**
-   * Options affecting the output of the compilation.
-   *
-   * See: http://webpack.github.io/docs/configuration.html#output
-   */
-  output: {
+module.exports = function (env) {
+  return webpackMerge(commonConfig({env: ENV}), {
     /**
-     * The output directory as absolute path (required).
+     * Switch loaders to debug mode.
      *
-     * See: http://webpack.github.io/docs/configuration.html#output-path
+     * See: http://webpack.github.io/docs/configuration.html#debug
      */
-    path: helpers.root('dist'),
+    debug: false,
 
     /**
-     * Specifies the name of each output file on disk.
-     * IMPORTANT: You must not specify an absolute path here!
+     * Developer tool to enhance debugging
      *
-     * See: http://webpack.github.io/docs/configuration.html#output-filename
+     * See: http://webpack.github.io/docs/configuration.html#devtool
+     * See: https://github.com/webpack/docs/wiki/build-performance#sourcemaps
      */
-    filename: '[name].[chunkhash].bundle.js',
+    devtool: 'source-map',
 
     /**
-     * The filename of the SourceMaps for the JavaScript files.
-     * They are inside the output.path directory.
+     * Options affecting the output of the compilation.
      *
-     * See: http://webpack.github.io/docs/configuration.html#output-sourcemapfilename
+     * See: http://webpack.github.io/docs/configuration.html#output
      */
-    sourceMapFilename: '[name].[chunkhash].bundle.map',
+    output: {
+      /**
+       * The output directory as absolute path (required).
+       *
+       * See: http://webpack.github.io/docs/configuration.html#output-path
+       */
+      path: helpers.root('dist'),
+
+      /**
+       * Specifies the name of each output file on disk.
+       * IMPORTANT: You must not specify an absolute path here!
+       *
+       * See: http://webpack.github.io/docs/configuration.html#output-filename
+       */
+      filename: '[name].[chunkhash].bundle.js',
+
+      /**
+       * The filename of the SourceMaps for the JavaScript files.
+       * They are inside the output.path directory.
+       *
+       * See: http://webpack.github.io/docs/configuration.html#output-sourcemapfilename
+       */
+      sourceMapFilename: '[name].[chunkhash].bundle.map',
+
+      /**
+       * The filename of non-entry chunks as relative path
+       * inside the output.path directory.
+       *
+       * See: http://webpack.github.io/docs/configuration.html#output-chunkfilename
+       */
+      chunkFilename: '[id].[chunkhash].chunk.js'
+
+    },
+
+    externals: {
+      webtorrent: 'WebTorrent'
+    },
 
     /**
-     * The filename of non-entry chunks as relative path
-     * inside the output.path directory.
+     * Add additional plugins to the compiler.
      *
-     * See: http://webpack.github.io/docs/configuration.html#output-chunkfilename
+     * See: http://webpack.github.io/docs/configuration.html#plugins
      */
-    chunkFilename: '[id].[chunkhash].chunk.js'
-
-  },
-
-  externals: {
-    webtorrent: 'WebTorrent'
-  },
-
-  /**
-   * Add additional plugins to the compiler.
-   *
-   * See: http://webpack.github.io/docs/configuration.html#plugins
-   */
-  plugins: [
-
-    /**
-     * Plugin: WebpackMd5Hash
-     * Description: Plugin to replace a standard webpack chunkhash with md5.
-     *
-     * See: https://www.npmjs.com/package/webpack-md5-hash
-     */
-    new WebpackMd5Hash(),
+    plugins: [
+
+      /**
+       * Plugin: WebpackMd5Hash
+       * Description: Plugin to replace a standard webpack chunkhash with md5.
+       *
+       * See: https://www.npmjs.com/package/webpack-md5-hash
+       */
+      new WebpackMd5Hash(),
+
+      /**
+       * Plugin: DedupePlugin
+       * Description: Prevents the inclusion of duplicate code into your bundle
+       * and instead applies a copy of the function at runtime.
+       *
+       * See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin
+       * See: https://github.com/webpack/docs/wiki/optimization#deduplication
+       */
+      // new DedupePlugin(),
+
+      /**
+       * Plugin: DefinePlugin
+       * Description: Define free variables.
+       * Useful for having development builds with debug logging or adding global constants.
+       *
+       * Environment helpers
+       *
+       * See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin
+       */
+      // NOTE: when adding more properties make sure you include them in custom-typings.d.ts
+      new DefinePlugin({
+        'ENV': JSON.stringify(METADATA.ENV),
+        'HMR': METADATA.HMR,
+        'process.env': {
+          'ENV': JSON.stringify(METADATA.ENV),
+          'NODE_ENV': JSON.stringify(METADATA.ENV),
+          'HMR': METADATA.HMR
+        }
+      }),
+
+      /**
+       * Plugin: UglifyJsPlugin
+       * Description: Minimize all JavaScript output of chunks.
+       * Loaders are switched into minimizing mode.
+       *
+       * See: https://webpack.github.io/docs/list-of-plugins.html#uglifyjsplugin
+       */
+      // NOTE: To debug prod builds uncomment //debug lines and comment //prod lines
+      new UglifyJsPlugin({
+        // beautify: true, //debug
+        // mangle: false, //debug
+        // dead_code: false, //debug
+        // unused: false, //debug
+        // deadCode: false, //debug
+        // compress: {
+        //   screw_ie8: true,
+        //   keep_fnames: true,
+        //   drop_debugger: false,
+        //   dead_code: false,
+        //   unused: false
+        // }, // debug
+        // comments: true, //debug
+
+        beautify: false, // prod
+        mangle: { screw_ie8: true, keep_fnames: true }, // prod
+        compress: { screw_ie8: true }, // prod
+        comments: false // prod
+      }),
+
+      new NormalModuleReplacementPlugin(
+        /angular2-hmr/,
+        helpers.root('config/modules/angular2-hmr-prod.js')
+      )
+
+      /**
+       * Plugin: CompressionPlugin
+       * Description: Prepares compressed versions of assets to serve
+       * them with Content-Encoding
+       *
+       * See: https://github.com/webpack/compression-webpack-plugin
+       */
+      // new CompressionPlugin({
+      //   regExp: /\.css$|\.html$|\.js$|\.map$/,
+      //   threshold: 2 * 1024
+      // })
 
-    /**
-     * Plugin: DedupePlugin
-     * Description: Prevents the inclusion of duplicate code into your bundle
-     * and instead applies a copy of the function at runtime.
-     *
-     * See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin
-     * See: https://github.com/webpack/docs/wiki/optimization#deduplication
-     */
-    new DedupePlugin(),
+    ],
 
     /**
-     * Plugin: DefinePlugin
-     * Description: Define free variables.
-     * Useful for having development builds with debug logging or adding global constants.
-     *
-     * Environment helpers
+     * Static analysis linter for TypeScript advanced options configuration
+     * Description: An extensible linter for the TypeScript language.
      *
-     * See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin
+     * See: https://github.com/wbuchwalter/tslint-loader
      */
-    // NOTE: when adding more properties make sure you include them in custom-typings.d.ts
-    new DefinePlugin({
-      'ENV': JSON.stringify(METADATA.ENV),
-      'HMR': METADATA.HMR,
-      'process.env': {
-        'ENV': JSON.stringify(METADATA.ENV),
-        'NODE_ENV': JSON.stringify(METADATA.ENV),
-        'HMR': METADATA.HMR
-      }
-    }),
+    tslint: {
+      emitErrors: true,
+      failOnHint: true,
+      resourcePath: 'src'
+    },
 
     /**
-     * Plugin: UglifyJsPlugin
-     * Description: Minimize all JavaScript output of chunks.
-     * Loaders are switched into minimizing mode.
+     * Html loader advanced options
      *
-     * See: https://webpack.github.io/docs/list-of-plugins.html#uglifyjsplugin
+     * See: https://github.com/webpack/html-loader#advanced-options
      */
-    // NOTE: To debug prod builds uncomment //debug lines and comment //prod lines
-    new UglifyJsPlugin({
-      // beautify: true, //debug
-      // mangle: false, //debug
-      // dead_code: false, //debug
-      // unused: false, //debug
-      // deadCode: false, //debug
-      // compress: {
-      //   screw_ie8: true,
-      //   keep_fnames: true,
-      //   drop_debugger: false,
-      //   dead_code: false,
-      //   unused: false
-      // }, // debug
-      // comments: true, //debug
-
-      beautify: false, // prod
-
-      mangle: {
-        screw_ie8: true,
-        keep_fnames: true
-      }, // prod
-
-      compress: {
-        screw_ie8: true
-      }, // prod
-
-      comments: false // prod
-    }),
-
-    /**
-     * Plugin: CompressionPlugin
-     * Description: Prepares compressed versions of assets to serve
-     * them with Content-Encoding
+    // TODO: Need to workaround Angular 2's html syntax => #id [bind] (event) *ngFor
+    htmlLoader: {
+      minimize: true,
+      removeAttributeQuotes: false,
+      caseSensitive: true,
+      customAttrSurround: [
+        [/#/, /(?:)/],
+        [/\*/, /(?:)/],
+        [/\[?\(?/, /(?:)/]
+      ],
+      customAttrAssign: [/\)?\]?=/]
+    },
+
+    /*
+     * Include polyfills or mocks for various node stuff
+     * Description: Node configuration
      *
-     * See: https://github.com/webpack/compression-webpack-plugin
+     * See: https://webpack.github.io/docs/configuration.html#node
      */
-    new CompressionPlugin({
-      regExp: /\.css$|\.html$|\.js$|\.map$/,
-      threshold: 2 * 1024
-    })
-
-  ],
-
-  /**
-   * Static analysis linter for TypeScript advanced options configuration
-   * Description: An extensible linter for the TypeScript language.
-   *
-   * See: https://github.com/wbuchwalter/tslint-loader
-   */
-  tslint: {
-    emitErrors: true,
-    failOnHint: true,
-    resourcePath: 'src'
-  },
-
-  /**
-   * Html loader advanced options
-   *
-   * See: https://github.com/webpack/html-loader#advanced-options
-   */
-  // TODO: Need to workaround Angular 2's html syntax => #id [bind] (event) *ngFor
-  htmlLoader: {
-    minimize: true,
-    removeAttributeQuotes: false,
-    caseSensitive: true,
-    customAttrSurround: [
-      [/#/, /(?:)/],
-      [/\*/, /(?:)/],
-      [/\[?\(?/, /(?:)/]
-    ],
-    customAttrAssign: [/\)?\]?=/]
-  },
-
-  /*
-   * Include polyfills or mocks for various node stuff
-   * Description: Node configuration
-   *
-   * See: https://webpack.github.io/docs/configuration.html#node
-   */
-  node: {
-    global: 'window',
-    crypto: 'empty',
-    process: false,
-    module: false,
-    clearImmediate: false,
-    setImmediate: false
-  }
-
-})
+    node: {
+      global: 'window',
+      crypto: 'empty',
+      process: false,
+      module: false,
+      clearImmediate: false,
+      setImmediate: false
+    }
+
+  })
+}
index a5c5d092b258872053f775614467244a44b7817b..cc116f3e56522399ab24d50934804b3a607addb3 100644 (file)
     "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 (file)
index 0000000..5a8847a
--- /dev/null
@@ -0,0 +1,27 @@
+<h3>Account</h3>
+
+<div *ngIf="information" class="alert alert-success">{{ information }}</div>
+<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
+
+<form role="form" (ngSubmit)="changePassword()" [formGroup]="form">
+  <div class="form-group">
+    <label for="new-password">New password</label>
+    <input
+      type="password" class="form-control" id="new-password"
+      formControlName="new-password"
+    >
+    <div *ngIf="formErrors['new-password']" class="alert alert-danger">
+      {{ formErrors['new-password'] }}
+    </div>
+  </div>
+
+  <div class="form-group">
+    <label for="name">Confirm new password</label>
+    <input
+      type="password" class="form-control" id="new-confirmed-password"
+      formControlName="new-confirmed-password"
+    >
+  </div>
+
+  <input type="submit" value="Change password" class="btn btn-default" [disabled]="!form.valid">
+</form>
diff --git a/client/src/app/account/account.component.ts b/client/src/app/account/account.component.ts
new file mode 100644 (file)
index 0000000..851eaf1
--- /dev/null
@@ -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 (file)
index 0000000..e348c6e
--- /dev/null
@@ -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 (file)
index 0000000..355bcef
--- /dev/null
@@ -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 (file)
index 0000000..823d9fe
--- /dev/null
@@ -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 (file)
index 0000000..64a7400
--- /dev/null
@@ -0,0 +1,8 @@
+import { Component } from '@angular/core';
+
+@Component({
+  template: '<router-outlet></router-outlet>'
+})
+
+export class AdminComponent {
+}
diff --git a/client/src/app/admin/admin.routes.ts b/client/src/app/admin/admin.routes.ts
new file mode 100644 (file)
index 0000000..edb8ba4
--- /dev/null
@@ -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 (file)
index 0000000..788f3b4
--- /dev/null
@@ -0,0 +1,26 @@
+<h3>Make friends</h3>
+
+<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
+
+<form (ngSubmit)="makeFriends()" [formGroup]="form">
+  <div class="form-group"  *ngFor="let url of urls; let id = index; trackBy:customTrackBy">
+    <label for="username">Url</label>
+
+    <div class="input-group">
+      <input
+        type="text" class="form-control" placeholder="http://domain.com"
+        [id]="'url-' + id" [formControlName]="'url-' + id"
+      />
+      <span class="input-group-btn">
+        <button *ngIf="displayAddField(id)" (click)="addField()" class="btn btn-default" type="button">+</button>
+        <button *ngIf="displayRemoveField(id)" (click)="removeField(id)" class="btn btn-default" type="button">-</button>
+      </span>
+    </div>
+
+    <div [hidden]="form.controls['url-' + id].valid || form.controls['url-' + id].pristine" class="alert alert-warning">
+      It should be a valid url.
+    </div>
+  </div>
+
+  <input type="submit" value="Make friends" class="btn btn-default" [disabled]="!isFormValid()">
+</form>
diff --git a/client/src/app/admin/friends/friend-add/friend-add.component.scss b/client/src/app/admin/friends/friend-add/friend-add.component.scss
new file mode 100644 (file)
index 0000000..5fde516
--- /dev/null
@@ -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 (file)
index 0000000..64165a9
--- /dev/null
@@ -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 (file)
index 0000000..a101b3b
--- /dev/null
@@ -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 (file)
index 0000000..d786a78
--- /dev/null
@@ -0,0 +1,29 @@
+<h3>Friends list</h3>
+
+<table class="table table-hover">
+  <thead>
+    <tr>
+      <th class="table-column-id">ID</th>
+      <th>Url</th>
+      <th>Score</th>
+      <th>Created Date</th>
+    </tr>
+  </thead>
+
+  <tbody>
+    <tr *ngFor="let friend of friends">
+      <td>{{ friend.id }}</td>
+      <td>{{ friend.url }}</td>
+      <td>{{ friend.score }}</td>
+      <td>{{ friend.createdDate | date: 'medium' }}</td>
+    </tr>
+  </tbody>
+</table>
+
+<a *ngIf="friends?.length !== 0" class="add-user btn btn-danger pull-left" (click)="quitFriends()">
+  Quit friends
+</a>
+
+<a *ngIf="friends?.length === 0" class="add-user btn btn-success pull-right" [routerLink]="['/admin/friends/add']">
+  Make friends
+</a>
diff --git a/client/src/app/admin/friends/friend-list/friend-list.component.scss b/client/src/app/admin/friends/friend-list/friend-list.component.scss
new file mode 100644 (file)
index 0000000..cb597e1
--- /dev/null
@@ -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 (file)
index 0000000..88c4800
--- /dev/null
@@ -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 (file)
index 0000000..354c978
--- /dev/null
@@ -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 (file)
index 0000000..bc3f541
--- /dev/null
@@ -0,0 +1,8 @@
+import { Component } from '@angular/core';
+
+@Component({
+    template: '<router-outlet></router-outlet>'
+})
+
+export class FriendsComponent {
+}
diff --git a/client/src/app/admin/friends/friends.routes.ts b/client/src/app/admin/friends/friends.routes.ts
new file mode 100644 (file)
index 0000000..7fdef68
--- /dev/null
@@ -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 (file)
index 0000000..dd4df25
--- /dev/null
@@ -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 (file)
index 0000000..7cb28f4
--- /dev/null
@@ -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 (file)
index 0000000..75826fc
--- /dev/null
@@ -0,0 +1,39 @@
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs/Observable';
+
+import { Friend } from './friend.model';
+import { AuthHttp, RestExtractor } from '../../../shared';
+
+@Injectable()
+export class FriendService {
+  private static BASE_FRIEND_URL: string = '/api/v1/pods/';
+
+  constructor (
+    private authHttp: AuthHttp,
+    private restExtractor: RestExtractor
+  ) {}
+
+  getFriends(): Observable<Friend[]> {
+    return this.authHttp.get(FriendService.BASE_FRIEND_URL)
+                        // Not implemented as a data list by the server yet
+                        // .map(this.restExtractor.extractDataList)
+                        .map((res) => res.json())
+                        .catch((res) => this.restExtractor.handleError(res));
+  }
+
+  makeFriends(notEmptyUrls) {
+    const body = {
+      urls: notEmptyUrls
+    };
+
+    return this.authHttp.post(FriendService.BASE_FRIEND_URL + 'makefriends', body)
+                        .map(this.restExtractor.extractDataBool)
+                        .catch((res) => this.restExtractor.handleError(res));
+  }
+
+  quitFriends() {
+    return this.authHttp.get(FriendService.BASE_FRIEND_URL + 'quitfriends')
+                        .map(res => res.status)
+                        .catch((res) => this.restExtractor.handleError(res));
+  }
+}
similarity index 51%
rename from client/src/app/friends/index.ts
rename to client/src/app/admin/friends/shared/index.ts
index 0adc256c43280b29ac261098e1d7d31a77f82dcb..0d671637d11b8cfd6cc7119080262ff6edf1db59 100644 (file)
@@ -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 (file)
index 0000000..493caed
--- /dev/null
@@ -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 (file)
index 0000000..e250615
--- /dev/null
@@ -0,0 +1,26 @@
+<menu class="col-md-2 col-sm-3 col-xs-3">
+
+  <div class="panel-block">
+    <div id="panel-users" class="panel-button">
+      <span class="hidden-xs glyphicon glyphicon-user"></span>
+      <a [routerLink]="['/admin/users/list']">List users</a>
+    </div>
+
+    <div id="panel-friends" class="panel-button">
+      <span class="hidden-xs glyphicon glyphicon-cloud"></span>
+      <a [routerLink]="['/admin/friends/list']">List friends</a>
+    </div>
+
+    <div id="panel-request-stats" class="panel-button">
+      <span class="hidden-xs glyphicon glyphicon-stats"></span>
+      <a [routerLink]="['/admin/requests/stats']">Request stats</a>
+    </div>
+  </div>
+
+  <div class="panel-block">
+    <div id="panel-quit-administration" class="panel-button">
+      <span class="hidden-xs glyphicon glyphicon-cog"></span>
+      <a [routerLink]="['/videos/list']">Quit admin.</a>
+    </div>
+  </div>
+</menu>
diff --git a/client/src/app/admin/menu-admin.component.ts b/client/src/app/admin/menu-admin.component.ts
new file mode 100644 (file)
index 0000000..59ffccf
--- /dev/null
@@ -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 (file)
index 0000000..236a9ee
--- /dev/null
@@ -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 (file)
index 0000000..be3a66f
--- /dev/null
@@ -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 (file)
index 0000000..b5ac59a
--- /dev/null
@@ -0,0 +1,23 @@
+<h3>Requests stats</h3>
+
+<div *ngIf="stats !== null">
+  <div>
+    <span class="label-description">Interval seconds between requests:</span>
+    {{ stats.secondsInterval }}
+  </div>
+
+  <div>
+    <span class="label-description">Remaining time before the scheduled request:</span>
+    {{ stats.remainingSeconds }}
+  </div>
+
+  <div>
+    <span class="label-description">Maximum number of requests per interval:</span>
+    {{ stats.maxRequestsInParallel }}
+  </div>
+
+  <div>
+    <span class="label-description">Remaining requests:</span>
+    {{ stats.requests.length }}
+  </div>
+</div>
diff --git a/client/src/app/admin/requests/request-stats/request-stats.component.scss b/client/src/app/admin/requests/request-stats/request-stats.component.scss
new file mode 100644 (file)
index 0000000..92c28dc
--- /dev/null
@@ -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 (file)
index 0000000..4b08445
--- /dev/null
@@ -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 (file)
index 0000000..471112b
--- /dev/null
@@ -0,0 +1,8 @@
+import { Component } from '@angular/core';
+
+@Component({
+    template: '<router-outlet></router-outlet>'
+})
+
+export class RequestsComponent {
+}
diff --git a/client/src/app/admin/requests/requests.routes.ts b/client/src/app/admin/requests/requests.routes.ts
new file mode 100644 (file)
index 0000000..78221a9
--- /dev/null
@@ -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 (file)
index 0000000..32ab576
--- /dev/null
@@ -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 (file)
index 0000000..766e808
--- /dev/null
@@ -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 (file)
index 0000000..aeec374
--- /dev/null
@@ -0,0 +1,22 @@
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs/Observable';
+
+import { RequestStats } from './request-stats.model';
+import { AuthHttp, RestExtractor } from '../../../shared';
+
+@Injectable()
+export class RequestService {
+  private static BASE_REQUEST_URL: string = '/api/v1/requests/';
+
+  constructor (
+    private authHttp: AuthHttp,
+    private restExtractor: RestExtractor
+  ) {}
+
+  getStats(): Observable<RequestStats> {
+    return this.authHttp.get(RequestService.BASE_REQUEST_URL + 'stats')
+                        .map(this.restExtractor.extractDataGet)
+                        .map((data) => new RequestStats(data))
+                        .catch((res) => this.restExtractor.handleError(res));
+  }
+}
diff --git a/client/src/app/admin/users/index.ts b/client/src/app/admin/users/index.ts
new file mode 100644 (file)
index 0000000..e98a81f
--- /dev/null
@@ -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 (file)
index 0000000..e17ee5c
--- /dev/null
@@ -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 (file)
index 0000000..13be553
--- /dev/null
@@ -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 (file)
index 0000000..66d5ca0
--- /dev/null
@@ -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 (file)
index 0000000..9b76c7c
--- /dev/null
@@ -0,0 +1,29 @@
+<h3>Add user</h3>
+
+<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
+
+<form role="form" (ngSubmit)="addUser()" [formGroup]="form">
+  <div class="form-group">
+    <label for="username">Username</label>
+    <input
+      type="text" class="form-control" id="username" placeholder="Username"
+      formControlName="username"
+    >
+    <div *ngIf="formErrors.username" class="alert alert-danger">
+      {{ formErrors.username }}
+    </div>
+  </div>
+
+  <div class="form-group">
+    <label for="password">Password</label>
+    <input
+      type="password" class="form-control" id="password" placeholder="Password"
+      formControlName="password"
+    >
+    <div *ngIf="formErrors.password" class="alert alert-danger">
+      {{ formErrors.password }}
+    </div>
+  </div>
+
+  <input type="submit" value="Add user" class="btn btn-default" [disabled]="!form.valid">
+</form>
diff --git a/client/src/app/admin/users/user-add/user-add.component.ts b/client/src/app/admin/users/user-add/user-add.component.ts
new file mode 100644 (file)
index 0000000..ab96fb0
--- /dev/null
@@ -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 (file)
index 0000000..51fbefa
--- /dev/null
@@ -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 (file)
index 0000000..328b1be
--- /dev/null
@@ -0,0 +1,28 @@
+<h3>Users list</h3>
+
+<table class="table table-hover">
+  <thead>
+    <tr>
+      <th class="table-column-id">ID</th>
+      <th>Username</th>
+      <th>Created Date</th>
+      <th class="text-right">Remove</th>
+    </tr>
+  </thead>
+
+  <tbody>
+    <tr *ngFor="let user of users">
+      <td>{{ user.id }}</td>
+      <td>{{ user.username }}</td>
+      <td>{{ user.createdDate | date: 'medium' }}</td>
+      <td class="text-right">
+        <span class="glyphicon glyphicon-remove" *ngIf="!user.isAdmin()" (click)="removeUser(user)"></span>
+      </td>
+    </tr>
+  </tbody>
+</table>
+
+<a class="add-user btn btn-success pull-right" [routerLink]="['/admin/users/add']">
+  <span class="glyphicon glyphicon-plus"></span>
+  Add user
+</a>
diff --git a/client/src/app/admin/users/user-list/user-list.component.scss b/client/src/app/admin/users/user-list/user-list.component.scss
new file mode 100644 (file)
index 0000000..e9f61e9
--- /dev/null
@@ -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 (file)
index 0000000..03f4e5c
--- /dev/null
@@ -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 (file)
index 0000000..37e3b15
--- /dev/null
@@ -0,0 +1,8 @@
+import { Component } from '@angular/core';
+
+@Component({
+    template: '<router-outlet></router-outlet>'
+})
+
+export class UsersComponent {
+}
diff --git a/client/src/app/admin/users/users.routes.ts b/client/src/app/admin/users/users.routes.ts
new file mode 100644 (file)
index 0000000..eb71bd0
--- /dev/null
@@ -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
+        }
+      ]
+    }
+];
index f2acffea4a2b1520169ebe5a25ff6a0047f81d65..04c32f596344624014b008ccc615d24a1d3cff3e 100644 (file)
 
 
   <div class="row">
-
-    <menu class="col-md-2 col-sm-3 col-xs-3">
-      <div class="panel-block">
-        <div id="panel-user-login" class="panel-button">
-          <span class="hidden-xs glyphicon glyphicon-user"></span>
-          <a *ngIf="!isLoggedIn" [routerLink]="['/login']">Login</a>
-          <a *ngIf="isLoggedIn" (click)="logout()">Logout</a>
-        </div>
-      </div>
-
-      <div class="panel-block">
-        <div id="panel-get-videos" class="panel-button">
-          <span class="hidden-xs glyphicon glyphicon-list"></span>
-          <a [routerLink]="['/videos/list']">Get videos</a>
-        </div>
-
-        <div id="panel-upload-video" class="panel-button" *ngIf="isLoggedIn">
-          <span class="hidden-xs glyphicon glyphicon-cloud-upload"></span>
-          <a [routerLink]="['/videos/add']">Upload a video</a>
-        </div>
-      </div>
-
-      <div class="panel-block" *ngIf="isLoggedIn">
-        <div id="panel-make-friends" class="panel-button">
-          <span class="hidden-xs glyphicon glyphicon-cloud"></span>
-          <a (click)='makeFriends()'>Make friends</a>
-        </div>
-
-        <div id="panel-quit-friends" class="panel-button">
-          <span class="hidden-xs glyphicon glyphicon-plane"></span>
-          <a (click)='quitFriends()'>Quit friends</a>
-        </div>
-      </div>
-    </menu>
+    <my-menu *ngIf="isInAdmin() === false"></my-menu>
+    <my-menu-admin *ngIf="isInAdmin() === true"></my-menu-admin>
 
     <div class="col-md-9 col-sm-8 col-xs-8 router-outlet-container">
       <router-outlet></router-outlet>
     </div>
-
   </div>
 
-
   <footer>
     PeerTube, CopyLeft 2015-2016
   </footer>
index 1b02b2f5744965ba094f4cf0cfa55b46b789d752..95f306d759cd3d46975098378edc719668ecf809 100644 (file)
@@ -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;
index b7a3d7c58c583e164ff1e62720b25750743a87d8..d6b83c684012153583120f0a0db2985889ad1181 100644 (file)
@@ -1,73 +1,16 @@
 import { Component } from '@angular/core';
-import { ActivatedRoute, Router, ROUTER_DIRECTIVES } from '@angular/router';
-
-import { FriendService } from './friends';
-import {
-  AuthService,
-  AuthStatus,
-  SearchComponent,
-  SearchService
-} from './shared';
-import { VideoService } from './videos';
+import { Router } from '@angular/router';
 
 @Component({
     selector: 'my-app',
-    template: require('./app.component.html'),
-    styles: [ require('./app.component.scss') ],
-    directives: [ ROUTER_DIRECTIVES, SearchComponent ],
-    providers: [ FriendService, VideoService, SearchService ]
+    templateUrl: './app.component.html',
+    styleUrls: [ './app.component.scss' ]
 })
 
 export class AppComponent {
-  choices = [];
-  isLoggedIn: boolean;
-
-  constructor(
-    private authService: AuthService,
-    private friendService: FriendService,
-    private route: ActivatedRoute,
-    private router: Router
-  ) {
-    this.isLoggedIn = this.authService.isLoggedIn();
-
-    this.authService.loginChangedSource.subscribe(
-      status => {
-        if (status === AuthStatus.LoggedIn) {
-          this.isLoggedIn = true;
-          console.log('Logged in.');
-        } else if (status === AuthStatus.LoggedOut) {
-          this.isLoggedIn = false;
-          console.log('Logged out.');
-        } else {
-          console.error('Unknown auth status: ' + status);
-        }
-      }
-    );
-  }
-
-  logout() {
-    this.authService.logout();
-  }
-
-  makeFriends() {
-    this.friendService.makeFriends().subscribe(
-      status => {
-        if (status === 409) {
-          alert('Already made friends!');
-        } else {
-          alert('Made friends!');
-        }
-      },
-      error => alert(error)
-    );
-  }
+  constructor(private router: Router) {}
 
-  quitFriends() {
-    this.friendService.quitFriends().subscribe(
-      status => {
-        alert('Quit friends!');
-      },
-      error => alert(error)
-    );
+  isInAdmin() {
+    return this.router.url.indexOf('/admin/') !== -1;
   }
 }
diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts
new file mode 100644 (file)
index 0000000..980625f
--- /dev/null
@@ -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;
+  }
+}
index 59ef4ce55ceb4e6f6cc254b749e4142cae43aff4..03e2bce5128102984d063c4cc6c4bf4043cb7cef 100644 (file)
@@ -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 (file)
index 0000000..033c219
--- /dev/null
@@ -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 (file)
index 0000000..8bba89c
--- /dev/null
@@ -0,0 +1,50 @@
+
+// Angular 2
+// rc2 workaround
+import { enableDebugTools, disableDebugTools } from '@angular/platform-browser';
+import { enableProdMode, ApplicationRef } from '@angular/core';
+// Environment Providers
+let PROVIDERS = [
+  // common env directives
+];
+
+// Angular debug tools in the dev console
+// https://github.com/angular/angular/blob/86405345b781a9dc2438c0fbe3e9409245647019/TOOLS_JS.md
+let _decorateModuleRef = function identity(value) { return value; };
+
+if ('production' === ENV) {
+  // Production
+  disableDebugTools();
+  enableProdMode();
+
+  PROVIDERS = [
+    ...PROVIDERS,
+    // custom providers in production
+  ];
+
+} else {
+
+  _decorateModuleRef = (modRef: any) => {
+    const appRef = modRef.injector.get(ApplicationRef);
+    const cmpRef = appRef.components[0];
+
+    let _ng = (<any>window).ng;
+    enableDebugTools(cmpRef);
+    (<any>window).ng.probe = _ng.probe;
+    (<any>window).ng.coreTokens = _ng.coreTokens;
+    return modRef;
+  };
+
+  // Development
+  PROVIDERS = [
+    ...PROVIDERS,
+    // custom providers in development
+  ];
+
+}
+
+export const decorateModuleRef = _decorateModuleRef;
+
+export const ENV_PROVIDERS = [
+  ...PROVIDERS
+];
diff --git a/client/src/app/friends/friend.service.ts b/client/src/app/friends/friend.service.ts
deleted file mode 100644 (file)
index 7710464..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-import { Injectable } from '@angular/core';
-import { Response } from '@angular/http';
-import { Observable } from 'rxjs/Observable';
-
-import { AuthHttp, AuthService } from '../shared';
-
-@Injectable()
-export class FriendService {
-  private static BASE_FRIEND_URL: string = '/api/v1/pods/';
-
-  constructor (private authHttp: AuthHttp, private authService: AuthService) {}
-
-  makeFriends() {
-    return this.authHttp.get(FriendService.BASE_FRIEND_URL + 'makefriends')
-                    .map(res => res.status)
-                    .catch(this.handleError);
-  }
-
-  quitFriends() {
-    return this.authHttp.get(FriendService.BASE_FRIEND_URL + 'quitfriends')
-                    .map(res => res.status)
-                    .catch(this.handleError);
-  }
-
-  private handleError (error: Response): Observable<number> {
-    console.error(error);
-    return Observable.throw(error.json().error || 'Server error');
-  }
-}
diff --git a/client/src/app/index.ts b/client/src/app/index.ts
new file mode 100644 (file)
index 0000000..da53f6a
--- /dev/null
@@ -0,0 +1 @@
+export * from './app.module';
index 5848fcba30d55f18f8e6f83867002af296c52768..94a405405baac501c98b536a8422702e826e2e88 100644 (file)
@@ -1,17 +1,16 @@
 <h3>Login</h3>
 
-
 <div *ngIf="error" class="alert alert-danger">{{ error }}</div>
 
-<form role="form" (ngSubmit)="login(username.value, password.value)" #loginForm="ngForm">
+<form role="form" (ngSubmit)="login()" [formGroup]="form">
   <div class="form-group">
     <label for="username">Username</label>
     <input
-      type="text" class="form-control" name="username" id="username" placeholder="Username" required
-      ngControl="username" #username="ngForm"
+      type="text" class="form-control" id="username" placeholder="Username" required
+      formControlName="username"
     >
-    <div [hidden]="username.valid || username.pristine" class="alert alert-danger">
-      Username is required
+    <div *ngIf="formErrors.username" class="alert alert-danger">
+      {{ formErrors.username }}
     </div>
   </div>
 
     <label for="password">Password</label>
     <input
       type="password" class="form-control" name="password" id="password" placeholder="Password" required
-      ngControl="password" #password="ngForm"
+      formControlName="password"
     >
-    <div [hidden]="password.valid || password.pristine" class="alert alert-danger">
-      Password is required
+    <div *ngIf="formErrors.password" class="alert alert-danger">
+      {{ formErrors.password }}
     </div>
   </div>
 
-  <input type="submit" value="Login" class="btn btn-default" [disabled]="!loginForm.form.valid">
+  <input type="submit" value="Login" class="btn btn-default" [disabled]="!form.valid">
 </form>
index ddd62462ebed68cbf100f00138c8f0d1e2cb6db1..c4ff7050bf1f422301180e31d1e645421844ac91 100644 (file)
@@ -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 (file)
index 0000000..29ef7f9
--- /dev/null
@@ -0,0 +1,39 @@
+<menu class="col-md-2 col-sm-3 col-xs-3">
+  <div class="panel-block">
+    <div id="panel-user-login" class="panel-button">
+      <span *ngIf="!isLoggedIn" >
+        <span class="hidden-xs glyphicon glyphicon-log-in"></span>
+        <a [routerLink]="['/login']">Login</a>
+      </span>
+
+      <span *ngIf="isLoggedIn">
+        <span class="hidden-xs glyphicon glyphicon-log-out"></span>
+        <a *ngIf="isLoggedIn" (click)="logout()">Logout</a>
+      </span>
+    </div>
+
+    <div *ngIf="isLoggedIn" id="panel-user-account" class="panel-button">
+      <span class="hidden-xs glyphicon glyphicon-user"></span>
+      <a [routerLink]="['/account']">My account</a>
+    </div>
+  </div>
+
+  <div class="panel-block">
+    <div id="panel-get-videos" class="panel-button">
+      <span class="hidden-xs glyphicon glyphicon-list"></span>
+      <a [routerLink]="['/videos/list']">Get videos</a>
+    </div>
+
+    <div id="panel-upload-video" class="panel-button" *ngIf="isLoggedIn">
+      <span class="hidden-xs glyphicon glyphicon-cloud-upload"></span>
+      <a [routerLink]="['/videos/add']">Upload a video</a>
+    </div>
+  </div>
+
+  <div class="panel-block" *ngIf="isUserAdmin()">
+    <div id="panel-get-videos" class="panel-button">
+      <span class="hidden-xs glyphicon glyphicon-cog"></span>
+      <a [routerLink]="['/admin']">Administration</a>
+    </div>
+  </div>
+</menu>
diff --git a/client/src/app/menu.component.ts b/client/src/app/menu.component.ts
new file mode 100644 (file)
index 0000000..6cfc854
--- /dev/null
@@ -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']);
+  }
+}
index 9c7ef4389d7395349fd73c3bcc5bb006baf69b93..2392898cae43521a56bcb348af5ea84d5c09ec09 100644 (file)
@@ -28,7 +28,7 @@ export class AuthHttp extends Http {
     return super.request(url, options)
                 .catch((err) => {
                   if (err.status === 401) {
-                    return this.handleTokenExpired(err, url, options);
+                    return this.handleTokenExpired(url, options);
                   }
 
                   return Observable.throw(err);
@@ -49,26 +49,29 @@ export class AuthHttp extends Http {
     return this.request(url, options);
   }
 
-  post(url: string, options?: RequestOptionsArgs): Observable<Response> {
+  post(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
     if (!options) options = {};
     options.method = RequestMethod.Post;
+    options.body = body;
 
     return this.request(url, options);
   }
 
-  put(url: string, options?: RequestOptionsArgs): Observable<Response> {
+  put(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
     if (!options) options = {};
     options.method = RequestMethod.Put;
+    options.body = body;
 
     return this.request(url, options);
   }
 
-  private handleTokenExpired(err: Response, url: string | Request, options: RequestOptionsArgs) {
-    return this.authService.refreshAccessToken().flatMap(() => {
-      this.setAuthorizationHeader(options.headers);
+  private handleTokenExpired(url: string | Request, options: RequestOptionsArgs) {
+    return this.authService.refreshAccessToken()
+                           .flatMap(() => {
+                              this.setAuthorizationHeader(options.headers);
 
-      return super.request(url, options);
-    });
+                              return super.request(url, options);
+                            });
   }
 
   private setAuthorizationHeader(headers: Headers) {
similarity index 72%
rename from client/src/app/shared/auth/user.model.ts
rename to client/src/app/shared/auth/auth-user.model.ts
index 98852f8355c834dfda754ef45591afa07365038b..bdd5ea5a9e30df298a0a80319278cd9e6e59f0e4 100644 (file)
@@ -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',
index 584298fff585605b69f11dc9ebb9ad85273a12b7..a30c79c8685edafb59aa2bbdc6ba2d64afd17789 100644 (file)
@@ -1,32 +1,39 @@
 import { Injectable } from '@angular/core';
 import { Headers, Http, Response, URLSearchParams } from '@angular/http';
+import { Router } from '@angular/router';
 import { Observable } from 'rxjs/Observable';
 import { Subject } from 'rxjs/Subject';
 
 import { AuthStatus } from './auth-status.model';
-import { User } from './user.model';
+import { AuthUser } from './auth-user.model';
+import { RestExtractor } from '../rest';
 
 @Injectable()
 export class AuthService {
-  private static BASE_CLIENT_URL = '/api/v1/users/client';
+  private static BASE_CLIENT_URL = '/api/v1/clients/local';
   private static BASE_TOKEN_URL = '/api/v1/users/token';
+  private static BASE_USER_INFORMATIONS_URL = '/api/v1/users/me';
 
   loginChangedSource: Observable<AuthStatus>;
 
   private clientId: string;
   private clientSecret: string;
   private loginChanged: Subject<AuthStatus>;
-  private user: User = null;
+  private user: AuthUser = null;
 
-  constructor(private http: Http) {
+  constructor(
+    private http: Http,
+    private restExtractor: RestExtractor,
+    private router: Router
+   ) {
     this.loginChanged = new Subject<AuthStatus>();
     this.loginChangedSource = this.loginChanged.asObservable();
 
     // Fetch the client_id/client_secret
     // FIXME: save in local storage?
     this.http.get(AuthService.BASE_CLIENT_URL)
-      .map(res => res.json())
-      .catch(this.handleError)
+      .map(this.restExtractor.extractDataGet)
+      .catch((res) => this.restExtractor.handleError(res))
       .subscribe(
         result => {
           this.clientId = result.client_id;
@@ -34,12 +41,15 @@ export class AuthService {
           console.log('Client credentials loaded.');
         },
         error => {
-          alert(error);
+          alert(
+            `Cannot retrieve OAuth Client credentials: ${error.text}. \n` +
+            'Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.'
+          );
         }
       );
 
     // Return null if there is nothing to load
-    this.user = User.load();
+    this.user = AuthUser.load();
   }
 
   getRefreshToken() {
@@ -64,10 +74,16 @@ export class AuthService {
     return this.user.getTokenType();
   }
 
-  getUser(): User {
+  getUser(): AuthUser {
     return this.user;
   }
 
+  isAdmin() {
+    if (this.user === null) return false;
+
+    return this.user.isAdmin();
+  }
+
   isLoggedIn() {
     if (this.getAccessToken()) {
       return true;
@@ -94,21 +110,23 @@ export class AuthService {
     };
 
     return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options)
-                    .map(res => res.json())
+                    .map(this.restExtractor.extractDataGet)
                     .map(res => {
                       res.username = username;
                       return res;
                     })
+                    .flatMap(res => this.fetchUserInformations(res))
                     .map(res => this.handleLogin(res))
-                    .catch(this.handleError);
+                    .catch((res) => this.restExtractor.handleError(res));
   }
 
   logout() {
     // TODO: make an HTTP request to revoke the tokens
     this.user = null;
-    User.flush();
 
-    this.setStatus(AuthStatus.LoggedIn);
+    AuthUser.flush();
+
+    this.setStatus(AuthStatus.LoggedOut);
   }
 
   refreshAccessToken() {
@@ -131,36 +149,64 @@ export class AuthService {
     };
 
     return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options)
-                    .map(res => res.json())
+                    .map(this.restExtractor.extractDataGet)
                     .map(res => this.handleRefreshToken(res))
-                    .catch(this.handleError);
+                    .catch((res: Response) => {
+                      // The refresh token is invalid?
+                      if (res.status === 400 && res.json() && res.json().error === 'invalid_grant') {
+                        console.error('Cannot refresh token -> logout...');
+                        this.logout();
+                        this.router.navigate(['/login']);
+
+                        return Observable.throw({
+                          json: '',
+                          text: 'You need to reconnect.'
+                        });
+                      }
+
+                      return this.restExtractor.handleError(res);
+                    });
   }
 
-  private setStatus(status: AuthStatus) {
-    this.loginChanged.next(status);
+  private fetchUserInformations (obj: any) {
+    // Do not call authHttp here to avoid circular dependencies headaches
+
+    const headers = new Headers();
+    headers.set('Authorization', `Bearer ${obj.access_token}`);
+
+    return this.http.get(AuthService.BASE_USER_INFORMATIONS_URL, { headers })
+             .map(res => res.json())
+             .map(res => {
+               obj.id = res.id;
+               obj.role = res.role;
+               return obj;
+             }
+    );
   }
 
   private handleLogin (obj: any) {
+    const id = obj.id;
     const username = obj.username;
-    const hash_tokens = {
+    const role = obj.role;
+    const hashTokens = {
       access_token: obj.access_token,
       token_type: obj.token_type,
       refresh_token: obj.refresh_token
     };
 
-    this.user = new User(username, hash_tokens);
+    this.user = new AuthUser({ id, username, role }, hashTokens);
     this.user.save();
 
     this.setStatus(AuthStatus.LoggedIn);
   }
 
-  private handleError (error: Response) {
-    console.error(error);
-    return Observable.throw(error.json() || { error: 'Server error' });
-  }
-
   private handleRefreshToken (obj: any) {
     this.user.refreshTokens(obj.access_token, obj.refresh_token);
     this.user.save();
   }
+
+  private setStatus(status: AuthStatus) {
+    this.loginChanged.next(status);
+  }
+
 }
index aafaacbf1ff35296c279d8c3e970ef6aee2866c8..ebd9e14cd48e20895b2361dddccab49d40213154 100644 (file)
@@ -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 (file)
index 0000000..1e8a697
--- /dev/null
@@ -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 (file)
index 0000000..1d2ae6f
--- /dev/null
@@ -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 (file)
index 0000000..67163b4
--- /dev/null
@@ -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 (file)
index 0000000..5b11ff2
--- /dev/null
@@ -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 (file)
index 0000000..3766d40
--- /dev/null
@@ -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 (file)
index 0000000..588ebb4
--- /dev/null
@@ -0,0 +1,2 @@
+export * from './form-validators';
+export * from './form-reactive';
index dfea4c67cdb959ed561a9dfc2d01774d0e1efca2..af34b4b64da57cad4e724a699e887ef7c7257d1b 100644 (file)
@@ -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 (file)
index 0000000..3c9509d
--- /dev/null
@@ -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 (file)
index 0000000..fcb1598
--- /dev/null
@@ -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);
+  }
+}
similarity index 65%
rename from client/src/app/videos/shared/pagination.model.ts
rename to client/src/app/shared/rest/rest-pagination.ts
index eda44ebfbae5a378bb56af33e2b2d8deee926953..0cfa4f4681aee7bcb4102d057d9da20f203e7c6e 100644 (file)
@@ -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 (file)
index 0000000..16b47e9
--- /dev/null
@@ -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;
+  }
+
+}
index e864fbc172350776b38ae8784cae22e4959721e3..b6237469b0efd2ead2440c0350abc77d1787be9c 100644 (file)
@@ -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);
   }
 
index c7993db3dd05df9ba299dbb372e3300a0d398f42..717a7fa500e044fecc9bdce2d0aa0440196220eb 100644 (file)
@@ -1,5 +1,6 @@
 import { Injectable } from '@angular/core';
 import { Subject } from 'rxjs/Subject';
+import { ReplaySubject } from 'rxjs/ReplaySubject';
 
 import { Search } from './search.model';
 
@@ -12,6 +13,6 @@ export class SearchService {
 
   constructor() {
     this.updateSearch = new Subject<Search>();
-    this.searchUpdated = new Subject<Search>();
+    this.searchUpdated = new ReplaySubject<Search>(1);
   }
 }
diff --git a/client/src/app/shared/users/index.ts b/client/src/app/shared/users/index.ts
new file mode 100644 (file)
index 0000000..5a670ce
--- /dev/null
@@ -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 (file)
index 0000000..726495d
--- /dev/null
@@ -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';
+  }
+}
index a54120f5d5d2c69a8a1e8f7a4523e138bf6e1e66..67d16ead155876bf31f47d54dc653fc3faa3cab6 100644 (file)
@@ -1,5 +1,4 @@
 export * from './loader';
-export * from './pagination.model';
 export * from './sort-field.type';
 export * from './video.model';
 export * from './video.service';
index cdd07d1b4e6a565b7607ff6d9939ef4e97332d02..e72d2f3f3d605bd0d5f0fcdb87f3a41e976229f9 100644 (file)
@@ -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 {
index b4396f76794abe1eebc2265e66959f3f6ce9f08d..ad855753344a2f289a2713bdda9f46c8b5b62062 100644 (file)
@@ -1,11 +1,10 @@
 import { Injectable } from '@angular/core';
-import { Http, Response, URLSearchParams } from '@angular/http';
+import { Http } from '@angular/http';
 import { Observable } from 'rxjs/Observable';
 
-import { Pagination } from './pagination.model';
 import { Search } from '../../shared';
 import { SortField } from './sort-field.type';
-import { AuthHttp, AuthService } from '../../shared';
+import { AuthHttp, AuthService, RestExtractor, RestPagination, RestService, ResultList } from '../../shared';
 import { Video } from './video.model';
 
 @Injectable()
@@ -15,68 +14,51 @@ export class VideoService {
   constructor(
     private authService: AuthService,
     private authHttp: AuthHttp,
-    private http: Http
+    private http: Http,
+    private restExtractor: RestExtractor,
+    private restService: RestService
   ) {}
 
-  getVideo(id: string) {
+  getVideo(id: string): Observable<Video> {
     return this.http.get(VideoService.BASE_VIDEO_URL + id)
-                    .map(res => <Video> res.json())
-                    .catch(this.handleError);
+                    .map(this.restExtractor.extractDataGet)
+                    .catch((res) => this.restExtractor.handleError(res));
   }
 
-  getVideos(pagination: Pagination, sort: SortField) {
-    const params = this.createPaginationParams(pagination);
-
-    if (sort) params.set('sort', sort);
+  getVideos(pagination: RestPagination, sort: SortField) {
+    const params = this.restService.buildRestGetParams(pagination, sort);
 
     return this.http.get(VideoService.BASE_VIDEO_URL, { search: params })
                     .map(res => res.json())
                     .map(this.extractVideos)
-                    .catch(this.handleError);
+                    .catch((res) => this.restExtractor.handleError(res));
   }
 
   removeVideo(id: string) {
     return this.authHttp.delete(VideoService.BASE_VIDEO_URL + id)
-                        .map(res => <number> res.status)
-                        .catch(this.handleError);
+                        .map(this.restExtractor.extractDataBool)
+                        .catch((res) => this.restExtractor.handleError(res));
   }
 
-  searchVideos(search: Search, pagination: Pagination, sort: SortField) {
-    const params = this.createPaginationParams(pagination);
+  searchVideos(search: Search, pagination: RestPagination, sort: SortField) {
+    const params = this.restService.buildRestGetParams(pagination, sort);
 
     if (search.field) params.set('field', search.field);
-    if (sort) params.set('sort', sort);
 
     return this.http.get(VideoService.BASE_VIDEO_URL + 'search/' + encodeURIComponent(search.value), { search: params })
-                    .map(res => res.json())
+                    .map(this.restExtractor.extractDataList)
                     .map(this.extractVideos)
-                    .catch(this.handleError);
-  }
-
-  private createPaginationParams(pagination: Pagination) {
-    const params = new URLSearchParams();
-    const start: number = (pagination.currentPage - 1) * pagination.itemsPerPage;
-    const count: number = pagination.itemsPerPage;
-
-    params.set('start', start.toString());
-    params.set('count', count.toString());
-
-    return params;
+                    .catch((res) => this.restExtractor.handleError(res));
   }
 
-  private extractVideos(body: any) {
-    const videos_json = body.data;
-    const totalVideos = body.total;
+  private extractVideos(result: ResultList) {
+    const videosJson = result.data;
+    const totalVideos = result.total;
     const videos = [];
-    for (const video_json of videos_json) {
-      videos.push(new Video(video_json));
+    for (const videoJson of videosJson) {
+      videos.push(new Video(videoJson));
     }
 
     return { videos, totalVideos };
   }
-
-  private handleError(error: Response) {
-    console.error(error);
-    return Observable.throw(error.json().error || 'Server error');
-  }
 }
index bcd78c7cb5dc96a1e47adddd3a1281e335267cb8..64320cae7dc5885ef7a9831ba34a54c2beed2ace 100644 (file)
@@ -2,31 +2,31 @@
 
 <div *ngIf="error" class="alert alert-danger">{{ error }}</div>
 
-<form novalidate (ngSubmit)="upload()" [ngFormModel]="videoForm">
+<form novalidate (ngSubmit)="upload()" [formGroup]="form">
   <div class="form-group">
     <label for="name">Name</label>
     <input
-      type="text" class="form-control" name="name" id="name"
-      ngControl="name" #name="ngForm" [(ngModel)]="video.name"
+      type="text" class="form-control" id="name"
+      formControlName="name"
     >
-    <div [hidden]="name.valid || name.pristine" class="alert alert-warning">
-      A name is required and should be between 3 and 50 characters long
+    <div *ngIf="formErrors.name" class="alert alert-danger">
+      {{ formErrors.name }}
     </div>
   </div>
 
   <div class="form-group">
     <label for="tags">Tags</label>
     <input
-      type="text" class="form-control" name="tags" id="tags"
-      ngControl="tags" #tags="ngForm" [disabled]="isTagsInputDisabled" (keyup)="onTagKeyPress($event)" [(ngModel)]="currentTag"
+      type="text" class="form-control" id="currentTag"
+      formControlName="currentTag" (keyup)="onTagKeyPress($event)"
     >
-    <div [hidden]="tags.valid || tags.pristine" class="alert alert-warning">
-      A tag should be between 2 and 10 characters (alphanumeric) long
+    <div *ngIf="formErrors.currentTag" class="alert alert-danger">
+      {{ formErrors.currentTag }}
     </div>
   </div>
 
   <div class="tags">
-    <div class="label label-primary tag" *ngFor="let tag of video.tags">
+    <div class="label label-primary tag" *ngFor="let tag of tags">
       {{ tag }}
       <span class="remove" (click)="removeTag(tag)">x</span>
     </div>
   <div class="form-group">
     <label for="description">Description</label>
     <textarea
-      name="description" id="description" class="form-control" placeholder="Description..."
-      ngControl="description"  #description="ngForm" [(ngModel)]="video.description"
+      id="description" class="form-control" placeholder="Description..."
+      formControlName="description"
     >
     </textarea>
-    <div [hidden]="description.valid || description.pristine" class="alert alert-warning">
-        A description is required and should be between 3 and 250 characters long
+    <div *ngIf="formErrors.description" class="alert alert-danger">
+      {{ formErrors.description }}
     </div>
   </div>
 
@@ -69,7 +69,7 @@
   <div class="form-group">
     <input
       type="submit" value="Upload" class="btn btn-default form-control" [title]="getInvalidFieldsTitle()"
-      [disabled]="!videoForm.valid || video.tags.length === 0 || filename === null"
+      [disabled]="!form.valid || tags.length === 0 || filename === null"
     >
   </div>
 </form>
index c0f8cb9c437921f63bdbb22e8b409bb9dd18ce1d..d12a7d572481365dc311818bde864f91426d7a05 100644 (file)
@@ -1,37 +1,42 @@
-import { Control, ControlGroup, Validators } from '@angular/common';
 import { Component, ElementRef, OnInit } from '@angular/core';
+import { FormBuilder, FormGroup } from '@angular/forms';
 import { Router } from '@angular/router';
 
-import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe';
-import { PROGRESSBAR_DIRECTIVES } from 'ng2-bootstrap/components/progressbar';
-import { FileSelectDirective, FileUploader } from 'ng2-file-upload/ng2-file-upload';
+import { FileUploader } from 'ng2-file-upload/ng2-file-upload';
 
-import { AuthService } from '../../shared';
+import { AuthService, FormReactive, VIDEO_NAME, VIDEO_DESCRIPTION, VIDEO_TAGS } from '../../shared';
 
 @Component({
   selector: 'my-videos-add',
-  styles: [ require('./video-add.component.scss') ],
-  template: require('./video-add.component.html'),
-  directives: [ FileSelectDirective, PROGRESSBAR_DIRECTIVES ],
-  pipes: [ BytesPipe ]
+  styleUrls: [ './video-add.component.scss' ],
+  templateUrl: './video-add.component.html'
 })
 
-export class VideoAddComponent implements OnInit {
-  currentTag: string; // Tag the user is writing in the input
-  error: string = null;
-  videoForm: ControlGroup;
+export class VideoAddComponent extends FormReactive implements OnInit {
+  tags: string[] = [];
   uploader: FileUploader;
-  video = {
+
+  error: string = null;
+  form: FormGroup;
+  formErrors = {
     name: '',
-    tags: [],
-    description: ''
+    description: '',
+    currentTag: ''
+  };
+  validationMessages = {
+    name: VIDEO_NAME.MESSAGES,
+    description: VIDEO_DESCRIPTION.MESSAGES,
+    currentTag: VIDEO_TAGS.MESSAGES
   };
 
   constructor(
     private authService: AuthService,
     private elementRef: ElementRef,
+    private formBuilder: FormBuilder,
     private router: Router
-  ) {}
+  ) {
+    super();
+  }
 
   get filename() {
     if (this.uploader.queue.length === 0) {
@@ -41,20 +46,26 @@ export class VideoAddComponent implements OnInit {
     return this.uploader.queue[0].file.name;
   }
 
-  get isTagsInputDisabled () {
-    return this.video.tags.length >= 3;
+  buildForm() {
+    this.form = this.formBuilder.group({
+      name: [ '', VIDEO_NAME.VALIDATORS ],
+      description: [ '', VIDEO_DESCRIPTION.VALIDATORS ],
+      currentTag: [ '', VIDEO_TAGS.VALIDATORS ]
+    });
+
+    this.form.valueChanges.subscribe(data => this.onValueChanged(data));
   }
 
   getInvalidFieldsTitle() {
     let title = '';
-    const nameControl = this.videoForm.controls['name'];
-    const descriptionControl = this.videoForm.controls['description'];
+    const nameControl = this.form.controls['name'];
+    const descriptionControl = this.form.controls['description'];
 
     if (!nameControl.valid) {
       title += 'A name is required\n';
     }
 
-    if (this.video.tags.length === 0) {
+    if (this.tags.length === 0) {
       title += 'At least one tag is required\n';
     }
 
@@ -70,13 +81,6 @@ export class VideoAddComponent implements OnInit {
   }
 
   ngOnInit() {
-    this.videoForm = new ControlGroup({
-      name: new Control('', Validators.compose([ Validators.required, Validators.minLength(3), Validators.maxLength(50) ])),
-      description: new Control('', Validators.compose([ Validators.required, Validators.minLength(3), Validators.maxLength(250) ])),
-      tags: new Control('', Validators.pattern('^[a-zA-Z0-9]{2,10}$'))
-    });
-
-
     this.uploader = new FileUploader({
       authToken: this.authService.getRequestHeaderValue(),
       queueLimit: 1,
@@ -85,26 +89,37 @@ export class VideoAddComponent implements OnInit {
     });
 
     this.uploader.onBuildItemForm = (item, form) => {
-      form.append('name', this.video.name);
-      form.append('description', this.video.description);
+      const name = this.form.value['name'];
+      const description = this.form.value['description'];
+
+      form.append('name', name);
+      form.append('description', description);
 
-      for (let i = 0; i < this.video.tags.length; i++) {
-        form.append(`tags[${i}]`, this.video.tags[i]);
+      for (let i = 0; i < this.tags.length; i++) {
+        form.append(`tags[${i}]`, this.tags[i]);
       }
     };
+
+    this.buildForm();
   }
 
   onTagKeyPress(event: KeyboardEvent) {
+    const currentTag = this.form.value['currentTag'];
+
     // Enter press
     if (event.keyCode === 13) {
       // Check if the tag is valid and does not already exist
       if (
-        this.currentTag !== '' &&
-        this.videoForm.controls['tags'].valid &&
-        this.video.tags.indexOf(this.currentTag) === -1
+        currentTag !== '' &&
+        this.form.controls['currentTag'].valid &&
+        this.tags.indexOf(currentTag) === -1
       ) {
-        this.video.tags.push(this.currentTag);
-        this.currentTag = '';
+        this.tags.push(currentTag);
+        this.form.patchValue({ currentTag: '' });
+
+        if (this.tags.length >= 3) {
+          this.form.get('currentTag').disable();
+        }
       }
     }
   }
@@ -114,7 +129,7 @@ export class VideoAddComponent implements OnInit {
   }
 
   removeTag(tag: string) {
-    this.video.tags.splice(this.video.tags.indexOf(tag), 1);
+    this.tags.splice(this.tags.indexOf(tag), 1);
   }
 
   upload() {
index 5691d684eee9cb5ab6ebaff98f5810d7ab150893..6b086e93880b34cee8c2fba58f4878406b215d7a 100644 (file)
@@ -1,39 +1,30 @@
 import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
-import { AsyncPipe } from '@angular/common';
-import { ActivatedRoute, Router, ROUTER_DIRECTIVES } from '@angular/router';
+import { ActivatedRoute, Router } from '@angular/router';
 import { BehaviorSubject } from 'rxjs/BehaviorSubject';
 
-import { PAGINATION_DIRECTIVES } from 'ng2-bootstrap/components/pagination';
-
 import {
-  LoaderComponent,
-  Pagination,
   SortField,
   Video,
   VideoService
 } from '../shared';
-import { AuthService, Search, SearchField, User } from '../../shared';
-import { VideoMiniatureComponent } from './video-miniature.component';
-import { VideoSortComponent } from './video-sort.component';
+import { AuthService, AuthUser, RestPagination, Search, SearchField } from '../../shared';
 import { SearchService } from '../../shared';
 
 @Component({
   selector: 'my-videos-list',
-  styles: [ require('./video-list.component.scss') ],
-  pipes: [ AsyncPipe ],
-  template: require('./video-list.component.html'),
-  directives: [ LoaderComponent, PAGINATION_DIRECTIVES, ROUTER_DIRECTIVES, VideoMiniatureComponent, VideoSortComponent ]
+  styleUrls: [ './video-list.component.scss' ],
+  templateUrl: './video-list.component.html'
 })
 
 export class VideoListComponent implements OnInit, OnDestroy {
   loading: BehaviorSubject<boolean> = new BehaviorSubject(false);
-  pagination: Pagination = {
+  pagination: RestPagination = {
     currentPage: 1,
     itemsPerPage: 9,
     totalItems: null
   };
   sort: SortField;
-  user: User = null;
+  user: AuthUser = null;
   videos: Video[] = [];
 
   private search: Search;
@@ -51,7 +42,7 @@ export class VideoListComponent implements OnInit, OnDestroy {
 
   ngOnInit() {
     if (this.authService.isLoggedIn()) {
-      this.user = User.load();
+      this.user = AuthUser.load();
     }
 
     // Subscribe to route changes
@@ -66,6 +57,8 @@ export class VideoListComponent implements OnInit, OnDestroy {
     // Subscribe to search changes
     this.subSearch = this.searchService.searchUpdated.subscribe(search => {
       this.search = search;
+      // Reset pagination
+      this.pagination.currentPage = 1;
 
       this.navigateToNewParams();
     });
@@ -76,7 +69,7 @@ export class VideoListComponent implements OnInit, OnDestroy {
     this.subSearch.unsubscribe();
   }
 
-  getVideos(detectChanges = true) {
+  getVideos() {
     this.loading.next(true);
     this.videos = [];
 
@@ -97,7 +90,7 @@ export class VideoListComponent implements OnInit, OnDestroy {
 
         this.loading.next(false);
       },
-      error => alert(error)
+      error => alert(error.text)
     );
   }
 
@@ -153,7 +146,11 @@ export class VideoListComponent implements OnInit, OnDestroy {
 
     this.sort = <SortField>routeParams['sort'] || '-createdDate';
 
-    this.pagination.currentPage = parseInt(routeParams['page']) || 1;
+    if (routeParams['page'] !== undefined) {
+      this.pagination.currentPage = parseInt(routeParams['page']);
+    } else {
+      this.pagination.currentPage = 1;
+    }
 
     this.changeDetector.detectChanges();
   }
index 84bab950e450d295b69dd74ca3728fcd3076a9ad..398d2db7507a2d4f4f2f6df78e9e99ca5b0488e8 100644 (file)
@@ -1,16 +1,12 @@
-import { DatePipe } from '@angular/common';
 import { Component, Input, Output, EventEmitter } from '@angular/core';
-import { ROUTER_DIRECTIVES } from '@angular/router';
 
 import { SortField, Video, VideoService } from '../shared';
 import { User } from '../../shared';
 
 @Component({
   selector: 'my-video-miniature',
-  styles: [ require('./video-miniature.component.scss') ],
-  template: require('./video-miniature.component.html'),
-  directives: [ ROUTER_DIRECTIVES ],
-  pipes: [ DatePipe ]
+  styleUrls: [ './video-miniature.component.scss' ],
+  templateUrl: './video-miniature.component.html'
 })
 
 export class VideoMiniatureComponent {
@@ -40,7 +36,7 @@ export class VideoMiniatureComponent {
     if (confirm('Do you really want to remove this video?')) {
       this.videoService.removeVideo(id).subscribe(
         status => this.removed.emit(true),
-        error => alert(error)
+        error => alert(error.text)
       );
     }
   }
index 0d76b54b7a7bd2202368af7afadfbbca83ab6bb8..ca94b07c2e7aa190ee061eb68c0493825883917c 100644 (file)
@@ -4,7 +4,7 @@ import { SortField } from '../shared';
 
 @Component({
   selector: 'my-video-sort',
-  template: require('./video-sort.component.html')
+  templateUrl: './video-sort.component.html'
 })
 
 export class VideoSortComponent {
index 3aaed0487e53ba9748eee3fa9854d593e4a73e58..239e24c990e8c82613faa92b4abdeb628c905305 100644 (file)
@@ -1,18 +1,13 @@
-import { Component, ElementRef, OnDestroy, OnInit } from '@angular/core';
+import { Component, ElementRef, NgZone, OnDestroy, OnInit } from '@angular/core';
 import { ActivatedRoute } from '@angular/router';
 
-import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe';
-
-import { LoaderComponent, Video, VideoService } from '../shared';
+import { Video, VideoService } from '../shared';
 import { WebTorrentService } from './webtorrent.service';
 
 @Component({
   selector: 'my-video-watch',
-  template: require('./video-watch.component.html'),
-  styles: [ require('./video-watch.component.scss') ],
-  providers: [ WebTorrentService ],
-  directives: [ LoaderComponent ],
-  pipes: [ BytesPipe ]
+  templateUrl: './video-watch.component.html',
+  styleUrls: [ './video-watch.component.scss' ]
 })
 
 export class VideoWatchComponent implements OnInit, OnDestroy {
@@ -31,6 +26,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
 
   constructor(
     private elementRef: ElementRef,
+    private ngZone: NgZone,
     private route: ActivatedRoute,
     private videoService: VideoService,
     private webTorrentService: WebTorrentService
@@ -65,12 +61,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
         }
       });
 
-      // Refresh each second
-      this.torrentInfosInterval = setInterval(() => {
-        this.downloadSpeed = torrent.downloadSpeed;
-        this.numPeers = torrent.numPeers;
-        this.uploadSpeed = torrent.uploadSpeed;
-      }, 1000);
+      this.runInProgress(torrent);
     });
   }
 
@@ -91,7 +82,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
           this.video = video;
           this.loadVideo();
         },
-        error => alert(error)
+        error => alert(error.text)
       );
     });
   }
@@ -100,4 +91,15 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     this.error = true;
     console.error('The video load seems to be abnormally long.');
   }
+
+  private runInProgress(torrent: any) {
+    // Refresh each second
+    this.torrentInfosInterval = setInterval(() => {
+      this.ngZone.run(() => {
+        this.downloadSpeed = torrent.downloadSpeed;
+        this.numPeers = torrent.numPeers;
+        this.uploadSpeed = torrent.uploadSpeed;
+      });
+    }, 1000);
+  }
 }
index 76252afbb25b75d6683f9118de23b31a278cac40..591e7523d1db690d18530044b9eed7090d8597e1 100644 (file)
@@ -1,9 +1,7 @@
 import { Component } from '@angular/core';
-import { ROUTER_DIRECTIVES } from '@angular/router';
 
 @Component({
-    template: '<router-outlet></router-outlet>',
-    directives: [ ROUTER_DIRECTIVES ]
+  template: '<router-outlet></router-outlet>'
 })
 
 export class VideosComponent {
index 1f088b376b1bb81e85f59c31831725cc43a845bf..074f96596e1a81e5feeb806e98877fc244cecfee 100644 (file)
@@ -1,11 +1,11 @@
-import { RouterConfig } from '@angular/router';
+import { Routes } from '@angular/router';
 
 import { VideoAddComponent } from './video-add';
 import { VideoListComponent } from './video-list';
 import { VideosComponent } from './videos.component';
 import { VideoWatchComponent } from './video-watch';
 
-export const VideosRoutes: RouterConfig = [
+export const VideosRoutes: Routes = [
   {
     path: 'videos',
     component: VideosComponent,
index 14c7d8aded9c4f0c5cebfbb015c7e01692be1d7a..95787181f4cb5e03a2e377c39dab9283d2f191e0 100644 (file)
@@ -1,15 +1,27 @@
 /*
  * Custom Type Definitions
  * When including 3rd party modules you also need to include the type definition for the module
- * if they don't provide one within the module. You can try to install it with typings
+ * if they don't provide one within the module. You can try to install it with @types
 
-typings install node --save
+npm install @types/node
+npm install @types/lodash
 
- * If you can't find the type definition in the registry we can make an ambient definition in
+ * If you can't find the type definition in the registry we can make an ambient/global definition in
  * this file for now. For example
 
-declare module "my-module" {
-  export function doesSomething(value: string): string;
+declare module 'my-module' {
+ export function doesSomething(value: string): string;
+}
+
+ * If you are using a CommonJS module that is using module.exports then you will have to write your
+ * types using export = yourObjectOrFunction with a namespace above it
+ * notice how we have to create a namespace that is equal to the function we're
+ * assigning the export to
+
+declare module 'jwt-decode' {
+  function jwtDecode(token: string): any;
+  namespace jwtDecode {}
+  export = jwtDecode;
 }
 
  *
@@ -17,33 +29,65 @@ declare module "my-module" {
  *
 
 declare var assert: any;
+declare var _: any;
+declare var $: any;
 
  *
  * If you're importing a module that uses Node.js modules which are CommonJS you need to import as
+ * in the files such as main.browser.ts or any file within app/
  *
 
 import * as _ from 'lodash'
 
- * You can include your type definitions in this file until you create one for the typings registry
- * see https://github.com/typings/registry
+ * You can include your type definitions in this file until you create one for the @types
  *
  */
 
+// support NodeJS modules without type definitions
+declare module '*';
 
 // Extra variables that live on Global that will be replaced by webpack DefinePlugin
 declare var ENV: string;
 declare var HMR: boolean;
+declare var System: SystemJS;
+
+interface SystemJS {
+  import: (path?: string) => Promise<any>;
+}
+
 interface GlobalEnvironment {
   ENV;
   HMR;
+  SystemJS: SystemJS;
+  System: SystemJS;
 }
 
+interface Es6PromiseLoader {
+  (id: string): (exportName?: string) => Promise<any>;
+}
+
+type FactoryEs6PromiseLoader = () => Es6PromiseLoader;
+type FactoryPromise = () => Promise<any>;
+
+type AsyncRoutes = {
+  [component: string]: Es6PromiseLoader |
+                               Function |
+                FactoryEs6PromiseLoader |
+                         FactoryPromise
+};
+
+
+type IdleCallbacks = Es6PromiseLoader |
+                             Function |
+              FactoryEs6PromiseLoader |
+                       FactoryPromise ;
+
 interface WebpackModule {
   hot: {
     data?: any,
     idle: any,
     accept(dependencies?: string | string[], callback?: (updatedDependencies?: any) => void): void;
-    decline(dependencies?: string | string[]): void;
+    decline(deps?: any | string | string[]): void;
     dispose(callback?: (data?: any) => void): void;
     addDisposeHandler(callback?: (data?: any) => void): void;
     removeDisposeHandler(callback?: (data?: any) => void): void;
@@ -54,66 +98,26 @@ interface WebpackModule {
   };
 }
 
+
 interface WebpackRequire {
-  context(file: string, flag?: boolean, exp?: RegExp): any;
+    (id: string): any;
+    (paths: string[], callback: (...modules: any[]) => void): void;
+    ensure(ids: string[], callback: (req: WebpackRequire) => void, chunkName?: string): void;
+    context(directory: string, useSubDirectories?: boolean, regExp?: RegExp): WebpackContext;
 }
 
+interface WebpackContext extends WebpackRequire {
+    keys(): string[];
+}
 
 interface ErrorStackTraceLimit {
   stackTraceLimit: number;
 }
 
 
-
 // Extend typings
 interface NodeRequire extends WebpackRequire {}
 interface ErrorConstructor extends ErrorStackTraceLimit {}
+interface NodeRequireFunction extends Es6PromiseLoader  {}
 interface NodeModule extends WebpackModule {}
 interface Global extends GlobalEnvironment  {}
-
-
-declare namespace Reflect {
-  function decorate(decorators: ClassDecorator[], target: Function): Function;
-  function decorate(
-    decorators: (PropertyDecorator | MethodDecorator)[],
-    target: Object,
-    targetKey: string | symbol,
-    descriptor?: PropertyDescriptor): PropertyDescriptor;
-
-  function metadata(metadataKey: any, metadataValue: any): {
-    (target: Function): void;
-    (target: Object, propertyKey: string | symbol): void;
-  };
-  function defineMetadata(metadataKey: any, metadataValue: any, target: Object): void;
-  function defineMetadata(
-    metadataKey: any,
-    metadataValue: any,
-    target: Object,
-    targetKey: string | symbol): void;
-  function hasMetadata(metadataKey: any, target: Object): boolean;
-  function hasMetadata(metadataKey: any, target: Object, targetKey: string | symbol): boolean;
-  function hasOwnMetadata(metadataKey: any, target: Object): boolean;
-  function hasOwnMetadata(metadataKey: any, target: Object, targetKey: string | symbol): boolean;
-  function getMetadata(metadataKey: any, target: Object): any;
-  function getMetadata(metadataKey: any, target: Object, targetKey: string | symbol): any;
-  function getOwnMetadata(metadataKey: any, target: Object): any;
-  function getOwnMetadata(metadataKey: any, target: Object, targetKey: string | symbol): any;
-  function getMetadataKeys(target: Object): any[];
-  function getMetadataKeys(target: Object, targetKey: string | symbol): any[];
-  function getOwnMetadataKeys(target: Object): any[];
-  function getOwnMetadataKeys(target: Object, targetKey: string | symbol): any[];
-  function deleteMetadata(metadataKey: any, target: Object): boolean;
-  function deleteMetadata(metadataKey: any, target: Object, targetKey: string | symbol): boolean;
-}
-
-
-// We need this here since there is a problem with Zone.js typings
-interface Thenable<T> {
-  then<U>(
-    onFulfilled?: (value: T) => U | Thenable<U>,
-    onRejected?: (error: any) => U | Thenable<U>): Thenable<U>;
-  then<U>(
-    onFulfilled?: (value: T) => U | Thenable<U>,
-    onRejected?: (error: any) => void): Thenable<U>;
-  catch<U>(onRejected?: (error: any) => U | Thenable<U>): Thenable<U>;
-}
index 5cf491221b54c5645135b2c1535a90355ad52add..f39d8d2cfc1a637247f0338b644e23d4f8959628 100644 (file)
@@ -1,3 +1,4 @@
+<!DOCTYPE html>
 <html>
   <head>
     <base href="/">
index a78d275ad4029191b3d1db1c460f4f3aba4b4ea5..70bf4853782ed6ac515d453e34b36ea955e0300a 100644 (file)
@@ -1,28 +1,20 @@
-import { enableProdMode, provide } from '@angular/core';
-import {
-  HTTP_PROVIDERS,
-  RequestOptions,
-  XHRBackend
-} from '@angular/http';
-import { bootstrap }    from '@angular/platform-browser-dynamic';
-import { provideRouter } from '@angular/router';
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+import { decorateModuleRef } from './app/environment';
+import { bootloader } from '@angularclass/hmr';
+/*
+ * App Module
+ * our top level module that holds all of our components
+ */
+import { AppModule } from './app';
 
-import { AppComponent } from './app/app.component';
-import { routes } from './app/app.routes';
-import { AuthHttp, AuthService } from './app/shared';
-
-if (process.env.ENV === 'production') {
-  enableProdMode();
+/*
+ * Bootstrap our Angular app with a top level NgModule
+ */
+export function main(): Promise<any> {
+  return platformBrowserDynamic()
+    .bootstrapModule(AppModule)
+    .then(decorateModuleRef)
+    .catch(err => console.error(err));
 }
 
-bootstrap(AppComponent, [
-  HTTP_PROVIDERS,
-  provide(AuthHttp, {
-    useFactory: (backend: XHRBackend, defaultOptions: RequestOptions, authService: AuthService) => {
-      return new AuthHttp(backend, defaultOptions, authService);
-    },
-    deps: [ XHRBackend, RequestOptions, AuthService ]
-  }),
-  AuthService,
-  provideRouter(routes)
-]);
+bootloader(main);
index 740a563bb96bf5b4197ce5cfad0d29ca4c38a21c..65e211459ba76a3dae8adfc8e350061e0020073a 100644 (file)
@@ -6,9 +6,28 @@ require('intl/locale-data/jsonp/en.js');
 import 'ie-shim'; // Internet Explorer
 
 // Prefer CoreJS over the polyfills above
-import 'core-js/es6';
+import 'core-js/es6/symbol';
+import 'core-js/es6/object';
+import 'core-js/es6/function';
+import 'core-js/es6/parse-int';
+import 'core-js/es6/parse-float';
+import 'core-js/es6/number';
+import 'core-js/es6/math';
+import 'core-js/es6/string';
+import 'core-js/es6/date';
+import 'core-js/es6/array';
+import 'core-js/es6/regexp';
+import 'core-js/es6/map';
+import 'core-js/es6/set';
+import 'core-js/es6/weak-map';
+import 'core-js/es6/weak-set';
+import 'core-js/es6/typed';
+import 'core-js/es6/reflect';
+// see issue https://github.com/AngularClass/angular2-webpack-starter/issues/709
+// import 'core-js/es6/promise';
+
 import 'core-js/es7/reflect';
-require('zone.js/dist/zone');
+import 'zone.js/dist/zone';
 
 // Typescript emit helpers polyfill
 import 'ts-helpers';
index 9c48b4627bd2adf4e72abd6c3eb1433f029ad2c8..b3bdffe5055150f15954aafc95fa4e2edf9df0ec 100644 (file)
@@ -6,6 +6,45 @@ body {
   }
 }
 
+menu {
+  @media screen and (max-width: 600px) {
+    margin-right: 3px !important;
+    padding: 3px !important;
+    min-height: 400px !important;
+  }
+
+  min-height: 600px;
+  margin-right: 20px;
+  border-right: 1px solid rgba(0, 0, 0, 0.2);
+
+  .panel-block:not(:last-child) {
+    border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+  }
+
+  .panel-button {
+    margin: 8px;
+    cursor: pointer;
+    transition: margin 0.2s;
+
+    &:hover {
+      margin-left: 15px;
+    }
+
+    a {
+      color: #333333;
+    }
+  }
+
+  .glyphicon {
+    margin: 5px;
+  }
+}
+
+.table-column-id {
+  width: 200px;
+}
+
+
 footer {
   border-top: 1px solid rgba(0, 0, 0, 0.2);
   padding-top: 10px;
index 8f029191a1ff0c1c3b64065d2338ca037a2c7748..95356d9d0a01172b1bc9ee41e57d24045210612e 100644 (file)
@@ -8,13 +8,17 @@ import '@angular/platform-browser';
 import '@angular/platform-browser-dynamic';
 import '@angular/core';
 import '@angular/common';
+import '@angular/forms';
 import '@angular/http';
 import '@angular/router';
 
+import '@angularclass/hmr';
+
 // RxJS
 import 'rxjs/Observable';
 import 'rxjs/Subject';
 import 'rxjs/add/operator/catch';
+import 'rxjs/add/operator/mergeMap';
 import 'rxjs/add/operator/map';
 import 'rxjs/add/observable/throw';
 
index 67d1fb4f179665382c89d2e336bf97b263f1c50e..10573b8ee4e1c188b8d90ba3ec8dfad03dcc3410 100644 (file)
@@ -3,74 +3,35 @@
     "target": "es5",
     "module": "commonjs",
     "moduleResolution": "node",
-    "sourceMap": true,
     "emitDecoratorMetadata": true,
     "experimentalDecorators": true,
-    "noImplicitAny": false,
-    "noEmitHelpers": true
+    "allowSyntheticDefaultImports": true,
+    "sourceMap": true,
+    "noEmitHelpers": true,
+    "strictNullChecks": false,
+    "baseUrl": "./src",
+    "paths": [
+    ],
+    "lib": [
+      "dom",
+      "es6"
+    ],
+    "types": [
+      "node",
+      "source-map",
+      "uglify-js",
+      "webpack"
+    ]
   },
+  "exclude": [
+    "node_modules",
+    "dist"
+  ],
   "awesomeTypescriptLoaderOptions": {
-    "forkChecker": true
+    "forkChecker": true,
+    "useWebpackText": true
   },
   "compileOnSave": false,
   "buildOnSave": false,
-  "atom": {
-    "rewriteTsconfig": true
-  },
-  "filesGlob": [
-    "**/*.ts",
-    "!node_modules/**"
-  ],
-  "exclude": [
-    "node_modules",
-    "typings/main",
-    "typings/main.d.ts"
-  ],
-  "files": [
-    "src/app/app.component.ts",
-    "src/app/app.routes.ts",
-    "src/app/friends/friend.service.ts",
-    "src/app/friends/index.ts",
-    "src/app/login/index.ts",
-    "src/app/login/login.component.ts",
-    "src/app/login/login.routes.ts",
-    "src/app/shared/auth/auth-http.service.ts",
-    "src/app/shared/auth/auth-status.model.ts",
-    "src/app/shared/auth/auth.service.ts",
-    "src/app/shared/auth/index.ts",
-    "src/app/shared/auth/user.model.ts",
-    "src/app/shared/index.ts",
-    "src/app/shared/search/index.ts",
-    "src/app/shared/search/search-field.type.ts",
-    "src/app/shared/search/search.component.ts",
-    "src/app/shared/search/search.model.ts",
-    "src/app/shared/search/search.service.ts",
-    "src/app/videos/index.ts",
-    "src/app/videos/shared/index.ts",
-    "src/app/videos/shared/loader/index.ts",
-    "src/app/videos/shared/loader/loader.component.ts",
-    "src/app/videos/shared/pagination.model.ts",
-    "src/app/videos/shared/sort-field.type.ts",
-    "src/app/videos/shared/video.model.ts",
-    "src/app/videos/shared/video.service.ts",
-    "src/app/videos/video-add/index.ts",
-    "src/app/videos/video-add/video-add.component.ts",
-    "src/app/videos/video-list/index.ts",
-    "src/app/videos/video-list/video-list.component.ts",
-    "src/app/videos/video-list/video-miniature.component.ts",
-    "src/app/videos/video-list/video-sort.component.ts",
-    "src/app/videos/video-watch/index.ts",
-    "src/app/videos/video-watch/video-watch.component.ts",
-    "src/app/videos/video-watch/webtorrent.service.ts",
-    "src/app/videos/videos.component.ts",
-    "src/app/videos/videos.routes.ts",
-    "src/custom-typings.d.ts",
-    "src/main.ts",
-    "src/polyfills.ts",
-    "src/vendor.ts",
-    "typings/globals/es6-shim/index.d.ts",
-    "typings/globals/jasmine/index.d.ts",
-    "typings/globals/node/index.d.ts",
-    "typings/index.d.ts"
-  ]
+  "atom": { "rewriteTsconfig": false }
 }
diff --git a/client/typings.json b/client/typings.json
deleted file mode 100644 (file)
index 9a8891f..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-{
-  "globalDependencies": {
-    "es6-shim": "registry:dt/es6-shim#0.31.2+20160317120654",
-    "jasmine": "registry:dt/jasmine#2.2.0+20160412134438",
-    "node": "registry:dt/node#4.0.0+20160509154515"
-  }
-}
index 8f54d88e5957384f4097c7a8ae32ca2a72cef41c..3d3af91ad10072b54857cbbd815ccdca5334688d 100644 (file)
@@ -1,16 +1,16 @@
 switch (process.env.NODE_ENV) {
   case 'prod':
   case 'production':
-    module.exports = require('./config/webpack.prod')
+    module.exports = require('./config/webpack.prod')({env: 'production'})
     break
 
   case 'test':
   case 'testing':
-    module.exports = require('./config/webpack.test')
+    module.exports = require('./config/webpack.test')({env: 'test'})
     break
 
   case 'dev':
   case 'development':
   default:
-    module.exports = require('./config/webpack.dev')
+    module.exports = require('./config/webpack.dev')({env: 'development'})
 }
index 9a8a57879340980f4db1477cd910ec8bfa88fb8d..b44be31b0ab2ad92fd0295a161e976e5032901fc 100644 (file)
@@ -19,8 +19,5 @@ storage:
   thumbnails: 'thumbnails/'
   torrents: 'torrents/'
 
-network:
-  friends: []
-
 electron:
   debug: false
index 6dc79d73fd5a120003103bb2325f52c8f20c20ec..8bc63ee92461967b65a65e29c15a4c7bdd382ce8 100644 (file)
@@ -1,4 +1,8 @@
+# Correspond to your reverse proxy "listen" configuration
 webserver:
   https: false
   host: 'example.com'
   port: 80
+
+database:
+  suffix: '-prod'
index 0998eaea1ae59221d589ee067822683f0e62dc01..a59566cc4f3362154565ec7817ac611b2835ade4 100644 (file)
@@ -15,7 +15,3 @@ storage:
   logs: 'test1/logs/'
   thumbnails: 'test1/thumbnails/'
   torrents: 'test1/torrents/'
-
-network:
-  friends:
-    - 'http://localhost:9002'
index ec2cff811b357ff43cfee9d01f4a5a826f4511df..1b937898feac428179aee63d1b0f7e5eb87b8112 100644 (file)
@@ -15,7 +15,3 @@ storage:
   logs: 'test2/logs/'
   thumbnails: 'test2/thumbnails/'
   torrents: 'test2/torrents/'
-
-network:
-  friends:
-    - 'http://localhost:9003'
index 24f5533e0a48f0bc77ee607a94ce694ce51a65e4..e522c13e7fcaa25d1c10b89172d0ce5bed350ea5 100644 (file)
@@ -15,7 +15,3 @@ storage:
   logs: 'test3/logs/'
   thumbnails: 'test3/thumbnails/'
   torrents: 'test3/torrents/'
-
-network:
-  friends:
-    - 'http://localhost:9001'
index 1f884dbf2114b338ebd261e34113eabcffdeee36..e30cd79789490fba263be80f8f70ba9b263a6618 100644 (file)
@@ -15,7 +15,3 @@ storage:
   logs: 'test4/logs/'
   thumbnails: 'test4/thumbnails/'
   torrents: 'test4/torrents/'
-
-network:
-  friends:
-    - 'http://localhost:9002'
index 08ed9f06870023cd28e0dc4699f2ed82f7765879..3a54599f5555761d5a1a506e91a0d09b93be3692 100644 (file)
@@ -15,8 +15,3 @@ storage:
   logs: 'test5/logs/'
   thumbnails: 'test5/thumbnails/'
   torrents: 'test5/torrents/'
-
-network:
-  friends:
-    - 'http://localhost:9001'
-    - 'http://localhost:9004'
index a57784cca484d6282d29faa5723ab1253bee2edf..31608add2ac32fd42bede89980bb00ee25f55d33 100644 (file)
@@ -15,9 +15,3 @@ storage:
   logs: 'test6/logs/'
   thumbnails: 'test6/thumbnails/'
   torrents: 'test6/torrents/'
-
-network:
-  friends:
-    - 'http://localhost:9001'
-    - 'http://localhost:9002'
-    - 'http://localhost:9003'
index 63d01437621dab774d44561ec27a5948c6b3408a..59c7a4332cae749448fe5e328458dbcb65d2f4ef 100644 (file)
@@ -35,6 +35,7 @@
   },
   "dependencies": {
     "async": "^2.0.0",
+    "bcrypt": "^0.8.7",
     "bittorrent-tracker": "^8.0.0",
     "body-parser": "^1.12.4",
     "concurrently": "^2.0.0",
@@ -59,7 +60,6 @@
     "request": "^2.57.0",
     "request-replay": "^1.0.2",
     "scripty": "^1.5.0",
-    "segfault-handler": "^1.0.0",
     "ursa": "^0.9.1",
     "winston": "^2.1.1",
     "ws": "^1.1.1"
@@ -67,9 +67,9 @@
   "devDependencies": {
     "chai": "^3.3.0",
     "commander": "^2.9.0",
-    "mocha": "^2.3.3",
-    "standard": "^7.0.1",
-    "supertest": "^1.1.0"
+    "mocha": "^3.0.1",
+    "standard": "^8.0.0",
+    "supertest": "^2.0.0"
   },
   "standard": {
     "ignore": [
index 478cae99ed615b73cb4cd32c732decd21e704387..e090e8082ecd3cd30ddb15cee169f01d45d38281 100755 (executable)
@@ -2,4 +2,4 @@
 
 cd client || exit -1
 
-npm run webpack -- --config config/webpack.prod.js  --progress --profile --colors --display-error-details --display-cached --bail
+npm run webpack -- --config config/webpack.prod.js  --progress --profile --bail
index 0033ed1dbb70e25d10cb3a0957373d6da2063788..5feb214764f183f10cd7f8b58e5407ff3ce0a98c 100644 (file)
--- a/server.js
+++ b/server.js
@@ -32,6 +32,7 @@ if (miss.length !== 0) {
 // ----------- PeerTube modules -----------
 const customValidators = require('./server/helpers/custom-validators')
 const installer = require('./server/initializers/installer')
+const migrator = require('./server/initializers/migrator')
 const mongoose = require('mongoose')
 const routes = require('./server/controllers')
 const Request = mongoose.model('Request')
@@ -46,18 +47,21 @@ const port = config.get('listen.port')
 // For the logger
 app.use(morgan('combined', { stream: logger.stream }))
 // For body requests
-app.use(bodyParser.json())
+app.use(bodyParser.json({ limit: '500kb' }))
 app.use(bodyParser.urlencoded({ extended: false }))
 // Validate some params for the API
 app.use(expressValidator({
-  customValidators: customValidators
+  customValidators: Object.assign(
+    {},
+    customValidators.misc,
+    customValidators.pods,
+    customValidators.users,
+    customValidators.videos
+  )
 }))
 
 // ----------- Views, routes and static files -----------
 
-// Catch sefaults
-require('segfault-handler').registerHandler()
-
 // API routes
 const apiRoute = '/api/' + constants.API_VERSION
 app.use(apiRoute, routes.api)
@@ -125,14 +129,19 @@ app.use(function (err, req, res, next) {
 installer.installApplication(function (err) {
   if (err) throw err
 
-  // ----------- Make the server listening -----------
-  server.listen(port, function () {
-    // Activate the pool requests
-    Request.activate()
+  // Run the migration scripts if needed
+  migrator.migrate(function (err) {
+    if (err) throw err
+
+    // ----------- Make the server listening -----------
+    server.listen(port, function () {
+      // Activate the pool requests
+      Request.activate()
 
-    logger.info('Seeded all the videos')
-    logger.info('Server listening on port %d', port)
-    app.emit('ready')
+      logger.info('Seeded all the videos')
+      logger.info('Server listening on port %d', port)
+      app.emit('ready')
+    })
   })
 })
 
diff --git a/server/controllers/api/v1/clients.js b/server/controllers/api/v1/clients.js
new file mode 100644 (file)
index 0000000..5b460db
--- /dev/null
@@ -0,0 +1,41 @@
+'use strict'
+
+const express = require('express')
+const mongoose = require('mongoose')
+
+const constants = require('../../../initializers/constants')
+
+const Client = mongoose.model('OAuthClient')
+
+const router = express.Router()
+
+router.get('/local', getLocalClient)
+
+// Get the client credentials for the PeerTube front end
+function getLocalClient (req, res, next) {
+  const serverHost = constants.CONFIG.WEBSERVER.HOST
+  const serverPort = constants.CONFIG.WEBSERVER.PORT
+  let headerHostShouldBe = serverHost
+  if (serverPort !== 80 && serverPort !== 443) {
+    headerHostShouldBe += ':' + serverPort
+  }
+
+  // Don't make this check if this is a test instance
+  if (process.env.NODE_ENV !== 'test' && req.get('host') !== headerHostShouldBe) {
+    return res.type('json').status(403).end()
+  }
+
+  Client.loadFirstClient(function (err, client) {
+    if (err) return next(err)
+    if (!client) return next(new Error('No client available.'))
+
+    res.json({
+      client_id: client._id,
+      client_secret: client.clientSecret
+    })
+  })
+}
+
+// ---------------------------------------------------------------------------
+
+module.exports = router
index e0c29a8a22845b424f99a08474dbcd30e5b892d7..2e4fb2dab5bebff8f09ce0b94a408c517341d6e0 100644 (file)
@@ -4,13 +4,17 @@ const express = require('express')
 
 const router = express.Router()
 
+const clientsController = require('./clients')
 const podsController = require('./pods')
 const remoteController = require('./remote')
+const requestsController = require('./requests')
 const usersController = require('./users')
 const videosController = require('./videos')
 
+router.use('/clients', clientsController)
 router.use('/pods', podsController)
 router.use('/remote', remoteController)
+router.use('/requests', requestsController)
 router.use('/users', usersController)
 router.use('/videos', videosController)
 router.use('/*', badRequest)
index 2bc761fef4348c93673c4b4b903e3564ca9c7779..8ffade578505a269ea64701b69a6156e03ced539 100644 (file)
@@ -8,7 +8,10 @@ const waterfall = require('async/waterfall')
 const logger = require('../../../helpers/logger')
 const friends = require('../../../lib/friends')
 const middlewares = require('../../../middlewares')
+const admin = middlewares.admin
 const oAuth = middlewares.oauth
+const podsMiddleware = middlewares.pods
+const checkSignature = middlewares.secure.checkSignature
 const validators = middlewares.validators.pods
 const signatureValidator = middlewares.validators.remote.signature
 
@@ -16,12 +19,30 @@ const router = express.Router()
 const Pod = mongoose.model('Pod')
 const Video = mongoose.model('Video')
 
-router.get('/', listPodsUrl)
-router.post('/', validators.podsAdd, addPods)
-router.get('/makefriends', oAuth.authenticate, validators.makeFriends, makeFriends)
-router.get('/quitfriends', oAuth.authenticate, quitFriends)
+router.get('/', listPods)
+router.post('/',
+  validators.podsAdd,
+  podsMiddleware.setBodyUrlPort,
+  addPods
+)
+router.post('/makefriends',
+  oAuth.authenticate,
+  admin.ensureIsAdmin,
+  validators.makeFriends,
+  podsMiddleware.setBodyUrlsPort,
+  makeFriends
+)
+router.get('/quitfriends',
+  oAuth.authenticate,
+  admin.ensureIsAdmin,
+  quitFriends
+)
 // Post because this is a secured request
-router.post('/remove', signatureValidator, removePods)
+router.post('/remove',
+  signatureValidator,
+  checkSignature,
+  removePods
+)
 
 // ---------------------------------------------------------------------------
 
@@ -64,20 +85,27 @@ function addPods (req, res, next) {
   })
 }
 
-function listPodsUrl (req, res, next) {
-  Pod.listOnlyUrls(function (err, podsUrlList) {
+function listPods (req, res, next) {
+  Pod.list(function (err, podsUrlList) {
     if (err) return next(err)
 
-    res.json(podsUrlList)
+    res.json(getFormatedPods(podsUrlList))
   })
 }
 
 function makeFriends (req, res, next) {
-  friends.makeFriends(function (err) {
-    if (err) return next(err)
+  const urls = req.body.urls
 
-    res.type('json').status(204).end()
+  friends.makeFriends(urls, function (err) {
+    if (err) {
+      logger.error('Could not make friends.', { error: err })
+      return
+    }
+
+    logger.info('Made friends!')
   })
+
+  res.type('json').status(204).end()
 }
 
 function removePods (req, res, next) {
@@ -125,3 +153,15 @@ function quitFriends (req, res, next) {
     res.type('json').status(204).end()
   })
 }
+
+// ---------------------------------------------------------------------------
+
+function getFormatedPods (pods) {
+  const formatedPods = []
+
+  pods.forEach(function (pod) {
+    formatedPods.push(pod.toFormatedJSON())
+  })
+
+  return formatedPods
+}
index f452986b8e951aa0498fb5b35b7a4ffca0f6dde3..a22c5d1515b2255033330701a71ba1145a9965e4 100644 (file)
@@ -16,6 +16,7 @@ const Video = mongoose.model('Video')
 router.post('/videos',
   validators.signature,
   validators.dataToDecrypt,
+  secureMiddleware.checkSignature,
   secureMiddleware.decryptBody,
   validators.remoteVideos,
   remoteVideos
diff --git a/server/controllers/api/v1/requests.js b/server/controllers/api/v1/requests.js
new file mode 100644 (file)
index 0000000..9761642
--- /dev/null
@@ -0,0 +1,38 @@
+'use strict'
+
+const express = require('express')
+const mongoose = require('mongoose')
+
+const constants = require('../../../initializers/constants')
+const middlewares = require('../../../middlewares')
+const admin = middlewares.admin
+const oAuth = middlewares.oauth
+
+const Request = mongoose.model('Request')
+
+const router = express.Router()
+
+router.get('/stats',
+  oAuth.authenticate,
+  admin.ensureIsAdmin,
+  getStatsRequests
+)
+
+// ---------------------------------------------------------------------------
+
+module.exports = router
+
+// ---------------------------------------------------------------------------
+
+function getStatsRequests (req, res, next) {
+  Request.list(function (err, requests) {
+    if (err) return next(err)
+
+    return res.json({
+      requests: requests,
+      maxRequestsInParallel: constants.REQUESTS_IN_PARALLEL,
+      remainingMilliSeconds: Request.remainingMilliSeconds(),
+      milliSecondsInterval: constants.REQUESTS_INTERVAL
+    })
+  })
+}
index fbbe6e4726276140409dee8b7247659034e0aa4a..975e25e68f5ea1979a501a7050d723a4bcb80e95 100644 (file)
@@ -1,18 +1,59 @@
 'use strict'
 
-const config = require('config')
-const mongoose = require('mongoose')
+const each = require('async/each')
 const express = require('express')
+const mongoose = require('mongoose')
+const waterfall = require('async/waterfall')
 
-const oAuth = require('../../../middlewares').oauth
+const constants = require('../../../initializers/constants')
+const friends = require('../../../lib/friends')
+const logger = require('../../../helpers/logger')
+const middlewares = require('../../../middlewares')
+const admin = middlewares.admin
+const oAuth = middlewares.oauth
+const pagination = middlewares.pagination
+const sort = middlewares.sort
+const validatorsPagination = middlewares.validators.pagination
+const validatorsSort = middlewares.validators.sort
+const validatorsUsers = middlewares.validators.users
 
-const Client = mongoose.model('OAuthClient')
+const User = mongoose.model('User')
+const Video = mongoose.model('Video')
 
 const router = express.Router()
 
-router.get('/client', getAngularClient)
+router.get('/me', oAuth.authenticate, getUserInformation)
+
+router.get('/',
+  validatorsPagination.pagination,
+  validatorsSort.usersSort,
+  sort.setUsersSort,
+  pagination.setPagination,
+  listUsers
+)
+
+router.post('/',
+  oAuth.authenticate,
+  admin.ensureIsAdmin,
+  validatorsUsers.usersAdd,
+  createUser
+)
+
+router.put('/:id',
+  oAuth.authenticate,
+  validatorsUsers.usersUpdate,
+  updateUser
+)
+
+router.delete('/:id',
+  oAuth.authenticate,
+  admin.ensureIsAdmin,
+  validatorsUsers.usersRemove,
+  removeUser
+)
+
 router.post('/token', oAuth.token, success)
-// TODO: Once https://github.com/oauthjs/node-oauth2-server/pull/289 is merged,, implement revoke token route
+// TODO: Once https://github.com/oauthjs/node-oauth2-server/pull/289 is merged, implement revoke token route
 
 // ---------------------------------------------------------------------------
 
@@ -20,26 +61,91 @@ module.exports = router
 
 // ---------------------------------------------------------------------------
 
-function getAngularClient (req, res, next) {
-  const serverHost = config.get('webserver.host')
-  const serverPort = config.get('webserver.port')
-  let headerHostShouldBe = serverHost
-  if (serverPort !== 80 && serverPort !== 443) {
-    headerHostShouldBe += ':' + serverPort
-  }
+function createUser (req, res, next) {
+  const user = new User({
+    username: req.body.username,
+    password: req.body.password,
+    role: constants.USER_ROLES.USER
+  })
 
-  // Don't make this check if this is a test instance
-  if (process.env.NODE_ENV !== 'test' && req.get('host') !== headerHostShouldBe) {
-    return res.type('json').status(403).end()
-  }
+  user.save(function (err, createdUser) {
+    if (err) return next(err)
 
-  Client.loadFirstClient(function (err, client) {
+    return res.type('json').status(204).end()
+  })
+}
+
+function getUserInformation (req, res, next) {
+  User.loadByUsername(res.locals.oauth.token.user.username, function (err, user) {
     if (err) return next(err)
-    if (!client) return next(new Error('No client available.'))
 
-    res.json({
-      client_id: client._id,
-      client_secret: client.clientSecret
+    return res.json(user.toFormatedJSON())
+  })
+}
+
+function listUsers (req, res, next) {
+  User.listForApi(req.query.start, req.query.count, req.query.sort, function (err, usersList, usersTotal) {
+    if (err) return next(err)
+
+    res.json(getFormatedUsers(usersList, usersTotal))
+  })
+}
+
+function removeUser (req, res, next) {
+  waterfall([
+    function getUser (callback) {
+      User.loadById(req.params.id, callback)
+    },
+
+    function getVideos (user, callback) {
+      Video.listOwnedByAuthor(user.username, function (err, videos) {
+        return callback(err, user, videos)
+      })
+    },
+
+    function removeVideosFromDB (user, videos, callback) {
+      each(videos, function (video, callbackEach) {
+        video.remove(callbackEach)
+      }, function (err) {
+        return callback(err, user, videos)
+      })
+    },
+
+    function sendInformationToFriends (user, videos, callback) {
+      videos.forEach(function (video) {
+        const params = {
+          name: video.name,
+          magnetUri: video.magnetUri
+        }
+
+        friends.removeVideoToFriends(params)
+      })
+
+      return callback(null, user)
+    },
+
+    function removeUserFromDB (user, callback) {
+      user.remove(callback)
+    }
+  ], function andFinally (err) {
+    if (err) {
+      logger.error('Errors when removed the user.', { error: err })
+      return next(err)
+    }
+
+    return res.sendStatus(204)
+  })
+}
+
+function updateUser (req, res, next) {
+  User.loadByUsername(res.locals.oauth.token.user.username, function (err, user) {
+    if (err) return next(err)
+
+    user.password = req.body.password
+    user.save(function (err) {
+      if (err) return next(err)
+
+      return res.sendStatus(204)
     })
   })
 }
@@ -47,3 +153,18 @@ function getAngularClient (req, res, next) {
 function success (req, res, next) {
   res.end()
 }
+
+// ---------------------------------------------------------------------------
+
+function getFormatedUsers (users, usersTotal) {
+  const formatedUsers = []
+
+  users.forEach(function (user) {
+    formatedUsers.push(user.toFormatedJSON())
+  })
+
+  return {
+    total: usersTotal,
+    data: formatedUsers
+  }
+}
index 1f939b077f1f80711c3560142a9c7f5708360319..70d22f139ba35dba729928509ec36f134f6a9e38 100644 (file)
@@ -1,11 +1,11 @@
 'use strict'
 
-const config = require('config')
 const express = require('express')
 const mongoose = require('mongoose')
 const multer = require('multer')
 const waterfall = require('async/waterfall')
 
+const constants = require('../../../initializers/constants')
 const logger = require('../../../helpers/logger')
 const friends = require('../../../lib/friends')
 const middlewares = require('../../../middlewares')
@@ -20,13 +20,12 @@ const sort = middlewares.sort
 const utils = require('../../../helpers/utils')
 
 const router = express.Router()
-const uploads = config.get('storage.uploads')
 const Video = mongoose.model('Video')
 
 // multer configuration
 const storage = multer.diskStorage({
   destination: function (req, file, cb) {
-    cb(null, uploads)
+    cb(null, constants.CONFIG.STORAGE.UPLOAD_DIR)
   },
 
   filename: function (req, file, cb) {
@@ -142,7 +141,7 @@ function getVideo (req, res, next) {
 }
 
 function listVideos (req, res, next) {
-  Video.list(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) {
+  Video.listForApi(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) {
     if (err) return next(err)
 
     res.json(getFormatedVideos(videosList, videosTotal))
diff --git a/server/helpers/custom-validators/index.js b/server/helpers/custom-validators/index.js
new file mode 100644 (file)
index 0000000..96b5b20
--- /dev/null
@@ -0,0 +1,17 @@
+'use strict'
+
+const miscValidators = require('./misc')
+const podsValidators = require('./pods')
+const usersValidators = require('./users')
+const videosValidators = require('./videos')
+
+const validators = {
+  misc: miscValidators,
+  pods: podsValidators,
+  users: usersValidators,
+  videos: videosValidators
+}
+
+// ---------------------------------------------------------------------------
+
+module.exports = validators
diff --git a/server/helpers/custom-validators/misc.js b/server/helpers/custom-validators/misc.js
new file mode 100644 (file)
index 0000000..0527262
--- /dev/null
@@ -0,0 +1,18 @@
+'use strict'
+
+const miscValidators = {
+  exists,
+  isArray
+}
+
+function exists (value) {
+  return value !== undefined && value !== null
+}
+
+function isArray (value) {
+  return Array.isArray(value)
+}
+
+// ---------------------------------------------------------------------------
+
+module.exports = miscValidators
diff --git a/server/helpers/custom-validators/pods.js b/server/helpers/custom-validators/pods.js
new file mode 100644 (file)
index 0000000..40f8b5d
--- /dev/null
@@ -0,0 +1,21 @@
+'use strict'
+
+const validator = require('express-validator').validator
+
+const miscValidators = require('./misc')
+
+const podsValidators = {
+  isEachUniqueUrlValid
+}
+
+function isEachUniqueUrlValid (urls) {
+  return miscValidators.isArray(urls) &&
+    urls.length !== 0 &&
+    urls.every(function (url) {
+      return validator.isURL(url) && urls.indexOf(url) === urls.lastIndexOf(url)
+    })
+}
+
+// ---------------------------------------------------------------------------
+
+module.exports = podsValidators
diff --git a/server/helpers/custom-validators/users.js b/server/helpers/custom-validators/users.js
new file mode 100644 (file)
index 0000000..88fa159
--- /dev/null
@@ -0,0 +1,31 @@
+'use strict'
+
+const validator = require('express-validator').validator
+const values = require('lodash/values')
+
+const constants = require('../../initializers/constants')
+const USERS_CONSTRAINTS_FIELDS = constants.CONSTRAINTS_FIELDS.USERS
+
+const usersValidators = {
+  isUserPasswordValid,
+  isUserRoleValid,
+  isUserUsernameValid
+}
+
+function isUserPasswordValid (value) {
+  return validator.isLength(value, USERS_CONSTRAINTS_FIELDS.PASSWORD)
+}
+
+function isUserRoleValid (value) {
+  return values(constants.USER_ROLES).indexOf(value) !== -1
+}
+
+function isUserUsernameValid (value) {
+  const max = USERS_CONSTRAINTS_FIELDS.USERNAME.max
+  const min = USERS_CONSTRAINTS_FIELDS.USERNAME.min
+  return validator.matches(value, new RegExp(`^[a-zA-Z0-9._]{${min},${max}}$`))
+}
+
+// ---------------------------------------------------------------------------
+
+module.exports = usersValidators
similarity index 50%
rename from server/helpers/custom-validators.js
rename to server/helpers/custom-validators/videos.js
index b666644c0167c81bc500570b5c746e7fe7197405..a507ff68617a7e46925e31356843dc0585e8d7ec 100644 (file)
@@ -2,66 +2,51 @@
 
 const validator = require('express-validator').validator
 
-const constants = require('../initializers/constants')
-const VIDEOS_CONSTRAINTS_FIELDS = constants.VIDEOS_CONSTRAINTS_FIELDS
-
-const customValidators = {
-  exists: exists,
-  isEachRemoteVideosValid: isEachRemoteVideosValid,
-  isArray: isArray,
-  isVideoAuthorValid: isVideoAuthorValid,
-  isVideoDateValid: isVideoDateValid,
-  isVideoDescriptionValid: isVideoDescriptionValid,
-  isVideoDurationValid: isVideoDurationValid,
-  isVideoMagnetUriValid: isVideoMagnetUriValid,
-  isVideoNameValid: isVideoNameValid,
-  isVideoPodUrlValid: isVideoPodUrlValid,
-  isVideoTagsValid: isVideoTagsValid,
-  isVideoThumbnailValid: isVideoThumbnailValid,
-  isVideoThumbnail64Valid: isVideoThumbnail64Valid
-}
-
-function exists (value) {
-  return value !== undefined && value !== null
+const constants = require('../../initializers/constants')
+const usersValidators = require('./users')
+const miscValidators = require('./misc')
+const VIDEOS_CONSTRAINTS_FIELDS = constants.CONSTRAINTS_FIELDS.VIDEOS
+
+const videosValidators = {
+  isEachRemoteVideosValid,
+  isVideoAuthorValid,
+  isVideoDateValid,
+  isVideoDescriptionValid,
+  isVideoDurationValid,
+  isVideoMagnetUriValid,
+  isVideoNameValid,
+  isVideoPodUrlValid,
+  isVideoTagsValid,
+  isVideoThumbnailValid,
+  isVideoThumbnail64Valid
 }
 
 function isEachRemoteVideosValid (requests) {
-  return requests.every(function (request) {
-    const video = request.data
-    return (
-      isRequestTypeAddValid(request.type) &&
-      isVideoAuthorValid(video.author) &&
-      isVideoDateValid(video.createdDate) &&
-      isVideoDescriptionValid(video.description) &&
-      isVideoDurationValid(video.duration) &&
-      isVideoMagnetUriValid(video.magnetUri) &&
-      isVideoNameValid(video.name) &&
-      isVideoPodUrlValid(video.podUrl) &&
-      isVideoTagsValid(video.tags) &&
-      isVideoThumbnail64Valid(video.thumbnailBase64)
-    ) ||
-    (
-      isRequestTypeRemoveValid(request.type) &&
-      isVideoNameValid(video.name) &&
-      isVideoMagnetUriValid(video.magnetUri)
-    )
-  })
-}
-
-function isArray (value) {
-  return Array.isArray(value)
-}
-
-function isRequestTypeAddValid (value) {
-  return value === 'add'
-}
-
-function isRequestTypeRemoveValid (value) {
-  return value === 'remove'
+  return miscValidators.isArray(requests) &&
+    requests.every(function (request) {
+      const video = request.data
+      return (
+        isRequestTypeAddValid(request.type) &&
+        isVideoAuthorValid(video.author) &&
+        isVideoDateValid(video.createdDate) &&
+        isVideoDescriptionValid(video.description) &&
+        isVideoDurationValid(video.duration) &&
+        isVideoMagnetUriValid(video.magnetUri) &&
+        isVideoNameValid(video.name) &&
+        isVideoPodUrlValid(video.podUrl) &&
+        isVideoTagsValid(video.tags) &&
+        isVideoThumbnail64Valid(video.thumbnailBase64)
+      ) ||
+      (
+        isRequestTypeRemoveValid(request.type) &&
+        isVideoNameValid(video.name) &&
+        isVideoMagnetUriValid(video.magnetUri)
+      )
+    })
 }
 
 function isVideoAuthorValid (value) {
-  return validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.AUTHOR)
+  return usersValidators.isUserUsernameValid(value)
 }
 
 function isVideoDateValid (value) {
@@ -90,7 +75,7 @@ function isVideoPodUrlValid (value) {
 }
 
 function isVideoTagsValid (tags) {
-  return isArray(tags) &&
+  return miscValidators.isArray(tags) &&
          validator.isInt(tags.length, VIDEOS_CONSTRAINTS_FIELDS.TAGS) &&
          tags.every(function (tag) {
            return validator.isAlphanumeric(tag) &&
@@ -109,6 +94,14 @@ function isVideoThumbnail64Valid (value) {
 
 // ---------------------------------------------------------------------------
 
-module.exports = customValidators
+module.exports = videosValidators
 
 // ---------------------------------------------------------------------------
+
+function isRequestTypeAddValid (value) {
+  return value === 'add'
+}
+
+function isRequestTypeRemoveValid (value) {
+  return value === 'remove'
+}
index 8ae90a4b2c80a935bec95170787fe74a090356a0..590ceaeb6c5be0ef69761166f497ed8a7d8b54ff 100644 (file)
@@ -1,23 +1,23 @@
 // Thanks http://tostring.it/2014/06/23/advanced-logging-with-nodejs/
 'use strict'
 
-const config = require('config')
 const mkdirp = require('mkdirp')
 const path = require('path')
 const winston = require('winston')
 winston.emitErrs = true
 
-const logDir = path.join(__dirname, '..', '..', config.get('storage.logs'))
-const label = config.get('webserver.host') + ':' + config.get('webserver.port')
+const constants = require('../initializers/constants')
+
+const label = constants.CONFIG.WEBSERVER.HOST + ':' + constants.CONFIG.WEBSERVER.PORT
 
 // Create the directory if it does not exist
-mkdirp.sync(logDir)
+mkdirp.sync(constants.CONFIG.STORAGE.LOG_DIR)
 
 const logger = new winston.Logger({
   transports: [
     new winston.transports.File({
       level: 'debug',
-      filename: path.join(logDir, 'all-logs.log'),
+      filename: path.join(constants.CONFIG.STORAGE.LOG_DIR, 'all-logs.log'),
       handleExceptions: true,
       json: true,
       maxsize: 5242880,
index 46dff8d034df69df5ce7e751cabc7d713d2f973a..1ff638b04ecce144a71698c9eaffea2efcef8391 100644 (file)
@@ -1,24 +1,24 @@
 'use strict'
 
-const config = require('config')
+const bcrypt = require('bcrypt')
 const crypto = require('crypto')
 const fs = require('fs')
 const openssl = require('openssl-wrapper')
-const path = require('path')
 const ursa = require('ursa')
 
+const constants = require('../initializers/constants')
 const logger = require('./logger')
 
-const certDir = path.join(__dirname, '..', '..', config.get('storage.certs'))
 const algorithm = 'aes-256-ctr'
 
 const peertubeCrypto = {
-  checkSignature: checkSignature,
-  createCertsIfNotExist: createCertsIfNotExist,
-  decrypt: decrypt,
-  encrypt: encrypt,
-  getCertDir: getCertDir,
-  sign: sign
+  checkSignature,
+  comparePassword,
+  createCertsIfNotExist,
+  cryptPassword,
+  decrypt,
+  encrypt,
+  sign
 }
 
 function checkSignature (publicKey, rawData, hexSignature) {
@@ -27,6 +27,14 @@ function checkSignature (publicKey, rawData, hexSignature) {
   return isValid
 }
 
+function comparePassword (plainPassword, hashPassword, callback) {
+  bcrypt.compare(plainPassword, hashPassword, function (err, isPasswordMatch) {
+    if (err) return callback(err)
+
+    return callback(null, isPasswordMatch)
+  })
+}
+
 function createCertsIfNotExist (callback) {
   certsExist(function (exist) {
     if (exist === true) {
@@ -39,8 +47,18 @@ function createCertsIfNotExist (callback) {
   })
 }
 
+function cryptPassword (password, callback) {
+  bcrypt.genSalt(constants.BCRYPT_SALT_SIZE, function (err, salt) {
+    if (err) return callback(err)
+
+    bcrypt.hash(password, salt, function (err, hash) {
+      return callback(err, hash)
+    })
+  })
+}
+
 function decrypt (key, data, callback) {
-  fs.readFile(getCertDir() + 'peertube.key.pem', function (err, file) {
+  fs.readFile(constants.CONFIG.STORAGE.CERT_DIR + 'peertube.key.pem', function (err, file) {
     if (err) return callback(err)
 
     const myPrivateKey = ursa.createPrivateKey(file)
@@ -67,12 +85,8 @@ function encrypt (publicKey, data, callback) {
   })
 }
 
-function getCertDir () {
-  return certDir
-}
-
 function sign (data) {
-  const myKey = ursa.createPrivateKey(fs.readFileSync(certDir + 'peertube.key.pem'))
+  const myKey = ursa.createPrivateKey(fs.readFileSync(constants.CONFIG.STORAGE.CERT_DIR + 'peertube.key.pem'))
   const signature = myKey.hashAndSign('sha256', data, 'utf8', 'hex')
 
   return signature
@@ -85,7 +99,7 @@ module.exports = peertubeCrypto
 // ---------------------------------------------------------------------------
 
 function certsExist (callback) {
-  fs.exists(certDir + 'peertube.key.pem', function (exists) {
+  fs.exists(constants.CONFIG.STORAGE.CERT_DIR + 'peertube.key.pem', function (exists) {
     return callback(exists)
   })
 }
@@ -99,15 +113,25 @@ function createCerts (callback) {
     }
 
     logger.info('Generating a RSA key...')
-    openssl.exec('genrsa', { 'out': certDir + 'peertube.key.pem', '2048': false }, function (err) {
+
+    let options = {
+      'out': constants.CONFIG.STORAGE.CERT_DIR + 'peertube.key.pem',
+      '2048': false
+    }
+    openssl.exec('genrsa', options, function (err) {
       if (err) {
         logger.error('Cannot create private key on this pod.')
         return callback(err)
       }
       logger.info('RSA key generated.')
 
+      options = {
+        'in': constants.CONFIG.STORAGE.CERT_DIR + 'peertube.key.pem',
+        'pubout': true,
+        'out': constants.CONFIG.STORAGE.CERT_DIR + 'peertube.pub'
+      }
       logger.info('Manage public key...')
-      openssl.exec('rsa', { 'in': certDir + 'peertube.key.pem', 'pubout': true, 'out': certDir + 'peertube.pub' }, function (err) {
+      openssl.exec('rsa', options, function (err) {
         if (err) {
           logger.error('Cannot create public key on this pod.')
           return callback(err)
index 547230adc973a0958ce672d26363c89588554612..95775c981c1a5545d0e82bd45bc4bb48b88770f0 100644 (file)
@@ -1,19 +1,14 @@
 'use strict'
 
-const config = require('config')
 const replay = require('request-replay')
 const request = require('request')
 
 const constants = require('../initializers/constants')
 const peertubeCrypto = require('./peertube-crypto')
 
-const http = config.get('webserver.https') ? 'https' : 'http'
-const host = config.get('webserver.host')
-const port = config.get('webserver.port')
-
 const requests = {
-  makeRetryRequest: makeRetryRequest,
-  makeSecureRequest: makeSecureRequest
+  makeRetryRequest,
+  makeSecureRequest
 }
 
 function makeRetryRequest (params, callback) {
@@ -29,8 +24,6 @@ function makeRetryRequest (params, callback) {
 }
 
 function makeSecureRequest (params, callback) {
-  const myUrl = http + '://' + host + ':' + port
-
   const requestParams = {
     url: params.toPod.url + params.path
   }
@@ -42,8 +35,8 @@ function makeSecureRequest (params, callback) {
     // Add signature if it is specified in the params
     if (params.sign === true) {
       requestParams.json.signature = {
-        url: myUrl,
-        signature: peertubeCrypto.sign(myUrl)
+        url: constants.CONFIG.WEBSERVER.URL,
+        signature: peertubeCrypto.sign(constants.CONFIG.WEBSERVER.URL)
       }
     }
 
index a77116e0833f44335848d3a4c8ccf0dec1af7bb9..9c2d402e3105047615d26bfe4ebd016deed2810d 100644 (file)
@@ -5,8 +5,8 @@ const crypto = require('crypto')
 const logger = require('./logger')
 
 const utils = {
-  cleanForExit: cleanForExit,
-  generateRandomString: generateRandomString
+  cleanForExit,
+  generateRandomString
 }
 
 function generateRandomString (size, callback) {
index 3831efb8d5eccffe3d75a464e4270181d6bb4917..91fbcfaf933ffe2885c491f44dc2e6de7c398f98 100644 (file)
@@ -7,9 +7,9 @@ const Client = mongoose.model('OAuthClient')
 const User = mongoose.model('User')
 
 const checker = {
-  checkConfig: checkConfig,
-  clientsExist: clientsExist,
-  usersExist: usersExist
+  checkConfig,
+  clientsExist,
+  usersExist
 }
 
 // Check the config files
@@ -17,8 +17,8 @@ function checkConfig () {
   const required = [ 'listen.port',
     'webserver.https', 'webserver.host', 'webserver.port',
     'database.host', 'database.port', 'database.suffix',
-    'storage.certs', 'storage.uploads', 'storage.logs',
-    'network.friends', 'electron.debug' ]
+    'storage.certs', 'storage.uploads', 'storage.logs', 'storage.thumbnails',
+    'electron.debug' ]
   const miss = []
 
   for (const key of required) {
@@ -39,10 +39,10 @@ function clientsExist (callback) {
 }
 
 function usersExist (callback) {
-  User.list(function (err, users) {
+  User.countTotal(function (err, totalUsers) {
     if (err) return callback(err)
 
-    return callback(null, users.length !== 0)
+    return callback(null, totalUsers !== 0)
   })
 }
 
index e0ea188af77ca549bac07b09d06fbecb642390ab..be2e3e9439bcf9bc6d8354a8ded910968b21814d 100644 (file)
 'use strict'
 
-// API version of our pod
+const config = require('config')
+const path = require('path')
+
+// ---------------------------------------------------------------------------
+
+// API version
 const API_VERSION = 'v1'
 
-// Score a pod has when we create it as a friend
-const FRIEND_SCORE = {
-  BASE: 100,
-  MAX: 1000
+// Number of results by default for the pagination
+const PAGINATION_COUNT_DEFAULT = 15
+
+// Sortable columns per schema
+const SEARCHABLE_COLUMNS = {
+  VIDEOS: [ 'name', 'magnetUri', 'podUrl', 'author', 'tags' ]
 }
 
-// Time to wait between requests to the friends (10 min)
-let INTERVAL = 600000
+// Sortable columns per schema
+const SORTABLE_COLUMNS = {
+  USERS: [ 'username', '-username', 'createdDate', '-createdDate' ],
+  VIDEOS: [ 'name', '-name', 'duration', '-duration', 'createdDate', '-createdDate' ]
+}
 
 const OAUTH_LIFETIME = {
   ACCESS_TOKEN: 3600 * 4, // 4 hours
   REFRESH_TOKEN: 1209600 // 2 weeks
 }
 
-// Number of results by default for the pagination
-const PAGINATION_COUNT_DEFAULT = 15
+// ---------------------------------------------------------------------------
+
+const CONFIG = {
+  DATABASE: {
+    DBNAME: 'peertube' + config.get('database.suffix'),
+    HOST: config.get('database.host'),
+    PORT: config.get('database.port')
+  },
+  ELECTRON: {
+    DEBUG: config.get('electron.debug')
+  },
+  STORAGE: {
+    CERT_DIR: path.join(__dirname, '..', '..', config.get('storage.certs')),
+    LOG_DIR: path.join(__dirname, '..', '..', config.get('storage.logs')),
+    UPLOAD_DIR: path.join(__dirname, '..', '..', config.get('storage.uploads')),
+    THUMBNAILS_DIR: path.join(__dirname, '..', '..', config.get('storage.thumbnails'))
+  },
+  WEBSERVER: {
+    SCHEME: config.get('webserver.https') === true ? 'https' : 'http',
+    HOST: config.get('webserver.host'),
+    PORT: config.get('webserver.port')
+  }
+}
+CONFIG.WEBSERVER.URL = CONFIG.WEBSERVER.SCHEME + '://' + CONFIG.WEBSERVER.HOST + ':' + CONFIG.WEBSERVER.PORT
+
+// ---------------------------------------------------------------------------
+
+const CONSTRAINTS_FIELDS = {
+  USERS: {
+    USERNAME: { min: 3, max: 20 }, // Length
+    PASSWORD: { min: 6, max: 255 } // Length
+  },
+  VIDEOS: {
+    NAME: { min: 3, max: 50 }, // Length
+    DESCRIPTION: { min: 3, max: 250 }, // Length
+    MAGNET_URI: { min: 10 }, // Length
+    DURATION: { min: 1, max: 7200 }, // Number
+    TAGS: { min: 1, max: 3 }, // Number of total tags
+    TAG: { min: 2, max: 10 }, // Length
+    THUMBNAIL: { min: 2, max: 30 },
+    THUMBNAIL64: { min: 0, max: 20000 } // Bytes
+  }
+}
+
+// ---------------------------------------------------------------------------
+
+// Score a pod has when we create it as a friend
+const FRIEND_SCORE = {
+  BASE: 100,
+  MAX: 1000
+}
+
+// ---------------------------------------------------------------------------
+
+const MONGO_MIGRATION_SCRIPTS = [
+  {
+    script: '0005-create-application',
+    version: 5
+  },
+  {
+    script: '0010-users-password',
+    version: 10
+  },
+  {
+    script: '0015-admin-role',
+    version: 15
+  }
+]
+const LAST_MONGO_SCHEMA_VERSION = 15
+
+// ---------------------------------------------------------------------------
 
 // Number of points we add/remove from a friend after a successful/bad request
 const PODS_SCORE = {
@@ -26,28 +105,22 @@ const PODS_SCORE = {
   BONUS: 10
 }
 
+// Time to wait between requests to the friends (10 min)
+let REQUESTS_INTERVAL = 600000
+
 // Number of requests in parallel we can make
 const REQUESTS_IN_PARALLEL = 10
 
-// How many requests we put in request (request scheduler)
+// How many requests we put in request
 const REQUESTS_LIMIT = 10
 
 // Number of requests to retry for replay requests module
 const RETRY_REQUESTS = 5
 
-// Sortable columns per schema
-const SEARCHABLE_COLUMNS = {
-  VIDEOS: [ 'name', 'magnetUri', 'podUrl', 'author', 'tags' ]
-}
-
-// Seeds in parallel we send to electron when "seed all"
-// Once a video is in seeding state we seed another video etc
-const SEEDS_IN_PARALLEL = 3
+// ---------------------------------------------------------------------------
 
-// Sortable columns per schema
-const SORTABLE_COLUMNS = {
-  VIDEOS: [ 'name', '-name', 'duration', '-duration', 'createdDate', '-createdDate' ]
-}
+// Password encryption
+const BCRYPT_SALT_SIZE = 10
 
 // Express static paths (router)
 const STATIC_PATHS = {
@@ -59,43 +132,47 @@ const STATIC_PATHS = {
 // Videos thumbnail size
 const THUMBNAILS_SIZE = '200x110'
 
-const VIDEOS_CONSTRAINTS_FIELDS = {
-  NAME: { min: 3, max: 50 }, // Length
-  DESCRIPTION: { min: 3, max: 250 }, // Length
-  MAGNET_URI: { min: 10 }, // Length
-  DURATION: { min: 1, max: 7200 }, // Number
-  AUTHOR: { min: 3, max: 20 }, // Length
-  TAGS: { min: 1, max: 3 }, // Number of total tags
-  TAG: { min: 2, max: 10 }, // Length
-  THUMBNAIL: { min: 2, max: 30 },
-  THUMBNAIL64: { min: 0, max: 20000 } // Bytes
+const USER_ROLES = {
+  ADMIN: 'admin',
+  USER: 'user'
 }
 
+// Seeds in parallel we send to electron when "seed all"
+// Once a video is in seeding state we seed another video etc
+const SEEDS_IN_PARALLEL = 3
+
+// ---------------------------------------------------------------------------
+
 // Special constants for a test instance
 if (isTestInstance() === true) {
+  CONSTRAINTS_FIELDS.VIDEOS.DURATION.max = 14
   FRIEND_SCORE.BASE = 20
-  INTERVAL = 10000
-  VIDEOS_CONSTRAINTS_FIELDS.DURATION.max = 14
+  REQUESTS_INTERVAL = 10000
 }
 
 // ---------------------------------------------------------------------------
 
 module.exports = {
-  API_VERSION: API_VERSION,
-  FRIEND_SCORE: FRIEND_SCORE,
-  INTERVAL: INTERVAL,
-  OAUTH_LIFETIME: OAUTH_LIFETIME,
-  PAGINATION_COUNT_DEFAULT: PAGINATION_COUNT_DEFAULT,
-  PODS_SCORE: PODS_SCORE,
-  REQUESTS_IN_PARALLEL: REQUESTS_IN_PARALLEL,
-  REQUESTS_LIMIT: REQUESTS_LIMIT,
-  RETRY_REQUESTS: RETRY_REQUESTS,
-  SEARCHABLE_COLUMNS: SEARCHABLE_COLUMNS,
-  SEEDS_IN_PARALLEL: SEEDS_IN_PARALLEL,
-  SORTABLE_COLUMNS: SORTABLE_COLUMNS,
-  STATIC_PATHS: STATIC_PATHS,
-  THUMBNAILS_SIZE: THUMBNAILS_SIZE,
-  VIDEOS_CONSTRAINTS_FIELDS: VIDEOS_CONSTRAINTS_FIELDS
+  API_VERSION,
+  BCRYPT_SALT_SIZE,
+  CONFIG,
+  CONSTRAINTS_FIELDS,
+  FRIEND_SCORE,
+  LAST_MONGO_SCHEMA_VERSION,
+  MONGO_MIGRATION_SCRIPTS,
+  OAUTH_LIFETIME,
+  PAGINATION_COUNT_DEFAULT,
+  PODS_SCORE,
+  REQUESTS_IN_PARALLEL,
+  REQUESTS_INTERVAL,
+  REQUESTS_LIMIT,
+  RETRY_REQUESTS,
+  SEARCHABLE_COLUMNS,
+  SEEDS_IN_PARALLEL,
+  SORTABLE_COLUMNS,
+  STATIC_PATHS,
+  THUMBNAILS_SIZE,
+  USER_ROLES
 }
 
 // ---------------------------------------------------------------------------
index 8626895ee6fd4cd60d8bfac71065d900399f2c84..45c8a240d79414ceefc71ee54ce494d8fb2ad6a6 100644 (file)
@@ -1,30 +1,27 @@
 'use strict'
 
-const config = require('config')
 const mongoose = require('mongoose')
 
+const constants = require('../initializers/constants')
 const logger = require('../helpers/logger')
 
 // Bootstrap models
+require('../models/application')
+require('../models/oauth-token')
 require('../models/user')
 require('../models/oauth-client')
-require('../models/oauth-token')
 require('../models/pods')
 require('../models/video')
 // Request model needs Video model
 require('../models/request')
 
-const dbname = 'peertube' + config.get('database.suffix')
-const host = config.get('database.host')
-const port = config.get('database.port')
-
 const database = {
   connect: connect
 }
 
 function connect () {
   mongoose.Promise = global.Promise
-  mongoose.connect('mongodb://' + host + ':' + port + '/' + dbname)
+  mongoose.connect('mongodb://' + constants.CONFIG.DATABASE.HOST + ':' + constants.CONFIG.DATABASE.PORT + '/' + constants.CONFIG.DATABASE.DBNAME)
   mongoose.connection.on('error', function () {
     throw new Error('Mongodb connection error.')
   })
index 32830d4dab5d3aa811428074296d4d7b56578a38..1df300ba8d467fb76d3a7b5647fe1efc76559294 100644 (file)
@@ -9,14 +9,16 @@ const path = require('path')
 const series = require('async/series')
 
 const checker = require('./checker')
+const constants = require('./constants')
 const logger = require('../helpers/logger')
 const peertubeCrypto = require('../helpers/peertube-crypto')
 
+const Application = mongoose.model('Application')
 const Client = mongoose.model('OAuthClient')
 const User = mongoose.model('User')
 
 const installer = {
-  installApplication: installApplication
+  installApplication
 }
 
 function installApplication (callback) {
@@ -34,7 +36,7 @@ function installApplication (callback) {
     },
 
     function createOAuthUser (callbackAsync) {
-      createOAuthUserIfNotExist(callbackAsync)
+      createOAuthAdminIfNotExist(callbackAsync)
     }
   ], callback)
 }
@@ -80,7 +82,7 @@ function createOAuthClientIfNotExist (callback) {
   })
 }
 
-function createOAuthUserIfNotExist (callback) {
+function createOAuthAdminIfNotExist (callback) {
   checker.usersExist(function (err, exist) {
     if (err) return callback(err)
 
@@ -90,6 +92,7 @@ function createOAuthUserIfNotExist (callback) {
     logger.info('Creating the administrator.')
 
     const username = 'root'
+    const role = constants.USER_ROLES.ADMIN
     let password = ''
 
     // Do not generate a random password for tests
@@ -104,17 +107,20 @@ function createOAuthUserIfNotExist (callback) {
     }
 
     const user = new User({
-      username: username,
-      password: password
+      username,
+      password,
+      role
     })
 
     user.save(function (err, createdUser) {
       if (err) return callback(err)
 
-      logger.info('Username: ' + createdUser.username)
-      logger.info('User password: ' + createdUser.password)
+      logger.info('Username: ' + username)
+      logger.info('User password: ' + password)
 
-      return callback(null)
+      logger.info('Creating Application collection.')
+      const application = new Application({ mongoSchemaVersion: constants.LAST_MONGO_SCHEMA_VERSION })
+      application.save(callback)
     })
   })
 }
diff --git a/server/initializers/migrations/0005-create-application.js b/server/initializers/migrations/0005-create-application.js
new file mode 100644 (file)
index 0000000..e99dec0
--- /dev/null
@@ -0,0 +1,17 @@
+/*
+  Create the application collection in MongoDB.
+  Used to store the actual MongoDB scheme version.
+*/
+
+const mongoose = require('mongoose')
+
+const Application = mongoose.model('Application')
+
+exports.up = function (callback) {
+  const application = new Application()
+  application.save(callback)
+}
+
+exports.down = function (callback) {
+  throw new Error('Not implemented.')
+}
diff --git a/server/initializers/migrations/0010-users-password.js b/server/initializers/migrations/0010-users-password.js
new file mode 100644 (file)
index 0000000..a0616a2
--- /dev/null
@@ -0,0 +1,22 @@
+/*
+  Convert plain user password to encrypted user password.
+*/
+
+const eachSeries = require('async/eachSeries')
+const mongoose = require('mongoose')
+
+const User = mongoose.model('User')
+
+exports.up = function (callback) {
+  User.list(function (err, users) {
+    if (err) return callback(err)
+
+    eachSeries(users, function (user, callbackEach) {
+      user.save(callbackEach)
+    }, callback)
+  })
+}
+
+exports.down = function (callback) {
+  throw new Error('Not implemented.')
+}
diff --git a/server/initializers/migrations/0015-admin-role.js b/server/initializers/migrations/0015-admin-role.js
new file mode 100644 (file)
index 0000000..af06dca
--- /dev/null
@@ -0,0 +1,16 @@
+/*
+  Set the admin role to the root user.
+*/
+
+const constants = require('../constants')
+const mongoose = require('mongoose')
+
+const User = mongoose.model('User')
+
+exports.up = function (callback) {
+  User.update({ username: 'root' }, { role: constants.USER_ROLES.ADMIN }, callback)
+}
+
+exports.down = function (callback) {
+  throw new Error('Not implemented.')
+}
diff --git a/server/initializers/migrator.js b/server/initializers/migrator.js
new file mode 100644 (file)
index 0000000..6b31d99
--- /dev/null
@@ -0,0 +1,56 @@
+'use strict'
+
+const eachSeries = require('async/eachSeries')
+const mongoose = require('mongoose')
+const path = require('path')
+
+const constants = require('./constants')
+const logger = require('../helpers/logger')
+
+const Application = mongoose.model('Application')
+
+const migrator = {
+  migrate: migrate
+}
+
+function migrate (callback) {
+  Application.loadMongoSchemaVersion(function (err, actualVersion) {
+    if (err) return callback(err)
+
+    // If there are a new mongo schemas
+    if (!actualVersion || actualVersion < constants.LAST_MONGO_SCHEMA_VERSION) {
+      logger.info('Begin migrations.')
+
+      eachSeries(constants.MONGO_MIGRATION_SCRIPTS, function (entity, callbackEach) {
+        const versionScript = entity.version
+
+        // Do not execute old migration scripts
+        if (versionScript <= actualVersion) return callbackEach(null)
+
+        // Load the migration module and run it
+        const migrationScriptName = entity.script
+        logger.info('Executing %s migration script.', migrationScriptName)
+
+        const migrationScript = require(path.join(__dirname, 'migrations', migrationScriptName))
+        migrationScript.up(function (err) {
+          if (err) return callbackEach(err)
+
+          // Update the new mongo version schema
+          Application.updateMongoSchemaVersion(versionScript, callbackEach)
+        })
+      }, function (err) {
+        if (err) return callback(err)
+
+        logger.info('Migrations finished. New mongo version schema: %s', constants.LAST_MONGO_SCHEMA_VERSION)
+        return callback(null)
+      })
+    } else {
+      return callback(null)
+    }
+  })
+}
+
+// ---------------------------------------------------------------------------
+
+module.exports = migrator
+
index 6e1516b94a14ae09d07ea387f53e9264c6e82263..556d2e7733e934f45d34a3d3098991ff56631c16 100644 (file)
@@ -1,6 +1,5 @@
 'use strict'
 
-const config = require('config')
 const each = require('async/each')
 const eachLimit = require('async/eachLimit')
 const eachSeries = require('async/eachSeries')
@@ -11,24 +10,20 @@ const waterfall = require('async/waterfall')
 
 const constants = require('../initializers/constants')
 const logger = require('../helpers/logger')
-const peertubeCrypto = require('../helpers/peertube-crypto')
 const requests = require('../helpers/requests')
 
-const http = config.get('webserver.https') ? 'https' : 'http'
-const host = config.get('webserver.host')
-const port = config.get('webserver.port')
 const Pod = mongoose.model('Pod')
 const Request = mongoose.model('Request')
 const Video = mongoose.model('Video')
 
 const friends = {
-  addVideoToFriends: addVideoToFriends,
-  hasFriends: hasFriends,
-  getMyCertificate: getMyCertificate,
-  makeFriends: makeFriends,
-  quitFriends: quitFriends,
-  removeVideoToFriends: removeVideoToFriends,
-  sendOwnedVideosToPod: sendOwnedVideosToPod
+  addVideoToFriends,
+  hasFriends,
+  getMyCertificate,
+  makeFriends,
+  quitFriends,
+  removeVideoToFriends,
+  sendOwnedVideosToPod
 }
 
 function addVideoToFriends (video) {
@@ -45,10 +40,10 @@ function hasFriends (callback) {
 }
 
 function getMyCertificate (callback) {
-  fs.readFile(peertubeCrypto.getCertDir() + 'peertube.pub', 'utf8', callback)
+  fs.readFile(constants.CONFIG.STORAGE.CERT_DIR + 'peertube.pub', 'utf8', callback)
 }
 
-function makeFriends (callback) {
+function makeFriends (urls, callback) {
   const podsScore = {}
 
   logger.info('Make friends!')
@@ -58,8 +53,6 @@ function makeFriends (callback) {
       return callback(err)
     }
 
-    const urls = config.get('network.friends')
-
     eachSeries(urls, function (url, callbackEach) {
       computeForeignPodsList(url, podsScore, callbackEach)
     }, function (err) {
@@ -205,7 +198,12 @@ function getForeignPodsList (url, callback) {
   request.get(url + path, function (err, response, body) {
     if (err) return callback(err)
 
-    callback(null, JSON.parse(body))
+    try {
+      const json = JSON.parse(body)
+      return callback(null, json)
+    } catch (err) {
+      return callback(err)
+    }
   })
 }
 
@@ -220,7 +218,7 @@ function makeRequestsToWinningPods (cert, podsList, callback) {
       url: pod.url + '/api/' + constants.API_VERSION + '/pods/',
       method: 'POST',
       json: {
-        url: http + '://' + host + ':' + port,
+        url: constants.CONFIG.WEBSERVER.URL,
         publicKey: cert
       }
     }
index d9f8b175a6df62698fd810ec4c5ec85e3dda618b..45f796796ca85880f67183a4af5853649a62008b 100644 (file)
@@ -8,12 +8,12 @@ const User = mongoose.model('User')
 
 // See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications
 const OAuthModel = {
-  getAccessToken: getAccessToken,
-  getClient: getClient,
-  getRefreshToken: getRefreshToken,
-  getUser: getUser,
-  revokeToken: revokeToken,
-  saveToken: saveToken
+  getAccessToken,
+  getClient,
+  getRefreshToken,
+  getUser,
+  revokeToken,
+  saveToken
 }
 
 // ---------------------------------------------------------------------------
@@ -41,7 +41,22 @@ function getRefreshToken (refreshToken, callback) {
 function getUser (username, password) {
   logger.debug('Getting User (username: ' + username + ', password: ' + password + ').')
 
-  return User.getByUsernameAndPassword(username, password)
+  return User.getByUsername(username).then(function (user) {
+    if (!user) return null
+
+    // We need to return a promise
+    return new Promise(function (resolve, reject) {
+      return user.isPasswordMatch(password, function (err, isPasswordMatch) {
+        if (err) return reject(err)
+
+        if (isPasswordMatch === true) {
+          return resolve(user)
+        }
+
+        return resolve(null)
+      })
+    })
+  })
 }
 
 function revokeToken (token) {
diff --git a/server/middlewares/admin.js b/server/middlewares/admin.js
new file mode 100644 (file)
index 0000000..e6d9dc8
--- /dev/null
@@ -0,0 +1,22 @@
+'use strict'
+
+const constants = require('../initializers/constants')
+const logger = require('../helpers/logger')
+
+const adminMiddleware = {
+  ensureIsAdmin
+}
+
+function ensureIsAdmin (req, res, next) {
+  const user = res.locals.oauth.token.user
+  if (user.role !== constants.USER_ROLES.ADMIN) {
+    logger.info('A non admin user is trying to access to an admin content.')
+    return res.sendStatus(403)
+  }
+
+  return next()
+}
+
+// ---------------------------------------------------------------------------
+
+module.exports = adminMiddleware
index 0a233e70106f52cb89b521102973816b1dddd190..3f253e31bec9d3a94d67df5f55125db463884d8c 100644 (file)
@@ -1,19 +1,23 @@
 'use strict'
 
-const oauth = require('./oauth')
-const pagination = require('./pagination')
+const adminMiddleware = require('./admin')
+const oauthMiddleware = require('./oauth')
+const paginationMiddleware = require('./pagination')
+const podsMiddleware = require('./pods')
 const validatorsMiddleware = require('./validators')
-const search = require('./search')
-const sort = require('./sort')
+const searchMiddleware = require('./search')
+const sortMiddleware = require('./sort')
 const secureMiddleware = require('./secure')
 
 const middlewares = {
-  oauth: oauth,
-  pagination: pagination,
-  validators: validatorsMiddleware,
-  search: search,
-  sort: sort,
-  secure: secureMiddleware
+  admin: adminMiddleware,
+  oauth: oauthMiddleware,
+  pagination: paginationMiddleware,
+  pods: podsMiddleware,
+  search: searchMiddleware,
+  secure: secureMiddleware,
+  sort: sortMiddleware,
+  validators: validatorsMiddleware
 }
 
 // ---------------------------------------------------------------------------
index 91a99050913eead153e1c281bd5b86c524fc5993..3a02b9b4863c1e16b06502ae781368f13a48b7cb 100644 (file)
@@ -12,8 +12,8 @@ const oAuthServer = new OAuthServer({
 })
 
 const oAuth = {
-  authenticate: authenticate,
-  token: token
+  authenticate,
+  token
 }
 
 function authenticate (req, res, next) {
@@ -23,7 +23,7 @@ function authenticate (req, res, next) {
       return res.sendStatus(500)
     }
 
-    if (res.statusCode === 401 || res.statusCode === 400) return res.end()
+    if (res.statusCode === 401 || res.statusCode === 400 || res.statusCode === 503) return res.end()
 
     return next()
   })
index a571e51f6c75ec3a6e7a28e3365998884adb0300..a90f60aab22dc684cd23e1078f6ff6dedea45fe7 100644 (file)
@@ -3,7 +3,7 @@
 const constants = require('../initializers/constants')
 
 const paginationMiddleware = {
-  setPagination: setPagination
+  setPagination
 }
 
 function setPagination (req, res, next) {
diff --git a/server/middlewares/pods.js b/server/middlewares/pods.js
new file mode 100644 (file)
index 0000000..6e0874a
--- /dev/null
@@ -0,0 +1,62 @@
+'use strict'
+
+const urlModule = require('url')
+
+const logger = require('../helpers/logger')
+
+const podsMiddleware = {
+  setBodyUrlsPort,
+  setBodyUrlPort
+}
+
+function setBodyUrlsPort (req, res, next) {
+  for (let i = 0; i < req.body.urls.length; i++) {
+    const urlWithPort = getUrlWithPort(req.body.urls[i])
+
+    // Problem with the url parsing?
+    if (urlWithPort === null) {
+      return res.sendStatus(500)
+    }
+
+    req.body.urls[i] = urlWithPort
+  }
+
+  return next()
+}
+
+function setBodyUrlPort (req, res, next) {
+  const urlWithPort = getUrlWithPort(req.body.url)
+
+  // Problem with the url parsing?
+  if (urlWithPort === null) {
+    return res.sendStatus(500)
+  }
+
+  req.body.url = urlWithPort
+
+  return next()
+}
+
+// ---------------------------------------------------------------------------
+
+module.exports = podsMiddleware
+
+// ---------------------------------------------------------------------------
+
+function getUrlWithPort (url) {
+  const urlObj = urlModule.parse(url)
+
+  // Add the port if it is not specified
+  if (urlObj.port === null) {
+    if (urlObj.protocol === 'http:') {
+      return url + ':80'
+    } else if (urlObj.protocol === 'https:') {
+      return url + ':443'
+    } else {
+      logger.error('Unknown url protocol: ' + urlObj.protocol)
+      return null
+    }
+  }
+
+  return url
+}
index 89302a5643fc7d68a14d347a0373b9c7b824c3d9..bb88faf545b23d5585d517aa52ff22cf0702be24 100644 (file)
@@ -1,7 +1,7 @@
 'use strict'
 
 const searchMiddleware = {
-  setVideosSearch: setVideosSearch
+  setVideosSearch
 }
 
 function setVideosSearch (req, res, next) {
index 9779c14ac2a4e30d53b50bfcccbb26b9192096ff..58f824d14cbb4561f00d42c42d2c214e9640ea03 100644 (file)
@@ -7,10 +7,11 @@ const peertubeCrypto = require('../helpers/peertube-crypto')
 const Pod = mongoose.model('Pod')
 
 const secureMiddleware = {
-  decryptBody: decryptBody
+  checkSignature,
+  decryptBody
 }
 
-function decryptBody (req, res, next) {
+function checkSignature (req, res, next) {
   const url = req.body.signature.url
   Pod.loadByUrl(url, function (err, pod) {
     if (err) {
@@ -28,21 +29,30 @@ function decryptBody (req, res, next) {
     const signatureOk = peertubeCrypto.checkSignature(pod.publicKey, url, req.body.signature.signature)
 
     if (signatureOk === true) {
-      peertubeCrypto.decrypt(req.body.key, req.body.data, function (err, decrypted) {
-        if (err) {
-          logger.error('Cannot decrypt data.', { error: err })
-          return res.sendStatus(500)
-        }
-
-        req.body.data = JSON.parse(decrypted)
-        delete req.body.key
-
-        next()
-      })
-    } else {
-      logger.error('Signature is not okay in decryptBody for %s.', req.body.signature.url)
-      return res.sendStatus(403)
+      return next()
+    }
+
+    logger.error('Signature is not okay in decryptBody for %s.', req.body.signature.url)
+    return res.sendStatus(403)
+  })
+}
+
+function decryptBody (req, res, next) {
+  peertubeCrypto.decrypt(req.body.key, req.body.data, function (err, decrypted) {
+    if (err) {
+      logger.error('Cannot decrypt data.', { error: err })
+      return res.sendStatus(500)
     }
+
+    try {
+      req.body.data = JSON.parse(decrypted)
+      delete req.body.key
+    } catch (err) {
+      logger.error('Error in JSON.parse', { error: err })
+      return res.sendStatus(500)
+    }
+
+    next()
   })
 }
 
index 9f52290a6446a1f85dae27da13374082e9963c0f..f0b7274eba11ea244a600f6e0037bef71e0793e6 100644 (file)
@@ -1,7 +1,14 @@
 'use strict'
 
 const sortMiddleware = {
-  setVideosSort: setVideosSort
+  setUsersSort,
+  setVideosSort
+}
+
+function setUsersSort (req, res, next) {
+  if (!req.query.sort) req.query.sort = '-createdDate'
+
+  return next()
 }
 
 function setVideosSort (req, res, next) {
index 0471b3f92c618b803206c8a0c88b445b0599feec..6c3a9c2b4d032c197c09652d64a8f9947c4e0184 100644 (file)
@@ -4,6 +4,7 @@ const paginationValidators = require('./pagination')
 const podsValidators = require('./pods')
 const remoteValidators = require('./remote')
 const sortValidators = require('./sort')
+const usersValidators = require('./users')
 const videosValidators = require('./videos')
 
 const validators = {
@@ -11,6 +12,7 @@ const validators = {
   pods: podsValidators,
   remote: remoteValidators,
   sort: sortValidators,
+  users: usersValidators,
   videos: videosValidators
 }
 
index 8e9a010536fb7cfd1f2a6d62e98c8e1c888f3845..16682696e5a49cafc9e00c0a74a9c9eea0fdc45b 100644 (file)
@@ -4,7 +4,7 @@ const checkErrors = require('./utils').checkErrors
 const logger = require('../../helpers/logger')
 
 const validatorsPagination = {
-  pagination: pagination
+  pagination
 }
 
 function pagination (req, res, next) {
index fda2e865f58157ebc6a852aa8e130ce0fa8d5a9c..fd3d1e2f243375ab5a2b52ccf07c330614b45d28 100644 (file)
@@ -5,23 +5,29 @@ const friends = require('../../lib/friends')
 const logger = require('../../helpers/logger')
 
 const validatorsPod = {
-  makeFriends: makeFriends,
-  podsAdd: podsAdd
+  makeFriends,
+  podsAdd
 }
 
 function makeFriends (req, res, next) {
-  friends.hasFriends(function (err, hasFriends) {
-    if (err) {
-      logger.error('Cannot know if we have friends.', { error: err })
-      res.sendStatus(500)
-    }
-
-    if (hasFriends === true) {
-      // We need to quit our friends before make new ones
-      res.sendStatus(409)
-    } else {
-      return next()
-    }
+  req.checkBody('urls', 'Should have an array of unique urls').isEachUniqueUrlValid()
+
+  logger.debug('Checking makeFriends parameters', { parameters: req.body })
+
+  checkErrors(req, res, function () {
+    friends.hasFriends(function (err, hasFriends) {
+      if (err) {
+        logger.error('Cannot know if we have friends.', { error: err })
+        res.sendStatus(500)
+      }
+
+      if (hasFriends === true) {
+        // We need to quit our friends before make new ones
+        res.sendStatus(409)
+      } else {
+        return next()
+      }
+    })
   })
 }
 
index 1be119458dd2c37235c2ffb31a561e0d81150fd8..8c29ef8ca40c63407c4353f1a0a3a1f610a435a9 100644 (file)
@@ -4,9 +4,9 @@ const checkErrors = require('./utils').checkErrors
 const logger = require('../../helpers/logger')
 
 const validatorsRemote = {
-  dataToDecrypt: dataToDecrypt,
-  remoteVideos: remoteVideos,
-  signature: signature
+  dataToDecrypt,
+  remoteVideos,
+  signature
 }
 
 function dataToDecrypt (req, res, next) {
@@ -19,7 +19,6 @@ function dataToDecrypt (req, res, next) {
 }
 
 function remoteVideos (req, res, next) {
-  req.checkBody('data').isArray()
   req.checkBody('data').isEachRemoteVideosValid()
 
   logger.debug('Checking remoteVideos parameters', { parameters: req.body })
index 56b63cc8b9ddaffd5c5e1304ec4c0c38683382da..431d3fffd1e8b3062e46bd2eea7e0e3f68a54c0d 100644 (file)
@@ -5,7 +5,18 @@ const constants = require('../../initializers/constants')
 const logger = require('../../helpers/logger')
 
 const validatorsSort = {
-  videosSort: videosSort
+  usersSort,
+  videosSort
+}
+
+function usersSort (req, res, next) {
+  const sortableColumns = constants.SORTABLE_COLUMNS.USERS
+
+  req.checkQuery('sort', 'Should have correct sortable column').optional().isIn(sortableColumns)
+
+  logger.debug('Checking sort parameters', { parameters: req.query })
+
+  checkErrors(req, res, next)
 }
 
 function videosSort (req, res, next) {
diff --git a/server/middlewares/validators/users.js b/server/middlewares/validators/users.js
new file mode 100644 (file)
index 0000000..d541e91
--- /dev/null
@@ -0,0 +1,67 @@
+'use strict'
+
+const mongoose = require('mongoose')
+
+const checkErrors = require('./utils').checkErrors
+const logger = require('../../helpers/logger')
+
+const User = mongoose.model('User')
+
+const validatorsUsers = {
+  usersAdd,
+  usersRemove,
+  usersUpdate
+}
+
+function usersAdd (req, res, next) {
+  req.checkBody('username', 'Should have a valid username').isUserUsernameValid()
+  req.checkBody('password', 'Should have a valid password').isUserPasswordValid()
+
+  logger.debug('Checking usersAdd parameters', { parameters: req.body })
+
+  checkErrors(req, res, function () {
+    User.loadByUsername(req.body.username, function (err, user) {
+      if (err) {
+        logger.error('Error in usersAdd request validator.', { error: err })
+        return res.sendStatus(500)
+      }
+
+      if (user) return res.status(409).send('User already exists.')
+
+      next()
+    })
+  })
+}
+
+function usersRemove (req, res, next) {
+  req.checkParams('id', 'Should have a valid id').notEmpty().isMongoId()
+
+  logger.debug('Checking usersRemove parameters', { parameters: req.params })
+
+  checkErrors(req, res, function () {
+    User.loadById(req.params.id, function (err, user) {
+      if (err) {
+        logger.error('Error in usersRemove request validator.', { error: err })
+        return res.sendStatus(500)
+      }
+
+      if (!user) return res.status(404).send('User not found')
+
+      next()
+    })
+  })
+}
+
+function usersUpdate (req, res, next) {
+  req.checkParams('id', 'Should have a valid id').notEmpty().isMongoId()
+  // Add old password verification
+  req.checkBody('password', 'Should have a valid password').isUserPasswordValid()
+
+  logger.debug('Checking usersUpdate parameters', { parameters: req.body })
+
+  checkErrors(req, res, next)
+}
+
+// ---------------------------------------------------------------------------
+
+module.exports = validatorsUsers
index f6e5b2b38206973cc9d2b26db940f4483f1ca0d1..3741b84c65c4c98ef7e26db287d9b25987704cf5 100644 (file)
@@ -5,7 +5,7 @@ const util = require('util')
 const logger = require('../../helpers/logger')
 
 const validatorsUtils = {
-  checkErrors: checkErrors
+  checkErrors
 }
 
 function checkErrors (req, res, next, statusCode) {
index 3e2af06fb0c253e45c6746f041764e16bbf212fb..76e943e77b2ed363a1385b8176d4475592a6c992 100644 (file)
@@ -4,20 +4,21 @@ const mongoose = require('mongoose')
 
 const checkErrors = require('./utils').checkErrors
 const constants = require('../../initializers/constants')
-const customValidators = require('../../helpers/custom-validators')
+const customVideosValidators = require('../../helpers/custom-validators').videos
 const logger = require('../../helpers/logger')
 
 const Video = mongoose.model('Video')
 
 const validatorsVideos = {
-  videosAdd: videosAdd,
-  videosGet: videosGet,
-  videosRemove: videosRemove,
-  videosSearch: videosSearch
+  videosAdd,
+  videosGet,
+  videosRemove,
+  videosSearch
 }
 
 function videosAdd (req, res, next) {
   req.checkFiles('videofile[0].originalname', 'Should have an input video').notEmpty()
+  // TODO: move to constants and function
   req.checkFiles('videofile[0].mimetype', 'Should have a correct mime type').matches(/video\/(webm)|(mp4)|(ogg)/i)
   req.checkBody('name', 'Should have a valid name').isVideoNameValid()
   req.checkBody('description', 'Should have a valid description').isVideoDescriptionValid()
@@ -33,8 +34,8 @@ function videosAdd (req, res, next) {
         return res.status(400).send('Cannot retrieve metadata of the file.')
       }
 
-      if (!customValidators.isVideoDurationValid(duration)) {
-        return res.status(400).send('Duration of the video file is too big (max: ' + constants.VIDEOS_CONSTRAINTS_FIELDS.DURATION.max + 's).')
+      if (!customVideosValidators.isVideoDurationValid(duration)) {
+        return res.status(400).send('Duration of the video file is too big (max: ' + constants.CONSTRAINTS_FIELDS.VIDEOS.DURATION.max + 's).')
       }
 
       videoFile.duration = duration
@@ -76,6 +77,7 @@ function videosRemove (req, res, next) {
 
       if (!video) return res.status(404).send('Video not found')
       else if (video.isOwned() === false) return res.status(403).send('Cannot remove video of another pod')
+      else if (video.author !== res.locals.oauth.token.user.username) return res.status(403).send('Cannot remove video of another user')
 
       next()
     })
diff --git a/server/models/application.js b/server/models/application.js
new file mode 100644 (file)
index 0000000..452ac42
--- /dev/null
@@ -0,0 +1,31 @@
+const mongoose = require('mongoose')
+
+// ---------------------------------------------------------------------------
+
+const ApplicationSchema = mongoose.Schema({
+  mongoSchemaVersion: {
+    type: Number,
+    default: 0
+  }
+})
+
+ApplicationSchema.statics = {
+  loadMongoSchemaVersion,
+  updateMongoSchemaVersion
+}
+
+mongoose.model('Application', ApplicationSchema)
+
+// ---------------------------------------------------------------------------
+
+function loadMongoSchemaVersion (callback) {
+  return this.findOne({}, { mongoSchemaVersion: 1 }, function (err, data) {
+    const version = data ? data.mongoSchemaVersion : 0
+
+    return callback(err, version)
+  })
+}
+
+function updateMongoSchemaVersion (newVersion, callback) {
+  return this.update({}, { mongoSchemaVersion: newVersion }, callback)
+}
index 45834c5a5374328d238f20a78f68a534d36e9cfa..a1aefa985283ad279a6e925e3a918784b4c8e244 100644 (file)
@@ -11,9 +11,9 @@ const OAuthClientSchema = mongoose.Schema({
 OAuthClientSchema.path('clientSecret').required(true)
 
 OAuthClientSchema.statics = {
-  getByIdAndSecret: getByIdAndSecret,
-  list: list,
-  loadFirstClient: loadFirstClient
+  getByIdAndSecret,
+  list,
+  loadFirstClient
 }
 
 mongoose.model('OAuthClient', OAuthClientSchema)
index f6a814c36fc37989c714c4076ed809b1e6e0048e..5beb47bedb29ab230a1f93b87839764acdad4d23 100644 (file)
@@ -18,9 +18,10 @@ OAuthTokenSchema.path('client').required(true)
 OAuthTokenSchema.path('user').required(true)
 
 OAuthTokenSchema.statics = {
-  getByRefreshTokenAndPopulateClient: getByRefreshTokenAndPopulateClient,
-  getByTokenAndPopulateUser: getByTokenAndPopulateUser,
-  getByRefreshToken: getByRefreshToken
+  getByRefreshTokenAndPopulateClient,
+  getByTokenAndPopulateUser,
+  getByRefreshToken,
+  removeByUserId
 }
 
 mongoose.model('OAuthToken', OAuthTokenSchema)
@@ -53,3 +54,7 @@ function getByTokenAndPopulateUser (bearerToken) {
 function getByRefreshToken (refreshToken) {
   return this.findOne({ refreshToken: refreshToken }).exec()
 }
+
+function removeByUserId (userId, callback) {
+  return this.remove({ user: userId }, callback)
+}
index bf43d7b25c14315672fe3c27031791d6763b4d57..4020a96034bed386feaa9723f74eb336ef685e9f 100644 (file)
@@ -11,7 +11,11 @@ const constants = require('../initializers/constants')
 const PodSchema = mongoose.Schema({
   url: String,
   publicKey: String,
-  score: { type: Number, max: constants.FRIEND_SCORE.MAX }
+  score: { type: Number, max: constants.FRIEND_SCORE.MAX },
+  createdDate: {
+    type: Date,
+    default: Date.now
+  }
 })
 
 // TODO: set options (TLD...)
@@ -19,16 +23,19 @@ PodSchema.path('url').validate(validator.isURL)
 PodSchema.path('publicKey').required(true)
 PodSchema.path('score').validate(function (value) { return !isNaN(value) })
 
+PodSchema.methods = {
+  toFormatedJSON
+}
+
 PodSchema.statics = {
-  countAll: countAll,
-  incrementScores: incrementScores,
-  list: list,
-  listAllIds: listAllIds,
-  listOnlyUrls: listOnlyUrls,
-  listBadPods: listBadPods,
-  load: load,
-  loadByUrl: loadByUrl,
-  removeAll: removeAll
+  countAll,
+  incrementScores,
+  list,
+  listAllIds,
+  listBadPods,
+  load,
+  loadByUrl,
+  removeAll
 }
 
 PodSchema.pre('save', function (next) {
@@ -46,6 +53,19 @@ PodSchema.pre('save', function (next) {
 
 const Pod = mongoose.model('Pod', PodSchema)
 
+// ------------------------------ METHODS ------------------------------
+
+function toFormatedJSON () {
+  const json = {
+    id: this._id,
+    url: this.url,
+    score: this.score,
+    createdDate: this.createdDate
+  }
+
+  return json
+}
+
 // ------------------------------ Statics ------------------------------
 
 function countAll (callback) {
@@ -69,10 +89,6 @@ function listAllIds (callback) {
   })
 }
 
-function listOnlyUrls (callback) {
-  return this.find({}, { _id: 0, url: 1 }, callback)
-}
-
 function listBadPods (callback) {
   return this.find({ score: 0 }, callback)
 }
index 4d521919a11185fd4cb3c522bd1f99be1e6061f3..2d1c5af15802804aa2743970a7e0f3cd0459ad84 100644 (file)
@@ -14,19 +14,22 @@ const Pod = mongoose.model('Pod')
 const Video = mongoose.model('Video')
 
 let timer = null
+let lastRequestTimestamp = 0
 
 // ---------------------------------------------------------------------------
 
 const RequestSchema = mongoose.Schema({
   request: mongoose.Schema.Types.Mixed,
-  to: [ { type: mongoose.Schema.Types.ObjectId, ref: 'users' } ]
+  to: [ { type: mongoose.Schema.Types.ObjectId, ref: 'Pod' } ]
 })
 
 RequestSchema.statics = {
   activate,
   deactivate,
   flush,
-  forceSend
+  forceSend,
+  list,
+  remainingMilliSeconds
 }
 
 RequestSchema.pre('save', function (next) {
@@ -53,12 +56,19 @@ mongoose.model('Request', RequestSchema)
 
 function activate () {
   logger.info('Requests scheduler activated.')
-  timer = setInterval(makeRequests.bind(this), constants.INTERVAL)
+  lastRequestTimestamp = Date.now()
+
+  const self = this
+  timer = setInterval(function () {
+    lastRequestTimestamp = Date.now()
+    makeRequests.call(self)
+  }, constants.REQUESTS_INTERVAL)
 }
 
 function deactivate () {
   logger.info('Requests scheduler deactivated.')
   clearInterval(timer)
+  timer = null
 }
 
 function flush () {
@@ -72,6 +82,16 @@ function forceSend () {
   makeRequests.call(this)
 }
 
+function list (callback) {
+  this.find({ }, callback)
+}
+
+function remainingMilliSeconds () {
+  if (timer === null) return -1
+
+  return constants.REQUESTS_INTERVAL - (Date.now() - lastRequestTimestamp)
+}
+
 // ---------------------------------------------------------------------------
 
 // Make a requests to friends of a certain type
@@ -91,7 +111,13 @@ function makeRequest (toPod, requestsToMake, callback) {
   // The function fire some useful callbacks
   requests.makeSecureRequest(params, function (err, res) {
     if (err || (res.statusCode !== 200 && res.statusCode !== 201 && res.statusCode !== 204)) {
-      logger.error('Error sending secure request to %s pod.', toPod.url, { error: err || new Error('Status code not 20x') })
+      logger.error(
+        'Error sending secure request to %s pod.',
+        toPod.url,
+        {
+          error: err || new Error('Status code not 20x : ' + res.statusCode)
+        }
+      )
 
       return callback(false)
     }
@@ -148,19 +174,14 @@ function makeRequests () {
           return callbackEach()
         }
 
-        // Maybe the pod is not our friend anymore so simply remove them
+        // Maybe the pod is not our friend anymore so simply remove it
         if (!toPod) {
+          logger.info('Removing %d requests of unexisting pod %s.', requestToMake.ids.length, toPodId)
           removePodOf.call(self, requestToMake.ids, toPodId)
           return callbackEach()
         }
 
         makeRequest(toPod, requestToMake.datas, function (success) {
-          if (err) {
-            logger.error('Errors when sent request to %s.', toPod.url, { error: err })
-            // Do not stop the process just for one error
-            return callbackEach()
-          }
-
           if (success === true) {
             logger.debug('Removing requests for %s pod.', toPodId, { requestsIds: requestToMake.ids })
 
index 14ffecbff1ca89ae5139a26946d1b54344fc5e12..a19de7072cd77ae5e881297e775f25c829ab45aa 100644 (file)
@@ -1,28 +1,98 @@
 const mongoose = require('mongoose')
 
+const customUsersValidators = require('../helpers/custom-validators').users
+const modelUtils = require('./utils')
+const peertubeCrypto = require('../helpers/peertube-crypto')
+
+const OAuthToken = mongoose.model('OAuthToken')
+
 // ---------------------------------------------------------------------------
 
 const UserSchema = mongoose.Schema({
+  createdDate: {
+    type: Date,
+    default: Date.now
+  },
   password: String,
-  username: String
+  username: String,
+  role: String
 })
 
-UserSchema.path('password').required(true)
-UserSchema.path('username').required(true)
+UserSchema.path('password').required(customUsersValidators.isUserPasswordValid)
+UserSchema.path('username').required(customUsersValidators.isUserUsernameValid)
+UserSchema.path('role').validate(customUsersValidators.isUserRoleValid)
+
+UserSchema.methods = {
+  isPasswordMatch,
+  toFormatedJSON
+}
 
 UserSchema.statics = {
-  getByUsernameAndPassword: getByUsernameAndPassword,
-  list: list
+  countTotal,
+  getByUsername,
+  list,
+  listForApi,
+  loadById,
+  loadByUsername
 }
 
+UserSchema.pre('save', function (next) {
+  const user = this
+
+  peertubeCrypto.cryptPassword(this.password, function (err, hash) {
+    if (err) return next(err)
+
+    user.password = hash
+
+    return next()
+  })
+})
+
+UserSchema.pre('remove', function (next) {
+  const user = this
+
+  OAuthToken.removeByUserId(user._id, next)
+})
+
 mongoose.model('User', UserSchema)
 
-// ---------------------------------------------------------------------------
+// ------------------------------ METHODS ------------------------------
+
+function isPasswordMatch (password, callback) {
+  return peertubeCrypto.comparePassword(password, this.password, callback)
+}
+
+function toFormatedJSON () {
+  return {
+    id: this._id,
+    username: this.username,
+    role: this.role,
+    createdDate: this.createdDate
+  }
+}
+// ------------------------------ STATICS ------------------------------
+
+function countTotal (callback) {
+  return this.count(callback)
+}
+
+function getByUsername (username) {
+  return this.findOne({ username: username })
+}
 
 function list (callback) {
   return this.find(callback)
 }
 
-function getByUsernameAndPassword (username, password) {
-  return this.findOne({ username: username, password: password })
+function listForApi (start, count, sort, callback) {
+  const query = {}
+  return modelUtils.listForApiWithCount.call(this, query, start, count, sort, callback)
+}
+
+function loadById (id, callback) {
+  return this.findById(id, callback)
+}
+
+function loadByUsername (username, callback) {
+  return this.findOne({ username: username }, callback)
 }
diff --git a/server/models/utils.js b/server/models/utils.js
new file mode 100644 (file)
index 0000000..e798aab
--- /dev/null
@@ -0,0 +1,30 @@
+'use strict'
+
+const parallel = require('async/parallel')
+
+const utils = {
+  listForApiWithCount
+}
+
+function listForApiWithCount (query, start, count, sort, callback) {
+  const self = this
+
+  parallel([
+    function (asyncCallback) {
+      self.find(query).skip(start).limit(count).sort(sort).exec(asyncCallback)
+    },
+    function (asyncCallback) {
+      self.count(query, asyncCallback)
+    }
+  ], function (err, results) {
+    if (err) return callback(err)
+
+    const data = results[0]
+    const total = results[1]
+    return callback(null, data, total)
+  })
+}
+
+// ---------------------------------------------------------------------------
+
+module.exports = utils
index 14e0df6f26cb96873264c276ffe9781782992a04..7d073cffa97cb33b43b03246909f7a6fe4e58b53 100644 (file)
@@ -11,8 +11,9 @@ const magnet = require('magnet-uri')
 const mongoose = require('mongoose')
 
 const constants = require('../initializers/constants')
-const customValidators = require('../helpers/custom-validators')
+const customVideosValidators = require('../helpers/custom-validators').videos
 const logger = require('../helpers/logger')
+const modelUtils = require('./utils')
 const utils = require('../helpers/utils')
 
 const http = config.get('webserver.https') === true ? 'https' : 'http'
@@ -42,34 +43,35 @@ const VideoSchema = mongoose.Schema({
   }
 })
 
-VideoSchema.path('name').validate(customValidators.isVideoNameValid)
-VideoSchema.path('description').validate(customValidators.isVideoDescriptionValid)
-VideoSchema.path('magnetUri').validate(customValidators.isVideoMagnetUriValid)
-VideoSchema.path('podUrl').validate(customValidators.isVideoPodUrlValid)
-VideoSchema.path('author').validate(customValidators.isVideoAuthorValid)
-VideoSchema.path('duration').validate(customValidators.isVideoDurationValid)
+VideoSchema.path('name').validate(customVideosValidators.isVideoNameValid)
+VideoSchema.path('description').validate(customVideosValidators.isVideoDescriptionValid)
+VideoSchema.path('magnetUri').validate(customVideosValidators.isVideoMagnetUriValid)
+VideoSchema.path('podUrl').validate(customVideosValidators.isVideoPodUrlValid)
+VideoSchema.path('author').validate(customVideosValidators.isVideoAuthorValid)
+VideoSchema.path('duration').validate(customVideosValidators.isVideoDurationValid)
 // The tumbnail can be the path or the data in base 64
 // The pre save hook will convert the base 64 data in a file on disk and replace the thumbnail key by the filename
 VideoSchema.path('thumbnail').validate(function (value) {
-  return customValidators.isVideoThumbnailValid(value) || customValidators.isVideoThumbnail64Valid(value)
+  return customVideosValidators.isVideoThumbnailValid(value) || customVideosValidators.isVideoThumbnail64Valid(value)
 })
-VideoSchema.path('tags').validate(customValidators.isVideoTagsValid)
+VideoSchema.path('tags').validate(customVideosValidators.isVideoTagsValid)
 
 VideoSchema.methods = {
-  isOwned: isOwned,
-  toFormatedJSON: toFormatedJSON,
-  toRemoteJSON: toRemoteJSON
+  isOwned,
+  toFormatedJSON,
+  toRemoteJSON
 }
 
 VideoSchema.statics = {
-  getDurationFromFile: getDurationFromFile,
-  list: list,
-  listByUrlAndMagnet: listByUrlAndMagnet,
-  listByUrls: listByUrls,
-  listOwned: listOwned,
-  listRemotes: listRemotes,
-  load: load,
-  search: search
+  getDurationFromFile,
+  listForApi,
+  listByUrlAndMagnet,
+  listByUrls,
+  listOwned,
+  listOwnedByAuthor,
+  listRemotes,
+  load,
+  search
 }
 
 VideoSchema.pre('remove', function (next) {
@@ -101,8 +103,8 @@ VideoSchema.pre('save', function (next) {
   const tasks = []
 
   if (video.isOwned()) {
-    const videoPath = pathUtils.join(uploadsDir, video.filename)
-    this.podUrl = http + '://' + host + ':' + port
+    const videoPath = pathUtils.join(constants.CONFIG.STORAGE.UPLOAD_DIR, video.filename)
+    this.podUrl = constants.CONFIG.WEBSERVER.URL
 
     tasks.push(
       // TODO: refractoring
@@ -174,7 +176,7 @@ function toRemoteJSON (callback) {
   const self = this
 
   // Convert thumbnail to base64
-  fs.readFile(pathUtils.join(thumbnailsDir, this.thumbnail), function (err, thumbnailData) {
+  fs.readFile(pathUtils.join(constants.CONFIG.STORAGE.THUMBNAILS_DIR, this.thumbnail), function (err, thumbnailData) {
     if (err) {
       logger.error('Cannot read the thumbnail of the video')
       return callback(err)
@@ -207,9 +209,9 @@ function getDurationFromFile (videoPath, callback) {
   })
 }
 
-function list (start, count, sort, callback) {
+function listForApi (start, count, sort, callback) {
   const query = {}
-  return findWithCount.call(this, query, start, count, sort, callback)
+  return modelUtils.listForApiWithCount.call(this, query, start, count, sort, callback)
 }
 
 function listByUrlAndMagnet (fromUrl, magnetUri, callback) {
@@ -225,6 +227,10 @@ function listOwned (callback) {
   this.find({ filename: { $ne: null } }, callback)
 }
 
+function listOwnedByAuthor (author, callback) {
+  this.find({ filename: { $ne: null }, author: author }, callback)
+}
+
 function listRemotes (callback) {
   this.find({ filename: null }, callback)
 }
@@ -242,36 +248,17 @@ function search (value, field, start, count, sort, callback) {
     query[field] = new RegExp(value)
   }
 
-  findWithCount.call(this, query, start, count, sort, callback)
+  modelUtils.listForApiWithCount.call(this, query, start, count, sort, callback)
 }
 
 // ---------------------------------------------------------------------------
 
-function findWithCount (query, start, count, sort, callback) {
-  const self = this
-
-  parallel([
-    function (asyncCallback) {
-      self.find(query).skip(start).limit(count).sort(sort).exec(asyncCallback)
-    },
-    function (asyncCallback) {
-      self.count(query, asyncCallback)
-    }
-  ], function (err, results) {
-    if (err) return callback(err)
-
-    const videos = results[0]
-    const totalVideos = results[1]
-    return callback(null, videos, totalVideos)
-  })
-}
-
 function removeThumbnail (video, callback) {
-  fs.unlink(thumbnailsDir + video.thumbnail, callback)
+  fs.unlink(constants.CONFIG.STORAGE.THUMBNAILS_DIR + video.thumbnail, callback)
 }
 
 function removeFile (video, callback) {
-  fs.unlink(uploadsDir + video.filename, callback)
+  fs.unlink(constants.CONFIG.STORAGE.UPLOAD_DIR + video.filename, callback)
 }
 
 // Maybe the torrent is not seeded, but we catch the error to don't stop the removing process
@@ -288,7 +275,7 @@ function createThumbnail (videoPath, callback) {
     })
     .thumbnail({
       count: 1,
-      folder: thumbnailsDir,
+      folder: constants.CONFIG.STORAGE.THUMBNAILS_DIR,
       size: constants.THUMBNAILS_SIZE,
       filename: filename
     })
@@ -300,7 +287,7 @@ function generateThumbnailFromBase64 (data, callback) {
     if (err) return callback(err)
 
     const thumbnailName = randomString + '.jpg'
-    fs.writeFile(thumbnailsDir + thumbnailName, data, { encoding: 'base64' }, function (err) {
+    fs.writeFile(constants.CONFIG.STORAGE.THUMBNAILS_DIR + thumbnailName, data, { encoding: 'base64' }, function (err) {
       if (err) return callback(err)
 
       return callback(null, thumbnailName)
diff --git a/server/tests/api/check-params.js b/server/tests/api/check-params.js
new file mode 100644 (file)
index 0000000..57b5ca0
--- /dev/null
@@ -0,0 +1,746 @@
+'use strict'
+
+const chai = require('chai')
+const expect = chai.expect
+const pathUtils = require('path')
+const request = require('supertest')
+const series = require('async/series')
+
+const loginUtils = require('../utils/login')
+const requestsUtils = require('../utils/requests')
+const serversUtils = require('../utils/servers')
+const usersUtils = require('../utils/users')
+
+describe('Test parameters validator', function () {
+  let server = null
+  let userAccessToken = null
+
+  // ---------------------------------------------------------------
+
+  before(function (done) {
+    this.timeout(20000)
+
+    series([
+      function (next) {
+        serversUtils.flushTests(next)
+      },
+      function (next) {
+        serversUtils.runServer(1, function (server1) {
+          server = server1
+
+          next()
+        })
+      },
+      function (next) {
+        loginUtils.loginAndGetAccessToken(server, function (err, token) {
+          if (err) throw err
+          server.accessToken = token
+
+          next()
+        })
+      }
+    ], done)
+  })
+
+  describe('Of the pods API', function () {
+    const path = '/api/v1/pods/'
+
+    describe('When making friends', function () {
+      let userAccessToken = null
+
+      before(function (done) {
+        usersUtils.createUser(server.url, server.accessToken, 'user1', 'password', function () {
+          server.user = {
+            username: 'user1',
+            password: 'password'
+          }
+
+          loginUtils.loginAndGetAccessToken(server, function (err, accessToken) {
+            if (err) throw err
+
+            userAccessToken = accessToken
+
+            done()
+          })
+        })
+      })
+
+      describe('When making friends', function () {
+        const body = {
+          urls: [ 'http://localhost:9002' ]
+        }
+
+        it('Should fail without urls', function (done) {
+          request(server.url)
+            .post(path + '/makefriends')
+            .set('Authorization', 'Bearer ' + server.accessToken)
+            .set('Accept', 'application/json')
+            .expect(400, done)
+        })
+
+        it('Should fail with urls is not an array', function (done) {
+          request(server.url)
+            .post(path + '/makefriends')
+            .send({ urls: 'http://localhost:9002' })
+            .set('Authorization', 'Bearer ' + server.accessToken)
+            .set('Accept', 'application/json')
+            .expect(400, done)
+        })
+
+        it('Should fail if the array is not composed by urls', function (done) {
+          request(server.url)
+            .post(path + '/makefriends')
+            .send({ urls: [ 'http://localhost:9002', 'localhost:coucou' ] })
+            .set('Authorization', 'Bearer ' + server.accessToken)
+            .set('Accept', 'application/json')
+            .expect(400, done)
+        })
+
+        it('Should fail if urls are not unique', function (done) {
+          request(server.url)
+            .post(path + '/makefriends')
+            .send({ urls: [ 'http://localhost:9002', 'http://localhost:9002' ] })
+            .set('Authorization', 'Bearer ' + server.accessToken)
+            .set('Accept', 'application/json')
+            .expect(400, done)
+        })
+
+        it('Should fail with a invalid token', function (done) {
+          request(server.url)
+            .post(path + '/makefriends')
+            .send(body)
+            .set('Authorization', 'Bearer faketoken')
+            .set('Accept', 'application/json')
+            .expect(401, done)
+        })
+
+        it('Should fail if the user is not an administrator', function (done) {
+          request(server.url)
+            .post(path + '/makefriends')
+            .send(body)
+            .set('Authorization', 'Bearer ' + userAccessToken)
+            .set('Accept', 'application/json')
+            .expect(403, done)
+        })
+      })
+
+      describe('When quitting friends', function () {
+        it('Should fail with a invalid token', function (done) {
+          request(server.url)
+            .get(path + '/quitfriends')
+            .query({ start: 'hello' })
+            .set('Authorization', 'Bearer faketoken')
+            .set('Accept', 'application/json')
+            .expect(401, done)
+        })
+
+        it('Should fail if the user is not an administrator', function (done) {
+          request(server.url)
+            .get(path + '/quitfriends')
+            .query({ start: 'hello' })
+            .set('Authorization', 'Bearer ' + userAccessToken)
+            .set('Accept', 'application/json')
+            .expect(403, done)
+        })
+      })
+    })
+
+    describe('When adding a pod', function () {
+      it('Should fail with nothing', function (done) {
+        const data = {}
+        requestsUtils.makePostBodyRequest(server.url, path, null, data, done)
+      })
+
+      it('Should fail without public key', function (done) {
+        const data = {
+          url: 'http://coucou.com'
+        }
+        requestsUtils.makePostBodyRequest(server.url, path, null, data, done)
+      })
+
+      it('Should fail without an url', function (done) {
+        const data = {
+          publicKey: 'mysuperpublickey'
+        }
+        requestsUtils.makePostBodyRequest(server.url, path, null, data, done)
+      })
+
+      it('Should fail with an incorrect url', function (done) {
+        const data = {
+          url: 'coucou.com',
+          publicKey: 'mysuperpublickey'
+        }
+        requestsUtils.makePostBodyRequest(server.url, path, null, data, function () {
+          data.url = 'http://coucou'
+          requestsUtils.makePostBodyRequest(server.url, path, null, data, function () {
+            data.url = 'coucou'
+            requestsUtils.makePostBodyRequest(server.url, path, null, data, done)
+          })
+        })
+      })
+
+      it('Should succeed with the correct parameters', function (done) {
+        const data = {
+          url: 'http://coucou.com',
+          publicKey: 'mysuperpublickey'
+        }
+        requestsUtils.makePostBodyRequest(server.url, path, null, data, done, 200)
+      })
+    })
+  })
+
+  describe('Of the videos API', function () {
+    const path = '/api/v1/videos/'
+
+    describe('When listing a video', function () {
+      it('Should fail with a bad start pagination', function (done) {
+        request(server.url)
+          .get(path)
+          .query({ start: 'hello' })
+          .set('Accept', 'application/json')
+          .expect(400, done)
+      })
+
+      it('Should fail with a bad count pagination', function (done) {
+        request(server.url)
+          .get(path)
+          .query({ count: 'hello' })
+          .set('Accept', 'application/json')
+          .expect(400, done)
+      })
+
+      it('Should fail with an incorrect sort', function (done) {
+        request(server.url)
+          .get(path)
+          .query({ sort: 'hello' })
+          .set('Accept', 'application/json')
+          .expect(400, done)
+      })
+    })
+
+    describe('When searching a video', function () {
+      it('Should fail with nothing', function (done) {
+        request(server.url)
+          .get(pathUtils.join(path, 'search'))
+          .set('Accept', 'application/json')
+          .expect(400, done)
+      })
+
+      it('Should fail with a bad start pagination', function (done) {
+        request(server.url)
+          .get(pathUtils.join(path, 'search', 'test'))
+          .query({ start: 'hello' })
+          .set('Accept', 'application/json')
+          .expect(400, done)
+      })
+
+      it('Should fail with a bad count pagination', function (done) {
+        request(server.url)
+          .get(pathUtils.join(path, 'search', 'test'))
+          .query({ count: 'hello' })
+          .set('Accept', 'application/json')
+          .expect(400, done)
+      })
+
+      it('Should fail with an incorrect sort', function (done) {
+        request(server.url)
+          .get(pathUtils.join(path, 'search', 'test'))
+          .query({ sort: 'hello' })
+          .set('Accept', 'application/json')
+          .expect(400, done)
+      })
+    })
+
+    describe('When adding a video', function () {
+      it('Should fail with nothing', function (done) {
+        const data = {}
+        const attach = {}
+        requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done)
+      })
+
+      it('Should fail without name', function (done) {
+        const data = {
+          description: 'my super description',
+          tags: [ 'tag1', 'tag2' ]
+        }
+        const attach = {
+          'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
+        }
+        requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done)
+      })
+
+      it('Should fail with a long name', function (done) {
+        const data = {
+          name: 'My very very very very very very very very very very very very very very very very long name',
+          description: 'my super description',
+          tags: [ 'tag1', 'tag2' ]
+        }
+        const attach = {
+          'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
+        }
+        requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done)
+      })
+
+      it('Should fail without description', function (done) {
+        const data = {
+          name: 'my super name',
+          tags: [ 'tag1', 'tag2' ]
+        }
+        const attach = {
+          'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
+        }
+        requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done)
+      })
+
+      it('Should fail with a long description', function (done) {
+        const data = {
+          name: 'my super name',
+          description: 'my super description which is very very very very very very very very very very very very very very' +
+                       'very very very very very very very very very very very very very very very very very very very very very' +
+                       'very very very very very very very very very very very very very very very long',
+          tags: [ 'tag1', 'tag2' ]
+        }
+        const attach = {
+          'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
+        }
+        requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done)
+      })
+
+      it('Should fail without tags', function (done) {
+        const data = {
+          name: 'my super name',
+          description: 'my super description'
+        }
+        const attach = {
+          'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
+        }
+        requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done)
+      })
+
+      it('Should fail with too many tags', function (done) {
+        const data = {
+          name: 'my super name',
+          description: 'my super description',
+          tags: [ 'tag1', 'tag2', 'tag3', 'tag4' ]
+        }
+        const attach = {
+          'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
+        }
+        requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done)
+      })
+
+      it('Should fail with not enough tags', function (done) {
+        const data = {
+          name: 'my super name',
+          description: 'my super description',
+          tags: [ ]
+        }
+        const attach = {
+          'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
+        }
+        requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done)
+      })
+
+      it('Should fail with a tag length too low', function (done) {
+        const data = {
+          name: 'my super name',
+          description: 'my super description',
+          tags: [ 'tag1', 't' ]
+        }
+        const attach = {
+          'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
+        }
+        requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done)
+      })
+
+      it('Should fail with a tag length too big', function (done) {
+        const data = {
+          name: 'my super name',
+          description: 'my super description',
+          tags: [ 'mysupertagtoolong', 'tag1' ]
+        }
+        const attach = {
+          'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
+        }
+        requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done)
+      })
+
+      it('Should fail with malformed tags', function (done) {
+        const data = {
+          name: 'my super name',
+          description: 'my super description',
+          tags: [ 'my tag' ]
+        }
+        const attach = {
+          'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
+        }
+        requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done)
+      })
+
+      it('Should fail without an input file', function (done) {
+        const data = {
+          name: 'my super name',
+          description: 'my super description',
+          tags: [ 'tag1', 'tag2' ]
+        }
+        const attach = {}
+        requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done)
+      })
+
+      it('Should fail without an incorrect input file', function (done) {
+        const data = {
+          name: 'my super name',
+          description: 'my super description',
+          tags: [ 'tag1', 'tag2' ]
+        }
+        const attach = {
+          'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short_fake.webm')
+        }
+        requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done)
+      })
+
+      it('Should fail with a too big duration', function (done) {
+        const data = {
+          name: 'my super name',
+          description: 'my super description',
+          tags: [ 'tag1', 'tag2' ]
+        }
+        const attach = {
+          'videofile': pathUtils.join(__dirname, 'fixtures', 'video_too_long.webm')
+        }
+        requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done)
+      })
+
+      it('Should succeed with the correct parameters', function (done) {
+        const data = {
+          name: 'my super name',
+          description: 'my super description',
+          tags: [ 'tag1', 'tag2' ]
+        }
+        const attach = {
+          'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
+        }
+        requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, function () {
+          attach.videofile = pathUtils.join(__dirname, 'fixtures', 'video_short.mp4')
+          requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, function () {
+            attach.videofile = pathUtils.join(__dirname, 'fixtures', 'video_short.ogv')
+            requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done, 204)
+          }, false)
+        }, false)
+      })
+    })
+
+    describe('When getting a video', function () {
+      it('Should return the list of the videos with nothing', function (done) {
+        request(server.url)
+          .get(path)
+          .set('Accept', 'application/json')
+          .expect(200)
+          .expect('Content-Type', /json/)
+          .end(function (err, res) {
+            if (err) throw err
+
+            expect(res.body.data).to.be.an('array')
+            expect(res.body.data.length).to.equal(3)
+
+            done()
+          })
+      })
+
+      it('Should fail without a mongodb id', function (done) {
+        request(server.url)
+          .get(path + 'coucou')
+          .set('Accept', 'application/json')
+          .expect(400, done)
+      })
+
+      it('Should return 404 with an incorrect video', function (done) {
+        request(server.url)
+          .get(path + '123456789012345678901234')
+          .set('Accept', 'application/json')
+          .expect(404, done)
+      })
+
+      it('Should succeed with the correct parameters')
+    })
+
+    describe('When removing a video', function () {
+      it('Should have 404 with nothing', function (done) {
+        request(server.url)
+          .delete(path)
+          .set('Authorization', 'Bearer ' + server.accessToken)
+          .expect(400, done)
+      })
+
+      it('Should fail without a mongodb id', function (done) {
+        request(server.url)
+          .delete(path + 'hello')
+          .set('Authorization', 'Bearer ' + server.accessToken)
+          .expect(400, done)
+      })
+
+      it('Should fail with a video which does not exist', function (done) {
+        request(server.url)
+          .delete(path + '123456789012345678901234')
+          .set('Authorization', 'Bearer ' + server.accessToken)
+          .expect(404, done)
+      })
+
+      it('Should fail with a video of another user')
+
+      it('Should fail with a video of another pod')
+
+      it('Should succeed with the correct parameters')
+    })
+  })
+
+  describe('Of the users API', function () {
+    const path = '/api/v1/users/'
+    let userId = null
+
+    describe('When listing users', function () {
+      it('Should fail with a bad start pagination', function (done) {
+        request(server.url)
+          .get(path)
+          .query({ start: 'hello' })
+          .set('Accept', 'application/json')
+          .expect(400, done)
+      })
+
+      it('Should fail with a bad count pagination', function (done) {
+        request(server.url)
+          .get(path)
+          .query({ count: 'hello' })
+          .set('Accept', 'application/json')
+          .expect(400, done)
+      })
+
+      it('Should fail with an incorrect sort', function (done) {
+        request(server.url)
+          .get(path)
+          .query({ sort: 'hello' })
+          .set('Accept', 'application/json')
+          .expect(400, done)
+      })
+    })
+
+    describe('When adding a new user', function () {
+      it('Should fail with a too small username', function (done) {
+        const data = {
+          username: 'ji',
+          password: 'mysuperpassword'
+        }
+
+        requestsUtils.makePostBodyRequest(server.url, path, server.accessToken, data, done)
+      })
+
+      it('Should fail with a too long username', function (done) {
+        const data = {
+          username: 'mysuperusernamewhichisverylong',
+          password: 'mysuperpassword'
+        }
+
+        requestsUtils.makePostBodyRequest(server.url, path, server.accessToken, data, done)
+      })
+
+      it('Should fail with an incorrect username', function (done) {
+        const data = {
+          username: 'my username',
+          password: 'mysuperpassword'
+        }
+
+        requestsUtils.makePostBodyRequest(server.url, path, server.accessToken, data, done)
+      })
+
+      it('Should fail with a too small password', function (done) {
+        const data = {
+          username: 'myusername',
+          password: 'bla'
+        }
+
+        requestsUtils.makePostBodyRequest(server.url, path, server.accessToken, data, done)
+      })
+
+      it('Should fail with a too long password', function (done) {
+        const data = {
+          username: 'myusername',
+          password: 'my super long password which is very very very very very very very very very very very very very very' +
+                    'very very very very very very very very very very very very very very very veryv very very very very' +
+                    'very very very very very very very very very very very very very very very very very very very very long'
+        }
+
+        requestsUtils.makePostBodyRequest(server.url, path, server.accessToken, data, done)
+      })
+
+      it('Should fail with an non authenticated user', function (done) {
+        const data = {
+          username: 'myusername',
+          password: 'my super password'
+        }
+
+        requestsUtils.makePostBodyRequest(server.url, path, 'super token', data, done, 401)
+      })
+
+      it('Should fail if we add a user with the same username', function (done) {
+        const data = {
+          username: 'user1',
+          password: 'my super password'
+        }
+
+        requestsUtils.makePostBodyRequest(server.url, path, server.accessToken, data, done, 409)
+      })
+
+      it('Should succeed with the correct params', function (done) {
+        const data = {
+          username: 'user2',
+          password: 'my super password'
+        }
+
+        requestsUtils.makePostBodyRequest(server.url, path, server.accessToken, data, done, 204)
+      })
+
+      it('Should fail with a non admin user', function (done) {
+        server.user = {
+          username: 'user1',
+          password: 'password'
+        }
+
+        loginUtils.loginAndGetAccessToken(server, function (err, accessToken) {
+          if (err) throw err
+
+          userAccessToken = accessToken
+
+          const data = {
+            username: 'user3',
+            password: 'my super password'
+          }
+
+          requestsUtils.makePostBodyRequest(server.url, path, userAccessToken, data, done, 403)
+        })
+      })
+    })
+
+    describe('When updating a user', function () {
+      before(function (done) {
+        usersUtils.getUsersList(server.url, function (err, res) {
+          if (err) throw err
+
+          userId = res.body.data[1].id
+          done()
+        })
+      })
+
+      it('Should fail with a too small password', function (done) {
+        const data = {
+          password: 'bla'
+        }
+
+        requestsUtils.makePutBodyRequest(server.url, path + userId, userAccessToken, data, done)
+      })
+
+      it('Should fail with a too long password', function (done) {
+        const data = {
+          password: 'my super long password which is very very very very very very very very very very very very very very' +
+                    'very very very very very very very very very very very very very very very veryv very very very very' +
+                    'very very very very very very very very very very very very very very very very very very very very long'
+        }
+
+        requestsUtils.makePutBodyRequest(server.url, path + userId, userAccessToken, data, done)
+      })
+
+      it('Should fail with an non authenticated user', function (done) {
+        const data = {
+          password: 'my super password'
+        }
+
+        requestsUtils.makePutBodyRequest(server.url, path + userId, 'super token', data, done, 401)
+      })
+
+      it('Should succeed with the correct params', function (done) {
+        const data = {
+          password: 'my super password'
+        }
+
+        requestsUtils.makePutBodyRequest(server.url, path + userId, userAccessToken, data, done, 204)
+      })
+    })
+
+    describe('When getting my information', function () {
+      it('Should fail with a non authenticated user', function (done) {
+        request(server.url)
+          .get(path + 'me')
+          .set('Authorization', 'Bearer faketoken')
+          .set('Accept', 'application/json')
+          .expect(401, done)
+      })
+
+      it('Should success with the correct parameters', function (done) {
+        request(server.url)
+          .get(path + 'me')
+          .set('Authorization', 'Bearer ' + userAccessToken)
+          .set('Accept', 'application/json')
+          .expect(200, done)
+      })
+    })
+
+    describe('When removing an user', function () {
+      it('Should fail with an incorrect id', function (done) {
+        request(server.url)
+          .delete(path + 'bla-bla')
+          .set('Authorization', 'Bearer ' + server.accessToken)
+          .expect(400, done)
+      })
+
+      it('Should return 404 with a non existing id', function (done) {
+        request(server.url)
+          .delete(path + '579f982228c99c221d8092b8')
+          .set('Authorization', 'Bearer ' + server.accessToken)
+          .expect(404, done)
+      })
+    })
+  })
+
+  describe('Of the remote videos API', function () {
+    describe('When making a secure request', function () {
+      it('Should check a secure request')
+    })
+
+    describe('When adding a video', function () {
+      it('Should check when adding a video')
+    })
+
+    describe('When removing a video', function () {
+      it('Should check when removing a video')
+    })
+  })
+
+  describe('Of the requests API', function () {
+    const path = '/api/v1/requests/stats'
+
+    it('Should fail with an non authenticated user', function (done) {
+      request(server.url)
+        .get(path)
+        .set('Accept', 'application/json')
+        .expect(401, done)
+    })
+
+    it('Should fail with a non admin user', function (done) {
+      request(server.url)
+        .get(path)
+        .set('Authorization', 'Bearer ' + userAccessToken)
+        .set('Accept', 'application/json')
+        .expect(403, done)
+    })
+  })
+
+  after(function (done) {
+    process.kill(-server.app.pid)
+
+    // Keep the logs if the test failed
+    if (this.ok) {
+      serversUtils.flushTests(done)
+    } else {
+      done()
+    }
+  })
+})
diff --git a/server/tests/api/checkParams.js b/server/tests/api/checkParams.js
deleted file mode 100644 (file)
index c1ba9c2..0000000
+++ /dev/null
@@ -1,456 +0,0 @@
-'use strict'
-
-const chai = require('chai')
-const expect = chai.expect
-const pathUtils = require('path')
-const request = require('supertest')
-const series = require('async/series')
-
-const utils = require('./utils')
-
-describe('Test parameters validator', function () {
-  let server = null
-
-  function makePostRequest (path, token, fields, attaches, done, fail) {
-    let statusCode = 400
-    if (fail !== undefined && fail === false) statusCode = 204
-
-    const req = request(server.url)
-      .post(path)
-      .set('Accept', 'application/json')
-
-    if (token) req.set('Authorization', 'Bearer ' + token)
-
-    Object.keys(fields).forEach(function (field) {
-      const value = fields[field]
-
-      if (Array.isArray(value)) {
-        for (let i = 0; i < value.length; i++) {
-          req.field(field + '[' + i + ']', value[i])
-        }
-      } else {
-        req.field(field, value)
-      }
-    })
-
-    Object.keys(attaches).forEach(function (attach) {
-      const value = attaches[attach]
-      req.attach(attach, value)
-    })
-
-    req.expect(statusCode, done)
-  }
-
-  function makePostBodyRequest (path, fields, done, fail) {
-    let statusCode = 400
-    if (fail !== undefined && fail === false) statusCode = 200
-
-    request(server.url)
-      .post(path)
-      .set('Accept', 'application/json')
-      .send(fields)
-      .expect(statusCode, done)
-  }
-
-  // ---------------------------------------------------------------
-
-  before(function (done) {
-    this.timeout(20000)
-
-    series([
-      function (next) {
-        utils.flushTests(next)
-      },
-      function (next) {
-        utils.runServer(1, function (server1) {
-          server = server1
-
-          next()
-        })
-      },
-      function (next) {
-        utils.loginAndGetAccessToken(server, function (err, token) {
-          if (err) throw err
-          server.accessToken = token
-
-          next()
-        })
-      }
-    ], done)
-  })
-
-  describe('Of the pods API', function () {
-    const path = '/api/v1/pods/'
-
-    describe('When adding a pod', function () {
-      it('Should fail with nothing', function (done) {
-        const data = {}
-        makePostBodyRequest(path, data, done)
-      })
-
-      it('Should fail without public key', function (done) {
-        const data = {
-          url: 'http://coucou.com'
-        }
-        makePostBodyRequest(path, data, done)
-      })
-
-      it('Should fail without an url', function (done) {
-        const data = {
-          publicKey: 'mysuperpublickey'
-        }
-        makePostBodyRequest(path, data, done)
-      })
-
-      it('Should fail with an incorrect url', function (done) {
-        const data = {
-          url: 'coucou.com',
-          publicKey: 'mysuperpublickey'
-        }
-        makePostBodyRequest(path, data, function () {
-          data.url = 'http://coucou'
-          makePostBodyRequest(path, data, function () {
-            data.url = 'coucou'
-            makePostBodyRequest(path, data, done)
-          })
-        })
-      })
-
-      it('Should succeed with the correct parameters', function (done) {
-        const data = {
-          url: 'http://coucou.com',
-          publicKey: 'mysuperpublickey'
-        }
-        makePostBodyRequest(path, data, done, false)
-      })
-    })
-  })
-
-  describe('Of the videos API', function () {
-    const path = '/api/v1/videos/'
-
-    describe('When listing a video', function () {
-      it('Should fail with a bad start pagination', function (done) {
-        request(server.url)
-          .get(path)
-          .query({ start: 'hello' })
-          .set('Accept', 'application/json')
-          .expect(400, done)
-      })
-
-      it('Should fail with a bad count pagination', function (done) {
-        request(server.url)
-          .get(path)
-          .query({ count: 'hello' })
-          .set('Accept', 'application/json')
-          .expect(400, done)
-      })
-
-      it('Should fail with an incorrect sort', function (done) {
-        request(server.url)
-          .get(path)
-          .query({ sort: 'hello' })
-          .set('Accept', 'application/json')
-          .expect(400, done)
-      })
-    })
-
-    describe('When searching a video', function () {
-      it('Should fail with nothing', function (done) {
-        request(server.url)
-          .get(pathUtils.join(path, 'search'))
-          .set('Accept', 'application/json')
-          .expect(400, done)
-      })
-
-      it('Should fail with a bad start pagination', function (done) {
-        request(server.url)
-          .get(pathUtils.join(path, 'search', 'test'))
-          .query({ start: 'hello' })
-          .set('Accept', 'application/json')
-          .expect(400, done)
-      })
-
-      it('Should fail with a bad count pagination', function (done) {
-        request(server.url)
-          .get(pathUtils.join(path, 'search', 'test'))
-          .query({ count: 'hello' })
-          .set('Accept', 'application/json')
-          .expect(400, done)
-      })
-
-      it('Should fail with an incorrect sort', function (done) {
-        request(server.url)
-          .get(pathUtils.join(path, 'search', 'test'))
-          .query({ sort: 'hello' })
-          .set('Accept', 'application/json')
-          .expect(400, done)
-      })
-    })
-
-    describe('When adding a video', function () {
-      it('Should fail with nothing', function (done) {
-        const data = {}
-        const attach = {}
-        makePostRequest(path, server.accessToken, data, attach, done)
-      })
-
-      it('Should fail without name', function (done) {
-        const data = {
-          description: 'my super description',
-          tags: [ 'tag1', 'tag2' ]
-        }
-        const attach = {
-          'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
-        }
-        makePostRequest(path, server.accessToken, data, attach, done)
-      })
-
-      it('Should fail with a long name', function (done) {
-        const data = {
-          name: 'My very very very very very very very very very very very very very very very very long name',
-          description: 'my super description',
-          tags: [ 'tag1', 'tag2' ]
-        }
-        const attach = {
-          'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
-        }
-        makePostRequest(path, server.accessToken, data, attach, done)
-      })
-
-      it('Should fail without description', function (done) {
-        const data = {
-          name: 'my super name',
-          tags: [ 'tag1', 'tag2' ]
-        }
-        const attach = {
-          'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
-        }
-        makePostRequest(path, server.accessToken, data, attach, done)
-      })
-
-      it('Should fail with a long description', function (done) {
-        const data = {
-          name: 'my super name',
-          description: 'my super description which is very very very very very very very very very very very very very very' +
-                       'very very very very very very very very very very very very very very very very very very very very very' +
-                       'very very very very very very very very very very very very very very very long',
-          tags: [ 'tag1', 'tag2' ]
-        }
-        const attach = {
-          'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
-        }
-        makePostRequest(path, server.accessToken, data, attach, done)
-      })
-
-      it('Should fail without tags', function (done) {
-        const data = {
-          name: 'my super name',
-          description: 'my super description'
-        }
-        const attach = {
-          'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
-        }
-        makePostRequest(path, server.accessToken, data, attach, done)
-      })
-
-      it('Should fail with too many tags', function (done) {
-        const data = {
-          name: 'my super name',
-          description: 'my super description',
-          tags: [ 'tag1', 'tag2', 'tag3', 'tag4' ]
-        }
-        const attach = {
-          'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
-        }
-        makePostRequest(path, server.accessToken, data, attach, done)
-      })
-
-      it('Should fail with not enough tags', function (done) {
-        const data = {
-          name: 'my super name',
-          description: 'my super description',
-          tags: [ ]
-        }
-        const attach = {
-          'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
-        }
-        makePostRequest(path, server.accessToken, data, attach, done)
-      })
-
-      it('Should fail with a tag length too low', function (done) {
-        const data = {
-          name: 'my super name',
-          description: 'my super description',
-          tags: [ 'tag1', 't' ]
-        }
-        const attach = {
-          'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
-        }
-        makePostRequest(path, server.accessToken, data, attach, done)
-      })
-
-      it('Should fail with a tag length too big', function (done) {
-        const data = {
-          name: 'my super name',
-          description: 'my super description',
-          tags: [ 'mysupertagtoolong', 'tag1' ]
-        }
-        const attach = {
-          'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
-        }
-        makePostRequest(path, server.accessToken, data, attach, done)
-      })
-
-      it('Should fail with malformed tags', function (done) {
-        const data = {
-          name: 'my super name',
-          description: 'my super description',
-          tags: [ 'my tag' ]
-        }
-        const attach = {
-          'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
-        }
-        makePostRequest(path, server.accessToken, data, attach, done)
-      })
-
-      it('Should fail without an input file', function (done) {
-        const data = {
-          name: 'my super name',
-          description: 'my super description',
-          tags: [ 'tag1', 'tag2' ]
-        }
-        const attach = {}
-        makePostRequest(path, server.accessToken, data, attach, done)
-      })
-
-      it('Should fail without an incorrect input file', function (done) {
-        const data = {
-          name: 'my super name',
-          description: 'my super description',
-          tags: [ 'tag1', 'tag2' ]
-        }
-        const attach = {
-          'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short_fake.webm')
-        }
-        makePostRequest(path, server.accessToken, data, attach, done)
-      })
-
-      it('Should fail with a too big duration', function (done) {
-        const data = {
-          name: 'my super name',
-          description: 'my super description',
-          tags: [ 'tag1', 'tag2' ]
-        }
-        const attach = {
-          'videofile': pathUtils.join(__dirname, 'fixtures', 'video_too_long.webm')
-        }
-        makePostRequest(path, server.accessToken, data, attach, done)
-      })
-
-      it('Should succeed with the correct parameters', function (done) {
-        const data = {
-          name: 'my super name',
-          description: 'my super description',
-          tags: [ 'tag1', 'tag2' ]
-        }
-        const attach = {
-          'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
-        }
-        makePostRequest(path, server.accessToken, data, attach, function () {
-          attach.videofile = pathUtils.join(__dirname, 'fixtures', 'video_short.mp4')
-          makePostRequest(path, server.accessToken, data, attach, function () {
-            attach.videofile = pathUtils.join(__dirname, 'fixtures', 'video_short.ogv')
-            makePostRequest(path, server.accessToken, data, attach, done, false)
-          }, false)
-        }, false)
-      })
-    })
-
-    describe('When getting a video', function () {
-      it('Should return the list of the videos with nothing', function (done) {
-        request(server.url)
-          .get(path)
-          .set('Accept', 'application/json')
-          .expect(200)
-          .expect('Content-Type', /json/)
-          .end(function (err, res) {
-            if (err) throw err
-
-            expect(res.body.data).to.be.an('array')
-            expect(res.body.data.length).to.equal(3)
-
-            done()
-          })
-      })
-
-      it('Should fail without a mongodb id', function (done) {
-        request(server.url)
-          .get(path + 'coucou')
-          .set('Accept', 'application/json')
-          .expect(400, done)
-      })
-
-      it('Should return 404 with an incorrect video', function (done) {
-        request(server.url)
-          .get(path + '123456789012345678901234')
-          .set('Accept', 'application/json')
-          .expect(404, done)
-      })
-
-      it('Should succeed with the correct parameters')
-    })
-
-    describe('When removing a video', function () {
-      it('Should have 404 with nothing', function (done) {
-        request(server.url)
-          .delete(path)
-          .set('Authorization', 'Bearer ' + server.accessToken)
-          .expect(400, done)
-      })
-
-      it('Should fail without a mongodb id', function (done) {
-        request(server.url)
-          .delete(path + 'hello')
-          .set('Authorization', 'Bearer ' + server.accessToken)
-          .expect(400, done)
-      })
-
-      it('Should fail with a video which does not exist', function (done) {
-        request(server.url)
-          .delete(path + '123456789012345678901234')
-          .set('Authorization', 'Bearer ' + server.accessToken)
-          .expect(404, done)
-      })
-
-      it('Should fail with a video of another pod')
-
-      it('Should succeed with the correct parameters')
-    })
-  })
-
-  describe('Of the remote videos API', function () {
-    describe('When making a secure request', function () {
-      it('Should check a secure request')
-    })
-
-    describe('When adding a video', function () {
-      it('Should check when adding a video')
-    })
-
-    describe('When removing a video', function () {
-      it('Should check when removing a video')
-    })
-  })
-
-  after(function (done) {
-    process.kill(-server.app.pid)
-
-    // Keep the logs if the test failed
-    if (this.ok) {
-      utils.flushTests(done)
-    } else {
-      done()
-    }
-  })
-})
similarity index 88%
rename from server/tests/api/friendsAdvanced.js
rename to server/tests/api/friends-advanced.js
index 603fbc16bec368b4d94c30f8b8af24a8a8fe8414..0d24481ef64c79676fbbbb45307bab08a5544796 100644 (file)
@@ -5,24 +5,27 @@ const each = require('async/each')
 const expect = chai.expect
 const series = require('async/series')
 
-const utils = require('./utils')
+const loginUtils = require('../utils/login')
+const podsUtils = require('../utils/pods')
+const serversUtils = require('../utils/servers')
+const videosUtils = require('../utils/videos')
 
 describe('Test advanced friends', function () {
   let servers = []
 
   function makeFriends (podNumber, callback) {
     const server = servers[podNumber - 1]
-    return utils.makeFriends(server.url, server.accessToken, callback)
+    return podsUtils.makeFriends(server.url, server.accessToken, callback)
   }
 
   function quitFriends (podNumber, callback) {
     const server = servers[podNumber - 1]
-    return utils.quitFriends(server.url, server.accessToken, callback)
+    return podsUtils.quitFriends(server.url, server.accessToken, callback)
   }
 
   function getFriendsList (podNumber, end) {
     const server = servers[podNumber - 1]
-    return utils.getFriendsList(server.url, end)
+    return podsUtils.getFriendsList(server.url, end)
   }
 
   function uploadVideo (podNumber, callback) {
@@ -32,22 +35,22 @@ describe('Test advanced friends', function () {
     const fixture = 'video_short.webm'
     const server = servers[podNumber - 1]
 
-    return utils.uploadVideo(server.url, server.accessToken, name, description, tags, fixture, callback)
+    return videosUtils.uploadVideo(server.url, server.accessToken, name, description, tags, fixture, callback)
   }
 
   function getVideos (podNumber, callback) {
-    return utils.getVideosList(servers[podNumber - 1].url, callback)
+    return videosUtils.getVideosList(servers[podNumber - 1].url, callback)
   }
 
   // ---------------------------------------------------------------
 
   before(function (done) {
     this.timeout(30000)
-    utils.flushAndRunMultipleServers(6, function (serversRun, urlsRun) {
+    serversUtils.flushAndRunMultipleServers(6, function (serversRun, urlsRun) {
       servers = serversRun
 
       each(servers, function (server, callbackEach) {
-        utils.loginAndGetAccessToken(server, function (err, accessToken) {
+        loginUtils.loginAndGetAccessToken(server, function (err, accessToken) {
           if (err) return callbackEach(err)
 
           server.accessToken = accessToken
@@ -169,7 +172,7 @@ describe('Test advanced friends', function () {
       },
       // Rerun server 4
       function (next) {
-        utils.runServer(4, function (server) {
+        serversUtils.runServer(4, function (server) {
           servers[3].app = server.app
           next()
         })
@@ -273,7 +276,7 @@ describe('Test advanced friends', function () {
     })
 
     if (this.ok) {
-      utils.flushTests(done)
+      serversUtils.flushTests(done)
     } else {
       done()
     }
similarity index 70%
rename from server/tests/api/friendsBasic.js
rename to server/tests/api/friends-basic.js
index c74a7f2249154c47e5a142102a4f19176cb7166d..f1393b5ec78dac8d71b4382660e1837a3ce74e58 100644 (file)
@@ -5,14 +5,17 @@ const each = require('async/each')
 const expect = chai.expect
 const series = require('async/series')
 
-const utils = require('./utils')
+const loginUtils = require('../utils/login')
+const miscsUtils = require('../utils/miscs')
+const podsUtils = require('../utils/pods')
+const serversUtils = require('../utils/servers')
 
 describe('Test basic friends', function () {
   let servers = []
 
   function makeFriends (podNumber, callback) {
     const server = servers[podNumber - 1]
-    return utils.makeFriends(server.url, server.accessToken, callback)
+    return podsUtils.makeFriends(server.url, server.accessToken, callback)
   }
 
   function testMadeFriends (servers, serverToTest, callback) {
@@ -22,7 +25,7 @@ describe('Test basic friends', function () {
       friends.push(servers[i].url)
     }
 
-    utils.getFriendsList(serverToTest.url, function (err, res) {
+    podsUtils.getFriendsList(serverToTest.url, function (err, res) {
       if (err) throw err
 
       const result = res.body
@@ -43,11 +46,11 @@ describe('Test basic friends', function () {
 
   before(function (done) {
     this.timeout(20000)
-    utils.flushAndRunMultipleServers(3, function (serversRun, urlsRun) {
+    serversUtils.flushAndRunMultipleServers(3, function (serversRun, urlsRun) {
       servers = serversRun
 
       each(servers, function (server, callbackEach) {
-        utils.loginAndGetAccessToken(server, function (err, accessToken) {
+        loginUtils.loginAndGetAccessToken(server, function (err, accessToken) {
           if (err) return callbackEach(err)
 
           server.accessToken = accessToken
@@ -59,7 +62,7 @@ describe('Test basic friends', function () {
 
   it('Should not have friends', function (done) {
     each(servers, function (server, callback) {
-      utils.getFriendsList(server.url, function (err, res) {
+      podsUtils.getFriendsList(server.url, function (err, res) {
         if (err) throw err
 
         const result = res.body
@@ -71,7 +74,7 @@ describe('Test basic friends', function () {
   })
 
   it('Should make friends', function (done) {
-    this.timeout(10000)
+    this.timeout(40000)
 
     series([
       // The second pod make friend with the third
@@ -80,30 +83,38 @@ describe('Test basic friends', function () {
       },
       // Wait for the request between pods
       function (next) {
-        setTimeout(next, 1000)
+        setTimeout(next, 11000)
       },
       // The second pod should have the third as a friend
       function (next) {
-        utils.getFriendsList(servers[1].url, function (err, res) {
+        podsUtils.getFriendsList(servers[1].url, function (err, res) {
           if (err) throw err
 
           const result = res.body
           expect(result).to.be.an('array')
           expect(result.length).to.equal(1)
-          expect(result[0].url).to.be.equal(servers[2].url)
+
+          const pod = result[0]
+          expect(pod.url).to.equal(servers[2].url)
+          expect(pod.score).to.equal(20)
+          expect(miscsUtils.dateIsValid(pod.createdDate)).to.be.true
 
           next()
         })
       },
       // Same here, the third pod should have the second pod as a friend
       function (next) {
-        utils.getFriendsList(servers[2].url, function (err, res) {
+        podsUtils.getFriendsList(servers[2].url, function (err, res) {
           if (err) throw err
 
           const result = res.body
           expect(result).to.be.an('array')
           expect(result.length).to.equal(1)
-          expect(result[0].url).to.be.equal(servers[1].url)
+
+          const pod = result[0]
+          expect(pod.url).to.equal(servers[1].url)
+          expect(pod.score).to.equal(20)
+          expect(miscsUtils.dateIsValid(pod.createdDate)).to.be.true
 
           next()
         })
@@ -114,7 +125,7 @@ describe('Test basic friends', function () {
       },
       // Wait for the request between pods
       function (next) {
-        setTimeout(next, 1000)
+        setTimeout(next, 11000)
       }
     ],
     // Now each pod should be friend with the other ones
@@ -128,7 +139,7 @@ describe('Test basic friends', function () {
 
   it('Should not be allowed to make friend again', function (done) {
     const server = servers[1]
-    utils.makeFriends(server.url, server.accessToken, 409, done)
+    podsUtils.makeFriends(server.url, server.accessToken, 409, done)
   })
 
   it('Should quit friends of pod 2', function (done) {
@@ -136,11 +147,11 @@ describe('Test basic friends', function () {
       // Pod 1 quit friends
       function (next) {
         const server = servers[1]
-        utils.quitFriends(server.url, server.accessToken, next)
+        podsUtils.quitFriends(server.url, server.accessToken, next)
       },
       // Pod 1 should not have friends anymore
       function (next) {
-        utils.getFriendsList(servers[1].url, function (err, res) {
+        podsUtils.getFriendsList(servers[1].url, function (err, res) {
           if (err) throw err
 
           const result = res.body
@@ -153,7 +164,7 @@ describe('Test basic friends', function () {
       // Other pods shouldn't have pod 1 too
       function (next) {
         each([ servers[0].url, servers[2].url ], function (url, callback) {
-          utils.getFriendsList(url, function (err, res) {
+          podsUtils.getFriendsList(url, function (err, res) {
             if (err) throw err
 
             const result = res.body
@@ -168,11 +179,15 @@ describe('Test basic friends', function () {
   })
 
   it('Should allow pod 2 to make friend again', function (done) {
+    this.timeout(20000)
+
     const server = servers[1]
-    utils.makeFriends(server.url, server.accessToken, function () {
-      each(servers, function (server, callback) {
-        testMadeFriends(servers, server, callback)
-      }, done)
+    podsUtils.makeFriends(server.url, server.accessToken, function () {
+      setTimeout(function () {
+        each(servers, function (server, callback) {
+          testMadeFriends(servers, server, callback)
+        }, done)
+      }, 11000)
     })
   })
 
@@ -182,7 +197,7 @@ describe('Test basic friends', function () {
     })
 
     if (this.ok) {
-      utils.flushTests(done)
+      serversUtils.flushTests(done)
     } else {
       done()
     }
index 61c9a7aca4a980d48b652482090f6de1961b150a..11f49e1e2cc3486811c9dfaa56f8ebf49f9ffc36 100644 (file)
@@ -1,9 +1,9 @@
 'use strict'
 
 // Order of the tests we want to execute
-require('./checkParams')
-require('./friendsBasic')
+require('./check-params')
+require('./friends-basic')
 require('./users')
-require('./singlePod')
-require('./multiplePods')
-require('./friendsAdvanced')
+require('./single-pod')
+require('./multiple-pods')
+require('./friends-advanced')
similarity index 82%
rename from server/tests/api/multiplePods.js
rename to server/tests/api/multiple-pods.js
index ac140f6bb4aef7ffb013ad37dead888857c36c07..b86f88c22643965bd17bc8208072f41f982583af 100644 (file)
@@ -6,7 +6,11 @@ const expect = chai.expect
 const pathUtils = require('path')
 const series = require('async/series')
 
-const utils = require('./utils')
+const loginUtils = require('../utils/login')
+const miscsUtils = require('../utils/miscs')
+const podsUtils = require('../utils/pods')
+const serversUtils = require('../utils/servers')
+const videosUtils = require('../utils/videos')
 const webtorrent = require(pathUtils.join(__dirname, '../../lib/webtorrent'))
 webtorrent.silent = true
 
@@ -20,7 +24,7 @@ describe('Test multiple pods', function () {
     series([
       // Run servers
       function (next) {
-        utils.flushAndRunMultipleServers(3, function (serversRun) {
+        serversUtils.flushAndRunMultipleServers(3, function (serversRun) {
           servers = serversRun
           next()
         })
@@ -28,7 +32,7 @@ describe('Test multiple pods', function () {
       // Get the access tokens
       function (next) {
         each(servers, function (server, callbackEach) {
-          utils.loginAndGetAccessToken(server, function (err, accessToken) {
+          loginUtils.loginAndGetAccessToken(server, function (err, accessToken) {
             if (err) return callbackEach(err)
 
             server.accessToken = accessToken
@@ -39,7 +43,7 @@ describe('Test multiple pods', function () {
       // The second pod make friend with the third
       function (next) {
         const server = servers[1]
-        utils.makeFriends(server.url, server.accessToken, next)
+        podsUtils.makeFriends(server.url, server.accessToken, next)
       },
       // Wait for the request between pods
       function (next) {
@@ -48,7 +52,7 @@ describe('Test multiple pods', function () {
       // Pod 1 make friends too
       function (next) {
         const server = servers[0]
-        utils.makeFriends(server.url, server.accessToken, next)
+        podsUtils.makeFriends(server.url, server.accessToken, next)
       },
       function (next) {
         webtorrent.create({ host: 'client', port: '1' }, next)
@@ -58,7 +62,7 @@ describe('Test multiple pods', function () {
 
   it('Should not have videos for all pods', function (done) {
     each(servers, function (server, callback) {
-      utils.getVideosList(server.url, function (err, res) {
+      videosUtils.getVideosList(server.url, function (err, res) {
         if (err) throw err
 
         const videos = res.body.data
@@ -80,7 +84,7 @@ describe('Test multiple pods', function () {
           const description = 'my super description for pod 1'
           const tags = [ 'tag1p1', 'tag2p1' ]
           const file = 'video_short1.webm'
-          utils.uploadVideo(servers[0].url, servers[0].accessToken, name, description, tags, file, next)
+          videosUtils.uploadVideo(servers[0].url, servers[0].accessToken, name, description, tags, file, next)
         },
         function (next) {
           setTimeout(next, 11000)
@@ -92,7 +96,7 @@ describe('Test multiple pods', function () {
           each(servers, function (server, callback) {
             let baseMagnet = null
 
-            utils.getVideosList(server.url, function (err, res) {
+            videosUtils.getVideosList(server.url, function (err, res) {
               if (err) throw err
 
               const videos = res.body.data
@@ -105,7 +109,7 @@ describe('Test multiple pods', function () {
               expect(video.magnetUri).to.exist
               expect(video.duration).to.equal(10)
               expect(video.tags).to.deep.equal([ 'tag1p1', 'tag2p1' ])
-              expect(utils.dateIsValid(video.createdDate)).to.be.true
+              expect(miscsUtils.dateIsValid(video.createdDate)).to.be.true
               expect(video.author).to.equal('root')
 
               if (server.url !== 'http://localhost:9001') {
@@ -121,7 +125,7 @@ describe('Test multiple pods', function () {
                 expect(video.magnetUri).to.equal.magnetUri
               }
 
-              utils.testImage(server.url, 'video_short1.webm', video.thumbnailPath, function (err, test) {
+              videosUtils.testVideoImage(server.url, 'video_short1.webm', video.thumbnailPath, function (err, test) {
                 if (err) throw err
                 expect(test).to.equal(true)
 
@@ -142,7 +146,7 @@ describe('Test multiple pods', function () {
           const description = 'my super description for pod 2'
           const tags = [ 'tag1p2', 'tag2p2', 'tag3p2' ]
           const file = 'video_short2.webm'
-          utils.uploadVideo(servers[1].url, servers[1].accessToken, name, description, tags, file, next)
+          videosUtils.uploadVideo(servers[1].url, servers[1].accessToken, name, description, tags, file, next)
         },
         function (next) {
           setTimeout(next, 11000)
@@ -154,7 +158,7 @@ describe('Test multiple pods', function () {
           each(servers, function (server, callback) {
             let baseMagnet = null
 
-            utils.getVideosList(server.url, function (err, res) {
+            videosUtils.getVideosList(server.url, function (err, res) {
               if (err) throw err
 
               const videos = res.body.data
@@ -167,7 +171,7 @@ describe('Test multiple pods', function () {
               expect(video.magnetUri).to.exist
               expect(video.duration).to.equal(5)
               expect(video.tags).to.deep.equal([ 'tag1p2', 'tag2p2', 'tag3p2' ])
-              expect(utils.dateIsValid(video.createdDate)).to.be.true
+              expect(miscsUtils.dateIsValid(video.createdDate)).to.be.true
               expect(video.author).to.equal('root')
 
               if (server.url !== 'http://localhost:9002') {
@@ -183,7 +187,7 @@ describe('Test multiple pods', function () {
                 expect(video.magnetUri).to.equal.magnetUri
               }
 
-              utils.testImage(server.url, 'video_short2.webm', video.thumbnailPath, function (err, test) {
+              videosUtils.testVideoImage(server.url, 'video_short2.webm', video.thumbnailPath, function (err, test) {
                 if (err) throw err
                 expect(test).to.equal(true)
 
@@ -204,14 +208,14 @@ describe('Test multiple pods', function () {
           const description = 'my super description for pod 3'
           const tags = [ 'tag1p3' ]
           const file = 'video_short3.webm'
-          utils.uploadVideo(servers[2].url, servers[2].accessToken, name, description, tags, file, next)
+          videosUtils.uploadVideo(servers[2].url, servers[2].accessToken, name, description, tags, file, next)
         },
         function (next) {
           const name = 'my super name for pod 3-2'
           const description = 'my super description for pod 3-2'
           const tags = [ 'tag2p3', 'tag3p3', 'tag4p3' ]
           const file = 'video_short.webm'
-          utils.uploadVideo(servers[2].url, servers[2].accessToken, name, description, tags, file, next)
+          videosUtils.uploadVideo(servers[2].url, servers[2].accessToken, name, description, tags, file, next)
         },
         function (next) {
           setTimeout(next, 22000)
@@ -222,7 +226,7 @@ describe('Test multiple pods', function () {
           let baseMagnet = null
           // All pods should have this video
           each(servers, function (server, callback) {
-            utils.getVideosList(server.url, function (err, res) {
+            videosUtils.getVideosList(server.url, function (err, res) {
               if (err) throw err
 
               const videos = res.body.data
@@ -247,7 +251,7 @@ describe('Test multiple pods', function () {
               expect(video1.duration).to.equal(5)
               expect(video1.tags).to.deep.equal([ 'tag1p3' ])
               expect(video1.author).to.equal('root')
-              expect(utils.dateIsValid(video1.createdDate)).to.be.true
+              expect(miscsUtils.dateIsValid(video1.createdDate)).to.be.true
 
               expect(video2.name).to.equal('my super name for pod 3-2')
               expect(video2.description).to.equal('my super description for pod 3-2')
@@ -256,7 +260,7 @@ describe('Test multiple pods', function () {
               expect(video2.duration).to.equal(5)
               expect(video2.tags).to.deep.equal([ 'tag2p3', 'tag3p3', 'tag4p3' ])
               expect(video2.author).to.equal('root')
-              expect(utils.dateIsValid(video2.createdDate)).to.be.true
+              expect(miscsUtils.dateIsValid(video2.createdDate)).to.be.true
 
               if (server.url !== 'http://localhost:9003') {
                 expect(video1.isLocal).to.be.false
@@ -273,11 +277,11 @@ describe('Test multiple pods', function () {
                 expect(video2.magnetUri).to.equal.magnetUri
               }
 
-              utils.testImage(server.url, 'video_short3.webm', video1.thumbnailPath, function (err, test) {
+              videosUtils.testVideoImage(server.url, 'video_short3.webm', video1.thumbnailPath, function (err, test) {
                 if (err) throw err
                 expect(test).to.equal(true)
 
-                utils.testImage(server.url, 'video_short.webm', video2.thumbnailPath, function (err, test) {
+                videosUtils.testVideoImage(server.url, 'video_short.webm', video2.thumbnailPath, function (err, test) {
                   if (err) throw err
                   expect(test).to.equal(true)
 
@@ -296,7 +300,7 @@ describe('Test multiple pods', function () {
       // Yes, this could be long
       this.timeout(200000)
 
-      utils.getVideosList(servers[2].url, function (err, res) {
+      videosUtils.getVideosList(servers[2].url, function (err, res) {
         if (err) throw err
 
         const video = res.body.data[0]
@@ -317,7 +321,7 @@ describe('Test multiple pods', function () {
       // Yes, this could be long
       this.timeout(200000)
 
-      utils.getVideosList(servers[0].url, function (err, res) {
+      videosUtils.getVideosList(servers[0].url, function (err, res) {
         if (err) throw err
 
         const video = res.body.data[1]
@@ -336,7 +340,7 @@ describe('Test multiple pods', function () {
       // Yes, this could be long
       this.timeout(200000)
 
-      utils.getVideosList(servers[1].url, function (err, res) {
+      videosUtils.getVideosList(servers[1].url, function (err, res) {
         if (err) throw err
 
         const video = res.body.data[2]
@@ -355,7 +359,7 @@ describe('Test multiple pods', function () {
       // Yes, this could be long
       this.timeout(200000)
 
-      utils.getVideosList(servers[0].url, function (err, res) {
+      videosUtils.getVideosList(servers[0].url, function (err, res) {
         if (err) throw err
 
         const video = res.body.data[3]
@@ -375,10 +379,10 @@ describe('Test multiple pods', function () {
 
       series([
         function (next) {
-          utils.removeVideo(servers[2].url, servers[2].accessToken, toRemove[0], next)
+          videosUtils.removeVideo(servers[2].url, servers[2].accessToken, toRemove[0], next)
         },
         function (next) {
-          utils.removeVideo(servers[2].url, servers[2].accessToken, toRemove[1], next)
+          videosUtils.removeVideo(servers[2].url, servers[2].accessToken, toRemove[1], next)
         }],
         function (err) {
           if (err) throw err
@@ -389,7 +393,7 @@ describe('Test multiple pods', function () {
 
     it('Should have videos 1 and 3 on each pod', function (done) {
       each(servers, function (server, callback) {
-        utils.getVideosList(server.url, function (err, res) {
+        videosUtils.getVideosList(server.url, function (err, res) {
           if (err) throw err
 
           const videos = res.body.data
@@ -415,7 +419,7 @@ describe('Test multiple pods', function () {
 
     // Keep the logs if the test failed
     if (this.ok) {
-      utils.flushTests(done)
+      serversUtils.flushTests(done)
     } else {
       done()
     }
diff --git a/server/tests/api/requests.js b/server/tests/api/requests.js
new file mode 100644 (file)
index 0000000..af36f6e
--- /dev/null
@@ -0,0 +1,128 @@
+'use strict'
+
+const chai = require('chai')
+const each = require('async/each')
+const expect = chai.expect
+const request = require('supertest')
+
+const loginUtils = require('../utils/login')
+const podsUtils = require('../utils/pods')
+const serversUtils = require('../utils/servers')
+const videosUtils = require('../utils/videos')
+
+describe('Test requests stats', function () {
+  const path = '/api/v1/requests/stats'
+  let servers = []
+
+  function uploadVideo (server, callback) {
+    const name = 'my super video'
+    const description = 'my super description'
+    const tags = [ 'tag1', 'tag2' ]
+    const fixture = 'video_short.webm'
+
+    videosUtils.uploadVideo(server.url, server.accessToken, name, description, tags, fixture, callback)
+  }
+
+  function getRequestsStats (server, callback) {
+    request(server.url)
+      .get(path)
+      .set('Accept', 'application/json')
+      .set('Authorization', 'Bearer ' + server.accessToken)
+      .expect(200)
+      .end(callback)
+  }
+
+  // ---------------------------------------------------------------
+
+  before(function (done) {
+    this.timeout(20000)
+    serversUtils.flushAndRunMultipleServers(2, function (serversRun, urlsRun) {
+      servers = serversRun
+
+      each(servers, function (server, callbackEach) {
+        loginUtils.loginAndGetAccessToken(server, function (err, accessToken) {
+          if (err) return callbackEach(err)
+
+          server.accessToken = accessToken
+          callbackEach()
+        })
+      }, function (err) {
+        if (err) throw err
+
+        const server1 = servers[0]
+        podsUtils.makeFriends(server1.url, server1.accessToken, done)
+      })
+    })
+  })
+
+  it('Should have a correct timer', function (done) {
+    const server = servers[0]
+
+    getRequestsStats(server, function (err, res) {
+      if (err) throw err
+
+      const body = res.body
+      expect(body.remainingMilliSeconds).to.be.at.least(0)
+      expect(body.remainingMilliSeconds).to.be.at.most(10000)
+
+      done()
+    })
+  })
+
+  it('Should have the correct request', function (done) {
+    this.timeout(15000)
+
+    const server = servers[0]
+    // Ensure the requests of pod 1 won't be made
+    servers[1].app.kill()
+
+    uploadVideo(server, function (err) {
+      if (err) throw err
+
+      getRequestsStats(server, function (err, res) {
+        if (err) throw err
+
+        const body = res.body
+        expect(body.requests).to.have.lengthOf(1)
+
+        const request = body.requests[0]
+        expect(request.to).to.have.lengthOf(1)
+        expect(request.request.type).to.equal('add')
+
+        // Wait one cycle
+        setTimeout(done, 10000)
+      })
+    })
+  })
+
+  it('Should have the correct requests', function (done) {
+    const server = servers[0]
+
+    uploadVideo(server, function (err) {
+      if (err) throw err
+
+      getRequestsStats(server, function (err, res) {
+        if (err) throw err
+
+        const body = res.body
+        expect(body.requests).to.have.lengthOf(2)
+
+        const request = body.requests[1]
+        expect(request.to).to.have.lengthOf(1)
+        expect(request.request.type).to.equal('add')
+
+        done()
+      })
+    })
+  })
+
+  after(function (done) {
+    process.kill(-servers[0].app.pid)
+
+    if (this.ok) {
+      serversUtils.flushTests(done)
+    } else {
+      done()
+    }
+  })
+})
similarity index 76%
rename from server/tests/api/singlePod.js
rename to server/tests/api/single-pod.js
index 6ed719f871528909ca47b0ca9042f85e5c7f1e3f..bdaaee46c9c439e02aaae807bcacb28f6e549a7f 100644 (file)
@@ -8,11 +8,13 @@ const keyBy = require('lodash/keyBy')
 const pathUtils = require('path')
 const series = require('async/series')
 
+const loginUtils = require('../utils/login')
+const miscsUtils = require('../utils/miscs')
+const serversUtils = require('../utils/servers')
+const videosUtils = require('../utils/videos')
 const webtorrent = require(pathUtils.join(__dirname, '../../lib/webtorrent'))
 webtorrent.silent = true
 
-const utils = require('./utils')
-
 describe('Test a single pod', function () {
   let server = null
   let videoId = -1
@@ -23,16 +25,16 @@ describe('Test a single pod', function () {
 
     series([
       function (next) {
-        utils.flushTests(next)
+        serversUtils.flushTests(next)
       },
       function (next) {
-        utils.runServer(1, function (server1) {
+        serversUtils.runServer(1, function (server1) {
           server = server1
           next()
         })
       },
       function (next) {
-        utils.loginAndGetAccessToken(server, function (err, token) {
+        loginUtils.loginAndGetAccessToken(server, function (err, token) {
           if (err) throw err
           server.accessToken = token
           next()
@@ -45,7 +47,7 @@ describe('Test a single pod', function () {
   })
 
   it('Should not have videos', function (done) {
-    utils.getVideosList(server.url, function (err, res) {
+    videosUtils.getVideosList(server.url, function (err, res) {
       if (err) throw err
 
       expect(res.body.total).to.equal(0)
@@ -62,14 +64,14 @@ describe('Test a single pod', function () {
     const description = 'my super description'
     const tags = [ 'tag1', 'tag2', 'tag3' ]
     const file = 'video_short.webm'
-    utils.uploadVideo(server.url, server.accessToken, name, description, tags, file, done)
+    videosUtils.uploadVideo(server.url, server.accessToken, name, description, tags, file, done)
   })
 
   it('Should seed the uploaded video', function (done) {
     // Yes, this could be long
     this.timeout(60000)
 
-    utils.getVideosList(server.url, function (err, res) {
+    videosUtils.getVideosList(server.url, function (err, res) {
       if (err) throw err
 
       expect(res.body.total).to.equal(1)
@@ -84,9 +86,9 @@ describe('Test a single pod', function () {
       expect(video.author).to.equal('root')
       expect(video.isLocal).to.be.true
       expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ])
-      expect(utils.dateIsValid(video.createdDate)).to.be.true
+      expect(miscsUtils.dateIsValid(video.createdDate)).to.be.true
 
-      utils.testImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) {
+      videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) {
         if (err) throw err
         expect(test).to.equal(true)
 
@@ -97,8 +99,7 @@ describe('Test a single pod', function () {
           expect(torrent.files.length).to.equal(1)
           expect(torrent.files[0].path).to.exist.and.to.not.equal('')
 
-          // We remove it because we'll add it again
-          webtorrent.remove(video.magnetUri, done)
+          done()
         })
       })
     })
@@ -108,7 +109,7 @@ describe('Test a single pod', function () {
     // Yes, this could be long
     this.timeout(60000)
 
-    utils.getVideo(server.url, videoId, function (err, res) {
+    videosUtils.getVideo(server.url, videoId, function (err, res) {
       if (err) throw err
 
       const video = res.body
@@ -119,25 +120,19 @@ describe('Test a single pod', function () {
       expect(video.author).to.equal('root')
       expect(video.isLocal).to.be.true
       expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ])
-      expect(utils.dateIsValid(video.createdDate)).to.be.true
+      expect(miscsUtils.dateIsValid(video.createdDate)).to.be.true
 
-      utils.testImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) {
+      videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) {
         if (err) throw err
         expect(test).to.equal(true)
 
-        webtorrent.add(video.magnetUri, function (torrent) {
-          expect(torrent.files).to.exist
-          expect(torrent.files.length).to.equal(1)
-          expect(torrent.files[0].path).to.exist.and.to.not.equal('')
-
-          done()
-        })
+        done()
       })
     })
   })
 
   it('Should search the video by name by default', function (done) {
-    utils.searchVideo(server.url, 'my', function (err, res) {
+    videosUtils.searchVideo(server.url, 'my', function (err, res) {
       if (err) throw err
 
       expect(res.body.total).to.equal(1)
@@ -151,9 +146,9 @@ describe('Test a single pod', function () {
       expect(video.author).to.equal('root')
       expect(video.isLocal).to.be.true
       expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ])
-      expect(utils.dateIsValid(video.createdDate)).to.be.true
+      expect(miscsUtils.dateIsValid(video.createdDate)).to.be.true
 
-      utils.testImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) {
+      videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) {
         if (err) throw err
         expect(test).to.equal(true)
 
@@ -163,7 +158,7 @@ describe('Test a single pod', function () {
   })
 
   it('Should search the video by podUrl', function (done) {
-    utils.searchVideo(server.url, '9001', 'podUrl', function (err, res) {
+    videosUtils.searchVideo(server.url, '9001', 'podUrl', function (err, res) {
       if (err) throw err
 
       expect(res.body.total).to.equal(1)
@@ -177,9 +172,9 @@ describe('Test a single pod', function () {
       expect(video.author).to.equal('root')
       expect(video.isLocal).to.be.true
       expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ])
-      expect(utils.dateIsValid(video.createdDate)).to.be.true
+      expect(miscsUtils.dateIsValid(video.createdDate)).to.be.true
 
-      utils.testImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) {
+      videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) {
         if (err) throw err
         expect(test).to.equal(true)
 
@@ -189,7 +184,7 @@ describe('Test a single pod', function () {
   })
 
   it('Should search the video by tag', function (done) {
-    utils.searchVideo(server.url, 'tag1', 'tags', function (err, res) {
+    videosUtils.searchVideo(server.url, 'tag1', 'tags', function (err, res) {
       if (err) throw err
 
       expect(res.body.total).to.equal(1)
@@ -203,9 +198,9 @@ describe('Test a single pod', function () {
       expect(video.author).to.equal('root')
       expect(video.isLocal).to.be.true
       expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ])
-      expect(utils.dateIsValid(video.createdDate)).to.be.true
+      expect(miscsUtils.dateIsValid(video.createdDate)).to.be.true
 
-      utils.testImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) {
+      videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) {
         if (err) throw err
         expect(test).to.equal(true)
 
@@ -215,7 +210,7 @@ describe('Test a single pod', function () {
   })
 
   it('Should not find a search by name by default', function (done) {
-    utils.searchVideo(server.url, 'hello', function (err, res) {
+    videosUtils.searchVideo(server.url, 'hello', function (err, res) {
       if (err) throw err
 
       expect(res.body.total).to.equal(0)
@@ -227,7 +222,7 @@ describe('Test a single pod', function () {
   })
 
   it('Should not find a search by author', function (done) {
-    utils.searchVideo(server.url, 'hello', 'author', function (err, res) {
+    videosUtils.searchVideo(server.url, 'hello', 'author', function (err, res) {
       if (err) throw err
 
       expect(res.body.total).to.equal(0)
@@ -239,7 +234,7 @@ describe('Test a single pod', function () {
   })
 
   it('Should not find a search by tag', function (done) {
-    utils.searchVideo(server.url, 'tag', 'tags', function (err, res) {
+    videosUtils.searchVideo(server.url, 'tag', 'tags', function (err, res) {
       if (err) throw err
 
       expect(res.body.total).to.equal(0)
@@ -251,7 +246,7 @@ describe('Test a single pod', function () {
   })
 
   it('Should remove the video', function (done) {
-    utils.removeVideo(server.url, server.accessToken, videoId, function (err) {
+    videosUtils.removeVideo(server.url, server.accessToken, videoId, function (err) {
       if (err) throw err
 
       fs.readdir(pathUtils.join(__dirname, '../../../test1/uploads/'), function (err, files) {
@@ -264,7 +259,7 @@ describe('Test a single pod', function () {
   })
 
   it('Should not have videos', function (done) {
-    utils.getVideosList(server.url, function (err, res) {
+    videosUtils.getVideosList(server.url, function (err, res) {
       if (err) throw err
 
       expect(res.body.total).to.equal(0)
@@ -286,12 +281,12 @@ describe('Test a single pod', function () {
       const description = video + ' description'
       const tags = [ 'tag1', 'tag2', 'tag3' ]
 
-      utils.uploadVideo(server.url, server.accessToken, name, description, tags, video, callbackEach)
+      videosUtils.uploadVideo(server.url, server.accessToken, name, description, tags, video, callbackEach)
     }, done)
   })
 
   it('Should have the correct durations', function (done) {
-    utils.getVideosList(server.url, function (err, res) {
+    videosUtils.getVideosList(server.url, function (err, res) {
       if (err) throw err
 
       expect(res.body.total).to.equal(6)
@@ -312,7 +307,7 @@ describe('Test a single pod', function () {
   })
 
   it('Should have the correct thumbnails', function (done) {
-    utils.getVideosList(server.url, function (err, res) {
+    videosUtils.getVideosList(server.url, function (err, res) {
       if (err) throw err
 
       const videos = res.body.data
@@ -323,7 +318,7 @@ describe('Test a single pod', function () {
         if (err) throw err
         const videoName = video.name.replace(' name', '')
 
-        utils.testImage(server.url, videoName, video.thumbnailPath, function (err, test) {
+        videosUtils.testVideoImage(server.url, videoName, video.thumbnailPath, function (err, test) {
           if (err) throw err
 
           expect(test).to.equal(true)
@@ -334,7 +329,7 @@ describe('Test a single pod', function () {
   })
 
   it('Should list only the two first videos', function (done) {
-    utils.getVideosListPagination(server.url, 0, 2, function (err, res) {
+    videosUtils.getVideosListPagination(server.url, 0, 2, function (err, res) {
       if (err) throw err
 
       const videos = res.body.data
@@ -348,7 +343,7 @@ describe('Test a single pod', function () {
   })
 
   it('Should list only the next three videos', function (done) {
-    utils.getVideosListPagination(server.url, 2, 3, function (err, res) {
+    videosUtils.getVideosListPagination(server.url, 2, 3, function (err, res) {
       if (err) throw err
 
       const videos = res.body.data
@@ -363,7 +358,7 @@ describe('Test a single pod', function () {
   })
 
   it('Should list the last video', function (done) {
-    utils.getVideosListPagination(server.url, 5, 6, function (err, res) {
+    videosUtils.getVideosListPagination(server.url, 5, 6, function (err, res) {
       if (err) throw err
 
       const videos = res.body.data
@@ -376,7 +371,7 @@ describe('Test a single pod', function () {
   })
 
   it('Should search the first video', function (done) {
-    utils.searchVideoWithPagination(server.url, 'webm', 'name', 0, 1, function (err, res) {
+    videosUtils.searchVideoWithPagination(server.url, 'webm', 'name', 0, 1, function (err, res) {
       if (err) throw err
 
       const videos = res.body.data
@@ -389,7 +384,7 @@ describe('Test a single pod', function () {
   })
 
   it('Should search the last two videos', function (done) {
-    utils.searchVideoWithPagination(server.url, 'webm', 'name', 2, 2, function (err, res) {
+    videosUtils.searchVideoWithPagination(server.url, 'webm', 'name', 2, 2, function (err, res) {
       if (err) throw err
 
       const videos = res.body.data
@@ -403,7 +398,7 @@ describe('Test a single pod', function () {
   })
 
   it('Should search all the webm videos', function (done) {
-    utils.searchVideoWithPagination(server.url, 'webm', 'name', 0, 15, function (err, res) {
+    videosUtils.searchVideoWithPagination(server.url, 'webm', 'name', 0, 15, function (err, res) {
       if (err) throw err
 
       const videos = res.body.data
@@ -415,7 +410,7 @@ describe('Test a single pod', function () {
   })
 
   it('Should search all the root author videos', function (done) {
-    utils.searchVideoWithPagination(server.url, 'root', 'author', 0, 15, function (err, res) {
+    videosUtils.searchVideoWithPagination(server.url, 'root', 'author', 0, 15, function (err, res) {
       if (err) throw err
 
       const videos = res.body.data
@@ -427,7 +422,7 @@ describe('Test a single pod', function () {
   })
 
   it('Should search all the 9001 port videos', function (done) {
-    utils.searchVideoWithPagination(server.url, '9001', 'podUrl', 0, 15, function (err, res) {
+    videosUtils.searchVideoWithPagination(server.url, '9001', 'podUrl', 0, 15, function (err, res) {
       if (err) throw err
 
       const videos = res.body.data
@@ -439,7 +434,7 @@ describe('Test a single pod', function () {
   })
 
   it('Should search all the localhost videos', function (done) {
-    utils.searchVideoWithPagination(server.url, 'localhost', 'podUrl', 0, 15, function (err, res) {
+    videosUtils.searchVideoWithPagination(server.url, 'localhost', 'podUrl', 0, 15, function (err, res) {
       if (err) throw err
 
       const videos = res.body.data
@@ -452,7 +447,7 @@ describe('Test a single pod', function () {
 
   it('Should search the good magnetUri video', function (done) {
     const video = videosListBase[0]
-    utils.searchVideoWithPagination(server.url, encodeURIComponent(video.magnetUri), 'magnetUri', 0, 15, function (err, res) {
+    videosUtils.searchVideoWithPagination(server.url, encodeURIComponent(video.magnetUri), 'magnetUri', 0, 15, function (err, res) {
       if (err) throw err
 
       const videos = res.body.data
@@ -465,7 +460,7 @@ describe('Test a single pod', function () {
   })
 
   it('Should list and sort by name in descending order', function (done) {
-    utils.getVideosListSort(server.url, '-name', function (err, res) {
+    videosUtils.getVideosListSort(server.url, '-name', function (err, res) {
       if (err) throw err
 
       const videos = res.body.data
@@ -483,7 +478,7 @@ describe('Test a single pod', function () {
   })
 
   it('Should search and sort by name in ascending order', function (done) {
-    utils.searchVideoWithSort(server.url, 'webm', 'name', function (err, res) {
+    videosUtils.searchVideoWithSort(server.url, 'webm', 'name', function (err, res) {
       if (err) throw err
 
       const videos = res.body.data
@@ -505,7 +500,7 @@ describe('Test a single pod', function () {
 
     // Keep the logs if the test failed
     if (this.ok) {
-      utils.flushTests(done)
+      serversUtils.flushTests(done)
     } else {
       done()
     }
index 68ba9de336d088e07542772d921d74898c0a896c..c6c892bf279c1a264197bfb1392d0c07db55e908 100644 (file)
@@ -5,25 +5,30 @@ const expect = chai.expect
 const pathUtils = require('path')
 const series = require('async/series')
 
+const loginUtils = require('../utils/login')
+const podsUtils = require('../utils/pods')
+const serversUtils = require('../utils/servers')
+const usersUtils = require('../utils/users')
+const videosUtils = require('../utils/videos')
 const webtorrent = require(pathUtils.join(__dirname, '../../lib/webtorrent'))
 webtorrent.silent = true
 
-const utils = require('./utils')
-
 describe('Test users', function () {
   let server = null
   let accessToken = null
-  let videoId
+  let accessTokenUser = null
+  let videoId = null
+  let userId = null
 
   before(function (done) {
     this.timeout(20000)
 
     series([
       function (next) {
-        utils.flushTests(next)
+        serversUtils.flushTests(next)
       },
       function (next) {
-        utils.runServer(1, function (server1) {
+        serversUtils.runServer(1, function (server1) {
           server = server1
           next()
         })
@@ -39,7 +44,7 @@ describe('Test users', function () {
 
   it('Should not login with an invalid client id', function (done) {
     const client = { id: 'client', password: server.client.secret }
-    utils.login(server.url, client, server.user, 400, function (err, res) {
+    loginUtils.login(server.url, client, server.user, 400, function (err, res) {
       if (err) throw err
 
       expect(res.body.error).to.equal('invalid_client')
@@ -49,7 +54,7 @@ describe('Test users', function () {
 
   it('Should not login with an invalid client password', function (done) {
     const client = { id: server.client.id, password: 'coucou' }
-    utils.login(server.url, client, server.user, 400, function (err, res) {
+    loginUtils.login(server.url, client, server.user, 400, function (err, res) {
       if (err) throw err
 
       expect(res.body.error).to.equal('invalid_client')
@@ -59,7 +64,7 @@ describe('Test users', function () {
 
   it('Should not login with an invalid username', function (done) {
     const user = { username: 'captain crochet', password: server.user.password }
-    utils.login(server.url, server.client, user, 400, function (err, res) {
+    loginUtils.login(server.url, server.client, user, 400, function (err, res) {
       if (err) throw err
 
       expect(res.body.error).to.equal('invalid_grant')
@@ -69,7 +74,7 @@ describe('Test users', function () {
 
   it('Should not login with an invalid password', function (done) {
     const user = { username: server.user.username, password: 'mewthree' }
-    utils.login(server.url, server.client, user, 400, function (err, res) {
+    loginUtils.login(server.url, server.client, user, 400, function (err, res) {
       if (err) throw err
 
       expect(res.body.error).to.equal('invalid_grant')
@@ -84,21 +89,21 @@ describe('Test users', function () {
     const description = 'my super description'
     const tags = [ 'tag1', 'tag2' ]
     const video = 'video_short.webm'
-    utils.uploadVideo(server.url, accessToken, name, description, tags, video, 401, done)
+    videosUtils.uploadVideo(server.url, accessToken, name, description, tags, video, 401, done)
   })
 
   it('Should not be able to make friends', function (done) {
     accessToken = 'mysupertoken'
-    utils.makeFriends(server.url, accessToken, 401, done)
+    podsUtils.makeFriends(server.url, accessToken, 401, done)
   })
 
   it('Should not be able to quit friends', function (done) {
     accessToken = 'mysupertoken'
-    utils.quitFriends(server.url, accessToken, 401, done)
+    podsUtils.quitFriends(server.url, accessToken, 401, done)
   })
 
   it('Should be able to login', function (done) {
-    utils.login(server.url, server.client, server.user, 200, function (err, res) {
+    loginUtils.login(server.url, server.client, server.user, 200, function (err, res) {
       if (err) throw err
 
       accessToken = res.body.access_token
@@ -111,10 +116,10 @@ describe('Test users', function () {
     const description = 'my super description'
     const tags = [ 'tag1', 'tag2' ]
     const video = 'video_short.webm'
-    utils.uploadVideo(server.url, accessToken, name, description, tags, video, 204, function (err, res) {
+    videosUtils.uploadVideo(server.url, accessToken, name, description, tags, video, 204, function (err, res) {
       if (err) throw err
 
-      utils.getVideosList(server.url, function (err, res) {
+      videosUtils.getVideosList(server.url, function (err, res) {
         if (err) throw err
 
         const video = res.body.data[0]
@@ -131,17 +136,17 @@ describe('Test users', function () {
     const description = 'my super description 2'
     const tags = [ 'tag1' ]
     const video = 'video_short.webm'
-    utils.uploadVideo(server.url, accessToken, name, description, tags, video, 204, done)
+    videosUtils.uploadVideo(server.url, accessToken, name, description, tags, video, 204, done)
   })
 
   it('Should not be able to remove the video with an incorrect token', function (done) {
-    utils.removeVideo(server.url, 'bad_token', videoId, 401, done)
+    videosUtils.removeVideo(server.url, 'bad_token', videoId, 401, done)
   })
 
   it('Should not be able to remove the video with the token of another account')
 
   it('Should be able to remove the video with the correct token', function (done) {
-    utils.removeVideo(server.url, accessToken, videoId, done)
+    videosUtils.removeVideo(server.url, accessToken, videoId, done)
   })
 
   it('Should logout (revoke token)')
@@ -158,12 +163,179 @@ describe('Test users', function () {
 
   it('Should be able to upload a video again')
 
+  it('Should be able to create a new user', function (done) {
+    usersUtils.createUser(server.url, accessToken, 'user_1', 'super password', done)
+  })
+
+  it('Should be able to login with this user', function (done) {
+    server.user = {
+      username: 'user_1',
+      password: 'super password'
+    }
+
+    loginUtils.loginAndGetAccessToken(server, function (err, token) {
+      if (err) throw err
+
+      accessTokenUser = token
+
+      done()
+    })
+  })
+
+  it('Should be able to get the user informations', function (done) {
+    usersUtils.getUserInformation(server.url, accessTokenUser, function (err, res) {
+      if (err) throw err
+
+      const user = res.body
+
+      expect(user.username).to.equal('user_1')
+      expect(user.id).to.exist
+
+      done()
+    })
+  })
+
+  it('Should be able to upload a video with this user', function (done) {
+    this.timeout(5000)
+
+    const name = 'my super name'
+    const description = 'my super description'
+    const tags = [ 'tag1', 'tag2', 'tag3' ]
+    const file = 'video_short.webm'
+    videosUtils.uploadVideo(server.url, accessTokenUser, name, description, tags, file, done)
+  })
+
+  it('Should list all the users', function (done) {
+    usersUtils.getUsersList(server.url, function (err, res) {
+      if (err) throw err
+
+      const result = res.body
+      const total = result.total
+      const users = result.data
+
+      expect(total).to.equal(2)
+      expect(users).to.be.an('array')
+      expect(users.length).to.equal(2)
+
+      const user = users[0]
+      expect(user.username).to.equal('user_1')
+
+      const rootUser = users[1]
+      expect(rootUser.username).to.equal('root')
+      userId = user.id
+
+      done()
+    })
+  })
+
+  it('Should list only the first user by username asc', function (done) {
+    usersUtils.getUsersListPaginationAndSort(server.url, 0, 1, 'username', function (err, res) {
+      if (err) throw err
+
+      const result = res.body
+      const total = result.total
+      const users = result.data
+
+      expect(total).to.equal(2)
+      expect(users.length).to.equal(1)
+
+      const user = users[0]
+      expect(user.username).to.equal('root')
+
+      done()
+    })
+  })
+
+  it('Should list only the first user by username desc', function (done) {
+    usersUtils.getUsersListPaginationAndSort(server.url, 0, 1, '-username', function (err, res) {
+      if (err) throw err
+
+      const result = res.body
+      const total = result.total
+      const users = result.data
+
+      expect(total).to.equal(2)
+      expect(users.length).to.equal(1)
+
+      const user = users[0]
+      expect(user.username).to.equal('user_1')
+
+      done()
+    })
+  })
+
+  it('Should list only the second user by createdDate desc', function (done) {
+    usersUtils.getUsersListPaginationAndSort(server.url, 0, 1, '-createdDate', function (err, res) {
+      if (err) throw err
+
+      const result = res.body
+      const total = result.total
+      const users = result.data
+
+      expect(total).to.equal(2)
+      expect(users.length).to.equal(1)
+
+      const user = users[0]
+      expect(user.username).to.equal('user_1')
+
+      done()
+    })
+  })
+
+  it('Should list all the users by createdDate asc', function (done) {
+    usersUtils.getUsersListPaginationAndSort(server.url, 0, 2, 'createdDate', function (err, res) {
+      if (err) throw err
+
+      const result = res.body
+      const total = result.total
+      const users = result.data
+
+      expect(total).to.equal(2)
+      expect(users.length).to.equal(2)
+
+      expect(users[0].username).to.equal('root')
+      expect(users[1].username).to.equal('user_1')
+
+      done()
+    })
+  })
+
+  it('Should update the user password', function (done) {
+    usersUtils.updateUser(server.url, userId, accessTokenUser, 'new password', function (err, res) {
+      if (err) throw err
+
+      server.user.password = 'new password'
+      loginUtils.login(server.url, server.client, server.user, 200, done)
+    })
+  })
+
+  it('Should be able to remove this user', function (done) {
+    usersUtils.removeUser(server.url, userId, accessToken, done)
+  })
+
+  it('Should not be able to login with this user', function (done) {
+    // server.user is already set to user 1
+    loginUtils.login(server.url, server.client, server.user, 400, done)
+  })
+
+  it('Should not have videos of this user', function (done) {
+    videosUtils.getVideosList(server.url, function (err, res) {
+      if (err) throw err
+
+      expect(res.body.total).to.equal(1)
+      const video = res.body.data[0]
+      expect(video.author).to.equal('root')
+
+      done()
+    })
+  })
+
   after(function (done) {
     process.kill(-server.app.pid)
 
     // Keep the logs if the test failed
     if (this.ok) {
-      utils.flushTests(done)
+      serversUtils.flushTests(done)
     } else {
       done()
     }
diff --git a/server/tests/api/utils.js b/server/tests/api/utils.js
deleted file mode 100644 (file)
index 3cc769f..0000000
+++ /dev/null
@@ -1,419 +0,0 @@
-'use strict'
-
-const childProcess = require('child_process')
-const exec = childProcess.exec
-const fork = childProcess.fork
-const fs = require('fs')
-const pathUtils = require('path')
-const request = require('supertest')
-
-const testUtils = {
-  dateIsValid: dateIsValid,
-  flushTests: flushTests,
-  getAllVideosListBy: getAllVideosListBy,
-  getClient: getClient,
-  getFriendsList: getFriendsList,
-  getVideo: getVideo,
-  getVideosList: getVideosList,
-  getVideosListPagination: getVideosListPagination,
-  getVideosListSort: getVideosListSort,
-  login: login,
-  loginAndGetAccessToken: loginAndGetAccessToken,
-  makeFriends: makeFriends,
-  quitFriends: quitFriends,
-  removeVideo: removeVideo,
-  flushAndRunMultipleServers: flushAndRunMultipleServers,
-  runServer: runServer,
-  searchVideo: searchVideo,
-  searchVideoWithPagination: searchVideoWithPagination,
-  searchVideoWithSort: searchVideoWithSort,
-  testImage: testImage,
-  uploadVideo: uploadVideo
-}
-
-// ---------------------- Export functions --------------------
-
-function dateIsValid (dateString) {
-  const dateToCheck = new Date(dateString)
-  const now = new Date()
-
-  // Check if the interval is more than 2 minutes
-  if (now - dateToCheck > 120000) return false
-
-  return true
-}
-
-function flushTests (callback) {
-  exec('npm run clean:server:test', callback)
-}
-
-function getAllVideosListBy (url, end) {
-  const path = '/api/v1/videos'
-
-  request(url)
-    .get(path)
-    .query({ sort: 'createdDate' })
-    .query({ start: 0 })
-    .query({ count: 10000 })
-    .set('Accept', 'application/json')
-    .expect(200)
-    .expect('Content-Type', /json/)
-    .end(end)
-}
-
-function getClient (url, end) {
-  const path = '/api/v1/users/client'
-
-  request(url)
-    .get(path)
-    .set('Accept', 'application/json')
-    .expect(200)
-    .expect('Content-Type', /json/)
-    .end(end)
-}
-
-function getFriendsList (url, end) {
-  const path = '/api/v1/pods/'
-
-  request(url)
-    .get(path)
-    .set('Accept', 'application/json')
-    .expect(200)
-    .expect('Content-Type', /json/)
-    .end(end)
-}
-
-function getVideo (url, id, end) {
-  const path = '/api/v1/videos/' + id
-
-  request(url)
-    .get(path)
-    .set('Accept', 'application/json')
-    .expect(200)
-    .expect('Content-Type', /json/)
-    .end(end)
-}
-
-function getVideosList (url, end) {
-  const path = '/api/v1/videos'
-
-  request(url)
-    .get(path)
-    .query({ sort: 'name' })
-    .set('Accept', 'application/json')
-    .expect(200)
-    .expect('Content-Type', /json/)
-    .end(end)
-}
-
-function getVideosListPagination (url, start, count, end) {
-  const path = '/api/v1/videos'
-
-  request(url)
-    .get(path)
-    .query({ start: start })
-    .query({ count: count })
-    .set('Accept', 'application/json')
-    .expect(200)
-    .expect('Content-Type', /json/)
-    .end(end)
-}
-
-function getVideosListSort (url, sort, end) {
-  const path = '/api/v1/videos'
-
-  request(url)
-    .get(path)
-    .query({ sort: sort })
-    .set('Accept', 'application/json')
-    .expect(200)
-    .expect('Content-Type', /json/)
-    .end(end)
-}
-
-function login (url, client, user, expectedStatus, end) {
-  if (!end) {
-    end = expectedStatus
-    expectedStatus = 200
-  }
-
-  const path = '/api/v1/users/token'
-
-  const body = {
-    client_id: client.id,
-    client_secret: client.secret,
-    username: user.username,
-    password: user.password,
-    response_type: 'code',
-    grant_type: 'password',
-    scope: 'upload'
-  }
-
-  request(url)
-    .post(path)
-    .type('form')
-    .send(body)
-    .expect(expectedStatus)
-    .end(end)
-}
-
-function loginAndGetAccessToken (server, callback) {
-  login(server.url, server.client, server.user, 200, function (err, res) {
-    if (err) return callback(err)
-
-    return callback(null, res.body.access_token)
-  })
-}
-
-function makeFriends (url, accessToken, expectedStatus, callback) {
-  if (!callback) {
-    callback = expectedStatus
-    expectedStatus = 204
-  }
-
-  const path = '/api/v1/pods/makefriends'
-
-  // The first pod make friend with the third
-  request(url)
-    .get(path)
-    .set('Accept', 'application/json')
-    .set('Authorization', 'Bearer ' + accessToken)
-    .expect(expectedStatus)
-    .end(function (err, res) {
-      if (err) throw err
-
-      // Wait for the request between pods
-      setTimeout(callback, 1000)
-    })
-}
-
-function quitFriends (url, accessToken, expectedStatus, callback) {
-  if (!callback) {
-    callback = expectedStatus
-    expectedStatus = 204
-  }
-
-  const path = '/api/v1/pods/quitfriends'
-
-  // The first pod make friend with the third
-  request(url)
-    .get(path)
-    .set('Accept', 'application/json')
-    .set('Authorization', 'Bearer ' + accessToken)
-    .expect(expectedStatus)
-    .end(function (err, res) {
-      if (err) throw err
-
-      // Wait for the request between pods
-      setTimeout(callback, 1000)
-    })
-}
-
-function removeVideo (url, token, id, expectedStatus, end) {
-  if (!end) {
-    end = expectedStatus
-    expectedStatus = 204
-  }
-
-  const path = '/api/v1/videos'
-
-  request(url)
-    .delete(path + '/' + id)
-    .set('Accept', 'application/json')
-    .set('Authorization', 'Bearer ' + token)
-    .expect(expectedStatus)
-    .end(end)
-}
-
-function flushAndRunMultipleServers (totalServers, serversRun) {
-  let apps = []
-  let urls = []
-  let i = 0
-
-  function anotherServerDone (number, app, url) {
-    apps[number - 1] = app
-    urls[number - 1] = url
-    i++
-    if (i === totalServers) {
-      serversRun(apps, urls)
-    }
-  }
-
-  flushTests(function () {
-    for (let j = 1; j <= totalServers; j++) {
-      // For the virtual buffer
-      setTimeout(function () {
-        runServer(j, function (app, url) {
-          anotherServerDone(j, app, url)
-        })
-      }, 1000 * j)
-    }
-  })
-}
-
-function runServer (number, callback) {
-  const server = {
-    app: null,
-    url: `http://localhost:${9000 + number}`,
-    client: {
-      id: null,
-      secret: null
-    },
-    user: {
-      username: null,
-      password: null
-    }
-  }
-
-  // These actions are async so we need to be sure that they have both been done
-  const serverRunString = {
-    'Connected to mongodb': false,
-    'Server listening on port': false
-  }
-
-  const regexps = {
-    client_id: 'Client id: ([a-f0-9]+)',
-    client_secret: 'Client secret: (.+)',
-    user_username: 'Username: (.+)',
-    user_password: 'User password: (.+)'
-  }
-
-  // Share the environment
-  const env = Object.create(process.env)
-  env.NODE_ENV = 'test'
-  env.NODE_APP_INSTANCE = number
-  const options = {
-    silent: true,
-    env: env,
-    detached: true
-  }
-
-  server.app = fork(pathUtils.join(__dirname, '../../../server.js'), [], options)
-  server.app.stdout.on('data', function onStdout (data) {
-    let dontContinue = false
-
-    // Capture things if we want to
-    for (const key of Object.keys(regexps)) {
-      const regexp = regexps[key]
-      const matches = data.toString().match(regexp)
-      if (matches !== null) {
-        if (key === 'client_id') server.client.id = matches[1]
-        else if (key === 'client_secret') server.client.secret = matches[1]
-        else if (key === 'user_username') server.user.username = matches[1]
-        else if (key === 'user_password') server.user.password = matches[1]
-      }
-    }
-
-    // Check if all required sentences are here
-    for (const key of Object.keys(serverRunString)) {
-      if (data.toString().indexOf(key) !== -1) serverRunString[key] = true
-      if (serverRunString[key] === false) dontContinue = true
-    }
-
-    // If no, there is maybe one thing not already initialized (mongodb...)
-    if (dontContinue === true) return
-
-    server.app.stdout.removeListener('data', onStdout)
-    callback(server)
-  })
-}
-
-function searchVideo (url, search, field, end) {
-  if (!end) {
-    end = field
-    field = null
-  }
-
-  const path = '/api/v1/videos'
-  const req = request(url)
-              .get(path + '/search/' + search)
-              .set('Accept', 'application/json')
-
-  if (field) req.query({ field: field })
-  req.expect(200)
-     .expect('Content-Type', /json/)
-     .end(end)
-}
-
-function searchVideoWithPagination (url, search, field, start, count, end) {
-  const path = '/api/v1/videos'
-
-  request(url)
-    .get(path + '/search/' + search)
-    .query({ start: start })
-    .query({ count: count })
-    .query({ field: field })
-    .set('Accept', 'application/json')
-    .expect(200)
-    .expect('Content-Type', /json/)
-    .end(end)
-}
-
-function searchVideoWithSort (url, search, sort, end) {
-  const path = '/api/v1/videos'
-
-  request(url)
-    .get(path + '/search/' + search)
-    .query({ sort: sort })
-    .set('Accept', 'application/json')
-    .expect(200)
-    .expect('Content-Type', /json/)
-    .end(end)
-}
-
-function testImage (url, videoName, imagePath, callback) {
-  // Don't test images if the node env is not set
-  // Because we need a special ffmpeg version for this test
-  if (process.env.NODE_TEST_IMAGE) {
-    request(url)
-      .get(imagePath)
-      .expect(200)
-      .end(function (err, res) {
-        if (err) return callback(err)
-
-        fs.readFile(pathUtils.join(__dirname, 'fixtures', videoName + '.jpg'), function (err, data) {
-          if (err) return callback(err)
-
-          callback(null, data.equals(res.body))
-        })
-      })
-  } else {
-    console.log('Do not test images. Enable it by setting NODE_TEST_IMAGE env variable.')
-    callback(null, true)
-  }
-}
-
-function uploadVideo (url, accessToken, name, description, tags, fixture, specialStatus, end) {
-  if (!end) {
-    end = specialStatus
-    specialStatus = 204
-  }
-
-  const path = '/api/v1/videos'
-
-  const req = request(url)
-              .post(path)
-              .set('Accept', 'application/json')
-              .set('Authorization', 'Bearer ' + accessToken)
-              .field('name', name)
-              .field('description', description)
-
-  for (let i = 0; i < tags.length; i++) {
-    req.field('tags[' + i + ']', tags[i])
-  }
-
-  let filepath = ''
-  if (pathUtils.isAbsolute(fixture)) {
-    filepath = fixture
-  } else {
-    filepath = pathUtils.join(__dirname, 'fixtures', fixture)
-  }
-
-  req.attach('videofile', filepath)
-     .expect(specialStatus)
-     .end(end)
-}
-
-// ---------------------------------------------------------------------------
-
-module.exports = testUtils
index b28796852911087a16c5c725df67e28511ca88c3..dba1970c58f9169e39cae38497b3b388bbfc02eb 100644 (file)
@@ -1,6 +1,6 @@
 'use strict'
 
-const each = require('each')
+const each = require('async/each')
 const isEqual = require('lodash/isEqual')
 const program = require('commander')
 const series = require('async/series')
@@ -8,7 +8,10 @@ const series = require('async/series')
 process.env.NODE_ENV = 'test'
 const constants = require('../../initializers/constants')
 
-const utils = require('../api/utils')
+const loginUtils = require('../utils/login')
+const podsUtils = require('../utils/pods')
+const serversUtils = require('../utils/servers')
+const videosUtils = require('../utils/videos')
 
 program
   .option('-c, --create [weight]', 'Weight for creating videos')
@@ -97,7 +100,7 @@ function runServers (numberOfPods, callback) {
   series([
     // Run servers
     function (next) {
-      utils.flushAndRunMultipleServers(numberOfPods, function (serversRun) {
+      serversUtils.flushAndRunMultipleServers(numberOfPods, function (serversRun) {
         servers = serversRun
         next()
       })
@@ -105,7 +108,7 @@ function runServers (numberOfPods, callback) {
     // Get the access tokens
     function (next) {
       each(servers, function (server, callbackEach) {
-        utils.loginAndGetAccessToken(server, function (err, accessToken) {
+        loginUtils.loginAndGetAccessToken(server, function (err, accessToken) {
           if (err) return callbackEach(err)
 
           server.accessToken = accessToken
@@ -115,26 +118,26 @@ function runServers (numberOfPods, callback) {
     },
     function (next) {
       const server = servers[1]
-      utils.makeFriends(server.url, server.accessToken, next)
+      podsUtils.makeFriends(server.url, server.accessToken, next)
     },
     function (next) {
       const server = servers[0]
-      utils.makeFriends(server.url, server.accessToken, next)
+      podsUtils.makeFriends(server.url, server.accessToken, next)
     },
     function (next) {
       setTimeout(next, 1000)
     },
     function (next) {
       const server = servers[3]
-      utils.makeFriends(server.url, server.accessToken, next)
+      podsUtils.makeFriends(server.url, server.accessToken, next)
     },
     function (next) {
       const server = servers[5]
-      utils.makeFriends(server.url, server.accessToken, next)
+      podsUtils.makeFriends(server.url, server.accessToken, next)
     },
     function (next) {
       const server = servers[4]
-      utils.makeFriends(server.url, server.accessToken, next)
+      podsUtils.makeFriends(server.url, server.accessToken, next)
     },
     function (next) {
       setTimeout(next, 1000)
@@ -151,7 +154,7 @@ function exitServers (servers, callback) {
     if (server.app) process.kill(-server.app.pid)
   })
 
-  if (flushAtExit) utils.flushTests(callback)
+  if (flushAtExit) serversUtils.flushTests(callback)
 }
 
 function upload (servers, numServer, callback) {
@@ -164,13 +167,13 @@ function upload (servers, numServer, callback) {
 
   console.log('Upload video to server ' + numServer)
 
-  utils.uploadVideo(servers[numServer].url, servers[numServer].accessToken, name, description, tags, file, callback)
+  videosUtils.uploadVideo(servers[numServer].url, servers[numServer].accessToken, name, description, tags, file, callback)
 }
 
 function remove (servers, numServer, callback) {
   if (!callback) callback = function () {}
 
-  utils.getVideosList(servers[numServer].url, function (err, res) {
+  videosUtils.getVideosList(servers[numServer].url, function (err, res) {
     if (err) throw err
 
     const videos = res.body.data
@@ -179,14 +182,14 @@ function remove (servers, numServer, callback) {
     const toRemove = videos[getRandomInt(0, videos.length)].id
 
     console.log('Removing video from server ' + numServer)
-    utils.removeVideo(servers[numServer].url, servers[numServer].accessToken, toRemove, callback)
+    videosUtils.removeVideo(servers[numServer].url, servers[numServer].accessToken, toRemove, callback)
   })
 }
 
 function checkIntegrity (servers, callback) {
   const videos = []
   each(servers, function (server, callback) {
-    utils.getAllVideosListBy(server.url, function (err, res) {
+    videosUtils.getAllVideosListBy(server.url, function (err, res) {
       if (err) throw err
       const serverVideos = res.body.data
       for (const serverVideo of serverVideos) {
diff --git a/server/tests/utils/clients.js b/server/tests/utils/clients.js
new file mode 100644 (file)
index 0000000..e3ded49
--- /dev/null
@@ -0,0 +1,24 @@
+'use strict'
+
+const request = require('supertest')
+
+const clientsUtils = {
+  getClient: getClient
+}
+
+// ---------------------- Export functions --------------------
+
+function getClient (url, end) {
+  const path = '/api/v1/users/client'
+
+  request(url)
+    .get(path)
+    .set('Accept', 'application/json')
+    .expect(200)
+    .expect('Content-Type', /json/)
+    .end(end)
+}
+
+// ---------------------------------------------------------------------------
+
+module.exports = clientsUtils
diff --git a/server/tests/utils/login.js b/server/tests/utils/login.js
new file mode 100644 (file)
index 0000000..465564e
--- /dev/null
@@ -0,0 +1,48 @@
+'use strict'
+
+const request = require('supertest')
+
+const loginUtils = {
+  login,
+  loginAndGetAccessToken
+}
+
+// ---------------------- Export functions --------------------
+
+function login (url, client, user, expectedStatus, end) {
+  if (!end) {
+    end = expectedStatus
+    expectedStatus = 200
+  }
+
+  const path = '/api/v1/users/token'
+
+  const body = {
+    client_id: client.id,
+    client_secret: client.secret,
+    username: user.username,
+    password: user.password,
+    response_type: 'code',
+    grant_type: 'password',
+    scope: 'upload'
+  }
+
+  request(url)
+    .post(path)
+    .type('form')
+    .send(body)
+    .expect(expectedStatus)
+    .end(end)
+}
+
+function loginAndGetAccessToken (server, callback) {
+  login(server.url, server.client, server.user, 200, function (err, res) {
+    if (err) return callback(err)
+
+    return callback(null, res.body.access_token)
+  })
+}
+
+// ---------------------------------------------------------------------------
+
+module.exports = loginUtils
diff --git a/server/tests/utils/miscs.js b/server/tests/utils/miscs.js
new file mode 100644 (file)
index 0000000..4ceff65
--- /dev/null
@@ -0,0 +1,21 @@
+'use strict'
+
+const miscsUtils = {
+  dateIsValid
+}
+
+// ---------------------- Export functions --------------------
+
+function dateIsValid (dateString) {
+  const dateToCheck = new Date(dateString)
+  const now = new Date()
+
+  // Check if the interval is more than 2 minutes
+  if (now - dateToCheck > 120000) return false
+
+  return true
+}
+
+// ---------------------------------------------------------------------------
+
+module.exports = miscsUtils
diff --git a/server/tests/utils/pods.js b/server/tests/utils/pods.js
new file mode 100644 (file)
index 0000000..a8551a4
--- /dev/null
@@ -0,0 +1,95 @@
+'use strict'
+
+const request = require('supertest')
+
+const podsUtils = {
+  getFriendsList,
+  makeFriends,
+  quitFriends
+}
+
+// ---------------------- Export functions --------------------
+
+function getFriendsList (url, end) {
+  const path = '/api/v1/pods/'
+
+  request(url)
+    .get(path)
+    .set('Accept', 'application/json')
+    .expect(200)
+    .expect('Content-Type', /json/)
+    .end(end)
+}
+
+function makeFriends (url, accessToken, expectedStatus, end) {
+  if (!end) {
+    end = expectedStatus
+    expectedStatus = 204
+  }
+
+  // Which pod makes friends with which pod
+  const friendsMatrix = {
+    'http://localhost:9001': [
+      'http://localhost:9002'
+    ],
+    'http://localhost:9002': [
+      'http://localhost:9003'
+    ],
+    'http://localhost:9003': [
+      'http://localhost:9001'
+    ],
+    'http://localhost:9004': [
+      'http://localhost:9002'
+    ],
+    'http://localhost:9005': [
+      'http://localhost:9001',
+      'http://localhost:9004'
+    ],
+    'http://localhost:9006': [
+      'http://localhost:9001',
+      'http://localhost:9002',
+      'http://localhost:9003'
+    ]
+  }
+  const path = '/api/v1/pods/makefriends'
+
+  // The first pod make friend with the third
+  request(url)
+    .post(path)
+    .set('Accept', 'application/json')
+    .set('Authorization', 'Bearer ' + accessToken)
+    .send({ 'urls': friendsMatrix[url] })
+    .expect(expectedStatus)
+    .end(function (err, res) {
+      if (err) throw err
+
+      // Wait for the request between pods
+      setTimeout(end, 1000)
+    })
+}
+
+function quitFriends (url, accessToken, expectedStatus, end) {
+  if (!end) {
+    end = expectedStatus
+    expectedStatus = 204
+  }
+
+  const path = '/api/v1/pods/quitfriends'
+
+  // The first pod make friend with the third
+  request(url)
+    .get(path)
+    .set('Accept', 'application/json')
+    .set('Authorization', 'Bearer ' + accessToken)
+    .expect(expectedStatus)
+    .end(function (err, res) {
+      if (err) throw err
+
+      // Wait for the request between pods
+      setTimeout(end, 1000)
+    })
+}
+
+// ---------------------------------------------------------------------------
+
+module.exports = podsUtils
diff --git a/server/tests/utils/requests.js b/server/tests/utils/requests.js
new file mode 100644 (file)
index 0000000..b147081
--- /dev/null
@@ -0,0 +1,68 @@
+'use strict'
+
+const request = require('supertest')
+
+const requestsUtils = {
+  makePostUploadRequest,
+  makePostBodyRequest,
+  makePutBodyRequest
+}
+
+// ---------------------- Export functions --------------------
+
+function makePostUploadRequest (url, path, token, fields, attaches, done, statusCodeExpected) {
+  if (!statusCodeExpected) statusCodeExpected = 400
+
+  const req = request(url)
+    .post(path)
+    .set('Accept', 'application/json')
+
+  if (token) req.set('Authorization', 'Bearer ' + token)
+
+  Object.keys(fields).forEach(function (field) {
+    const value = fields[field]
+
+    if (Array.isArray(value)) {
+      for (let i = 0; i < value.length; i++) {
+        req.field(field + '[' + i + ']', value[i])
+      }
+    } else {
+      req.field(field, value)
+    }
+  })
+
+  Object.keys(attaches).forEach(function (attach) {
+    const value = attaches[attach]
+    req.attach(attach, value)
+  })
+
+  req.expect(statusCodeExpected, done)
+}
+
+function makePostBodyRequest (url, path, token, fields, done, statusCodeExpected) {
+  if (!statusCodeExpected) statusCodeExpected = 400
+
+  const req = request(url)
+    .post(path)
+    .set('Accept', 'application/json')
+
+  if (token) req.set('Authorization', 'Bearer ' + token)
+
+  req.send(fields).expect(statusCodeExpected, done)
+}
+
+function makePutBodyRequest (url, path, token, fields, done, statusCodeExpected) {
+  if (!statusCodeExpected) statusCodeExpected = 400
+
+  const req = request(url)
+    .put(path)
+    .set('Accept', 'application/json')
+
+  if (token) req.set('Authorization', 'Bearer ' + token)
+
+  req.send(fields).expect(statusCodeExpected, done)
+}
+
+// ---------------------------------------------------------------------------
+
+module.exports = requestsUtils
diff --git a/server/tests/utils/servers.js b/server/tests/utils/servers.js
new file mode 100644 (file)
index 0000000..d62838b
--- /dev/null
@@ -0,0 +1,115 @@
+'use strict'
+
+const childProcess = require('child_process')
+const exec = childProcess.exec
+const fork = childProcess.fork
+const pathUtils = require('path')
+
+const serversUtils = {
+  flushAndRunMultipleServers,
+  flushTests,
+  runServer
+}
+
+// ---------------------- Export functions --------------------
+
+function flushAndRunMultipleServers (totalServers, serversRun) {
+  let apps = []
+  let urls = []
+  let i = 0
+
+  function anotherServerDone (number, app, url) {
+    apps[number - 1] = app
+    urls[number - 1] = url
+    i++
+    if (i === totalServers) {
+      serversRun(apps, urls)
+    }
+  }
+
+  flushTests(function () {
+    for (let j = 1; j <= totalServers; j++) {
+      // For the virtual buffer
+      setTimeout(function () {
+        runServer(j, function (app, url) {
+          anotherServerDone(j, app, url)
+        })
+      }, 1000 * j)
+    }
+  })
+}
+
+function flushTests (callback) {
+  exec('npm run clean:server:test', callback)
+}
+
+function runServer (number, callback) {
+  const server = {
+    app: null,
+    url: `http://localhost:${9000 + number}`,
+    client: {
+      id: null,
+      secret: null
+    },
+    user: {
+      username: null,
+      password: null
+    }
+  }
+
+  // These actions are async so we need to be sure that they have both been done
+  const serverRunString = {
+    'Connected to mongodb': false,
+    'Server listening on port': false
+  }
+
+  const regexps = {
+    client_id: 'Client id: ([a-f0-9]+)',
+    client_secret: 'Client secret: (.+)',
+    user_username: 'Username: (.+)',
+    user_password: 'User password: (.+)'
+  }
+
+  // Share the environment
+  const env = Object.create(process.env)
+  env.NODE_ENV = 'test'
+  env.NODE_APP_INSTANCE = number
+  const options = {
+    silent: true,
+    env: env,
+    detached: true
+  }
+
+  server.app = fork(pathUtils.join(__dirname, '../../../server.js'), [], options)
+  server.app.stdout.on('data', function onStdout (data) {
+    let dontContinue = false
+
+    // Capture things if we want to
+    for (const key of Object.keys(regexps)) {
+      const regexp = regexps[key]
+      const matches = data.toString().match(regexp)
+      if (matches !== null) {
+        if (key === 'client_id') server.client.id = matches[1]
+        else if (key === 'client_secret') server.client.secret = matches[1]
+        else if (key === 'user_username') server.user.username = matches[1]
+        else if (key === 'user_password') server.user.password = matches[1]
+      }
+    }
+
+    // Check if all required sentences are here
+    for (const key of Object.keys(serverRunString)) {
+      if (data.toString().indexOf(key) !== -1) serverRunString[key] = true
+      if (serverRunString[key] === false) dontContinue = true
+    }
+
+    // If no, there is maybe one thing not already initialized (mongodb...)
+    if (dontContinue === true) return
+
+    server.app.stdout.removeListener('data', onStdout)
+    callback(server)
+  })
+}
+
+// ---------------------------------------------------------------------------
+
+module.exports = serversUtils
diff --git a/server/tests/utils/users.js b/server/tests/utils/users.js
new file mode 100644 (file)
index 0000000..2bf9c6e
--- /dev/null
@@ -0,0 +1,100 @@
+'use strict'
+
+const request = require('supertest')
+
+const usersUtils = {
+  createUser,
+  getUserInformation,
+  getUsersList,
+  getUsersListPaginationAndSort,
+  removeUser,
+  updateUser
+}
+
+// ---------------------- Export functions --------------------
+
+function createUser (url, accessToken, username, password, specialStatus, end) {
+  if (!end) {
+    end = specialStatus
+    specialStatus = 204
+  }
+
+  const path = '/api/v1/users'
+
+  request(url)
+    .post(path)
+    .set('Accept', 'application/json')
+    .set('Authorization', 'Bearer ' + accessToken)
+    .send({ username: username, password: password })
+    .expect(specialStatus)
+    .end(end)
+}
+
+function getUserInformation (url, accessToken, end) {
+  const path = '/api/v1/users/me'
+
+  request(url)
+    .get(path)
+    .set('Accept', 'application/json')
+    .set('Authorization', 'Bearer ' + accessToken)
+    .expect(200)
+    .expect('Content-Type', /json/)
+    .end(end)
+}
+
+function getUsersList (url, end) {
+  const path = '/api/v1/users'
+
+  request(url)
+    .get(path)
+    .set('Accept', 'application/json')
+    .expect(200)
+    .expect('Content-Type', /json/)
+    .end(end)
+}
+
+function getUsersListPaginationAndSort (url, start, count, sort, end) {
+  const path = '/api/v1/users'
+
+  request(url)
+    .get(path)
+    .query({ start: start })
+    .query({ count: count })
+    .query({ sort: sort })
+    .set('Accept', 'application/json')
+    .expect(200)
+    .expect('Content-Type', /json/)
+    .end(end)
+}
+
+function removeUser (url, userId, accessToken, expectedStatus, end) {
+  if (!end) {
+    end = expectedStatus
+    expectedStatus = 204
+  }
+
+  const path = '/api/v1/users'
+
+  request(url)
+    .delete(path + '/' + userId)
+    .set('Accept', 'application/json')
+    .set('Authorization', 'Bearer ' + accessToken)
+    .expect(expectedStatus)
+    .end(end)
+}
+
+function updateUser (url, userId, accessToken, newPassword, end) {
+  const path = '/api/v1/users/' + userId
+
+  request(url)
+    .put(path)
+    .set('Accept', 'application/json')
+    .set('Authorization', 'Bearer ' + accessToken)
+    .send({ password: newPassword })
+    .expect(204)
+    .end(end)
+}
+
+// ---------------------------------------------------------------------------
+
+module.exports = usersUtils
diff --git a/server/tests/utils/videos.js b/server/tests/utils/videos.js
new file mode 100644 (file)
index 0000000..536093d
--- /dev/null
@@ -0,0 +1,199 @@
+'use strict'
+
+const fs = require('fs')
+const pathUtils = require('path')
+const request = require('supertest')
+
+const videosUtils = {
+  getAllVideosListBy,
+  getVideo,
+  getVideosList,
+  getVideosListPagination,
+  getVideosListSort,
+  removeVideo,
+  searchVideo,
+  searchVideoWithPagination,
+  searchVideoWithSort,
+  testVideoImage,
+  uploadVideo
+}
+
+// ---------------------- Export functions --------------------
+
+function getAllVideosListBy (url, end) {
+  const path = '/api/v1/videos'
+
+  request(url)
+    .get(path)
+    .query({ sort: 'createdDate' })
+    .query({ start: 0 })
+    .query({ count: 10000 })
+    .set('Accept', 'application/json')
+    .expect(200)
+    .expect('Content-Type', /json/)
+    .end(end)
+}
+
+function getVideo (url, id, end) {
+  const path = '/api/v1/videos/' + id
+
+  request(url)
+    .get(path)
+    .set('Accept', 'application/json')
+    .expect(200)
+    .expect('Content-Type', /json/)
+    .end(end)
+}
+
+function getVideosList (url, end) {
+  const path = '/api/v1/videos'
+
+  request(url)
+    .get(path)
+    .query({ sort: 'name' })
+    .set('Accept', 'application/json')
+    .expect(200)
+    .expect('Content-Type', /json/)
+    .end(end)
+}
+
+function getVideosListPagination (url, start, count, end) {
+  const path = '/api/v1/videos'
+
+  request(url)
+    .get(path)
+    .query({ start: start })
+    .query({ count: count })
+    .set('Accept', 'application/json')
+    .expect(200)
+    .expect('Content-Type', /json/)
+    .end(end)
+}
+
+function getVideosListSort (url, sort, end) {
+  const path = '/api/v1/videos'
+
+  request(url)
+    .get(path)
+    .query({ sort: sort })
+    .set('Accept', 'application/json')
+    .expect(200)
+    .expect('Content-Type', /json/)
+    .end(end)
+}
+
+function removeVideo (url, token, id, expectedStatus, end) {
+  if (!end) {
+    end = expectedStatus
+    expectedStatus = 204
+  }
+
+  const path = '/api/v1/videos'
+
+  request(url)
+    .delete(path + '/' + id)
+    .set('Accept', 'application/json')
+    .set('Authorization', 'Bearer ' + token)
+    .expect(expectedStatus)
+    .end(end)
+}
+
+function searchVideo (url, search, field, end) {
+  if (!end) {
+    end = field
+    field = null
+  }
+
+  const path = '/api/v1/videos'
+  const req = request(url)
+              .get(path + '/search/' + search)
+              .set('Accept', 'application/json')
+
+  if (field) req.query({ field: field })
+  req.expect(200)
+     .expect('Content-Type', /json/)
+     .end(end)
+}
+
+function searchVideoWithPagination (url, search, field, start, count, end) {
+  const path = '/api/v1/videos'
+
+  request(url)
+    .get(path + '/search/' + search)
+    .query({ start: start })
+    .query({ count: count })
+    .query({ field: field })
+    .set('Accept', 'application/json')
+    .expect(200)
+    .expect('Content-Type', /json/)
+    .end(end)
+}
+
+function searchVideoWithSort (url, search, sort, end) {
+  const path = '/api/v1/videos'
+
+  request(url)
+    .get(path + '/search/' + search)
+    .query({ sort: sort })
+    .set('Accept', 'application/json')
+    .expect(200)
+    .expect('Content-Type', /json/)
+    .end(end)
+}
+
+function testVideoImage (url, videoName, imagePath, callback) {
+  // Don't test images if the node env is not set
+  // Because we need a special ffmpeg version for this test
+  if (process.env.NODE_TEST_IMAGE) {
+    request(url)
+      .get(imagePath)
+      .expect(200)
+      .end(function (err, res) {
+        if (err) return callback(err)
+
+        fs.readFile(pathUtils.join(__dirname, '..', 'api', 'fixtures', videoName + '.jpg'), function (err, data) {
+          if (err) return callback(err)
+
+          callback(null, data.equals(res.body))
+        })
+      })
+  } else {
+    console.log('Do not test images. Enable it by setting NODE_TEST_IMAGE env variable.')
+    callback(null, true)
+  }
+}
+
+function uploadVideo (url, accessToken, name, description, tags, fixture, specialStatus, end) {
+  if (!end) {
+    end = specialStatus
+    specialStatus = 204
+  }
+
+  const path = '/api/v1/videos'
+
+  const req = request(url)
+              .post(path)
+              .set('Accept', 'application/json')
+              .set('Authorization', 'Bearer ' + accessToken)
+              .field('name', name)
+              .field('description', description)
+
+  for (let i = 0; i < tags.length; i++) {
+    req.field('tags[' + i + ']', tags[i])
+  }
+
+  let filepath = ''
+  if (pathUtils.isAbsolute(fixture)) {
+    filepath = fixture
+  } else {
+    filepath = pathUtils.join(__dirname, '..', 'api', 'fixtures', fixture)
+  }
+
+  req.attach('videofile', filepath)
+     .expect(specialStatus)
+     .end(end)
+}
+
+// ---------------------------------------------------------------------------
+
+module.exports = videosUtils