aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <florian.bigard@gmail.com>2016-10-02 15:39:09 +0200
committerChocobozzz <florian.bigard@gmail.com>2016-10-02 15:39:09 +0200
commita6375e69668ea42e19531c6bc68dcd37f3f7cbd7 (patch)
tree03204a408d56311692c3528bedcf95d2455e94f2
parent052937db8a8d282eccdbdf38d487ed8d85d3c0a7 (diff)
parentc4403b29ad4db097af528a7f04eea07e0ed320d0 (diff)
downloadPeerTube-a6375e69668ea42e19531c6bc68dcd37f3f7cbd7.tar.gz
PeerTube-a6375e69668ea42e19531c6bc68dcd37f3f7cbd7.tar.zst
PeerTube-a6375e69668ea42e19531c6bc68dcd37f3f7cbd7.zip
Merge branch 'master' into webseed-merged
-rw-r--r--.gitignore7
-rw-r--r--.travis.yml8
-rw-r--r--README.md19
-rw-r--r--client/config/helpers.js5
-rw-r--r--client/config/webpack.common.js399
-rw-r--r--client/config/webpack.dev.js218
-rw-r--r--client/config/webpack.prod.js357
-rw-r--r--client/package.json67
-rw-r--r--client/src/app/account/account.component.html27
-rw-r--r--client/src/app/account/account.component.ts67
-rw-r--r--client/src/app/account/account.routes.ts5
-rw-r--r--client/src/app/account/account.service.ts25
-rw-r--r--client/src/app/account/index.ts3
-rw-r--r--client/src/app/admin/admin.component.ts8
-rw-r--r--client/src/app/admin/admin.routes.ts23
-rw-r--r--client/src/app/admin/friends/friend-add/friend-add.component.html26
-rw-r--r--client/src/app/admin/friends/friend-add/friend-add.component.scss7
-rw-r--r--client/src/app/admin/friends/friend-add/friend-add.component.ts108
-rw-r--r--client/src/app/admin/friends/friend-add/index.ts1
-rw-r--r--client/src/app/admin/friends/friend-list/friend-list.component.html29
-rw-r--r--client/src/app/admin/friends/friend-list/friend-list.component.scss3
-rw-r--r--client/src/app/admin/friends/friend-list/friend-list.component.ts38
-rw-r--r--client/src/app/admin/friends/friend-list/index.ts1
-rw-r--r--client/src/app/admin/friends/friends.component.ts8
-rw-r--r--client/src/app/admin/friends/friends.routes.ts27
-rw-r--r--client/src/app/admin/friends/index.ts5
-rw-r--r--client/src/app/admin/friends/shared/friend.model.ts6
-rw-r--r--client/src/app/admin/friends/shared/friend.service.ts39
-rw-r--r--client/src/app/admin/friends/shared/index.ts (renamed from client/src/app/friends/index.ts)1
-rw-r--r--client/src/app/admin/index.ts6
-rw-r--r--client/src/app/admin/menu-admin.component.html26
-rw-r--r--client/src/app/admin/menu-admin.component.ts7
-rw-r--r--client/src/app/admin/requests/index.ts4
-rw-r--r--client/src/app/admin/requests/request-stats/index.ts1
-rw-r--r--client/src/app/admin/requests/request-stats/request-stats.component.html23
-rw-r--r--client/src/app/admin/requests/request-stats/request-stats.component.scss6
-rw-r--r--client/src/app/admin/requests/request-stats/request-stats.component.ts51
-rw-r--r--client/src/app/admin/requests/requests.component.ts8
-rw-r--r--client/src/app/admin/requests/requests.routes.ts22
-rw-r--r--client/src/app/admin/requests/shared/index.ts2
-rw-r--r--client/src/app/admin/requests/shared/request-stats.model.ts32
-rw-r--r--client/src/app/admin/requests/shared/request.service.ts22
-rw-r--r--client/src/app/admin/users/index.ts5
-rw-r--r--client/src/app/admin/users/shared/index.ts1
-rw-r--r--client/src/app/admin/users/shared/user.service.ts47
-rw-r--r--client/src/app/admin/users/user-add/index.ts1
-rw-r--r--client/src/app/admin/users/user-add/user-add.component.html29
-rw-r--r--client/src/app/admin/users/user-add/user-add.component.ts57
-rw-r--r--client/src/app/admin/users/user-list/index.ts1
-rw-r--r--client/src/app/admin/users/user-list/user-list.component.html28
-rw-r--r--client/src/app/admin/users/user-list/user-list.component.scss7
-rw-r--r--client/src/app/admin/users/user-list/user-list.component.ts42
-rw-r--r--client/src/app/admin/users/users.component.ts8
-rw-r--r--client/src/app/admin/users/users.routes.ts27
-rw-r--r--client/src/app/app.component.html38
-rw-r--r--client/src/app/app.component.scss34
-rw-r--r--client/src/app/app.component.ts69
-rw-r--r--client/src/app/app.module.ts146
-rw-r--r--client/src/app/app.routes.ts9
-rw-r--r--client/src/app/app.service.ts36
-rw-r--r--client/src/app/environment.ts50
-rw-r--r--client/src/app/friends/friend.service.ts29
-rw-r--r--client/src/app/index.ts1
-rw-r--r--client/src/app/login/login.component.html19
-rw-r--r--client/src/app/login/login.component.ts58
-rw-r--r--client/src/app/menu.component.html39
-rw-r--r--client/src/app/menu.component.ts45
-rw-r--r--client/src/app/shared/auth/auth-http.service.ts19
-rw-r--r--client/src/app/shared/auth/auth-user.model.ts (renamed from client/src/app/shared/auth/user.model.ts)31
-rw-r--r--client/src/app/shared/auth/auth.service.ts94
-rw-r--r--client/src/app/shared/auth/index.ts2
-rw-r--r--client/src/app/shared/forms/form-reactive.ts24
-rw-r--r--client/src/app/shared/forms/form-validators/index.ts3
-rw-r--r--client/src/app/shared/forms/form-validators/url.validator.ts11
-rw-r--r--client/src/app/shared/forms/form-validators/user.ts17
-rw-r--r--client/src/app/shared/forms/form-validators/video.ts25
-rw-r--r--client/src/app/shared/forms/index.ts2
-rw-r--r--client/src/app/shared/index.ts3
-rw-r--r--client/src/app/shared/rest/index.ts3
-rw-r--r--client/src/app/shared/rest/rest-extractor.service.ts52
-rw-r--r--client/src/app/shared/rest/rest-pagination.ts (renamed from client/src/app/videos/shared/pagination.model.ts)4
-rw-r--r--client/src/app/shared/rest/rest.service.ts27
-rw-r--r--client/src/app/shared/search/search.component.ts16
-rw-r--r--client/src/app/shared/search/search.service.ts3
-rw-r--r--client/src/app/shared/users/index.ts1
-rw-r--r--client/src/app/shared/users/user.model.ts20
-rw-r--r--client/src/app/videos/shared/index.ts1
-rw-r--r--client/src/app/videos/shared/loader/loader.component.ts4
-rw-r--r--client/src/app/videos/shared/video.service.ts62
-rw-r--r--client/src/app/videos/video-add/video-add.component.html30
-rw-r--r--client/src/app/videos/video-add/video-add.component.ts93
-rw-r--r--client/src/app/videos/video-list/video-list.component.ts35
-rw-r--r--client/src/app/videos/video-list/video-miniature.component.ts10
-rw-r--r--client/src/app/videos/video-list/video-sort.component.ts2
-rw-r--r--client/src/app/videos/video-watch/video-watch.component.ts34
-rw-r--r--client/src/app/videos/videos.component.ts4
-rw-r--r--client/src/app/videos/videos.routes.ts4
-rw-r--r--client/src/custom-typings.d.ts118
-rw-r--r--client/src/index.html1
-rw-r--r--client/src/main.ts42
-rw-r--r--client/src/polyfills.ts23
-rw-r--r--client/src/sass/application.scss39
-rw-r--r--client/src/vendor.ts4
-rw-r--r--client/tsconfig.json87
-rw-r--r--client/typings.json7
-rw-r--r--client/webpack.config.js6
-rw-r--r--config/default.yaml3
-rw-r--r--config/production.yaml.example4
-rw-r--r--config/test-1.yaml4
-rw-r--r--config/test-2.yaml4
-rw-r--r--config/test-3.yaml4
-rw-r--r--config/test-4.yaml4
-rw-r--r--config/test-5.yaml5
-rw-r--r--config/test-6.yaml6
-rw-r--r--package.json8
-rwxr-xr-xscripts/build/client/prod.sh2
-rw-r--r--server.js33
-rw-r--r--server/controllers/api/v1/clients.js41
-rw-r--r--server/controllers/api/v1/index.js4
-rw-r--r--server/controllers/api/v1/pods.js62
-rw-r--r--server/controllers/api/v1/remote.js1
-rw-r--r--server/controllers/api/v1/requests.js38
-rw-r--r--server/controllers/api/v1/users.js165
-rw-r--r--server/controllers/api/v1/videos.js7
-rw-r--r--server/helpers/custom-validators/index.js17
-rw-r--r--server/helpers/custom-validators/misc.js18
-rw-r--r--server/helpers/custom-validators/pods.js21
-rw-r--r--server/helpers/custom-validators/users.js31
-rw-r--r--server/helpers/custom-validators/videos.js (renamed from server/helpers/custom-validators.js)105
-rw-r--r--server/helpers/logger.js10
-rw-r--r--server/helpers/peertube-crypto.js60
-rw-r--r--server/helpers/requests.js15
-rw-r--r--server/helpers/utils.js4
-rw-r--r--server/initializers/checker.js14
-rw-r--r--server/initializers/constants.js175
-rw-r--r--server/initializers/database.js11
-rw-r--r--server/initializers/installer.js22
-rw-r--r--server/initializers/migrations/0005-create-application.js17
-rw-r--r--server/initializers/migrations/0010-users-password.js22
-rw-r--r--server/initializers/migrations/0015-admin-role.js16
-rw-r--r--server/initializers/migrator.js56
-rw-r--r--server/lib/friends.js34
-rw-r--r--server/lib/oauth-model.js29
-rw-r--r--server/middlewares/admin.js22
-rw-r--r--server/middlewares/index.js24
-rw-r--r--server/middlewares/oauth.js6
-rw-r--r--server/middlewares/pagination.js2
-rw-r--r--server/middlewares/pods.js62
-rw-r--r--server/middlewares/search.js2
-rw-r--r--server/middlewares/secure.js42
-rw-r--r--server/middlewares/sort.js9
-rw-r--r--server/middlewares/validators/index.js2
-rw-r--r--server/middlewares/validators/pagination.js2
-rw-r--r--server/middlewares/validators/pods.js34
-rw-r--r--server/middlewares/validators/remote.js7
-rw-r--r--server/middlewares/validators/sort.js13
-rw-r--r--server/middlewares/validators/users.js67
-rw-r--r--server/middlewares/validators/utils.js2
-rw-r--r--server/middlewares/validators/videos.js16
-rw-r--r--server/models/application.js31
-rw-r--r--server/models/oauth-client.js6
-rw-r--r--server/models/oauth-token.js11
-rw-r--r--server/models/pods.js44
-rw-r--r--server/models/request.js43
-rw-r--r--server/models/user.js86
-rw-r--r--server/models/utils.js30
-rw-r--r--server/models/video.js85
-rw-r--r--server/tests/api/check-params.js746
-rw-r--r--server/tests/api/checkParams.js456
-rw-r--r--server/tests/api/friends-advanced.js (renamed from server/tests/api/friendsAdvanced.js)23
-rw-r--r--server/tests/api/friends-basic.js (renamed from server/tests/api/friendsBasic.js)59
-rw-r--r--server/tests/api/index.js10
-rw-r--r--server/tests/api/multiple-pods.js (renamed from server/tests/api/multiplePods.js)62
-rw-r--r--server/tests/api/requests.js128
-rw-r--r--server/tests/api/single-pod.js (renamed from server/tests/api/singlePod.js)101
-rw-r--r--server/tests/api/users.js210
-rw-r--r--server/tests/api/utils.js419
-rw-r--r--server/tests/real-world/real-world.js31
-rw-r--r--server/tests/utils/clients.js24
-rw-r--r--server/tests/utils/login.js48
-rw-r--r--server/tests/utils/miscs.js21
-rw-r--r--server/tests/utils/pods.js95
-rw-r--r--server/tests/utils/requests.js68
-rw-r--r--server/tests/utils/servers.js115
-rw-r--r--server/tests/utils/users.js100
-rw-r--r--server/tests/utils/videos.js199
186 files changed, 5670 insertions, 2501 deletions
diff --git a/.gitignore b/.gitignore
index ec5b4b2f0..7ca89dca8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,4 +14,11 @@ uploads
14thumbnails 14thumbnails
15config/production.yaml 15config/production.yaml
16ffmpeg 16ffmpeg
17<<<<<<< HEAD
17torrents 18torrents
19=======
20.tags
21*.sublime-project
22*.sublime-workspace
23torrents/
24>>>>>>> master
diff --git a/.travis.yml b/.travis.yml
index e6a92d883..7b025f0b9 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,8 +1,8 @@
1language: node_js 1language: node_js
2 2
3node_js: 3node_js:
4 - "4.4" 4 - "4.5"
5 - "6.2" 5 - "6.6"
6 6
7env: 7env:
8 - CXX=g++-4.8 8 - CXX=g++-4.8
@@ -19,8 +19,10 @@ sudo: false
19services: 19services:
20 - mongodb 20 - mongodb
21 21
22before_install: if [[ `npm -v` != 3* ]]; then npm i -g npm@3; fi
23
22before_script: 24before_script:
23 - npm install electron-prebuilt -g 25 - npm install electron -g
24 - npm run build 26 - npm run build
25 - wget --no-check-certificate "https://download.cpy.re/ffmpeg/ffmpeg-release-3.0.2-64bit-static.tar.xz" 27 - wget --no-check-certificate "https://download.cpy.re/ffmpeg/ffmpeg-release-3.0.2-64bit-static.tar.xz"
26 - tar xf ffmpeg-release-3.0.2-64bit-static.tar.xz 28 - tar xf ffmpeg-release-3.0.2-64bit-static.tar.xz
diff --git a/README.md b/README.md
index 1a3470711..777df6d7d 100644
--- a/README.md
+++ b/README.md
@@ -60,6 +60,7 @@ Want to see in action?
60 60
61 * 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) 61 * 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)
62 * You can find [a video](https://vimeo.com/164881662 "Yes Vimeo, please don't judge me") to see how the "decentralization feature" looks like 62 * You can find [a video](https://vimeo.com/164881662 "Yes Vimeo, please don't judge me") to see how the "decentralization feature" looks like
63 * 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.
63 64
64## Why 65## Why
65 66
@@ -95,10 +96,12 @@ Thanks to [WebTorrent](https://github.com/feross/webtorrent), we can make P2P (t
95- [ ] Validate the prototype (test PeerTube in a real world with many pods and videos) 96- [ ] Validate the prototype (test PeerTube in a real world with many pods and videos)
96- [ ] Manage API breaks 97- [ ] Manage API breaks
97- [ ] Add "DDOS" security (check if a pod don't send too many requests for example) 98- [ ] Add "DDOS" security (check if a pod don't send too many requests for example)
98- [ ] Admin panel 99- [X] Admin panel
99 - [ ] Stats about the network (how many friends, how many requests per hour...) 100 - [X] Stats
100 - [ ] Stats about videos 101 - [X] Friends list
101 - [ ] Manage users (create/remove) 102 - [X] Manage users (create/remove)
103- [ ] User playlists
104- [ ] User subscriptions (by tags, author...)
102 105
103 106
104## Installation 107## Installation
@@ -111,6 +114,7 @@ Thanks to [WebTorrent](https://github.com/feross/webtorrent), we can make P2P (t
111### Dependencies 114### Dependencies
112 115
113 * **NodeJS >= 4.2** 116 * **NodeJS >= 4.2**
117 * **npm >= 3.0**
114 * OpenSSL (cli) 118 * OpenSSL (cli)
115 * MongoDB 119 * MongoDB
116 * ffmpeg xvfb-run libgtk2.0-0 libgconf-2-4 libnss3 libasound2 libxtst6 libxss1 libnotify-bin (for electron) 120 * 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
123 127
124 # apt-get update 128 # apt-get update
125 # apt-get install ffmpeg mongodb openssl xvfb curl sudo git build-essential libgtk2.0-0 libgconf-2-4 libnss3 libasound2 libxtst6 libxss1 libnotify-bin 129 # apt-get install ffmpeg mongodb openssl xvfb curl sudo git build-essential libgtk2.0-0 libgconf-2-4 libnss3 libasound2 libxtst6 libxss1 libnotify-bin
126 # npm install -g electron-prebuilt 130 # npm install -g npm@3
131 # npm install -g electron
127 132
128#### Other distribution... (PR welcome) 133#### Other distribution... (PR welcome)
129 134
@@ -160,6 +165,10 @@ Finally, run the server with the `production` `NODE_ENV` variable set.
160 165
161 $ NODE_ENV=production npm start 166 $ NODE_ENV=production npm start
162 167
168**Nginx template** (reverse proxy): https://github.com/Chocobozzz/PeerTube/tree/master/support/nginx
169
170**Systemd template**: https://github.com/Chocobozzz/PeerTube/tree/master/support/systemd
171
163### Other commands 172### Other commands
164 173
165To print all available command run: 174To print all available command run:
diff --git a/client/config/helpers.js b/client/config/helpers.js
index 24d7dae9f..6268d2628 100644
--- a/client/config/helpers.js
+++ b/client/config/helpers.js
@@ -8,10 +8,15 @@ function hasProcessFlag (flag) {
8 return process.argv.join('').indexOf(flag) > -1 8 return process.argv.join('').indexOf(flag) > -1
9} 9}
10 10
11function isWebpackDevServer () {
12 return process.argv[1] && !!(/webpack-dev-server$/.exec(process.argv[1]))
13}
14
11function root (args) { 15function root (args) {
12 args = Array.prototype.slice.call(arguments, 0) 16 args = Array.prototype.slice.call(arguments, 0)
13 return path.join.apply(path, [ROOT].concat(args)) 17 return path.join.apply(path, [ROOT].concat(args))
14} 18}
15 19
16exports.hasProcessFlag = hasProcessFlag 20exports.hasProcessFlag = hasProcessFlag
21exports.isWebpackDevServer = isWebpackDevServer
17exports.root = root 22exports.root = root
diff --git a/client/config/webpack.common.js b/client/config/webpack.common.js
index 2ff3a1506..882013a9e 100644
--- a/client/config/webpack.common.js
+++ b/client/config/webpack.common.js
@@ -5,9 +5,11 @@ const helpers = require('./helpers')
5 * Webpack Plugins 5 * Webpack Plugins
6 */ 6 */
7 7
8var CopyWebpackPlugin = (CopyWebpackPlugin = require('copy-webpack-plugin'), CopyWebpackPlugin.default || CopyWebpackPlugin) 8const CopyWebpackPlugin = require('copy-webpack-plugin')
9const HtmlWebpackPlugin = require('html-webpack-plugin') 9const HtmlWebpackPlugin = require('html-webpack-plugin')
10const ForkCheckerPlugin = require('awesome-typescript-loader').ForkCheckerPlugin 10const ForkCheckerPlugin = require('awesome-typescript-loader').ForkCheckerPlugin
11const AssetsPlugin = require('assets-webpack-plugin')
12const ContextReplacementPlugin = require('webpack/lib/ContextReplacementPlugin')
11const WebpackNotifierPlugin = require('webpack-notifier') 13const WebpackNotifierPlugin = require('webpack-notifier')
12 14
13/* 15/*
@@ -15,7 +17,8 @@ const WebpackNotifierPlugin = require('webpack-notifier')
15 */ 17 */
16const METADATA = { 18const METADATA = {
17 title: 'PeerTube', 19 title: 'PeerTube',
18 baseUrl: '/' 20 baseUrl: '/',
21 isDevServer: helpers.isWebpackDevServer()
19} 22}
20 23
21/* 24/*
@@ -23,247 +26,241 @@ const METADATA = {
23 * 26 *
24 * See: http://webpack.github.io/docs/configuration.html#cli 27 * See: http://webpack.github.io/docs/configuration.html#cli
25 */ 28 */
26module.exports = { 29module.exports = function (options) {
27 /* 30 var isProd = options.env === 'production'
28 * Static metadata for index.html
29 *
30 * See: (custom attribute)
31 */
32 metadata: METADATA,
33 31
34 /* 32 return {
35 * Cache generated modules and chunks to improve performance for multiple incremental builds. 33 /*
36 * This is enabled by default in watch mode. 34 * Static metadata for index.html
37 * You can pass false to disable it. 35 *
38 * 36 * See: (custom attribute)
39 * See: http://webpack.github.io/docs/configuration.html#cache 37 */
40 */ 38 metadata: METADATA,
41 // cache: false,
42
43 /*
44 * The entry point for the bundle
45 * Our Angular.js app
46 *
47 * See: http://webpack.github.io/docs/configuration.html#entry
48 */
49 entry: {
50 'polyfills': './src/polyfills.ts',
51 'vendor': './src/vendor.ts',
52 'main': './src/main.ts'
53 },
54 39
55 /*
56 * Options affecting the resolving of modules.
57 *
58 * See: http://webpack.github.io/docs/configuration.html#resolve
59 */
60 resolve: {
61 /* 40 /*
62 * An array of extensions that should be used to resolve modules. 41 * Cache generated modules and chunks to improve performance for multiple incremental builds.
42 * This is enabled by default in watch mode.
43 * You can pass false to disable it.
63 * 44 *
64 * See: http://webpack.github.io/docs/configuration.html#resolve-extensions 45 * See: http://webpack.github.io/docs/configuration.html#cache
65 */ 46 */
66 extensions: [ '', '.ts', '.js', '.scss' ], 47 // cache: false,
67 48
68 // Make sure root is src 49 /*
69 root: helpers.root('src'), 50 * The entry point for the bundle
51 * Our Angular.js app
52 *
53 * See: http://webpack.github.io/docs/configuration.html#entry
54 */
55 entry: {
56 'polyfills': './src/polyfills.ts',
57 'vendor': './src/vendor.ts',
58 'main': './src/main.ts'
59 },
70 60
71 // remove other default values 61 /*
72 modulesDirectories: [ 'node_modules' ], 62 * Options affecting the resolving of modules.
63 *
64 * See: http://webpack.github.io/docs/configuration.html#resolve
65 */
66 resolve: {
67 /*
68 * An array of extensions that should be used to resolve modules.
69 *
70 * See: http://webpack.github.io/docs/configuration.html#resolve-extensions
71 */
72 extensions: [ '', '.ts', '.js', '.scss' ],
73 73
74 packageAlias: 'browser' 74 // Make sure root is src
75 root: helpers.root('src'),
75 76
76 }, 77 // remove other default values
78 modulesDirectories: [ 'node_modules' ]
79 },
77 80
78 output: { 81 output: {
79 publicPath: '/client/' 82 publicPath: '/client/'
80 }, 83 },
81 84
82 /*
83 * Options affecting the normal modules.
84 *
85 * See: http://webpack.github.io/docs/configuration.html#module
86 */
87 module: {
88 /* 85 /*
89 * An array of applied pre and post loaders. 86 * Options affecting the normal modules.
90 * 87 *
91 * See: http://webpack.github.io/docs/configuration.html#module-preloaders-module-postloaders 88 * See: http://webpack.github.io/docs/configuration.html#module
92 */ 89 */
93 preLoaders: [ 90 module: {
94
95 /* 91 /*
96 * Tslint loader support for *.ts files 92 * An array of applied pre and post loaders.
97 * 93 *
98 * See: https://github.com/wbuchwalter/tslint-loader 94 * See: http://webpack.github.io/docs/configuration.html#module-preloaders-module-postloaders
99 */ 95 */
100 // { test: /\.ts$/, loader: 'tslint-loader', exclude: [ helpers.root('node_modules') ] }, 96 preLoaders: [
97 {
98 test: /\.ts$/,
99 loader: 'string-replace-loader',
100 query: {
101 search: '(System|SystemJS)(.*[\\n\\r]\\s*\\.|\\.)import\\((.+)\\)',
102 replace: '$1.import($3).then(mod => (mod.__esModule && mod.default) ? mod.default : mod)',
103 flags: 'g'
104 },
105 include: [helpers.root('src')]
106 }
107 ],
101 108
102 /* 109 /*
103 * Source map loader support for *.js files 110 * An array of automatically applied loaders.
104 * Extracts SourceMaps for source files that as added as sourceMappingURL comment. 111 *
112 * IMPORTANT: The loaders here are resolved relative to the resource which they are applied to.
113 * This means they are not resolved relative to the configuration file.
105 * 114 *
106 * See: https://github.com/webpack/source-map-loader 115 * See: http://webpack.github.io/docs/configuration.html#module-loaders
107 */ 116 */
108 { 117 loaders: [
109 test: /\.js$/, 118
110 loader: 'source-map-loader', 119 /*
111 exclude: [ 120 * Typescript loader support for .ts and Angular 2 async routes via .async.ts
112 // these packages have problems with their sourcemaps 121 *
113 helpers.root('node_modules/rxjs'), 122 * See: https://github.com/s-panferov/awesome-typescript-loader
114 helpers.root('node_modules/@angular') 123 */
115 ] 124 {
116 } 125 test: /\.ts$/,
117 126 loaders: [
118 ], 127 '@angularclass/hmr-loader?pretty=' + !isProd + '&prod=' + isProd,
128 'awesome-typescript-loader',
129 'angular2-template-loader'
130 ],
131 exclude: [/\.(spec|e2e)\.ts$/]
132 },
133
134 /*
135 * Json loader support for *.json files.
136 *
137 * See: https://github.com/webpack/json-loader
138 */
139 {
140 test: /\.json$/,
141 loader: 'json-loader'
142 },
143
144 {
145 test: /\.(sass|scss)$/,
146 loaders: ['css-to-string-loader', 'css-loader?sourceMap', 'resolve-url', 'sass-loader?sourceMap']
147 },
148 { test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'url?limit=10000&minetype=application/font-woff' },
149 { test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'file' },
150
151 /* Raw loader support for *.html
152 * Returns file content as string
153 *
154 * See: https://github.com/webpack/raw-loader
155 */
156 {
157 test: /\.html$/,
158 loader: 'raw-loader',
159 exclude: [ helpers.root('src/index.html') ]
160 }
161
162 ]
163
164 },
165
166 sassLoader: {
167 precision: 10
168 },
119 169
120 /* 170 /*
121 * An array of automatically applied loaders. 171 * Add additional plugins to the compiler.
122 *
123 * IMPORTANT: The loaders here are resolved relative to the resource which they are applied to.
124 * This means they are not resolved relative to the configuration file.
125 * 172 *
126 * See: http://webpack.github.io/docs/configuration.html#module-loaders 173 * See: http://webpack.github.io/docs/configuration.html#plugins
127 */ 174 */
128 loaders: [ 175 plugins: [
176 new AssetsPlugin({
177 path: helpers.root('dist'),
178 filename: 'webpack-assets.json',
179 prettyPrint: true
180 }),
129 181
130 /* 182 /*
131 * Typescript loader support for .ts and Angular 2 async routes via .async.ts 183 * Plugin: ForkCheckerPlugin
184 * Description: Do type checking in a separate process, so webpack don't need to wait.
132 * 185 *
133 * See: https://github.com/s-panferov/awesome-typescript-loader 186 * See: https://github.com/s-panferov/awesome-typescript-loader#forkchecker-boolean-defaultfalse
134 */ 187 */
135 { 188 new ForkCheckerPlugin(),
136 test: /\.ts$/,
137 loader: 'awesome-typescript-loader',
138 exclude: [/\.(spec|e2e)\.ts$/]
139 },
140 189
141 /* 190 /*
142 * Json loader support for *.json files. 191 * Plugin: CommonsChunkPlugin
192 * Description: Shares common code between the pages.
193 * It identifies common modules and put them into a commons chunk.
143 * 194 *
144 * See: https://github.com/webpack/json-loader 195 * See: https://webpack.github.io/docs/list-of-plugins.html#commonschunkplugin
196 * See: https://github.com/webpack/docs/wiki/optimization#multi-page-app
145 */ 197 */
146 { 198 new webpack.optimize.CommonsChunkPlugin({
147 test: /\.json$/, 199 name: [ 'polyfills', 'vendor' ].reverse()
148 loader: 'json-loader' 200 }),
149 },
150 201
151 { 202 /**
152 test: /\.scss$/, 203 * Plugin: ContextReplacementPlugin
153 exclude: /node_modules/, 204 * Description: Provides context to Angular's use of System.import
154 loaders: [ 'raw-loader', 'sass-loader' ] 205 *
155 }, 206 * See: https://webpack.github.io/docs/list-of-plugins.html#contextreplacementplugin
156 207 * See: https://github.com/angular/angular/issues/11580
157 { 208 */
158 test: /\.(woff2?|ttf|eot|svg)$/, 209 new ContextReplacementPlugin(
159 loader: 'url?limit=10000&name=assets/fonts/[hash].[ext]' 210 // The (\\|\/) piece accounts for path separators in *nix and Windows
160 }, 211 /angular(\\|\/)core(\\|\/)(esm(\\|\/)src|src)(\\|\/)linker/,
212 helpers.root('src') // location of your src
213 ),
161 214
162 /* Raw loader support for *.html 215 /*
163 * Returns file content as string 216 * Plugin: CopyWebpackPlugin
217 * Description: Copy files and directories in webpack.
164 * 218 *
165 * See: https://github.com/webpack/raw-loader 219 * Copies project static assets.
220 *
221 * See: https://www.npmjs.com/package/copy-webpack-plugin
166 */ 222 */
167 { 223 new CopyWebpackPlugin([
168 test: /\.html$/, 224 {
169 loader: 'raw-loader', 225 from: 'src/assets',
170 exclude: [ helpers.root('src/index.html') ] 226 to: 'assets'
171 } 227 },
172 228 {
173 ] 229 from: 'node_modules/webtorrent/webtorrent.min.js',
174 230 to: 'assets/webtorrent'
175 }, 231 }
232 ]),
176 233
177 sassLoader: { 234 /*
178 precision: 10 235 * Plugin: HtmlWebpackPlugin
179 }, 236 * Description: Simplifies creation of HTML files to serve your webpack bundles.
180 237 * This is especially useful for webpack bundles that include a hash in the filename
181 /* 238 * which changes every compilation.
182 * Add additional plugins to the compiler. 239 *
183 * 240 * See: https://github.com/ampedandwired/html-webpack-plugin
184 * See: http://webpack.github.io/docs/configuration.html#plugins 241 */
185 */ 242 new HtmlWebpackPlugin({
186 plugins: [ 243 template: 'src/index.html',
187 244 chunksSortMode: 'dependency'
188 /* 245 }),
189 * Plugin: ForkCheckerPlugin
190 * Description: Do type checking in a separate process, so webpack don't need to wait.
191 *
192 * See: https://github.com/s-panferov/awesome-typescript-loader#forkchecker-boolean-defaultfalse
193 */
194 new ForkCheckerPlugin(),
195
196 /*
197 * Plugin: OccurenceOrderPlugin
198 * Description: Varies the distribution of the ids to get the smallest id length
199 * for often used ids.
200 *
201 * See: https://webpack.github.io/docs/list-of-plugins.html#occurrenceorderplugin
202 * See: https://github.com/webpack/docs/wiki/optimization#minimize
203 */
204 new webpack.optimize.OccurenceOrderPlugin(true),
205
206 /*
207 * Plugin: CommonsChunkPlugin
208 * Description: Shares common code between the pages.
209 * It identifies common modules and put them into a commons chunk.
210 *
211 * See: https://webpack.github.io/docs/list-of-plugins.html#commonschunkplugin
212 * See: https://github.com/webpack/docs/wiki/optimization#multi-page-app
213 */
214 new webpack.optimize.CommonsChunkPlugin({
215 name: [ 'polyfills', 'vendor' ].reverse()
216 }),
217 246
218 /* 247 new WebpackNotifierPlugin({ alwaysNotify: true })
219 * Plugin: CopyWebpackPlugin 248 ],
220 * Description: Copy files and directories in webpack.
221 *
222 * Copies project static assets.
223 *
224 * See: https://www.npmjs.com/package/copy-webpack-plugin
225 */
226 new CopyWebpackPlugin([
227 {
228 from: 'src/assets',
229 to: 'assets'
230 },
231 {
232 from: 'node_modules/webtorrent/webtorrent.min.js',
233 to: 'assets/webtorrent'
234 }
235 ]),
236 249
237 /* 250 /*
238 * Plugin: HtmlWebpackPlugin 251 * Include polyfills or mocks for various node stuff
239 * Description: Simplifies creation of HTML files to serve your webpack bundles. 252 * Description: Node configuration
240 * This is especially useful for webpack bundles that include a hash in the filename
241 * which changes every compilation.
242 * 253 *
243 * See: https://github.com/ampedandwired/html-webpack-plugin 254 * See: https://webpack.github.io/docs/configuration.html#node
244 */ 255 */
245 new HtmlWebpackPlugin({ 256 node: {
246 template: 'src/index.html', 257 global: 'window',
247 chunksSortMode: 'dependency' 258 crypto: 'empty',
248 }), 259 fs: 'empty',
249 260 events: true,
250 new WebpackNotifierPlugin({ alwaysNotify: true }) 261 module: false,
251 ], 262 clearImmediate: false,
252 263 setImmediate: false
253 /* 264 }
254 * Include polyfills or mocks for various node stuff
255 * Description: Node configuration
256 *
257 * See: https://webpack.github.io/docs/configuration.html#node
258 */
259 node: {
260 global: 'window',
261 crypto: 'empty',
262 fs: 'empty',
263 events: true,
264 module: false,
265 clearImmediate: false,
266 setImmediate: false
267 } 265 }
268
269} 266}
diff --git a/client/config/webpack.dev.js b/client/config/webpack.dev.js
index 50193bf58..0b6c00cbd 100644
--- a/client/config/webpack.dev.js
+++ b/client/config/webpack.dev.js
@@ -6,15 +6,18 @@ const commonConfig = require('./webpack.common.js') // the settings that are com
6 * Webpack Plugins 6 * Webpack Plugins
7 */ 7 */
8const DefinePlugin = require('webpack/lib/DefinePlugin') 8const DefinePlugin = require('webpack/lib/DefinePlugin')
9const NamedModulesPlugin = require('webpack/lib/NamedModulesPlugin')
9 10
10/** 11/**
11 * Webpack Constants 12 * Webpack Constants
12 */ 13 */
13const ENV = process.env.ENV = process.env.NODE_ENV = 'development' 14const ENV = process.env.ENV = process.env.NODE_ENV = 'development'
15const HOST = process.env.HOST || 'localhost'
16const PORT = process.env.PORT || 3000
14const HMR = helpers.hasProcessFlag('hot') 17const HMR = helpers.hasProcessFlag('hot')
15const METADATA = webpackMerge(commonConfig.metadata, { 18const METADATA = webpackMerge(commonConfig({env: ENV}).metadata, {
16 host: 'localhost', 19 host: HOST,
17 port: 3000, 20 port: PORT,
18 ENV: ENV, 21 ENV: ENV,
19 HMR: HMR 22 HMR: HMR
20}) 23})
@@ -24,119 +27,136 @@ const METADATA = webpackMerge(commonConfig.metadata, {
24 * 27 *
25 * See: http://webpack.github.io/docs/configuration.html#cli 28 * See: http://webpack.github.io/docs/configuration.html#cli
26 */ 29 */
27module.exports = webpackMerge(commonConfig, { 30module.exports = function (env) {
28 /** 31 return webpackMerge(commonConfig({env: ENV}), {
29 * Merged metadata from webpack.common.js for index.html
30 *
31 * See: (custom attribute)
32 */
33 metadata: METADATA,
34
35 /**
36 * Switch loaders to debug mode.
37 *
38 * See: http://webpack.github.io/docs/configuration.html#debug
39 */
40 debug: true,
41
42 /**
43 * Developer tool to enhance debugging
44 *
45 * See: http://webpack.github.io/docs/configuration.html#devtool
46 * See: https://github.com/webpack/docs/wiki/build-performance#sourcemaps
47 */
48 devtool: 'cheap-module-source-map',
49
50 /**
51 * Options affecting the output of the compilation.
52 *
53 * See: http://webpack.github.io/docs/configuration.html#output
54 */
55 output: {
56 /** 32 /**
57 * The output directory as absolute path (required). 33 * Merged metadata from webpack.common.js for index.html
58 * 34 *
59 * See: http://webpack.github.io/docs/configuration.html#output-path 35 * See: (custom attribute)
60 */ 36 */
61 path: helpers.root('dist'), 37 metadata: METADATA,
62 38
63 /** 39 /**
64 * Specifies the name of each output file on disk. 40 * Switch loaders to debug mode.
65 * IMPORTANT: You must not specify an absolute path here!
66 * 41 *
67 * See: http://webpack.github.io/docs/configuration.html#output-filename 42 * See: http://webpack.github.io/docs/configuration.html#debug
68 */ 43 */
69 filename: '[name].bundle.js', 44 debug: true,
70 45
71 /** 46 /**
72 * The filename of the SourceMaps for the JavaScript files. 47 * Developer tool to enhance debugging
73 * They are inside the output.path directory.
74 * 48 *
75 * See: http://webpack.github.io/docs/configuration.html#output-sourcemapfilename 49 * See: http://webpack.github.io/docs/configuration.html#devtool
50 * See: https://github.com/webpack/docs/wiki/build-performance#sourcemaps
76 */ 51 */
77 sourceMapFilename: '[name].map', 52 devtool: 'cheap-module-source-map',
78 53
79 /** The filename of non-entry chunks as relative path 54 /**
80 * inside the output.path directory. 55 * Options affecting the output of the compilation.
81 * 56 *
82 * See: http://webpack.github.io/docs/configuration.html#output-chunkfilename 57 * See: http://webpack.github.io/docs/configuration.html#output
83 */ 58 */
84 chunkFilename: '[id].chunk.js' 59 output: {
85 60 /**
86 }, 61 * The output directory as absolute path (required).
87 62 *
88 externals: { 63 * See: http://webpack.github.io/docs/configuration.html#output-path
89 webtorrent: 'WebTorrent' 64 */
90 }, 65 path: helpers.root('dist'),
66
67 /**
68 * Specifies the name of each output file on disk.
69 * IMPORTANT: You must not specify an absolute path here!
70 *
71 * See: http://webpack.github.io/docs/configuration.html#output-filename
72 */
73 filename: '[name].bundle.js',
74
75 /**
76 * The filename of the SourceMaps for the JavaScript files.
77 * They are inside the output.path directory.
78 *
79 * See: http://webpack.github.io/docs/configuration.html#output-sourcemapfilename
80 */
81 sourceMapFilename: '[name].map',
82
83 /** The filename of non-entry chunks as relative path
84 * inside the output.path directory.
85 *
86 * See: http://webpack.github.io/docs/configuration.html#output-chunkfilename
87 */
88 chunkFilename: '[id].chunk.js',
89
90 library: 'ac_[name]',
91 libraryTarget: 'var'
92
93 },
94
95 externals: {
96 webtorrent: 'WebTorrent'
97 },
98
99 plugins: [
100
101 /**
102 * Plugin: DefinePlugin
103 * Description: Define free variables.
104 * Useful for having development builds with debug logging or adding global constants.
105 *
106 * Environment helpers
107 *
108 * See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin
109 */
110 // NOTE: when adding more properties, make sure you include them in custom-typings.d.ts
111 new DefinePlugin({
112 'ENV': JSON.stringify(METADATA.ENV),
113 'HMR': METADATA.HMR,
114 'process.env': {
115 'ENV': JSON.stringify(METADATA.ENV),
116 'NODE_ENV': JSON.stringify(METADATA.ENV),
117 'HMR': METADATA.HMR
118 }
119 }),
91 120
92 plugins: [ 121 new NamedModulesPlugin()
122 ],
93 123
94 /** 124 /**
95 * Plugin: DefinePlugin 125 * Static analysis linter for TypeScript advanced options configuration
96 * Description: Define free variables. 126 * Description: An extensible linter for the TypeScript language.
97 * Useful for having development builds with debug logging or adding global constants.
98 * 127 *
99 * Environment helpers 128 * See: https://github.com/wbuchwalter/tslint-loader
129 */
130 tslint: {
131 emitErrors: false,
132 failOnHint: false,
133 resourcePath: 'src'
134 },
135
136 devServer: {
137 port: METADATA.port,
138 host: METADATA.host,
139 historyApiFallback: true,
140 watchOptions: {
141 aggregateTimeout: 300,
142 poll: 1000
143 },
144 outputPath: helpers.root('dist')
145 },
146
147 /*
148 * Include polyfills or mocks for various node stuff
149 * Description: Node configuration
100 * 150 *
101 * See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin 151 * See: https://webpack.github.io/docs/configuration.html#node
102 */ 152 */
103 // NOTE: when adding more properties, make sure you include them in custom-typings.d.ts 153 node: {
104 new DefinePlugin({ 154 global: 'window',
105 'ENV': JSON.stringify(METADATA.ENV), 155 crypto: 'empty',
106 'HMR': METADATA.HMR, 156 process: true,
107 'process.env': { 157 module: false,
108 'ENV': JSON.stringify(METADATA.ENV), 158 clearImmediate: false,
109 'NODE_ENV': JSON.stringify(METADATA.ENV), 159 setImmediate: false
110 'HMR': METADATA.HMR 160 }
111 } 161 })
112 }) 162}
113 ],
114
115 /**
116 * Static analysis linter for TypeScript advanced options configuration
117 * Description: An extensible linter for the TypeScript language.
118 *
119 * See: https://github.com/wbuchwalter/tslint-loader
120 */
121 tslint: {
122 emitErrors: false,
123 failOnHint: false,
124 resourcePath: 'src'
125 },
126
127 /*
128 * Include polyfills or mocks for various node stuff
129 * Description: Node configuration
130 *
131 * See: https://webpack.github.io/docs/configuration.html#node
132 */
133 node: {
134 global: 'window',
135 crypto: 'empty',
136 process: true,
137 module: false,
138 clearImmediate: false,
139 setImmediate: false
140 }
141
142})
diff --git a/client/config/webpack.prod.js b/client/config/webpack.prod.js
index 7ce5727d3..46db54482 100644
--- a/client/config/webpack.prod.js
+++ b/client/config/webpack.prod.js
@@ -9,10 +9,12 @@ const commonConfig = require('./webpack.common.js') // the settings that are com
9/** 9/**
10 * Webpack Plugins 10 * Webpack Plugins
11 */ 11 */
12// const ProvidePlugin = require('webpack/lib/ProvidePlugin')
12const DefinePlugin = require('webpack/lib/DefinePlugin') 13const DefinePlugin = require('webpack/lib/DefinePlugin')
13const DedupePlugin = require('webpack/lib/optimize/DedupePlugin') 14const NormalModuleReplacementPlugin = require('webpack/lib/NormalModuleReplacementPlugin')
15// const IgnorePlugin = require('webpack/lib/IgnorePlugin')
16// const DedupePlugin = require('webpack/lib/optimize/DedupePlugin')
14const UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin') 17const UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin')
15const CompressionPlugin = require('compression-webpack-plugin')
16const WebpackMd5Hash = require('webpack-md5-hash') 18const WebpackMd5Hash = require('webpack-md5-hash')
17 19
18/** 20/**
@@ -21,211 +23,210 @@ const WebpackMd5Hash = require('webpack-md5-hash')
21const ENV = process.env.NODE_ENV = process.env.ENV = 'production' 23const ENV = process.env.NODE_ENV = process.env.ENV = 'production'
22const HOST = process.env.HOST || 'localhost' 24const HOST = process.env.HOST || 'localhost'
23const PORT = process.env.PORT || 8080 25const PORT = process.env.PORT || 8080
24const METADATA = webpackMerge(commonConfig.metadata, { 26const METADATA = webpackMerge(commonConfig({env: ENV}).metadata, {
25 host: HOST, 27 host: HOST,
26 port: PORT, 28 port: PORT,
27 ENV: ENV, 29 ENV: ENV,
28 HMR: false 30 HMR: false
29}) 31})
30 32
31module.exports = webpackMerge(commonConfig, { 33module.exports = function (env) {
32 /** 34 return webpackMerge(commonConfig({env: ENV}), {
33 * Switch loaders to debug mode.
34 *
35 * See: http://webpack.github.io/docs/configuration.html#debug
36 */
37 debug: false,
38
39 /**
40 * Developer tool to enhance debugging
41 *
42 * See: http://webpack.github.io/docs/configuration.html#devtool
43 * See: https://github.com/webpack/docs/wiki/build-performance#sourcemaps
44 */
45 devtool: 'source-map',
46
47 /**
48 * Options affecting the output of the compilation.
49 *
50 * See: http://webpack.github.io/docs/configuration.html#output
51 */
52 output: {
53 /** 35 /**
54 * The output directory as absolute path (required). 36 * Switch loaders to debug mode.
55 * 37 *
56 * See: http://webpack.github.io/docs/configuration.html#output-path 38 * See: http://webpack.github.io/docs/configuration.html#debug
57 */ 39 */
58 path: helpers.root('dist'), 40 debug: false,
59 41
60 /** 42 /**
61 * Specifies the name of each output file on disk. 43 * Developer tool to enhance debugging
62 * IMPORTANT: You must not specify an absolute path here!
63 * 44 *
64 * See: http://webpack.github.io/docs/configuration.html#output-filename 45 * See: http://webpack.github.io/docs/configuration.html#devtool
46 * See: https://github.com/webpack/docs/wiki/build-performance#sourcemaps
65 */ 47 */
66 filename: '[name].[chunkhash].bundle.js', 48 devtool: 'source-map',
67 49
68 /** 50 /**
69 * The filename of the SourceMaps for the JavaScript files. 51 * Options affecting the output of the compilation.
70 * They are inside the output.path directory.
71 * 52 *
72 * See: http://webpack.github.io/docs/configuration.html#output-sourcemapfilename 53 * See: http://webpack.github.io/docs/configuration.html#output
73 */ 54 */
74 sourceMapFilename: '[name].[chunkhash].bundle.map', 55 output: {
56 /**
57 * The output directory as absolute path (required).
58 *
59 * See: http://webpack.github.io/docs/configuration.html#output-path
60 */
61 path: helpers.root('dist'),
62
63 /**
64 * Specifies the name of each output file on disk.
65 * IMPORTANT: You must not specify an absolute path here!
66 *
67 * See: http://webpack.github.io/docs/configuration.html#output-filename
68 */
69 filename: '[name].[chunkhash].bundle.js',
70
71 /**
72 * The filename of the SourceMaps for the JavaScript files.
73 * They are inside the output.path directory.
74 *
75 * See: http://webpack.github.io/docs/configuration.html#output-sourcemapfilename
76 */
77 sourceMapFilename: '[name].[chunkhash].bundle.map',
78
79 /**
80 * The filename of non-entry chunks as relative path
81 * inside the output.path directory.
82 *
83 * See: http://webpack.github.io/docs/configuration.html#output-chunkfilename
84 */
85 chunkFilename: '[id].[chunkhash].chunk.js'
86
87 },
88
89 externals: {
90 webtorrent: 'WebTorrent'
91 },
75 92
76 /** 93 /**
77 * The filename of non-entry chunks as relative path 94 * Add additional plugins to the compiler.
78 * inside the output.path directory.
79 * 95 *
80 * See: http://webpack.github.io/docs/configuration.html#output-chunkfilename 96 * See: http://webpack.github.io/docs/configuration.html#plugins
81 */ 97 */
82 chunkFilename: '[id].[chunkhash].chunk.js' 98 plugins: [
83 99
84 }, 100 /**
85 101 * Plugin: WebpackMd5Hash
86 externals: { 102 * Description: Plugin to replace a standard webpack chunkhash with md5.
87 webtorrent: 'WebTorrent' 103 *
88 }, 104 * See: https://www.npmjs.com/package/webpack-md5-hash
89 105 */
90 /** 106 new WebpackMd5Hash(),
91 * Add additional plugins to the compiler. 107
92 * 108 /**
93 * See: http://webpack.github.io/docs/configuration.html#plugins 109 * Plugin: DedupePlugin
94 */ 110 * Description: Prevents the inclusion of duplicate code into your bundle
95 plugins: [ 111 * and instead applies a copy of the function at runtime.
96 112 *
97 /** 113 * See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin
98 * Plugin: WebpackMd5Hash 114 * See: https://github.com/webpack/docs/wiki/optimization#deduplication
99 * Description: Plugin to replace a standard webpack chunkhash with md5. 115 */
100 * 116 // new DedupePlugin(),
101 * See: https://www.npmjs.com/package/webpack-md5-hash 117
102 */ 118 /**
103 new WebpackMd5Hash(), 119 * Plugin: DefinePlugin
120 * Description: Define free variables.
121 * Useful for having development builds with debug logging or adding global constants.
122 *
123 * Environment helpers
124 *
125 * See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin
126 */
127 // NOTE: when adding more properties make sure you include them in custom-typings.d.ts
128 new DefinePlugin({
129 'ENV': JSON.stringify(METADATA.ENV),
130 'HMR': METADATA.HMR,
131 'process.env': {
132 'ENV': JSON.stringify(METADATA.ENV),
133 'NODE_ENV': JSON.stringify(METADATA.ENV),
134 'HMR': METADATA.HMR
135 }
136 }),
137
138 /**
139 * Plugin: UglifyJsPlugin
140 * Description: Minimize all JavaScript output of chunks.
141 * Loaders are switched into minimizing mode.
142 *
143 * See: https://webpack.github.io/docs/list-of-plugins.html#uglifyjsplugin
144 */
145 // NOTE: To debug prod builds uncomment //debug lines and comment //prod lines
146 new UglifyJsPlugin({
147 // beautify: true, //debug
148 // mangle: false, //debug
149 // dead_code: false, //debug
150 // unused: false, //debug
151 // deadCode: false, //debug
152 // compress: {
153 // screw_ie8: true,
154 // keep_fnames: true,
155 // drop_debugger: false,
156 // dead_code: false,
157 // unused: false
158 // }, // debug
159 // comments: true, //debug
160
161 beautify: false, // prod
162 mangle: { screw_ie8: true, keep_fnames: true }, // prod
163 compress: { screw_ie8: true }, // prod
164 comments: false // prod
165 }),
166
167 new NormalModuleReplacementPlugin(
168 /angular2-hmr/,
169 helpers.root('config/modules/angular2-hmr-prod.js')
170 )
171
172 /**
173 * Plugin: CompressionPlugin
174 * Description: Prepares compressed versions of assets to serve
175 * them with Content-Encoding
176 *
177 * See: https://github.com/webpack/compression-webpack-plugin
178 */
179 // new CompressionPlugin({
180 // regExp: /\.css$|\.html$|\.js$|\.map$/,
181 // threshold: 2 * 1024
182 // })
104 183
105 /** 184 ],
106 * Plugin: DedupePlugin
107 * Description: Prevents the inclusion of duplicate code into your bundle
108 * and instead applies a copy of the function at runtime.
109 *
110 * See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin
111 * See: https://github.com/webpack/docs/wiki/optimization#deduplication
112 */
113 new DedupePlugin(),
114 185
115 /** 186 /**
116 * Plugin: DefinePlugin 187 * Static analysis linter for TypeScript advanced options configuration
117 * Description: Define free variables. 188 * Description: An extensible linter for the TypeScript language.
118 * Useful for having development builds with debug logging or adding global constants.
119 *
120 * Environment helpers
121 * 189 *
122 * See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin 190 * See: https://github.com/wbuchwalter/tslint-loader
123 */ 191 */
124 // NOTE: when adding more properties make sure you include them in custom-typings.d.ts 192 tslint: {
125 new DefinePlugin({ 193 emitErrors: true,
126 'ENV': JSON.stringify(METADATA.ENV), 194 failOnHint: true,
127 'HMR': METADATA.HMR, 195 resourcePath: 'src'
128 'process.env': { 196 },
129 'ENV': JSON.stringify(METADATA.ENV),
130 'NODE_ENV': JSON.stringify(METADATA.ENV),
131 'HMR': METADATA.HMR
132 }
133 }),
134 197
135 /** 198 /**
136 * Plugin: UglifyJsPlugin 199 * Html loader advanced options
137 * Description: Minimize all JavaScript output of chunks.
138 * Loaders are switched into minimizing mode.
139 * 200 *
140 * See: https://webpack.github.io/docs/list-of-plugins.html#uglifyjsplugin 201 * See: https://github.com/webpack/html-loader#advanced-options
141 */ 202 */
142 // NOTE: To debug prod builds uncomment //debug lines and comment //prod lines 203 // TODO: Need to workaround Angular 2's html syntax => #id [bind] (event) *ngFor
143 new UglifyJsPlugin({ 204 htmlLoader: {
144 // beautify: true, //debug 205 minimize: true,
145 // mangle: false, //debug 206 removeAttributeQuotes: false,
146 // dead_code: false, //debug 207 caseSensitive: true,
147 // unused: false, //debug 208 customAttrSurround: [
148 // deadCode: false, //debug 209 [/#/, /(?:)/],
149 // compress: { 210 [/\*/, /(?:)/],
150 // screw_ie8: true, 211 [/\[?\(?/, /(?:)/]
151 // keep_fnames: true, 212 ],
152 // drop_debugger: false, 213 customAttrAssign: [/\)?\]?=/]
153 // dead_code: false, 214 },
154 // unused: false 215
155 // }, // debug 216 /*
156 // comments: true, //debug 217 * Include polyfills or mocks for various node stuff
157 218 * Description: Node configuration
158 beautify: false, // prod
159
160 mangle: {
161 screw_ie8: true,
162 keep_fnames: true
163 }, // prod
164
165 compress: {
166 screw_ie8: true
167 }, // prod
168
169 comments: false // prod
170 }),
171
172 /**
173 * Plugin: CompressionPlugin
174 * Description: Prepares compressed versions of assets to serve
175 * them with Content-Encoding
176 * 219 *
177 * See: https://github.com/webpack/compression-webpack-plugin 220 * See: https://webpack.github.io/docs/configuration.html#node
178 */ 221 */
179 new CompressionPlugin({ 222 node: {
180 regExp: /\.css$|\.html$|\.js$|\.map$/, 223 global: 'window',
181 threshold: 2 * 1024 224 crypto: 'empty',
182 }) 225 process: false,
183 226 module: false,
184 ], 227 clearImmediate: false,
185 228 setImmediate: false
186 /** 229 }
187 * Static analysis linter for TypeScript advanced options configuration 230
188 * Description: An extensible linter for the TypeScript language. 231 })
189 * 232}
190 * See: https://github.com/wbuchwalter/tslint-loader
191 */
192 tslint: {
193 emitErrors: true,
194 failOnHint: true,
195 resourcePath: 'src'
196 },
197
198 /**
199 * Html loader advanced options
200 *
201 * See: https://github.com/webpack/html-loader#advanced-options
202 */
203 // TODO: Need to workaround Angular 2's html syntax => #id [bind] (event) *ngFor
204 htmlLoader: {
205 minimize: true,
206 removeAttributeQuotes: false,
207 caseSensitive: true,
208 customAttrSurround: [
209 [/#/, /(?:)/],
210 [/\*/, /(?:)/],
211 [/\[?\(?/, /(?:)/]
212 ],
213 customAttrAssign: [/\)?\]?=/]
214 },
215
216 /*
217 * Include polyfills or mocks for various node stuff
218 * Description: Node configuration
219 *
220 * See: https://webpack.github.io/docs/configuration.html#node
221 */
222 node: {
223 global: 'window',
224 crypto: 'empty',
225 process: false,
226 module: false,
227 clearImmediate: false,
228 setImmediate: false
229 }
230
231})
diff --git a/client/package.json b/client/package.json
index a5c5d092b..cc116f3e5 100644
--- a/client/package.json
+++ b/client/package.json
@@ -13,61 +13,72 @@
13 "url": "git://github.com/Chocobozzz/PeerTube.git" 13 "url": "git://github.com/Chocobozzz/PeerTube.git"
14 }, 14 },
15 "scripts": { 15 "scripts": {
16 "postinstall": "typings install",
17 "test": "standard && tslint -c ./tslint.json src/**/*.ts", 16 "test": "standard && tslint -c ./tslint.json src/**/*.ts",
18 "webpack": "webpack" 17 "webpack": "webpack"
19 }, 18 },
20 "license": "GPLv3", 19 "license": "GPLv3",
21 "dependencies": { 20 "dependencies": {
22 "@angular/common": "2.0.0-rc.4", 21 "@angular/common": "^2.0.0",
23 "@angular/compiler": "2.0.0-rc.4", 22 "@angular/compiler": "^2.0.0",
24 "@angular/core": "2.0.0-rc.4", 23 "@angular/core": "^2.0.0",
25 "@angular/http": "2.0.0-rc.4", 24 "@angular/forms": "^2.0.0",
26 "@angular/platform-browser": "2.0.0-rc.4", 25 "@angular/http": "^2.0.0",
27 "@angular/platform-browser-dynamic": "2.0.0-rc.4", 26 "@angular/platform-browser": "^2.0.0",
28 "@angular/router": "3.0.0-beta.2", 27 "@angular/platform-browser-dynamic": "^2.0.0",
29 "angular-pipes": "^2.0.0", 28 "@angular/router": "^3.0.0",
30 "awesome-typescript-loader": "^0.17.0", 29 "@angularclass/hmr": "^1.2.0",
31 "bootstrap-loader": "^1.0.8", 30 "@angularclass/hmr-loader": "^3.0.2",
31 "@types/core-js": "^0.9.28",
32 "@types/node": "^6.0.38",
33 "@types/source-map": "^0.1.26",
34 "@types/uglify-js": "^2.0.27",
35 "@types/webpack": "^1.12.29",
36 "angular-pipes": "^3.0.0",
37 "angular2-template-loader": "^0.5.0",
38 "assets-webpack-plugin": "^3.4.0",
39 "awesome-typescript-loader": "^2.2.1",
40 "bootstrap-loader": "^2.0.0-beta.11",
32 "bootstrap-sass": "^3.3.6", 41 "bootstrap-sass": "^3.3.6",
33 "compression-webpack-plugin": "^0.3.1", 42 "compression-webpack-plugin": "^0.3.1",
34 "copy-webpack-plugin": "^3.0.1", 43 "copy-webpack-plugin": "^3.0.1",
35 "core-js": "^2.4.0", 44 "core-js": "^2.4.1",
36 "css-loader": "^0.23.1", 45 "css-loader": "^0.25.0",
46 "css-to-string-loader": "https://github.com/Chocobozzz/css-to-string-loader#patch-1",
37 "es6-promise": "^3.0.2", 47 "es6-promise": "^3.0.2",
38 "es6-promise-loader": "^1.0.1", 48 "es6-promise-loader": "^1.0.1",
39 "es6-shim": "^0.35.0", 49 "es6-shim": "^0.35.0",
40 "file-loader": "^0.8.5", 50 "extract-text-webpack-plugin": "^2.0.0-beta.4",
51 "file-loader": "^0.9.0",
41 "html-webpack-plugin": "^2.19.0", 52 "html-webpack-plugin": "^2.19.0",
42 "ie-shim": "^0.1.0", 53 "ie-shim": "^0.1.0",
43 "intl": "^1.2.4", 54 "intl": "^1.2.4",
44 "json-loader": "^0.5.4", 55 "json-loader": "^0.5.4",
45 "ng2-bootstrap": "1.0.16", 56 "ng2-bootstrap": "^1.1.5",
46 "ng2-file-upload": "^1.0.3", 57 "ng2-file-upload": "^1.0.3",
47 "node-sass": "^3.7.0", 58 "node-sass": "^3.10.0",
48 "normalize.css": "^4.1.1", 59 "normalize.css": "^4.1.1",
49 "raw-loader": "^0.5.1", 60 "raw-loader": "^0.5.1",
50 "reflect-metadata": "0.1.3", 61 "reflect-metadata": "0.1.3",
51 "resolve-url-loader": "^1.4.3", 62 "resolve-url-loader": "^1.6.0",
52 "rxjs": "5.0.0-beta.6", 63 "rxjs": "5.0.0-beta.12",
53 "sass-loader": "^3.2.0", 64 "sass-loader": "^4.0.2",
54 "source-map-loader": "^0.1.5", 65 "source-map-loader": "^0.1.5",
66 "string-replace-loader": "^1.0.3",
55 "style-loader": "^0.13.1", 67 "style-loader": "^0.13.1",
56 "ts-helpers": "^1.1.1", 68 "ts-helpers": "^1.1.1",
57 "tslint": "^3.7.4", 69 "tslint": "3.15.1",
58 "tslint-loader": "^2.1.4", 70 "tslint-loader": "^2.1.4",
59 "typescript": "^1.8.10", 71 "typescript": "^2.0.0",
60 "typings": "^1.0.4",
61 "url-loader": "^0.5.7", 72 "url-loader": "^0.5.7",
62 "webpack": "^1.13.1", 73 "webpack": "2.1.0-beta.22",
63 "webpack-md5-hash": "0.0.5", 74 "webpack-md5-hash": "0.0.5",
64 "webpack-merge": "^0.13.0", 75 "webpack-merge": "^0.14.1",
65 "webpack-notifier": "^1.3.0", 76 "webpack-notifier": "^1.3.0",
66 "webtorrent": "^0.95.2", 77 "webtorrent": "^0.96.0",
67 "zone.js": "0.6.12" 78 "zone.js": "0.6.23"
68 }, 79 },
69 "devDependencies": { 80 "devDependencies": {
70 "codelyzer": "0.0.19", 81 "codelyzer": "0.0.28",
71 "standard": "^7.0.1" 82 "standard": "^8.0.0"
72 } 83 }
73} 84}
diff --git a/client/src/app/account/account.component.html b/client/src/app/account/account.component.html
new file mode 100644
index 000000000..5a8847acd
--- /dev/null
+++ b/client/src/app/account/account.component.html
@@ -0,0 +1,27 @@
1<h3>Account</h3>
2
3<div *ngIf="information" class="alert alert-success">{{ information }}</div>
4<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
5
6<form role="form" (ngSubmit)="changePassword()" [formGroup]="form">
7 <div class="form-group">
8 <label for="new-password">New password</label>
9 <input
10 type="password" class="form-control" id="new-password"
11 formControlName="new-password"
12 >
13 <div *ngIf="formErrors['new-password']" class="alert alert-danger">
14 {{ formErrors['new-password'] }}
15 </div>
16 </div>
17
18 <div class="form-group">
19 <label for="name">Confirm new password</label>
20 <input
21 type="password" class="form-control" id="new-confirmed-password"
22 formControlName="new-confirmed-password"
23 >
24 </div>
25
26 <input type="submit" value="Change password" class="btn btn-default" [disabled]="!form.valid">
27</form>
diff --git a/client/src/app/account/account.component.ts b/client/src/app/account/account.component.ts
new file mode 100644
index 000000000..851eaf198
--- /dev/null
+++ b/client/src/app/account/account.component.ts
@@ -0,0 +1,67 @@
1import { } from '@angular/common';
2import { Component, OnInit } from '@angular/core';
3import { FormBuilder, FormGroup } from '@angular/forms';
4import { Router } from '@angular/router';
5
6import { AccountService } from './account.service';
7import { FormReactive, USER_PASSWORD } from '../shared';
8
9@Component({
10 selector: 'my-account',
11 templateUrl: './account.component.html'
12})
13
14export class AccountComponent extends FormReactive implements OnInit {
15 information: string = null;
16 error: string = null;
17
18 form: FormGroup;
19 formErrors = {
20 'new-password': '',
21 'new-confirmed-password': ''
22 };
23 validationMessages = {
24 'new-password': USER_PASSWORD.MESSAGES,
25 'new-confirmed-password': USER_PASSWORD.MESSAGES
26 };
27
28 constructor(
29 private accountService: AccountService,
30 private formBuilder: FormBuilder,
31 private router: Router
32 ) {
33 super();
34 }
35
36 buildForm() {
37 this.form = this.formBuilder.group({
38 'new-password': [ '', USER_PASSWORD.VALIDATORS ],
39 'new-confirmed-password': [ '', USER_PASSWORD.VALIDATORS ],
40 });
41
42 this.form.valueChanges.subscribe(data => this.onValueChanged(data));
43 }
44
45 ngOnInit() {
46 this.buildForm();
47 }
48
49 changePassword() {
50 const newPassword = this.form.value['new-password'];
51 const newConfirmedPassword = this.form.value['new-confirmed-password'];
52
53 this.information = null;
54 this.error = null;
55
56 if (newPassword !== newConfirmedPassword) {
57 this.error = 'The new password and the confirmed password do not correspond.';
58 return;
59 }
60
61 this.accountService.changePassword(newPassword).subscribe(
62 ok => this.information = 'Password updated.',
63
64 err => this.error = err
65 );
66 }
67}
diff --git a/client/src/app/account/account.routes.ts b/client/src/app/account/account.routes.ts
new file mode 100644
index 000000000..e348c6ebe
--- /dev/null
+++ b/client/src/app/account/account.routes.ts
@@ -0,0 +1,5 @@
1import { AccountComponent } from './account.component';
2
3export const AccountRoutes = [
4 { path: 'account', component: AccountComponent }
5];
diff --git a/client/src/app/account/account.service.ts b/client/src/app/account/account.service.ts
new file mode 100644
index 000000000..355bcef74
--- /dev/null
+++ b/client/src/app/account/account.service.ts
@@ -0,0 +1,25 @@
1import { Injectable } from '@angular/core';
2
3import { AuthHttp, AuthService, RestExtractor } from '../shared';
4
5@Injectable()
6export class AccountService {
7 private static BASE_USERS_URL = '/api/v1/users/';
8
9 constructor(
10 private authHttp: AuthHttp,
11 private authService: AuthService,
12 private restExtractor: RestExtractor
13 ) {}
14
15 changePassword(newPassword: string) {
16 const url = AccountService.BASE_USERS_URL + this.authService.getUser().id;
17 const body = {
18 password: newPassword
19 };
20
21 return this.authHttp.put(url, body)
22 .map(this.restExtractor.extractDataBool)
23 .catch((res) => this.restExtractor.handleError(res));
24 }
25}
diff --git a/client/src/app/account/index.ts b/client/src/app/account/index.ts
new file mode 100644
index 000000000..823d9fe5f
--- /dev/null
+++ b/client/src/app/account/index.ts
@@ -0,0 +1,3 @@
1export * from './account.component';
2export * from './account.routes';
3export * from './account.service';
diff --git a/client/src/app/admin/admin.component.ts b/client/src/app/admin/admin.component.ts
new file mode 100644
index 000000000..64a7400e7
--- /dev/null
+++ b/client/src/app/admin/admin.component.ts
@@ -0,0 +1,8 @@
1import { Component } from '@angular/core';
2
3@Component({
4 template: '<router-outlet></router-outlet>'
5})
6
7export class AdminComponent {
8}
diff --git a/client/src/app/admin/admin.routes.ts b/client/src/app/admin/admin.routes.ts
new file mode 100644
index 000000000..edb8ba49f
--- /dev/null
+++ b/client/src/app/admin/admin.routes.ts
@@ -0,0 +1,23 @@
1import { Routes } from '@angular/router';
2
3import { AdminComponent } from './admin.component';
4import { FriendsRoutes } from './friends';
5import { RequestsRoutes } from './requests';
6import { UsersRoutes } from './users';
7
8export const AdminRoutes: Routes = [
9 {
10 path: 'admin',
11 component: AdminComponent,
12 children: [
13 {
14 path: '',
15 redirectTo: 'users',
16 pathMatch: 'full'
17 },
18 ...FriendsRoutes,
19 ...RequestsRoutes,
20 ...UsersRoutes
21 ]
22 }
23];
diff --git a/client/src/app/admin/friends/friend-add/friend-add.component.html b/client/src/app/admin/friends/friend-add/friend-add.component.html
new file mode 100644
index 000000000..788f3b44d
--- /dev/null
+++ b/client/src/app/admin/friends/friend-add/friend-add.component.html
@@ -0,0 +1,26 @@
1<h3>Make friends</h3>
2
3<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
4
5<form (ngSubmit)="makeFriends()" [formGroup]="form">
6 <div class="form-group" *ngFor="let url of urls; let id = index; trackBy:customTrackBy">
7 <label for="username">Url</label>
8
9 <div class="input-group">
10 <input
11 type="text" class="form-control" placeholder="http://domain.com"
12 [id]="'url-' + id" [formControlName]="'url-' + id"
13 />
14 <span class="input-group-btn">
15 <button *ngIf="displayAddField(id)" (click)="addField()" class="btn btn-default" type="button">+</button>
16 <button *ngIf="displayRemoveField(id)" (click)="removeField(id)" class="btn btn-default" type="button">-</button>
17 </span>
18 </div>
19
20 <div [hidden]="form.controls['url-' + id].valid || form.controls['url-' + id].pristine" class="alert alert-warning">
21 It should be a valid url.
22 </div>
23 </div>
24
25 <input type="submit" value="Make friends" class="btn btn-default" [disabled]="!isFormValid()">
26</form>
diff --git a/client/src/app/admin/friends/friend-add/friend-add.component.scss b/client/src/app/admin/friends/friend-add/friend-add.component.scss
new file mode 100644
index 000000000..5fde51636
--- /dev/null
+++ b/client/src/app/admin/friends/friend-add/friend-add.component.scss
@@ -0,0 +1,7 @@
1table {
2 margin-bottom: 40px;
3}
4
5.input-group-btn button {
6 width: 35px;
7}
diff --git a/client/src/app/admin/friends/friend-add/friend-add.component.ts b/client/src/app/admin/friends/friend-add/friend-add.component.ts
new file mode 100644
index 000000000..64165a9a5
--- /dev/null
+++ b/client/src/app/admin/friends/friend-add/friend-add.component.ts
@@ -0,0 +1,108 @@
1import { Component, OnInit } from '@angular/core';
2import { FormControl, FormGroup } from '@angular/forms';
3import { Router } from '@angular/router';
4
5import { validateUrl } from '../../../shared';
6import { FriendService } from '../shared';
7
8@Component({
9 selector: 'my-friend-add',
10 templateUrl: './friend-add.component.html',
11 styleUrls: [ './friend-add.component.scss' ]
12})
13export class FriendAddComponent implements OnInit {
14 form: FormGroup;
15 urls = [ ];
16 error: string = null;
17
18 constructor(private router: Router, private friendService: FriendService) {}
19
20 ngOnInit() {
21 this.form = new FormGroup({});
22 this.addField();
23 }
24
25 addField() {
26 this.form.addControl(`url-${this.urls.length}`, new FormControl('', [ validateUrl ]));
27 this.urls.push('');
28 }
29
30 customTrackBy(index: number, obj: any): any {
31 return index;
32 }
33
34 displayAddField(index: number) {
35 return index === (this.urls.length - 1);
36 }
37
38 displayRemoveField(index: number) {
39 return (index !== 0 || this.urls.length > 1) && index !== (this.urls.length - 1);
40 }
41
42 isFormValid() {
43 // Do not check the last input
44 for (let i = 0; i < this.urls.length - 1; i++) {
45 if (!this.form.controls[`url-${i}`].valid) return false;
46 }
47
48 const lastIndex = this.urls.length - 1;
49 // If the last input (which is not the first) is empty, it's ok
50 if (this.urls[lastIndex] === '' && lastIndex !== 0) {
51 return true;
52 } else {
53 return this.form.controls[`url-${lastIndex}`].valid;
54 }
55 }
56
57 removeField(index: number) {
58 // Remove the last control
59 this.form.removeControl(`url-${this.urls.length - 1}`);
60 this.urls.splice(index, 1);
61 }
62
63 makeFriends() {
64 this.error = '';
65
66 const notEmptyUrls = this.getNotEmptyUrls();
67 if (notEmptyUrls.length === 0) {
68 this.error = 'You need to specify at less 1 url.';
69 return;
70 }
71
72 if (!this.isUrlsUnique(notEmptyUrls)) {
73 this.error = 'Urls need to be unique.';
74 return;
75 }
76
77 const confirmMessage = 'Are you sure to make friends with:\n - ' + notEmptyUrls.join('\n - ');
78 if (!confirm(confirmMessage)) return;
79
80 this.friendService.makeFriends(notEmptyUrls).subscribe(
81 status => {
82 // TODO: extractdatastatus
83 // if (status === 409) {
84 // alert('Already made friends!');
85 // } else {
86 alert('Make friends request sent!');
87 this.router.navigate([ '/admin/friends/list' ]);
88 // }
89 },
90 error => alert(error.text)
91 );
92 }
93
94 private getNotEmptyUrls() {
95 const notEmptyUrls = [];
96
97 Object.keys(this.form.value).forEach((urlKey) => {
98 const url = this.form.value[urlKey];
99 if (url !== '') notEmptyUrls.push(url);
100 });
101
102 return notEmptyUrls;
103 }
104
105 private isUrlsUnique(urls: string[]) {
106 return urls.every(url => urls.indexOf(url) === urls.lastIndexOf(url));
107 }
108}
diff --git a/client/src/app/admin/friends/friend-add/index.ts b/client/src/app/admin/friends/friend-add/index.ts
new file mode 100644
index 000000000..a101b3be5
--- /dev/null
+++ b/client/src/app/admin/friends/friend-add/index.ts
@@ -0,0 +1 @@
export * from './friend-add.component';
diff --git a/client/src/app/admin/friends/friend-list/friend-list.component.html b/client/src/app/admin/friends/friend-list/friend-list.component.html
new file mode 100644
index 000000000..d786a7846
--- /dev/null
+++ b/client/src/app/admin/friends/friend-list/friend-list.component.html
@@ -0,0 +1,29 @@
1<h3>Friends list</h3>
2
3<table class="table table-hover">
4 <thead>
5 <tr>
6 <th class="table-column-id">ID</th>
7 <th>Url</th>
8 <th>Score</th>
9 <th>Created Date</th>
10 </tr>
11 </thead>
12
13 <tbody>
14 <tr *ngFor="let friend of friends">
15 <td>{{ friend.id }}</td>
16 <td>{{ friend.url }}</td>
17 <td>{{ friend.score }}</td>
18 <td>{{ friend.createdDate | date: 'medium' }}</td>
19 </tr>
20 </tbody>
21</table>
22
23<a *ngIf="friends?.length !== 0" class="add-user btn btn-danger pull-left" (click)="quitFriends()">
24 Quit friends
25</a>
26
27<a *ngIf="friends?.length === 0" class="add-user btn btn-success pull-right" [routerLink]="['/admin/friends/add']">
28 Make friends
29</a>
diff --git a/client/src/app/admin/friends/friend-list/friend-list.component.scss b/client/src/app/admin/friends/friend-list/friend-list.component.scss
new file mode 100644
index 000000000..cb597e12b
--- /dev/null
+++ b/client/src/app/admin/friends/friend-list/friend-list.component.scss
@@ -0,0 +1,3 @@
1table {
2 margin-bottom: 40px;
3}
diff --git a/client/src/app/admin/friends/friend-list/friend-list.component.ts b/client/src/app/admin/friends/friend-list/friend-list.component.ts
new file mode 100644
index 000000000..88c4800ee
--- /dev/null
+++ b/client/src/app/admin/friends/friend-list/friend-list.component.ts
@@ -0,0 +1,38 @@
1import { Component, OnInit } from '@angular/core';
2
3import { Friend, FriendService } from '../shared';
4
5@Component({
6 selector: 'my-friend-list',
7 templateUrl: './friend-list.component.html',
8 styleUrls: [ './friend-list.component.scss' ]
9})
10export class FriendListComponent implements OnInit {
11 friends: Friend[];
12
13 constructor(private friendService: FriendService) { }
14
15 ngOnInit() {
16 this.getFriends();
17 }
18
19 quitFriends() {
20 if (!confirm('Are you sure?')) return;
21
22 this.friendService.quitFriends().subscribe(
23 status => {
24 alert('Quit friends!');
25 this.getFriends();
26 },
27 error => alert(error.text)
28 );
29 }
30
31 private getFriends() {
32 this.friendService.getFriends().subscribe(
33 friends => this.friends = friends,
34
35 err => alert(err.text)
36 );
37 }
38}
diff --git a/client/src/app/admin/friends/friend-list/index.ts b/client/src/app/admin/friends/friend-list/index.ts
new file mode 100644
index 000000000..354c978a4
--- /dev/null
+++ b/client/src/app/admin/friends/friend-list/index.ts
@@ -0,0 +1 @@
export * from './friend-list.component';
diff --git a/client/src/app/admin/friends/friends.component.ts b/client/src/app/admin/friends/friends.component.ts
new file mode 100644
index 000000000..bc3f54158
--- /dev/null
+++ b/client/src/app/admin/friends/friends.component.ts
@@ -0,0 +1,8 @@
1import { Component } from '@angular/core';
2
3@Component({
4 template: '<router-outlet></router-outlet>'
5})
6
7export class FriendsComponent {
8}
diff --git a/client/src/app/admin/friends/friends.routes.ts b/client/src/app/admin/friends/friends.routes.ts
new file mode 100644
index 000000000..7fdef68f9
--- /dev/null
+++ b/client/src/app/admin/friends/friends.routes.ts
@@ -0,0 +1,27 @@
1import { Routes } from '@angular/router';
2
3import { FriendsComponent } from './friends.component';
4import { FriendAddComponent } from './friend-add';
5import { FriendListComponent } from './friend-list';
6
7export const FriendsRoutes: Routes = [
8 {
9 path: 'friends',
10 component: FriendsComponent,
11 children: [
12 {
13 path: '',
14 redirectTo: 'list',
15 pathMatch: 'full'
16 },
17 {
18 path: 'list',
19 component: FriendListComponent
20 },
21 {
22 path: 'add',
23 component: FriendAddComponent
24 }
25 ]
26 }
27];
diff --git a/client/src/app/admin/friends/index.ts b/client/src/app/admin/friends/index.ts
new file mode 100644
index 000000000..dd4df2538
--- /dev/null
+++ b/client/src/app/admin/friends/index.ts
@@ -0,0 +1,5 @@
1export * from './friend-add';
2export * from './friend-list';
3export * from './shared';
4export * from './friends.component';
5export * from './friends.routes';
diff --git a/client/src/app/admin/friends/shared/friend.model.ts b/client/src/app/admin/friends/shared/friend.model.ts
new file mode 100644
index 000000000..7cb28f440
--- /dev/null
+++ b/client/src/app/admin/friends/shared/friend.model.ts
@@ -0,0 +1,6 @@
1export interface Friend {
2 id: string;
3 url: string;
4 score: number;
5 createdDate: Date;
6}
diff --git a/client/src/app/admin/friends/shared/friend.service.ts b/client/src/app/admin/friends/shared/friend.service.ts
new file mode 100644
index 000000000..75826fc17
--- /dev/null
+++ b/client/src/app/admin/friends/shared/friend.service.ts
@@ -0,0 +1,39 @@
1import { Injectable } from '@angular/core';
2import { Observable } from 'rxjs/Observable';
3
4import { Friend } from './friend.model';
5import { AuthHttp, RestExtractor } from '../../../shared';
6
7@Injectable()
8export class FriendService {
9 private static BASE_FRIEND_URL: string = '/api/v1/pods/';
10
11 constructor (
12 private authHttp: AuthHttp,
13 private restExtractor: RestExtractor
14 ) {}
15
16 getFriends(): Observable<Friend[]> {
17 return this.authHttp.get(FriendService.BASE_FRIEND_URL)
18 // Not implemented as a data list by the server yet
19 // .map(this.restExtractor.extractDataList)
20 .map((res) => res.json())
21 .catch((res) => this.restExtractor.handleError(res));
22 }
23
24 makeFriends(notEmptyUrls) {
25 const body = {
26 urls: notEmptyUrls
27 };
28
29 return this.authHttp.post(FriendService.BASE_FRIEND_URL + 'makefriends', body)
30 .map(this.restExtractor.extractDataBool)
31 .catch((res) => this.restExtractor.handleError(res));
32 }
33
34 quitFriends() {
35 return this.authHttp.get(FriendService.BASE_FRIEND_URL + 'quitfriends')
36 .map(res => res.status)
37 .catch((res) => this.restExtractor.handleError(res));
38 }
39}
diff --git a/client/src/app/friends/index.ts b/client/src/app/admin/friends/shared/index.ts
index 0adc256c4..0d671637d 100644
--- a/client/src/app/friends/index.ts
+++ b/client/src/app/admin/friends/shared/index.ts
@@ -1 +1,2 @@
1export * from './friend.model';
1export * from './friend.service'; 2export * from './friend.service';
diff --git a/client/src/app/admin/index.ts b/client/src/app/admin/index.ts
new file mode 100644
index 000000000..493caed15
--- /dev/null
+++ b/client/src/app/admin/index.ts
@@ -0,0 +1,6 @@
1export * from './friends';
2export * from './requests';
3export * from './users';
4export * from './admin.component';
5export * from './admin.routes';
6export * from './menu-admin.component';
diff --git a/client/src/app/admin/menu-admin.component.html b/client/src/app/admin/menu-admin.component.html
new file mode 100644
index 000000000..e250615aa
--- /dev/null
+++ b/client/src/app/admin/menu-admin.component.html
@@ -0,0 +1,26 @@
1<menu class="col-md-2 col-sm-3 col-xs-3">
2
3 <div class="panel-block">
4 <div id="panel-users" class="panel-button">
5 <span class="hidden-xs glyphicon glyphicon-user"></span>
6 <a [routerLink]="['/admin/users/list']">List users</a>
7 </div>
8
9 <div id="panel-friends" class="panel-button">
10 <span class="hidden-xs glyphicon glyphicon-cloud"></span>
11 <a [routerLink]="['/admin/friends/list']">List friends</a>
12 </div>
13
14 <div id="panel-request-stats" class="panel-button">
15 <span class="hidden-xs glyphicon glyphicon-stats"></span>
16 <a [routerLink]="['/admin/requests/stats']">Request stats</a>
17 </div>
18 </div>
19
20 <div class="panel-block">
21 <div id="panel-quit-administration" class="panel-button">
22 <span class="hidden-xs glyphicon glyphicon-cog"></span>
23 <a [routerLink]="['/videos/list']">Quit admin.</a>
24 </div>
25 </div>
26</menu>
diff --git a/client/src/app/admin/menu-admin.component.ts b/client/src/app/admin/menu-admin.component.ts
new file mode 100644
index 000000000..59ffccf9f
--- /dev/null
+++ b/client/src/app/admin/menu-admin.component.ts
@@ -0,0 +1,7 @@
1import { Component } from '@angular/core';
2
3@Component({
4 selector: 'my-menu-admin',
5 templateUrl: './menu-admin.component.html'
6})
7export class MenuAdminComponent { }
diff --git a/client/src/app/admin/requests/index.ts b/client/src/app/admin/requests/index.ts
new file mode 100644
index 000000000..236a9ee8f
--- /dev/null
+++ b/client/src/app/admin/requests/index.ts
@@ -0,0 +1,4 @@
1export * from './request-stats';
2export * from './shared';
3export * from './requests.component';
4export * from './requests.routes';
diff --git a/client/src/app/admin/requests/request-stats/index.ts b/client/src/app/admin/requests/request-stats/index.ts
new file mode 100644
index 000000000..be3a66f77
--- /dev/null
+++ b/client/src/app/admin/requests/request-stats/index.ts
@@ -0,0 +1 @@
export * from './request-stats.component';
diff --git a/client/src/app/admin/requests/request-stats/request-stats.component.html b/client/src/app/admin/requests/request-stats/request-stats.component.html
new file mode 100644
index 000000000..b5ac59a9a
--- /dev/null
+++ b/client/src/app/admin/requests/request-stats/request-stats.component.html
@@ -0,0 +1,23 @@
1<h3>Requests stats</h3>
2
3<div *ngIf="stats !== null">
4 <div>
5 <span class="label-description">Interval seconds between requests:</span>
6 {{ stats.secondsInterval }}
7 </div>
8
9 <div>
10 <span class="label-description">Remaining time before the scheduled request:</span>
11 {{ stats.remainingSeconds }}
12 </div>
13
14 <div>
15 <span class="label-description">Maximum number of requests per interval:</span>
16 {{ stats.maxRequestsInParallel }}
17 </div>
18
19 <div>
20 <span class="label-description">Remaining requests:</span>
21 {{ stats.requests.length }}
22 </div>
23</div>
diff --git a/client/src/app/admin/requests/request-stats/request-stats.component.scss b/client/src/app/admin/requests/request-stats/request-stats.component.scss
new file mode 100644
index 000000000..92c28dc99
--- /dev/null
+++ b/client/src/app/admin/requests/request-stats/request-stats.component.scss
@@ -0,0 +1,6 @@
1.label-description {
2 display: inline-block;
3 width: 350px;
4 font-weight: bold;
5 color: black;
6}
diff --git a/client/src/app/admin/requests/request-stats/request-stats.component.ts b/client/src/app/admin/requests/request-stats/request-stats.component.ts
new file mode 100644
index 000000000..4b0844574
--- /dev/null
+++ b/client/src/app/admin/requests/request-stats/request-stats.component.ts
@@ -0,0 +1,51 @@
1import { Component, OnInit, OnDestroy } from '@angular/core';
2
3import { RequestService, RequestStats } from '../shared';
4
5@Component({
6 selector: 'my-request-stats',
7 templateUrl: './request-stats.component.html',
8 styleUrls: [ './request-stats.component.scss' ]
9})
10export class RequestStatsComponent implements OnInit, OnDestroy {
11 stats: RequestStats = null;
12
13 private interval: NodeJS.Timer = null;
14
15 constructor(private requestService: RequestService) { }
16
17 ngOnInit() {
18 this.getStats();
19 }
20
21 ngOnDestroy() {
22 if (this.stats.secondsInterval !== null) {
23 clearInterval(this.interval);
24 }
25 }
26
27 getStats() {
28 this.requestService.getStats().subscribe(
29 stats => {
30 console.log(stats);
31 this.stats = stats;
32 this.runInterval();
33 },
34
35 err => alert(err.text)
36 );
37 }
38
39 private runInterval() {
40 this.interval = setInterval(() => {
41 this.stats.remainingMilliSeconds -= 1000;
42
43 if (this.stats.remainingMilliSeconds <= 0) {
44 setTimeout(() => this.getStats(), this.stats.remainingMilliSeconds + 100);
45 clearInterval(this.interval);
46 }
47 }, 1000);
48 }
49
50
51}
diff --git a/client/src/app/admin/requests/requests.component.ts b/client/src/app/admin/requests/requests.component.ts
new file mode 100644
index 000000000..471112b45
--- /dev/null
+++ b/client/src/app/admin/requests/requests.component.ts
@@ -0,0 +1,8 @@
1import { Component } from '@angular/core';
2
3@Component({
4 template: '<router-outlet></router-outlet>'
5})
6
7export class RequestsComponent {
8}
diff --git a/client/src/app/admin/requests/requests.routes.ts b/client/src/app/admin/requests/requests.routes.ts
new file mode 100644
index 000000000..78221a9ff
--- /dev/null
+++ b/client/src/app/admin/requests/requests.routes.ts
@@ -0,0 +1,22 @@
1import { Routes } from '@angular/router';
2
3import { RequestsComponent } from './requests.component';
4import { RequestStatsComponent } from './request-stats';
5
6export const RequestsRoutes: Routes = [
7 {
8 path: 'requests',
9 component: RequestsComponent,
10 children: [
11 {
12 path: '',
13 redirectTo: 'stats',
14 pathMatch: 'full'
15 },
16 {
17 path: 'stats',
18 component: RequestStatsComponent
19 }
20 ]
21 }
22];
diff --git a/client/src/app/admin/requests/shared/index.ts b/client/src/app/admin/requests/shared/index.ts
new file mode 100644
index 000000000..32ab5767b
--- /dev/null
+++ b/client/src/app/admin/requests/shared/index.ts
@@ -0,0 +1,2 @@
1export * from './request-stats.model';
2export * from './request.service';
diff --git a/client/src/app/admin/requests/shared/request-stats.model.ts b/client/src/app/admin/requests/shared/request-stats.model.ts
new file mode 100644
index 000000000..766e80836
--- /dev/null
+++ b/client/src/app/admin/requests/shared/request-stats.model.ts
@@ -0,0 +1,32 @@
1export interface Request {
2 request: any;
3 to: any;
4}
5
6export class RequestStats {
7 maxRequestsInParallel: number;
8 milliSecondsInterval: number;
9 remainingMilliSeconds: number;
10 requests: Request[];
11
12 constructor(hash: {
13 maxRequestsInParallel: number,
14 milliSecondsInterval: number,
15 remainingMilliSeconds: number,
16 requests: Request[];
17 }) {
18 this.maxRequestsInParallel = hash.maxRequestsInParallel;
19 this.milliSecondsInterval = hash.milliSecondsInterval;
20 this.remainingMilliSeconds = hash.remainingMilliSeconds;
21 this.requests = hash.requests;
22 }
23
24 get remainingSeconds() {
25 return Math.floor(this.remainingMilliSeconds / 1000);
26 }
27
28 get secondsInterval() {
29 return Math.floor(this.milliSecondsInterval / 1000);
30 }
31
32}
diff --git a/client/src/app/admin/requests/shared/request.service.ts b/client/src/app/admin/requests/shared/request.service.ts
new file mode 100644
index 000000000..aeec37448
--- /dev/null
+++ b/client/src/app/admin/requests/shared/request.service.ts
@@ -0,0 +1,22 @@
1import { Injectable } from '@angular/core';
2import { Observable } from 'rxjs/Observable';
3
4import { RequestStats } from './request-stats.model';
5import { AuthHttp, RestExtractor } from '../../../shared';
6
7@Injectable()
8export class RequestService {
9 private static BASE_REQUEST_URL: string = '/api/v1/requests/';
10
11 constructor (
12 private authHttp: AuthHttp,
13 private restExtractor: RestExtractor
14 ) {}
15
16 getStats(): Observable<RequestStats> {
17 return this.authHttp.get(RequestService.BASE_REQUEST_URL + 'stats')
18 .map(this.restExtractor.extractDataGet)
19 .map((data) => new RequestStats(data))
20 .catch((res) => this.restExtractor.handleError(res));
21 }
22}
diff --git a/client/src/app/admin/users/index.ts b/client/src/app/admin/users/index.ts
new file mode 100644
index 000000000..e98a81f62
--- /dev/null
+++ b/client/src/app/admin/users/index.ts
@@ -0,0 +1,5 @@
1export * from './shared';
2export * from './user-add';
3export * from './user-list';
4export * from './users.component';
5export * from './users.routes';
diff --git a/client/src/app/admin/users/shared/index.ts b/client/src/app/admin/users/shared/index.ts
new file mode 100644
index 000000000..e17ee5c7a
--- /dev/null
+++ b/client/src/app/admin/users/shared/index.ts
@@ -0,0 +1 @@
export * from './user.service';
diff --git a/client/src/app/admin/users/shared/user.service.ts b/client/src/app/admin/users/shared/user.service.ts
new file mode 100644
index 000000000..13be553c0
--- /dev/null
+++ b/client/src/app/admin/users/shared/user.service.ts
@@ -0,0 +1,47 @@
1import { Injectable } from '@angular/core';
2
3import { AuthHttp, RestExtractor, ResultList, User } from '../../../shared';
4
5@Injectable()
6export class UserService {
7 // TODO: merge this constant with account
8 private static BASE_USERS_URL = '/api/v1/users/';
9
10 constructor(
11 private authHttp: AuthHttp,
12 private restExtractor: RestExtractor
13 ) {}
14
15 addUser(username: string, password: string) {
16 const body = {
17 username,
18 password
19 };
20
21 return this.authHttp.post(UserService.BASE_USERS_URL, body)
22 .map(this.restExtractor.extractDataBool)
23 .catch(this.restExtractor.handleError);
24 }
25
26 getUsers() {
27 return this.authHttp.get(UserService.BASE_USERS_URL)
28 .map(this.restExtractor.extractDataList)
29 .map(this.extractUsers)
30 .catch((res) => this.restExtractor.handleError(res));
31 }
32
33 removeUser(user: User) {
34 return this.authHttp.delete(UserService.BASE_USERS_URL + user.id);
35 }
36
37 private extractUsers(result: ResultList) {
38 const usersJson = result.data;
39 const totalUsers = result.total;
40 const users = [];
41 for (const userJson of usersJson) {
42 users.push(new User(userJson));
43 }
44
45 return { users, totalUsers };
46 }
47}
diff --git a/client/src/app/admin/users/user-add/index.ts b/client/src/app/admin/users/user-add/index.ts
new file mode 100644
index 000000000..66d5ca04f
--- /dev/null
+++ b/client/src/app/admin/users/user-add/index.ts
@@ -0,0 +1 @@
export * from './user-add.component';
diff --git a/client/src/app/admin/users/user-add/user-add.component.html b/client/src/app/admin/users/user-add/user-add.component.html
new file mode 100644
index 000000000..9b76c7c1b
--- /dev/null
+++ b/client/src/app/admin/users/user-add/user-add.component.html
@@ -0,0 +1,29 @@
1<h3>Add user</h3>
2
3<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
4
5<form role="form" (ngSubmit)="addUser()" [formGroup]="form">
6 <div class="form-group">
7 <label for="username">Username</label>
8 <input
9 type="text" class="form-control" id="username" placeholder="Username"
10 formControlName="username"
11 >
12 <div *ngIf="formErrors.username" class="alert alert-danger">
13 {{ formErrors.username }}
14 </div>
15 </div>
16
17 <div class="form-group">
18 <label for="password">Password</label>
19 <input
20 type="password" class="form-control" id="password" placeholder="Password"
21 formControlName="password"
22 >
23 <div *ngIf="formErrors.password" class="alert alert-danger">
24 {{ formErrors.password }}
25 </div>
26 </div>
27
28 <input type="submit" value="Add user" class="btn btn-default" [disabled]="!form.valid">
29</form>
diff --git a/client/src/app/admin/users/user-add/user-add.component.ts b/client/src/app/admin/users/user-add/user-add.component.ts
new file mode 100644
index 000000000..ab96fb01d
--- /dev/null
+++ b/client/src/app/admin/users/user-add/user-add.component.ts
@@ -0,0 +1,57 @@
1import { Component, OnInit } from '@angular/core';
2import { FormBuilder, FormGroup } from '@angular/forms';
3import { Router } from '@angular/router';
4
5import { UserService } from '../shared';
6import { FormReactive, USER_USERNAME, USER_PASSWORD } from '../../../shared';
7
8@Component({
9 selector: 'my-user-add',
10 templateUrl: './user-add.component.html'
11})
12export class UserAddComponent extends FormReactive implements OnInit {
13 error: string = null;
14
15 form: FormGroup;
16 formErrors = {
17 'username': '',
18 'password': ''
19 };
20 validationMessages = {
21 'username': USER_USERNAME.MESSAGES,
22 'password': USER_PASSWORD.MESSAGES,
23 };
24
25 constructor(
26 private formBuilder: FormBuilder,
27 private router: Router,
28 private userService: UserService
29 ) {
30 super();
31 }
32
33 buildForm() {
34 this.form = this.formBuilder.group({
35 username: [ '', USER_USERNAME.VALIDATORS ],
36 password: [ '', USER_PASSWORD.VALIDATORS ],
37 });
38
39 this.form.valueChanges.subscribe(data => this.onValueChanged(data));
40 }
41
42 ngOnInit() {
43 this.buildForm();
44 }
45
46 addUser() {
47 this.error = null;
48
49 const { username, password } = this.form.value;
50
51 this.userService.addUser(username, password).subscribe(
52 ok => this.router.navigate([ '/admin/users/list' ]),
53
54 err => this.error = err.text
55 );
56 }
57}
diff --git a/client/src/app/admin/users/user-list/index.ts b/client/src/app/admin/users/user-list/index.ts
new file mode 100644
index 000000000..51fbefa80
--- /dev/null
+++ b/client/src/app/admin/users/user-list/index.ts
@@ -0,0 +1 @@
export * from './user-list.component';
diff --git a/client/src/app/admin/users/user-list/user-list.component.html b/client/src/app/admin/users/user-list/user-list.component.html
new file mode 100644
index 000000000..328b1be77
--- /dev/null
+++ b/client/src/app/admin/users/user-list/user-list.component.html
@@ -0,0 +1,28 @@
1<h3>Users list</h3>
2
3<table class="table table-hover">
4 <thead>
5 <tr>
6 <th class="table-column-id">ID</th>
7 <th>Username</th>
8 <th>Created Date</th>
9 <th class="text-right">Remove</th>
10 </tr>
11 </thead>
12
13 <tbody>
14 <tr *ngFor="let user of users">
15 <td>{{ user.id }}</td>
16 <td>{{ user.username }}</td>
17 <td>{{ user.createdDate | date: 'medium' }}</td>
18 <td class="text-right">
19 <span class="glyphicon glyphicon-remove" *ngIf="!user.isAdmin()" (click)="removeUser(user)"></span>
20 </td>
21 </tr>
22 </tbody>
23</table>
24
25<a class="add-user btn btn-success pull-right" [routerLink]="['/admin/users/add']">
26 <span class="glyphicon glyphicon-plus"></span>
27 Add user
28</a>
diff --git a/client/src/app/admin/users/user-list/user-list.component.scss b/client/src/app/admin/users/user-list/user-list.component.scss
new file mode 100644
index 000000000..e9f61e900
--- /dev/null
+++ b/client/src/app/admin/users/user-list/user-list.component.scss
@@ -0,0 +1,7 @@
1.glyphicon-remove {
2 cursor: pointer;
3}
4
5.add-user {
6 margin-top: 10px;
7}
diff --git a/client/src/app/admin/users/user-list/user-list.component.ts b/client/src/app/admin/users/user-list/user-list.component.ts
new file mode 100644
index 000000000..03f4e5c0a
--- /dev/null
+++ b/client/src/app/admin/users/user-list/user-list.component.ts
@@ -0,0 +1,42 @@
1import { Component, OnInit } from '@angular/core';
2
3import { User } from '../../../shared';
4import { UserService } from '../shared';
5
6@Component({
7 selector: 'my-user-list',
8 templateUrl: './user-list.component.html',
9 styleUrls: [ './user-list.component.scss' ]
10})
11export class UserListComponent implements OnInit {
12 totalUsers: number;
13 users: User[];
14
15 constructor(private userService: UserService) {}
16
17 ngOnInit() {
18 this.getUsers();
19 }
20
21 getUsers() {
22 this.userService.getUsers().subscribe(
23 ({ users, totalUsers }) => {
24 this.users = users;
25 this.totalUsers = totalUsers;
26 },
27
28 err => alert(err.text)
29 );
30 }
31
32
33 removeUser(user: User) {
34 if (confirm('Are you sure?')) {
35 this.userService.removeUser(user).subscribe(
36 () => this.getUsers(),
37
38 err => alert(err.text)
39 );
40 }
41 }
42}
diff --git a/client/src/app/admin/users/users.component.ts b/client/src/app/admin/users/users.component.ts
new file mode 100644
index 000000000..37e3b158d
--- /dev/null
+++ b/client/src/app/admin/users/users.component.ts
@@ -0,0 +1,8 @@
1import { Component } from '@angular/core';
2
3@Component({
4 template: '<router-outlet></router-outlet>'
5})
6
7export class UsersComponent {
8}
diff --git a/client/src/app/admin/users/users.routes.ts b/client/src/app/admin/users/users.routes.ts
new file mode 100644
index 000000000..eb71bd0ae
--- /dev/null
+++ b/client/src/app/admin/users/users.routes.ts
@@ -0,0 +1,27 @@
1import { Routes } from '@angular/router';
2
3import { UsersComponent } from './users.component';
4import { UserAddComponent } from './user-add';
5import { UserListComponent } from './user-list';
6
7export const UsersRoutes: Routes = [
8 {
9 path: 'users',
10 component: UsersComponent,
11 children: [
12 {
13 path: '',
14 redirectTo: 'list',
15 pathMatch: 'full'
16 },
17 {
18 path: 'list',
19 component: UserListComponent
20 },
21 {
22 path: 'add',
23 component: UserAddComponent
24 }
25 ]
26 }
27];
diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html
index f2acffea4..04c32f596 100644
--- a/client/src/app/app.component.html
+++ b/client/src/app/app.component.html
@@ -14,48 +14,14 @@
14 14
15 15
16 <div class="row"> 16 <div class="row">
17 17 <my-menu *ngIf="isInAdmin() === false"></my-menu>
18 <menu class="col-md-2 col-sm-3 col-xs-3"> 18 <my-menu-admin *ngIf="isInAdmin() === true"></my-menu-admin>
19 <div class="panel-block">
20 <div id="panel-user-login" class="panel-button">
21 <span class="hidden-xs glyphicon glyphicon-user"></span>
22 <a *ngIf="!isLoggedIn" [routerLink]="['/login']">Login</a>
23 <a *ngIf="isLoggedIn" (click)="logout()">Logout</a>
24 </div>
25 </div>
26
27 <div class="panel-block">
28 <div id="panel-get-videos" class="panel-button">
29 <span class="hidden-xs glyphicon glyphicon-list"></span>
30 <a [routerLink]="['/videos/list']">Get videos</a>
31 </div>
32
33 <div id="panel-upload-video" class="panel-button" *ngIf="isLoggedIn">
34 <span class="hidden-xs glyphicon glyphicon-cloud-upload"></span>
35 <a [routerLink]="['/videos/add']">Upload a video</a>
36 </div>
37 </div>
38
39 <div class="panel-block" *ngIf="isLoggedIn">
40 <div id="panel-make-friends" class="panel-button">
41 <span class="hidden-xs glyphicon glyphicon-cloud"></span>
42 <a (click)='makeFriends()'>Make friends</a>
43 </div>
44
45 <div id="panel-quit-friends" class="panel-button">
46 <span class="hidden-xs glyphicon glyphicon-plane"></span>
47 <a (click)='quitFriends()'>Quit friends</a>
48 </div>
49 </div>
50 </menu>
51 19
52 <div class="col-md-9 col-sm-8 col-xs-8 router-outlet-container"> 20 <div class="col-md-9 col-sm-8 col-xs-8 router-outlet-container">
53 <router-outlet></router-outlet> 21 <router-outlet></router-outlet>
54 </div> 22 </div>
55
56 </div> 23 </div>
57 24
58
59 <footer> 25 <footer>
60 PeerTube, CopyLeft 2015-2016 26 PeerTube, CopyLeft 2015-2016
61 </footer> 27 </footer>
diff --git a/client/src/app/app.component.scss b/client/src/app/app.component.scss
index 1b02b2f57..95f306d75 100644
--- a/client/src/app/app.component.scss
+++ b/client/src/app/app.component.scss
@@ -12,40 +12,6 @@ header div {
12 margin-bottom: 30px; 12 margin-bottom: 30px;
13} 13}
14 14
15menu {
16 @media screen and (max-width: 600px) {
17 margin-right: 3px !important;
18 padding: 3px !important;
19 min-height: 400px !important;
20 }
21
22 min-height: 600px;
23 margin-right: 20px;
24 border-right: 1px solid rgba(0, 0, 0, 0.2);
25
26 .panel-button {
27 margin: 8px;
28 cursor: pointer;
29 transition: margin 0.2s;
30
31 &:hover {
32 margin-left: 15px;
33 }
34
35 a {
36 color: #333333;
37 }
38 }
39
40 .glyphicon {
41 margin: 5px;
42 }
43}
44
45.panel-block:not(:last-child) {
46 border-bottom: 1px solid rgba(0, 0, 0, 0.1);
47}
48
49.router-outlet-container { 15.router-outlet-container {
50 @media screen and (max-width: 400px) { 16 @media screen and (max-width: 400px) {
51 padding: 0 3px 0 3px; 17 padding: 0 3px 0 3px;
diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts
index b7a3d7c58..d6b83c684 100644
--- a/client/src/app/app.component.ts
+++ b/client/src/app/app.component.ts
@@ -1,73 +1,16 @@
1import { Component } from '@angular/core'; 1import { Component } from '@angular/core';
2import { ActivatedRoute, Router, ROUTER_DIRECTIVES } from '@angular/router'; 2import { Router } from '@angular/router';
3
4import { FriendService } from './friends';
5import {
6 AuthService,
7 AuthStatus,
8 SearchComponent,
9 SearchService
10} from './shared';
11import { VideoService } from './videos';
12 3
13@Component({ 4@Component({
14 selector: 'my-app', 5 selector: 'my-app',
15 template: require('./app.component.html'), 6 templateUrl: './app.component.html',
16 styles: [ require('./app.component.scss') ], 7 styleUrls: [ './app.component.scss' ]
17 directives: [ ROUTER_DIRECTIVES, SearchComponent ],
18 providers: [ FriendService, VideoService, SearchService ]
19}) 8})
20 9
21export class AppComponent { 10export class AppComponent {
22 choices = []; 11 constructor(private router: Router) {}
23 isLoggedIn: boolean;
24
25 constructor(
26 private authService: AuthService,
27 private friendService: FriendService,
28 private route: ActivatedRoute,
29 private router: Router
30 ) {
31 this.isLoggedIn = this.authService.isLoggedIn();
32
33 this.authService.loginChangedSource.subscribe(
34 status => {
35 if (status === AuthStatus.LoggedIn) {
36 this.isLoggedIn = true;
37 console.log('Logged in.');
38 } else if (status === AuthStatus.LoggedOut) {
39 this.isLoggedIn = false;
40 console.log('Logged out.');
41 } else {
42 console.error('Unknown auth status: ' + status);
43 }
44 }
45 );
46 }
47
48 logout() {
49 this.authService.logout();
50 }
51
52 makeFriends() {
53 this.friendService.makeFriends().subscribe(
54 status => {
55 if (status === 409) {
56 alert('Already made friends!');
57 } else {
58 alert('Made friends!');
59 }
60 },
61 error => alert(error)
62 );
63 }
64 12
65 quitFriends() { 13 isInAdmin() {
66 this.friendService.quitFriends().subscribe( 14 return this.router.url.indexOf('/admin/') !== -1;
67 status => {
68 alert('Quit friends!');
69 },
70 error => alert(error)
71 );
72 } 15 }
73} 16}
diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts
new file mode 100644
index 000000000..980625f13
--- /dev/null
+++ b/client/src/app/app.module.ts
@@ -0,0 +1,146 @@
1import { ApplicationRef, NgModule } from '@angular/core';
2import { BrowserModule } from '@angular/platform-browser';
3import { FormsModule, ReactiveFormsModule } from '@angular/forms';
4import { HttpModule, RequestOptions, XHRBackend } from '@angular/http';
5import { RouterModule } from '@angular/router';
6import { removeNgStyles, createNewHosts } from '@angularclass/hmr';
7
8import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe';
9import { ProgressbarModule } from 'ng2-bootstrap/components/progressbar';
10import { PaginationModule } from 'ng2-bootstrap/components/pagination';
11import { FileUploadModule } from 'ng2-file-upload/ng2-file-upload';
12
13/*
14 * Platform and Environment providers/directives/pipes
15 */
16import { ENV_PROVIDERS } from './environment';
17import { routes } from './app.routes';
18// App is our top level component
19import { AppComponent } from './app.component';
20import { AppState } from './app.service';
21
22import {
23 AdminComponent,
24 FriendsComponent,
25 FriendAddComponent,
26 FriendListComponent,
27 FriendService,
28 MenuAdminComponent,
29 RequestsComponent,
30 RequestStatsComponent,
31 RequestService,
32 UsersComponent,
33 UserAddComponent,
34 UserListComponent,
35 UserService
36} from './admin';
37import { AccountComponent, AccountService } from './account';
38import { LoginComponent } from './login';
39import { MenuComponent } from './menu.component';
40import { AuthService, AuthHttp, RestExtractor, RestService, SearchComponent, SearchService } from './shared';
41import {
42 LoaderComponent,
43 VideosComponent,
44 VideoAddComponent,
45 VideoListComponent,
46 VideoMiniatureComponent,
47 VideoSortComponent,
48 VideoWatchComponent,
49 VideoService,
50 WebTorrentService
51} from './videos';
52
53// Application wide providers
54const APP_PROVIDERS = [
55 AppState,
56
57 {
58 provide: AuthHttp,
59 useFactory: (backend: XHRBackend, defaultOptions: RequestOptions, authService: AuthService) => {
60 return new AuthHttp(backend, defaultOptions, authService);
61 },
62 deps: [ XHRBackend, RequestOptions, AuthService ]
63 },
64
65 AuthService,
66 RestExtractor,
67 RestService,
68
69 VideoService,
70 SearchService,
71 FriendService,
72 RequestService,
73 UserService,
74 AccountService,
75 WebTorrentService
76];
77/**
78 * `AppModule` is the main entry point into Angular2's bootstraping process
79 */
80@NgModule({
81 bootstrap: [ AppComponent ],
82 declarations: [
83 AccountComponent,
84 AdminComponent,
85 AppComponent,
86 BytesPipe,
87 FriendAddComponent,
88 FriendListComponent,
89 FriendsComponent,
90 LoaderComponent,
91 LoginComponent,
92 MenuAdminComponent,
93 MenuComponent,
94 RequestsComponent,
95 RequestStatsComponent,
96 SearchComponent,
97 UserAddComponent,
98 UserListComponent,
99 UsersComponent,
100 VideoAddComponent,
101 VideoListComponent,
102 VideoMiniatureComponent,
103 VideosComponent,
104 VideoSortComponent,
105 VideoWatchComponent,
106 ],
107 imports: [ // import Angular's modules
108 BrowserModule,
109 FormsModule,
110 ReactiveFormsModule,
111 HttpModule,
112 RouterModule.forRoot(routes),
113
114 ProgressbarModule,
115 PaginationModule,
116 FileUploadModule
117 ],
118 providers: [ // expose our Services and Providers into Angular's dependency injection
119 ENV_PROVIDERS,
120 APP_PROVIDERS
121 ]
122})
123export class AppModule {
124 constructor(public appRef: ApplicationRef, public appState: AppState) {}
125 hmrOnInit(store) {
126 if (!store || !store.state) return;
127 console.log('HMR store', store);
128 this.appState._state = store.state;
129 this.appRef.tick();
130 delete store.state;
131 }
132 hmrOnDestroy(store) {
133 const cmpLocation = this.appRef.components.map(cmp => cmp.location.nativeElement);
134 // recreate elements
135 const state = this.appState._state;
136 store.state = state;
137 store.disposeOldHosts = createNewHosts(cmpLocation);
138 // remove styles
139 removeNgStyles();
140 }
141 hmrAfterDestroy(store) {
142 // display new elements
143 store.disposeOldHosts();
144 delete store.disposeOldHosts;
145 }
146}
diff --git a/client/src/app/app.routes.ts b/client/src/app/app.routes.ts
index 59ef4ce55..03e2bce51 100644
--- a/client/src/app/app.routes.ts
+++ b/client/src/app/app.routes.ts
@@ -1,15 +1,18 @@
1import { RouterConfig } from '@angular/router'; 1import { Routes } from '@angular/router';
2 2
3import { AccountRoutes } from './account';
3import { LoginRoutes } from './login'; 4import { LoginRoutes } from './login';
5import { AdminRoutes } from './admin';
4import { VideosRoutes } from './videos'; 6import { VideosRoutes } from './videos';
5 7
6export const routes: RouterConfig = [ 8export const routes: Routes = [
7 { 9 {
8 path: '', 10 path: '',
9 redirectTo: '/videos/list', 11 redirectTo: '/videos/list',
10 pathMatch: 'full' 12 pathMatch: 'full'
11 }, 13 },
12 14 ...AdminRoutes,
15 ...AccountRoutes,
13 ...LoginRoutes, 16 ...LoginRoutes,
14 ...VideosRoutes 17 ...VideosRoutes
15]; 18];
diff --git a/client/src/app/app.service.ts b/client/src/app/app.service.ts
new file mode 100644
index 000000000..033c21900
--- /dev/null
+++ b/client/src/app/app.service.ts
@@ -0,0 +1,36 @@
1
2import { Injectable } from '@angular/core';
3
4@Injectable()
5export class AppState {
6 _state = { };
7
8 constructor() { ; }
9
10 // already return a clone of the current state
11 get state() {
12 return this._state = this._clone(this._state);
13 }
14 // never allow mutation
15 set state(value) {
16 throw new Error('do not mutate the `.state` directly');
17 }
18
19
20 get(prop?: any) {
21 // use our state getter for the clone
22 const state = this.state;
23 return state.hasOwnProperty(prop) ? state[prop] : state;
24 }
25
26 set(prop: string, value: any) {
27 // internally mutate our state
28 return this._state[prop] = value;
29 }
30
31
32 _clone(object) {
33 // simple object clone
34 return JSON.parse(JSON.stringify( object ));
35 }
36}
diff --git a/client/src/app/environment.ts b/client/src/app/environment.ts
new file mode 100644
index 000000000..8bba89c4e
--- /dev/null
+++ b/client/src/app/environment.ts
@@ -0,0 +1,50 @@
1
2// Angular 2
3// rc2 workaround
4import { enableDebugTools, disableDebugTools } from '@angular/platform-browser';
5import { enableProdMode, ApplicationRef } from '@angular/core';
6// Environment Providers
7let PROVIDERS = [
8 // common env directives
9];
10
11// Angular debug tools in the dev console
12// https://github.com/angular/angular/blob/86405345b781a9dc2438c0fbe3e9409245647019/TOOLS_JS.md
13let _decorateModuleRef = function identity(value) { return value; };
14
15if ('production' === ENV) {
16 // Production
17 disableDebugTools();
18 enableProdMode();
19
20 PROVIDERS = [
21 ...PROVIDERS,
22 // custom providers in production
23 ];
24
25} else {
26
27 _decorateModuleRef = (modRef: any) => {
28 const appRef = modRef.injector.get(ApplicationRef);
29 const cmpRef = appRef.components[0];
30
31 let _ng = (<any>window).ng;
32 enableDebugTools(cmpRef);
33 (<any>window).ng.probe = _ng.probe;
34 (<any>window).ng.coreTokens = _ng.coreTokens;
35 return modRef;
36 };
37
38 // Development
39 PROVIDERS = [
40 ...PROVIDERS,
41 // custom providers in development
42 ];
43
44}
45
46export const decorateModuleRef = _decorateModuleRef;
47
48export const ENV_PROVIDERS = [
49 ...PROVIDERS
50];
diff --git a/client/src/app/friends/friend.service.ts b/client/src/app/friends/friend.service.ts
deleted file mode 100644
index 771046484..000000000
--- a/client/src/app/friends/friend.service.ts
+++ /dev/null
@@ -1,29 +0,0 @@
1import { Injectable } from '@angular/core';
2import { Response } from '@angular/http';
3import { Observable } from 'rxjs/Observable';
4
5import { AuthHttp, AuthService } from '../shared';
6
7@Injectable()
8export class FriendService {
9 private static BASE_FRIEND_URL: string = '/api/v1/pods/';
10
11 constructor (private authHttp: AuthHttp, private authService: AuthService) {}
12
13 makeFriends() {
14 return this.authHttp.get(FriendService.BASE_FRIEND_URL + 'makefriends')
15 .map(res => res.status)
16 .catch(this.handleError);
17 }
18
19 quitFriends() {
20 return this.authHttp.get(FriendService.BASE_FRIEND_URL + 'quitfriends')
21 .map(res => res.status)
22 .catch(this.handleError);
23 }
24
25 private handleError (error: Response): Observable<number> {
26 console.error(error);
27 return Observable.throw(error.json().error || 'Server error');
28 }
29}
diff --git a/client/src/app/index.ts b/client/src/app/index.ts
new file mode 100644
index 000000000..da53f6aef
--- /dev/null
+++ b/client/src/app/index.ts
@@ -0,0 +1 @@
export * from './app.module';
diff --git a/client/src/app/login/login.component.html b/client/src/app/login/login.component.html
index 5848fcba3..94a405405 100644
--- a/client/src/app/login/login.component.html
+++ b/client/src/app/login/login.component.html
@@ -1,17 +1,16 @@
1<h3>Login</h3> 1<h3>Login</h3>
2 2
3
4<div *ngIf="error" class="alert alert-danger">{{ error }}</div> 3<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
5 4
6<form role="form" (ngSubmit)="login(username.value, password.value)" #loginForm="ngForm"> 5<form role="form" (ngSubmit)="login()" [formGroup]="form">
7 <div class="form-group"> 6 <div class="form-group">
8 <label for="username">Username</label> 7 <label for="username">Username</label>
9 <input 8 <input
10 type="text" class="form-control" name="username" id="username" placeholder="Username" required 9 type="text" class="form-control" id="username" placeholder="Username" required
11 ngControl="username" #username="ngForm" 10 formControlName="username"
12 > 11 >
13 <div [hidden]="username.valid || username.pristine" class="alert alert-danger"> 12 <div *ngIf="formErrors.username" class="alert alert-danger">
14 Username is required 13 {{ formErrors.username }}
15 </div> 14 </div>
16 </div> 15 </div>
17 16
@@ -19,12 +18,12 @@
19 <label for="password">Password</label> 18 <label for="password">Password</label>
20 <input 19 <input
21 type="password" class="form-control" name="password" id="password" placeholder="Password" required 20 type="password" class="form-control" name="password" id="password" placeholder="Password" required
22 ngControl="password" #password="ngForm" 21 formControlName="password"
23 > 22 >
24 <div [hidden]="password.valid || password.pristine" class="alert alert-danger"> 23 <div *ngIf="formErrors.password" class="alert alert-danger">
25 Password is required 24 {{ formErrors.password }}
26 </div> 25 </div>
27 </div> 26 </div>
28 27
29 <input type="submit" value="Login" class="btn btn-default" [disabled]="!loginForm.form.valid"> 28 <input type="submit" value="Login" class="btn btn-default" [disabled]="!form.valid">
30</form> 29</form>
diff --git a/client/src/app/login/login.component.ts b/client/src/app/login/login.component.ts
index ddd62462e..c4ff7050b 100644
--- a/client/src/app/login/login.component.ts
+++ b/client/src/app/login/login.component.ts
@@ -1,35 +1,67 @@
1import { Component } from '@angular/core'; 1import { Component, OnInit } from '@angular/core';
2import { FormBuilder, FormGroup, Validators } from '@angular/forms';
2import { Router } from '@angular/router'; 3import { Router } from '@angular/router';
3 4
4import { AuthService } from '../shared'; 5import { AuthService, FormReactive } from '../shared';
5 6
6@Component({ 7@Component({
7 selector: 'my-login', 8 selector: 'my-login',
8 template: require('./login.component.html') 9 templateUrl: './login.component.html'
9}) 10})
10 11
11export class LoginComponent { 12export class LoginComponent extends FormReactive implements OnInit {
12 error: string = null; 13 error: string = null;
13 14
15 form: FormGroup;
16 formErrors = {
17 'username': '',
18 'password': ''
19 };
20 validationMessages = {
21 'username': {
22 'required': 'Username is required.',
23 },
24 'password': {
25 'required': 'Password is required.'
26 }
27 };
28
14 constructor( 29 constructor(
15 private authService: AuthService, 30 private authService: AuthService,
31 private formBuilder: FormBuilder,
16 private router: Router 32 private router: Router
17 ) {} 33 ) {
34 super();
35 }
36
37 buildForm() {
38 this.form = this.formBuilder.group({
39 username: [ '', Validators.required ],
40 password: [ '', Validators.required ],
41 });
42
43 this.form.valueChanges.subscribe(data => this.onValueChanged(data));
44 }
45
46 ngOnInit() {
47 this.buildForm();
48 }
49
50 login() {
51 this.error = null;
52
53 const { username, password } = this.form.value;
18 54
19 login(username: string, password: string) {
20 this.authService.login(username, password).subscribe( 55 this.authService.login(username, password).subscribe(
21 result => { 56 result => this.router.navigate(['/videos/list']),
22 this.error = null;
23 57
24 this.router.navigate(['/videos/list']);
25 },
26 error => { 58 error => {
27 console.error(error); 59 console.error(error.json);
28 60
29 if (error.error === 'invalid_grant') { 61 if (error.json.error === 'invalid_grant') {
30 this.error = 'Credentials are invalid.'; 62 this.error = 'Credentials are invalid.';
31 } else { 63 } else {
32 this.error = `${error.error}: ${error.error_description}`; 64 this.error = `${error.json.error}: ${error.json.error_description}`;
33 } 65 }
34 } 66 }
35 ); 67 );
diff --git a/client/src/app/menu.component.html b/client/src/app/menu.component.html
new file mode 100644
index 000000000..29ef7f9cf
--- /dev/null
+++ b/client/src/app/menu.component.html
@@ -0,0 +1,39 @@
1<menu class="col-md-2 col-sm-3 col-xs-3">
2 <div class="panel-block">
3 <div id="panel-user-login" class="panel-button">
4 <span *ngIf="!isLoggedIn" >
5 <span class="hidden-xs glyphicon glyphicon-log-in"></span>
6 <a [routerLink]="['/login']">Login</a>
7 </span>
8
9 <span *ngIf="isLoggedIn">
10 <span class="hidden-xs glyphicon glyphicon-log-out"></span>
11 <a *ngIf="isLoggedIn" (click)="logout()">Logout</a>
12 </span>
13 </div>
14
15 <div *ngIf="isLoggedIn" id="panel-user-account" class="panel-button">
16 <span class="hidden-xs glyphicon glyphicon-user"></span>
17 <a [routerLink]="['/account']">My account</a>
18 </div>
19 </div>
20
21 <div class="panel-block">
22 <div id="panel-get-videos" class="panel-button">
23 <span class="hidden-xs glyphicon glyphicon-list"></span>
24 <a [routerLink]="['/videos/list']">Get videos</a>
25 </div>
26
27 <div id="panel-upload-video" class="panel-button" *ngIf="isLoggedIn">
28 <span class="hidden-xs glyphicon glyphicon-cloud-upload"></span>
29 <a [routerLink]="['/videos/add']">Upload a video</a>
30 </div>
31 </div>
32
33 <div class="panel-block" *ngIf="isUserAdmin()">
34 <div id="panel-get-videos" class="panel-button">
35 <span class="hidden-xs glyphicon glyphicon-cog"></span>
36 <a [routerLink]="['/admin']">Administration</a>
37 </div>
38 </div>
39</menu>
diff --git a/client/src/app/menu.component.ts b/client/src/app/menu.component.ts
new file mode 100644
index 000000000..6cfc854dd
--- /dev/null
+++ b/client/src/app/menu.component.ts
@@ -0,0 +1,45 @@
1import { Component, OnInit } from '@angular/core';
2import { Router } from '@angular/router';
3
4import { AuthService, AuthStatus } from './shared';
5
6@Component({
7 selector: 'my-menu',
8 templateUrl: './menu.component.html'
9})
10export class MenuComponent implements OnInit {
11 isLoggedIn: boolean;
12
13 constructor (
14 private authService: AuthService,
15 private router: Router
16 ) {}
17
18 ngOnInit() {
19 this.isLoggedIn = this.authService.isLoggedIn();
20
21 this.authService.loginChangedSource.subscribe(
22 status => {
23 if (status === AuthStatus.LoggedIn) {
24 this.isLoggedIn = true;
25 console.log('Logged in.');
26 } else if (status === AuthStatus.LoggedOut) {
27 this.isLoggedIn = false;
28 console.log('Logged out.');
29 } else {
30 console.error('Unknown auth status: ' + status);
31 }
32 }
33 );
34 }
35
36 isUserAdmin() {
37 return this.authService.isAdmin();
38 }
39
40 logout() {
41 this.authService.logout();
42 // Redirect to home page
43 this.router.navigate(['/videos/list']);
44 }
45}
diff --git a/client/src/app/shared/auth/auth-http.service.ts b/client/src/app/shared/auth/auth-http.service.ts
index 9c7ef4389..2392898ca 100644
--- a/client/src/app/shared/auth/auth-http.service.ts
+++ b/client/src/app/shared/auth/auth-http.service.ts
@@ -28,7 +28,7 @@ export class AuthHttp extends Http {
28 return super.request(url, options) 28 return super.request(url, options)
29 .catch((err) => { 29 .catch((err) => {
30 if (err.status === 401) { 30 if (err.status === 401) {
31 return this.handleTokenExpired(err, url, options); 31 return this.handleTokenExpired(url, options);
32 } 32 }
33 33
34 return Observable.throw(err); 34 return Observable.throw(err);
@@ -49,26 +49,29 @@ export class AuthHttp extends Http {
49 return this.request(url, options); 49 return this.request(url, options);
50 } 50 }
51 51
52 post(url: string, options?: RequestOptionsArgs): Observable<Response> { 52 post(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
53 if (!options) options = {}; 53 if (!options) options = {};
54 options.method = RequestMethod.Post; 54 options.method = RequestMethod.Post;
55 options.body = body;
55 56
56 return this.request(url, options); 57 return this.request(url, options);
57 } 58 }
58 59
59 put(url: string, options?: RequestOptionsArgs): Observable<Response> { 60 put(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
60 if (!options) options = {}; 61 if (!options) options = {};
61 options.method = RequestMethod.Put; 62 options.method = RequestMethod.Put;
63 options.body = body;
62 64
63 return this.request(url, options); 65 return this.request(url, options);
64 } 66 }
65 67
66 private handleTokenExpired(err: Response, url: string | Request, options: RequestOptionsArgs) { 68 private handleTokenExpired(url: string | Request, options: RequestOptionsArgs) {
67 return this.authService.refreshAccessToken().flatMap(() => { 69 return this.authService.refreshAccessToken()
68 this.setAuthorizationHeader(options.headers); 70 .flatMap(() => {
71 this.setAuthorizationHeader(options.headers);
69 72
70 return super.request(url, options); 73 return super.request(url, options);
71 }); 74 });
72 } 75 }
73 76
74 private setAuthorizationHeader(headers: Headers) { 77 private setAuthorizationHeader(headers: Headers) {
diff --git a/client/src/app/shared/auth/user.model.ts b/client/src/app/shared/auth/auth-user.model.ts
index 98852f835..bdd5ea5a9 100644
--- a/client/src/app/shared/auth/user.model.ts
+++ b/client/src/app/shared/auth/auth-user.model.ts
@@ -1,15 +1,28 @@
1export class User { 1import { User } from '../users';
2
3export class AuthUser extends User {
2 private static KEYS = { 4 private static KEYS = {
5 ID: 'id',
6 ROLE: 'role',
3 USERNAME: 'username' 7 USERNAME: 'username'
4 }; 8 };
5 9
10 id: string;
11 role: string;
6 username: string; 12 username: string;
7 tokens: Tokens; 13 tokens: Tokens;
8 14
9 static load() { 15 static load() {
10 const usernameLocalStorage = localStorage.getItem(this.KEYS.USERNAME); 16 const usernameLocalStorage = localStorage.getItem(this.KEYS.USERNAME);
11 if (usernameLocalStorage) { 17 if (usernameLocalStorage) {
12 return new User(localStorage.getItem(this.KEYS.USERNAME), Tokens.load()); 18 return new AuthUser(
19 {
20 id: localStorage.getItem(this.KEYS.ID),
21 username: localStorage.getItem(this.KEYS.USERNAME),
22 role: localStorage.getItem(this.KEYS.ROLE)
23 },
24 Tokens.load()
25 );
13 } 26 }
14 27
15 return null; 28 return null;
@@ -17,12 +30,14 @@ export class User {
17 30
18 static flush() { 31 static flush() {
19 localStorage.removeItem(this.KEYS.USERNAME); 32 localStorage.removeItem(this.KEYS.USERNAME);
33 localStorage.removeItem(this.KEYS.ID);
34 localStorage.removeItem(this.KEYS.ROLE);
20 Tokens.flush(); 35 Tokens.flush();
21 } 36 }
22 37
23 constructor(username: string, hash_tokens: any) { 38 constructor(userHash: { id: string, username: string, role: string }, hashTokens: any) {
24 this.username = username; 39 super(userHash);
25 this.tokens = new Tokens(hash_tokens); 40 this.tokens = new Tokens(hashTokens);
26 } 41 }
27 42
28 getAccessToken() { 43 getAccessToken() {
@@ -43,12 +58,14 @@ export class User {
43 } 58 }
44 59
45 save() { 60 save() {
46 localStorage.setItem('username', this.username); 61 localStorage.setItem(AuthUser.KEYS.ID, this.id);
62 localStorage.setItem(AuthUser.KEYS.USERNAME, this.username);
63 localStorage.setItem(AuthUser.KEYS.ROLE, this.role);
47 this.tokens.save(); 64 this.tokens.save();
48 } 65 }
49} 66}
50 67
51// Private class used only by User 68// Private class only used by User
52class Tokens { 69class Tokens {
53 private static KEYS = { 70 private static KEYS = {
54 ACCESS_TOKEN: 'access_token', 71 ACCESS_TOKEN: 'access_token',
diff --git a/client/src/app/shared/auth/auth.service.ts b/client/src/app/shared/auth/auth.service.ts
index 584298fff..a30c79c86 100644
--- a/client/src/app/shared/auth/auth.service.ts
+++ b/client/src/app/shared/auth/auth.service.ts
@@ -1,32 +1,39 @@
1import { Injectable } from '@angular/core'; 1import { Injectable } from '@angular/core';
2import { Headers, Http, Response, URLSearchParams } from '@angular/http'; 2import { Headers, Http, Response, URLSearchParams } from '@angular/http';
3import { Router } from '@angular/router';
3import { Observable } from 'rxjs/Observable'; 4import { Observable } from 'rxjs/Observable';
4import { Subject } from 'rxjs/Subject'; 5import { Subject } from 'rxjs/Subject';
5 6
6import { AuthStatus } from './auth-status.model'; 7import { AuthStatus } from './auth-status.model';
7import { User } from './user.model'; 8import { AuthUser } from './auth-user.model';
9import { RestExtractor } from '../rest';
8 10
9@Injectable() 11@Injectable()
10export class AuthService { 12export class AuthService {
11 private static BASE_CLIENT_URL = '/api/v1/users/client'; 13 private static BASE_CLIENT_URL = '/api/v1/clients/local';
12 private static BASE_TOKEN_URL = '/api/v1/users/token'; 14 private static BASE_TOKEN_URL = '/api/v1/users/token';
15 private static BASE_USER_INFORMATIONS_URL = '/api/v1/users/me';
13 16
14 loginChangedSource: Observable<AuthStatus>; 17 loginChangedSource: Observable<AuthStatus>;
15 18
16 private clientId: string; 19 private clientId: string;
17 private clientSecret: string; 20 private clientSecret: string;
18 private loginChanged: Subject<AuthStatus>; 21 private loginChanged: Subject<AuthStatus>;
19 private user: User = null; 22 private user: AuthUser = null;
20 23
21 constructor(private http: Http) { 24 constructor(
25 private http: Http,
26 private restExtractor: RestExtractor,
27 private router: Router
28 ) {
22 this.loginChanged = new Subject<AuthStatus>(); 29 this.loginChanged = new Subject<AuthStatus>();
23 this.loginChangedSource = this.loginChanged.asObservable(); 30 this.loginChangedSource = this.loginChanged.asObservable();
24 31
25 // Fetch the client_id/client_secret 32 // Fetch the client_id/client_secret
26 // FIXME: save in local storage? 33 // FIXME: save in local storage?
27 this.http.get(AuthService.BASE_CLIENT_URL) 34 this.http.get(AuthService.BASE_CLIENT_URL)
28 .map(res => res.json()) 35 .map(this.restExtractor.extractDataGet)
29 .catch(this.handleError) 36 .catch((res) => this.restExtractor.handleError(res))
30 .subscribe( 37 .subscribe(
31 result => { 38 result => {
32 this.clientId = result.client_id; 39 this.clientId = result.client_id;
@@ -34,12 +41,15 @@ export class AuthService {
34 console.log('Client credentials loaded.'); 41 console.log('Client credentials loaded.');
35 }, 42 },
36 error => { 43 error => {
37 alert(error); 44 alert(
45 `Cannot retrieve OAuth Client credentials: ${error.text}. \n` +
46 'Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.'
47 );
38 } 48 }
39 ); 49 );
40 50
41 // Return null if there is nothing to load 51 // Return null if there is nothing to load
42 this.user = User.load(); 52 this.user = AuthUser.load();
43 } 53 }
44 54
45 getRefreshToken() { 55 getRefreshToken() {
@@ -64,10 +74,16 @@ export class AuthService {
64 return this.user.getTokenType(); 74 return this.user.getTokenType();
65 } 75 }
66 76
67 getUser(): User { 77 getUser(): AuthUser {
68 return this.user; 78 return this.user;
69 } 79 }
70 80
81 isAdmin() {
82 if (this.user === null) return false;
83
84 return this.user.isAdmin();
85 }
86
71 isLoggedIn() { 87 isLoggedIn() {
72 if (this.getAccessToken()) { 88 if (this.getAccessToken()) {
73 return true; 89 return true;
@@ -94,21 +110,23 @@ export class AuthService {
94 }; 110 };
95 111
96 return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options) 112 return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options)
97 .map(res => res.json()) 113 .map(this.restExtractor.extractDataGet)
98 .map(res => { 114 .map(res => {
99 res.username = username; 115 res.username = username;
100 return res; 116 return res;
101 }) 117 })
118 .flatMap(res => this.fetchUserInformations(res))
102 .map(res => this.handleLogin(res)) 119 .map(res => this.handleLogin(res))
103 .catch(this.handleError); 120 .catch((res) => this.restExtractor.handleError(res));
104 } 121 }
105 122
106 logout() { 123 logout() {
107 // TODO: make an HTTP request to revoke the tokens 124 // TODO: make an HTTP request to revoke the tokens
108 this.user = null; 125 this.user = null;
109 User.flush();
110 126
111 this.setStatus(AuthStatus.LoggedIn); 127 AuthUser.flush();
128
129 this.setStatus(AuthStatus.LoggedOut);
112 } 130 }
113 131
114 refreshAccessToken() { 132 refreshAccessToken() {
@@ -131,36 +149,64 @@ export class AuthService {
131 }; 149 };
132 150
133 return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options) 151 return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options)
134 .map(res => res.json()) 152 .map(this.restExtractor.extractDataGet)
135 .map(res => this.handleRefreshToken(res)) 153 .map(res => this.handleRefreshToken(res))
136 .catch(this.handleError); 154 .catch((res: Response) => {
155 // The refresh token is invalid?
156 if (res.status === 400 && res.json() && res.json().error === 'invalid_grant') {
157 console.error('Cannot refresh token -> logout...');
158 this.logout();
159 this.router.navigate(['/login']);
160
161 return Observable.throw({
162 json: '',
163 text: 'You need to reconnect.'
164 });
165 }
166
167 return this.restExtractor.handleError(res);
168 });
137 } 169 }
138 170
139 private setStatus(status: AuthStatus) { 171 private fetchUserInformations (obj: any) {
140 this.loginChanged.next(status); 172 // Do not call authHttp here to avoid circular dependencies headaches
173
174 const headers = new Headers();
175 headers.set('Authorization', `Bearer ${obj.access_token}`);
176
177 return this.http.get(AuthService.BASE_USER_INFORMATIONS_URL, { headers })
178 .map(res => res.json())
179 .map(res => {
180 obj.id = res.id;
181 obj.role = res.role;
182 return obj;
183 }
184 );
141 } 185 }
142 186
143 private handleLogin (obj: any) { 187 private handleLogin (obj: any) {
188 const id = obj.id;
144 const username = obj.username; 189 const username = obj.username;
145 const hash_tokens = { 190 const role = obj.role;
191 const hashTokens = {
146 access_token: obj.access_token, 192 access_token: obj.access_token,
147 token_type: obj.token_type, 193 token_type: obj.token_type,
148 refresh_token: obj.refresh_token 194 refresh_token: obj.refresh_token
149 }; 195 };
150 196
151 this.user = new User(username, hash_tokens); 197 this.user = new AuthUser({ id, username, role }, hashTokens);
152 this.user.save(); 198 this.user.save();
153 199
154 this.setStatus(AuthStatus.LoggedIn); 200 this.setStatus(AuthStatus.LoggedIn);
155 } 201 }
156 202
157 private handleError (error: Response) {
158 console.error(error);
159 return Observable.throw(error.json() || { error: 'Server error' });
160 }
161
162 private handleRefreshToken (obj: any) { 203 private handleRefreshToken (obj: any) {
163 this.user.refreshTokens(obj.access_token, obj.refresh_token); 204 this.user.refreshTokens(obj.access_token, obj.refresh_token);
164 this.user.save(); 205 this.user.save();
165 } 206 }
207
208 private setStatus(status: AuthStatus) {
209 this.loginChanged.next(status);
210 }
211
166} 212}
diff --git a/client/src/app/shared/auth/index.ts b/client/src/app/shared/auth/index.ts
index aafaacbf1..ebd9e14cd 100644
--- a/client/src/app/shared/auth/index.ts
+++ b/client/src/app/shared/auth/index.ts
@@ -1,4 +1,4 @@
1export * from './auth-http.service'; 1export * from './auth-http.service';
2export * from './auth-status.model'; 2export * from './auth-status.model';
3export * from './auth.service'; 3export * from './auth.service';
4export * from './user.model'; 4export * from './auth-user.model';
diff --git a/client/src/app/shared/forms/form-reactive.ts b/client/src/app/shared/forms/form-reactive.ts
new file mode 100644
index 000000000..1e8a69771
--- /dev/null
+++ b/client/src/app/shared/forms/form-reactive.ts
@@ -0,0 +1,24 @@
1import { FormGroup } from '@angular/forms';
2
3export abstract class FormReactive {
4 abstract form: FormGroup;
5 abstract formErrors: Object;
6 abstract validationMessages: Object;
7
8 abstract buildForm(): void;
9
10 protected onValueChanged(data?: any) {
11 for (const field in this.formErrors) {
12 // clear previous error message (if any)
13 this.formErrors[field] = '';
14 const control = this.form.get(field);
15
16 if (control && control.dirty && !control.valid) {
17 const messages = this.validationMessages[field];
18 for (const key in control.errors) {
19 this.formErrors[field] += messages[key] + ' ';
20 }
21 }
22 }
23 }
24}
diff --git a/client/src/app/shared/forms/form-validators/index.ts b/client/src/app/shared/forms/form-validators/index.ts
new file mode 100644
index 000000000..1d2ae6f68
--- /dev/null
+++ b/client/src/app/shared/forms/form-validators/index.ts
@@ -0,0 +1,3 @@
1export * from './url.validator';
2export * from './user';
3export * from './video';
diff --git a/client/src/app/shared/forms/form-validators/url.validator.ts b/client/src/app/shared/forms/form-validators/url.validator.ts
new file mode 100644
index 000000000..67163b4e9
--- /dev/null
+++ b/client/src/app/shared/forms/form-validators/url.validator.ts
@@ -0,0 +1,11 @@
1import { FormControl } from '@angular/forms';
2
3export function validateUrl(c: FormControl) {
4 let URL_REGEXP = new RegExp('^https?://(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$');
5
6 return URL_REGEXP.test(c.value) ? null : {
7 validateUrl: {
8 valid: false
9 }
10 };
11}
diff --git a/client/src/app/shared/forms/form-validators/user.ts b/client/src/app/shared/forms/form-validators/user.ts
new file mode 100644
index 000000000..5b11ff265
--- /dev/null
+++ b/client/src/app/shared/forms/form-validators/user.ts
@@ -0,0 +1,17 @@
1import { Validators } from '@angular/forms';
2
3export const USER_USERNAME = {
4 VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(20) ],
5 MESSAGES: {
6 'required': 'Username is required.',
7 'minlength': 'Username must be at least 3 characters long.',
8 'maxlength': 'Username cannot be more than 20 characters long.'
9 }
10};
11export const USER_PASSWORD = {
12 VALIDATORS: [ Validators.required, Validators.minLength(6) ],
13 MESSAGES: {
14 'required': 'Password is required.',
15 'minlength': 'Password must be at least 6 characters long.',
16 }
17};
diff --git a/client/src/app/shared/forms/form-validators/video.ts b/client/src/app/shared/forms/form-validators/video.ts
new file mode 100644
index 000000000..3766d4018
--- /dev/null
+++ b/client/src/app/shared/forms/form-validators/video.ts
@@ -0,0 +1,25 @@
1import { Validators } from '@angular/forms';
2
3export const VIDEO_NAME = {
4 VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(50) ],
5 MESSAGES: {
6 'required': 'Video name is required.',
7 'minlength': 'Video name must be at least 3 characters long.',
8 'maxlength': 'Video name cannot be more than 50 characters long.'
9 }
10};
11export const VIDEO_DESCRIPTION = {
12 VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(250) ],
13 MESSAGES: {
14 'required': 'Video description is required.',
15 'minlength': 'Video description must be at least 3 characters long.',
16 'maxlength': 'Video description cannot be more than 250 characters long.'
17 }
18};
19
20export const VIDEO_TAGS = {
21 VALIDATORS: [ Validators.pattern('^[a-zA-Z0-9]{2,10}$') ],
22 MESSAGES: {
23 'pattern': 'A tag should be between 2 and 10 alphanumeric characters long.'
24 }
25};
diff --git a/client/src/app/shared/forms/index.ts b/client/src/app/shared/forms/index.ts
new file mode 100644
index 000000000..588ebb4be
--- /dev/null
+++ b/client/src/app/shared/forms/index.ts
@@ -0,0 +1,2 @@
1export * from './form-validators';
2export * from './form-reactive';
diff --git a/client/src/app/shared/index.ts b/client/src/app/shared/index.ts
index dfea4c67c..af34b4b64 100644
--- a/client/src/app/shared/index.ts
+++ b/client/src/app/shared/index.ts
@@ -1,2 +1,5 @@
1export * from './auth'; 1export * from './auth';
2export * from './forms';
3export * from './rest';
2export * from './search'; 4export * from './search';
5export * from './users';
diff --git a/client/src/app/shared/rest/index.ts b/client/src/app/shared/rest/index.ts
new file mode 100644
index 000000000..3c9509dc7
--- /dev/null
+++ b/client/src/app/shared/rest/index.ts
@@ -0,0 +1,3 @@
1export * from './rest-extractor.service';
2export * from './rest-pagination';
3export * from './rest.service';
diff --git a/client/src/app/shared/rest/rest-extractor.service.ts b/client/src/app/shared/rest/rest-extractor.service.ts
new file mode 100644
index 000000000..fcb1598f4
--- /dev/null
+++ b/client/src/app/shared/rest/rest-extractor.service.ts
@@ -0,0 +1,52 @@
1import { Injectable } from '@angular/core';
2import { Response } from '@angular/http';
3import { Observable } from 'rxjs/Observable';
4
5export interface ResultList {
6 data: any[];
7 total: number;
8}
9
10@Injectable()
11export class RestExtractor {
12
13 constructor () { ; }
14
15 extractDataBool(res: Response) {
16 return true;
17 }
18
19 extractDataList(res: Response) {
20 const body = res.json();
21
22 const ret: ResultList = {
23 data: body.data,
24 total: body.total
25 };
26
27 return ret;
28 }
29
30 extractDataGet(res: Response) {
31 return res.json();
32 }
33
34 handleError(res: Response) {
35 let text = 'Server error: ';
36 text += res.text();
37 let json = '';
38
39 try {
40 json = res.json();
41 } catch (err) { ; }
42
43 const error = {
44 json,
45 text
46 };
47
48 console.error(error);
49
50 return Observable.throw(error);
51 }
52}
diff --git a/client/src/app/videos/shared/pagination.model.ts b/client/src/app/shared/rest/rest-pagination.ts
index eda44ebfb..0cfa4f468 100644
--- a/client/src/app/videos/shared/pagination.model.ts
+++ b/client/src/app/shared/rest/rest-pagination.ts
@@ -1,5 +1,5 @@
1export interface Pagination { 1export interface RestPagination {
2 currentPage: number; 2 currentPage: number;
3 itemsPerPage: number; 3 itemsPerPage: number;
4 totalItems: number; 4 totalItems: number;
5} 5};
diff --git a/client/src/app/shared/rest/rest.service.ts b/client/src/app/shared/rest/rest.service.ts
new file mode 100644
index 000000000..16b47e957
--- /dev/null
+++ b/client/src/app/shared/rest/rest.service.ts
@@ -0,0 +1,27 @@
1import { Injectable } from '@angular/core';
2import { URLSearchParams } from '@angular/http';
3
4import { RestPagination } from './rest-pagination';
5
6@Injectable()
7export class RestService {
8
9 buildRestGetParams(pagination?: RestPagination, sort?: string) {
10 const params = new URLSearchParams();
11
12 if (pagination) {
13 const start: number = (pagination.currentPage - 1) * pagination.itemsPerPage;
14 const count: number = pagination.itemsPerPage;
15
16 params.set('start', start.toString());
17 params.set('count', count.toString());
18 }
19
20 if (sort) {
21 params.set('sort', sort);
22 }
23
24 return params;
25 }
26
27}
diff --git a/client/src/app/shared/search/search.component.ts b/client/src/app/shared/search/search.component.ts
index e864fbc17..b6237469b 100644
--- a/client/src/app/shared/search/search.component.ts
+++ b/client/src/app/shared/search/search.component.ts
@@ -1,15 +1,13 @@
1import { Component, OnInit } from '@angular/core'; 1import { Component, OnInit } from '@angular/core';
2 2import { Router } from '@angular/router';
3import { DROPDOWN_DIRECTIVES} from 'ng2-bootstrap/components/dropdown';
4 3
5import { Search } from './search.model'; 4import { Search } from './search.model';
6import { SearchField } from './search-field.type'; 5import { SearchField } from './search-field.type';
7import { SearchService } from './search.service'; 6import { SearchService } from './search.service';
8 7
9@Component({ 8@Component({
10 selector: 'my-search', 9 selector: 'my-search',
11 template: require('./search.component.html'), 10 templateUrl: './search.component.html'
12 directives: [ DROPDOWN_DIRECTIVES ]
13}) 11})
14 12
15export class SearchComponent implements OnInit { 13export class SearchComponent implements OnInit {
@@ -25,10 +23,10 @@ export class SearchComponent implements OnInit {
25 value: '' 23 value: ''
26 }; 24 };
27 25
28 constructor(private searchService: SearchService) {} 26 constructor(private searchService: SearchService, private router: Router) {}
29 27
30 ngOnInit() { 28 ngOnInit() {
31 // Subscribe is the search changed 29 // Subscribe if the search changed
32 // Usually changed by videos list component 30 // Usually changed by videos list component
33 this.searchService.updateSearch.subscribe( 31 this.searchService.updateSearch.subscribe(
34 newSearchCriterias => { 32 newSearchCriterias => {
@@ -58,6 +56,10 @@ export class SearchComponent implements OnInit {
58 } 56 }
59 57
60 doSearch() { 58 doSearch() {
59 if (this.router.url.indexOf('/videos/list') === -1) {
60 this.router.navigate([ '/videos/list' ]);
61 }
62
61 this.searchService.searchUpdated.next(this.searchCriterias); 63 this.searchService.searchUpdated.next(this.searchCriterias);
62 } 64 }
63 65
diff --git a/client/src/app/shared/search/search.service.ts b/client/src/app/shared/search/search.service.ts
index c7993db3d..717a7fa50 100644
--- a/client/src/app/shared/search/search.service.ts
+++ b/client/src/app/shared/search/search.service.ts
@@ -1,5 +1,6 @@
1import { Injectable } from '@angular/core'; 1import { Injectable } from '@angular/core';
2import { Subject } from 'rxjs/Subject'; 2import { Subject } from 'rxjs/Subject';
3import { ReplaySubject } from 'rxjs/ReplaySubject';
3 4
4import { Search } from './search.model'; 5import { Search } from './search.model';
5 6
@@ -12,6 +13,6 @@ export class SearchService {
12 13
13 constructor() { 14 constructor() {
14 this.updateSearch = new Subject<Search>(); 15 this.updateSearch = new Subject<Search>();
15 this.searchUpdated = new Subject<Search>(); 16 this.searchUpdated = new ReplaySubject<Search>(1);
16 } 17 }
17} 18}
diff --git a/client/src/app/shared/users/index.ts b/client/src/app/shared/users/index.ts
new file mode 100644
index 000000000..5a670ce8f
--- /dev/null
+++ b/client/src/app/shared/users/index.ts
@@ -0,0 +1 @@
export * from './user.model';
diff --git a/client/src/app/shared/users/user.model.ts b/client/src/app/shared/users/user.model.ts
new file mode 100644
index 000000000..726495d11
--- /dev/null
+++ b/client/src/app/shared/users/user.model.ts
@@ -0,0 +1,20 @@
1export class User {
2 id: string;
3 username: string;
4 role: string;
5 createdDate: Date;
6
7 constructor(hash: { id: string, username: string, role: string, createdDate?: Date }) {
8 this.id = hash.id;
9 this.username = hash.username;
10 this.role = hash.role;
11
12 if (hash.createdDate) {
13 this.createdDate = hash.createdDate;
14 }
15 }
16
17 isAdmin() {
18 return this.role === 'admin';
19 }
20}
diff --git a/client/src/app/videos/shared/index.ts b/client/src/app/videos/shared/index.ts
index a54120f5d..67d16ead1 100644
--- a/client/src/app/videos/shared/index.ts
+++ b/client/src/app/videos/shared/index.ts
@@ -1,5 +1,4 @@
1export * from './loader'; 1export * from './loader';
2export * from './pagination.model';
3export * from './sort-field.type'; 2export * from './sort-field.type';
4export * from './video.model'; 3export * from './video.model';
5export * from './video.service'; 4export * from './video.service';
diff --git a/client/src/app/videos/shared/loader/loader.component.ts b/client/src/app/videos/shared/loader/loader.component.ts
index cdd07d1b4..e72d2f3f3 100644
--- a/client/src/app/videos/shared/loader/loader.component.ts
+++ b/client/src/app/videos/shared/loader/loader.component.ts
@@ -2,8 +2,8 @@ import { Component, Input } from '@angular/core';
2 2
3@Component({ 3@Component({
4 selector: 'my-loader', 4 selector: 'my-loader',
5 styles: [ require('./loader.component.scss') ], 5 styleUrls: [ './loader.component.scss' ],
6 template: require('./loader.component.html') 6 templateUrl: './loader.component.html'
7}) 7})
8 8
9export class LoaderComponent { 9export class LoaderComponent {
diff --git a/client/src/app/videos/shared/video.service.ts b/client/src/app/videos/shared/video.service.ts
index b4396f767..ad8557533 100644
--- a/client/src/app/videos/shared/video.service.ts
+++ b/client/src/app/videos/shared/video.service.ts
@@ -1,11 +1,10 @@
1import { Injectable } from '@angular/core'; 1import { Injectable } from '@angular/core';
2import { Http, Response, URLSearchParams } from '@angular/http'; 2import { Http } from '@angular/http';
3import { Observable } from 'rxjs/Observable'; 3import { Observable } from 'rxjs/Observable';
4 4
5import { Pagination } from './pagination.model';
6import { Search } from '../../shared'; 5import { Search } from '../../shared';
7import { SortField } from './sort-field.type'; 6import { SortField } from './sort-field.type';
8import { AuthHttp, AuthService } from '../../shared'; 7import { AuthHttp, AuthService, RestExtractor, RestPagination, RestService, ResultList } from '../../shared';
9import { Video } from './video.model'; 8import { Video } from './video.model';
10 9
11@Injectable() 10@Injectable()
@@ -15,68 +14,51 @@ export class VideoService {
15 constructor( 14 constructor(
16 private authService: AuthService, 15 private authService: AuthService,
17 private authHttp: AuthHttp, 16 private authHttp: AuthHttp,
18 private http: Http 17 private http: Http,
18 private restExtractor: RestExtractor,
19 private restService: RestService
19 ) {} 20 ) {}
20 21
21 getVideo(id: string) { 22 getVideo(id: string): Observable<Video> {
22 return this.http.get(VideoService.BASE_VIDEO_URL + id) 23 return this.http.get(VideoService.BASE_VIDEO_URL + id)
23 .map(res => <Video> res.json()) 24 .map(this.restExtractor.extractDataGet)
24 .catch(this.handleError); 25 .catch((res) => this.restExtractor.handleError(res));
25 } 26 }
26 27
27 getVideos(pagination: Pagination, sort: SortField) { 28 getVideos(pagination: RestPagination, sort: SortField) {
28 const params = this.createPaginationParams(pagination); 29 const params = this.restService.buildRestGetParams(pagination, sort);
29
30 if (sort) params.set('sort', sort);
31 30
32 return this.http.get(VideoService.BASE_VIDEO_URL, { search: params }) 31 return this.http.get(VideoService.BASE_VIDEO_URL, { search: params })
33 .map(res => res.json()) 32 .map(res => res.json())
34 .map(this.extractVideos) 33 .map(this.extractVideos)
35 .catch(this.handleError); 34 .catch((res) => this.restExtractor.handleError(res));
36 } 35 }
37 36
38 removeVideo(id: string) { 37 removeVideo(id: string) {
39 return this.authHttp.delete(VideoService.BASE_VIDEO_URL + id) 38 return this.authHttp.delete(VideoService.BASE_VIDEO_URL + id)
40 .map(res => <number> res.status) 39 .map(this.restExtractor.extractDataBool)
41 .catch(this.handleError); 40 .catch((res) => this.restExtractor.handleError(res));
42 } 41 }
43 42
44 searchVideos(search: Search, pagination: Pagination, sort: SortField) { 43 searchVideos(search: Search, pagination: RestPagination, sort: SortField) {
45 const params = this.createPaginationParams(pagination); 44 const params = this.restService.buildRestGetParams(pagination, sort);
46 45
47 if (search.field) params.set('field', search.field); 46 if (search.field) params.set('field', search.field);
48 if (sort) params.set('sort', sort);
49 47
50 return this.http.get(VideoService.BASE_VIDEO_URL + 'search/' + encodeURIComponent(search.value), { search: params }) 48 return this.http.get(VideoService.BASE_VIDEO_URL + 'search/' + encodeURIComponent(search.value), { search: params })
51 .map(res => res.json()) 49 .map(this.restExtractor.extractDataList)
52 .map(this.extractVideos) 50 .map(this.extractVideos)
53 .catch(this.handleError); 51 .catch((res) => this.restExtractor.handleError(res));
54 }
55
56 private createPaginationParams(pagination: Pagination) {
57 const params = new URLSearchParams();
58 const start: number = (pagination.currentPage - 1) * pagination.itemsPerPage;
59 const count: number = pagination.itemsPerPage;
60
61 params.set('start', start.toString());
62 params.set('count', count.toString());
63
64 return params;
65 } 52 }
66 53
67 private extractVideos(body: any) { 54 private extractVideos(result: ResultList) {
68 const videos_json = body.data; 55 const videosJson = result.data;
69 const totalVideos = body.total; 56 const totalVideos = result.total;
70 const videos = []; 57 const videos = [];
71 for (const video_json of videos_json) { 58 for (const videoJson of videosJson) {
72 videos.push(new Video(video_json)); 59 videos.push(new Video(videoJson));
73 } 60 }
74 61
75 return { videos, totalVideos }; 62 return { videos, totalVideos };
76 } 63 }
77
78 private handleError(error: Response) {
79 console.error(error);
80 return Observable.throw(error.json().error || 'Server error');
81 }
82} 64}
diff --git a/client/src/app/videos/video-add/video-add.component.html b/client/src/app/videos/video-add/video-add.component.html
index bcd78c7cb..64320cae7 100644
--- a/client/src/app/videos/video-add/video-add.component.html
+++ b/client/src/app/videos/video-add/video-add.component.html
@@ -2,31 +2,31 @@
2 2
3<div *ngIf="error" class="alert alert-danger">{{ error }}</div> 3<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
4 4
5<form novalidate (ngSubmit)="upload()" [ngFormModel]="videoForm"> 5<form novalidate (ngSubmit)="upload()" [formGroup]="form">
6 <div class="form-group"> 6 <div class="form-group">
7 <label for="name">Name</label> 7 <label for="name">Name</label>
8 <input 8 <input
9 type="text" class="form-control" name="name" id="name" 9 type="text" class="form-control" id="name"
10 ngControl="name" #name="ngForm" [(ngModel)]="video.name" 10 formControlName="name"
11 > 11 >
12 <div [hidden]="name.valid || name.pristine" class="alert alert-warning"> 12 <div *ngIf="formErrors.name" class="alert alert-danger">
13 A name is required and should be between 3 and 50 characters long 13 {{ formErrors.name }}
14 </div> 14 </div>
15 </div> 15 </div>
16 16
17 <div class="form-group"> 17 <div class="form-group">
18 <label for="tags">Tags</label> 18 <label for="tags">Tags</label>
19 <input 19 <input
20 type="text" class="form-control" name="tags" id="tags" 20 type="text" class="form-control" id="currentTag"
21 ngControl="tags" #tags="ngForm" [disabled]="isTagsInputDisabled" (keyup)="onTagKeyPress($event)" [(ngModel)]="currentTag" 21 formControlName="currentTag" (keyup)="onTagKeyPress($event)"
22 > 22 >
23 <div [hidden]="tags.valid || tags.pristine" class="alert alert-warning"> 23 <div *ngIf="formErrors.currentTag" class="alert alert-danger">
24 A tag should be between 2 and 10 characters (alphanumeric) long 24 {{ formErrors.currentTag }}
25 </div> 25 </div>
26 </div> 26 </div>
27 27
28 <div class="tags"> 28 <div class="tags">
29 <div class="label label-primary tag" *ngFor="let tag of video.tags"> 29 <div class="label label-primary tag" *ngFor="let tag of tags">
30 {{ tag }} 30 {{ tag }}
31 <span class="remove" (click)="removeTag(tag)">x</span> 31 <span class="remove" (click)="removeTag(tag)">x</span>
32 </div> 32 </div>
@@ -53,12 +53,12 @@
53 <div class="form-group"> 53 <div class="form-group">
54 <label for="description">Description</label> 54 <label for="description">Description</label>
55 <textarea 55 <textarea
56 name="description" id="description" class="form-control" placeholder="Description..." 56 id="description" class="form-control" placeholder="Description..."
57 ngControl="description" #description="ngForm" [(ngModel)]="video.description" 57 formControlName="description"
58 > 58 >
59 </textarea> 59 </textarea>
60 <div [hidden]="description.valid || description.pristine" class="alert alert-warning"> 60 <div *ngIf="formErrors.description" class="alert alert-danger">
61 A description is required and should be between 3 and 250 characters long 61 {{ formErrors.description }}
62 </div> 62 </div>
63 </div> 63 </div>
64 64
@@ -69,7 +69,7 @@
69 <div class="form-group"> 69 <div class="form-group">
70 <input 70 <input
71 type="submit" value="Upload" class="btn btn-default form-control" [title]="getInvalidFieldsTitle()" 71 type="submit" value="Upload" class="btn btn-default form-control" [title]="getInvalidFieldsTitle()"
72 [disabled]="!videoForm.valid || video.tags.length === 0 || filename === null" 72 [disabled]="!form.valid || tags.length === 0 || filename === null"
73 > 73 >
74 </div> 74 </div>
75</form> 75</form>
diff --git a/client/src/app/videos/video-add/video-add.component.ts b/client/src/app/videos/video-add/video-add.component.ts
index c0f8cb9c4..d12a7d572 100644
--- a/client/src/app/videos/video-add/video-add.component.ts
+++ b/client/src/app/videos/video-add/video-add.component.ts
@@ -1,37 +1,42 @@
1import { Control, ControlGroup, Validators } from '@angular/common';
2import { Component, ElementRef, OnInit } from '@angular/core'; 1import { Component, ElementRef, OnInit } from '@angular/core';
2import { FormBuilder, FormGroup } from '@angular/forms';
3import { Router } from '@angular/router'; 3import { Router } from '@angular/router';
4 4
5import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe'; 5import { FileUploader } from 'ng2-file-upload/ng2-file-upload';
6import { PROGRESSBAR_DIRECTIVES } from 'ng2-bootstrap/components/progressbar';
7import { FileSelectDirective, FileUploader } from 'ng2-file-upload/ng2-file-upload';
8 6
9import { AuthService } from '../../shared'; 7import { AuthService, FormReactive, VIDEO_NAME, VIDEO_DESCRIPTION, VIDEO_TAGS } from '../../shared';
10 8
11@Component({ 9@Component({
12 selector: 'my-videos-add', 10 selector: 'my-videos-add',
13 styles: [ require('./video-add.component.scss') ], 11 styleUrls: [ './video-add.component.scss' ],
14 template: require('./video-add.component.html'), 12 templateUrl: './video-add.component.html'
15 directives: [ FileSelectDirective, PROGRESSBAR_DIRECTIVES ],
16 pipes: [ BytesPipe ]
17}) 13})
18 14
19export class VideoAddComponent implements OnInit { 15export class VideoAddComponent extends FormReactive implements OnInit {
20 currentTag: string; // Tag the user is writing in the input 16 tags: string[] = [];
21 error: string = null;
22 videoForm: ControlGroup;
23 uploader: FileUploader; 17 uploader: FileUploader;
24 video = { 18
19 error: string = null;
20 form: FormGroup;
21 formErrors = {
25 name: '', 22 name: '',
26 tags: [], 23 description: '',
27 description: '' 24 currentTag: ''
25 };
26 validationMessages = {
27 name: VIDEO_NAME.MESSAGES,
28 description: VIDEO_DESCRIPTION.MESSAGES,
29 currentTag: VIDEO_TAGS.MESSAGES
28 }; 30 };
29 31
30 constructor( 32 constructor(
31 private authService: AuthService, 33 private authService: AuthService,
32 private elementRef: ElementRef, 34 private elementRef: ElementRef,
35 private formBuilder: FormBuilder,
33 private router: Router 36 private router: Router
34 ) {} 37 ) {
38 super();
39 }
35 40
36 get filename() { 41 get filename() {
37 if (this.uploader.queue.length === 0) { 42 if (this.uploader.queue.length === 0) {
@@ -41,20 +46,26 @@ export class VideoAddComponent implements OnInit {
41 return this.uploader.queue[0].file.name; 46 return this.uploader.queue[0].file.name;
42 } 47 }
43 48
44 get isTagsInputDisabled () { 49 buildForm() {
45 return this.video.tags.length >= 3; 50 this.form = this.formBuilder.group({
51 name: [ '', VIDEO_NAME.VALIDATORS ],
52 description: [ '', VIDEO_DESCRIPTION.VALIDATORS ],
53 currentTag: [ '', VIDEO_TAGS.VALIDATORS ]
54 });
55
56 this.form.valueChanges.subscribe(data => this.onValueChanged(data));
46 } 57 }
47 58
48 getInvalidFieldsTitle() { 59 getInvalidFieldsTitle() {
49 let title = ''; 60 let title = '';
50 const nameControl = this.videoForm.controls['name']; 61 const nameControl = this.form.controls['name'];
51 const descriptionControl = this.videoForm.controls['description']; 62 const descriptionControl = this.form.controls['description'];
52 63
53 if (!nameControl.valid) { 64 if (!nameControl.valid) {
54 title += 'A name is required\n'; 65 title += 'A name is required\n';
55 } 66 }
56 67
57 if (this.video.tags.length === 0) { 68 if (this.tags.length === 0) {
58 title += 'At least one tag is required\n'; 69 title += 'At least one tag is required\n';
59 } 70 }
60 71
@@ -70,13 +81,6 @@ export class VideoAddComponent implements OnInit {
70 } 81 }
71 82
72 ngOnInit() { 83 ngOnInit() {
73 this.videoForm = new ControlGroup({
74 name: new Control('', Validators.compose([ Validators.required, Validators.minLength(3), Validators.maxLength(50) ])),
75 description: new Control('', Validators.compose([ Validators.required, Validators.minLength(3), Validators.maxLength(250) ])),
76 tags: new Control('', Validators.pattern('^[a-zA-Z0-9]{2,10}$'))
77 });
78
79
80 this.uploader = new FileUploader({ 84 this.uploader = new FileUploader({
81 authToken: this.authService.getRequestHeaderValue(), 85 authToken: this.authService.getRequestHeaderValue(),
82 queueLimit: 1, 86 queueLimit: 1,
@@ -85,26 +89,37 @@ export class VideoAddComponent implements OnInit {
85 }); 89 });
86 90
87 this.uploader.onBuildItemForm = (item, form) => { 91 this.uploader.onBuildItemForm = (item, form) => {
88 form.append('name', this.video.name); 92 const name = this.form.value['name'];
89 form.append('description', this.video.description); 93 const description = this.form.value['description'];
94
95 form.append('name', name);
96 form.append('description', description);
90 97
91 for (let i = 0; i < this.video.tags.length; i++) { 98 for (let i = 0; i < this.tags.length; i++) {
92 form.append(`tags[${i}]`, this.video.tags[i]); 99 form.append(`tags[${i}]`, this.tags[i]);
93 } 100 }
94 }; 101 };
102
103 this.buildForm();
95 } 104 }
96 105
97 onTagKeyPress(event: KeyboardEvent) { 106 onTagKeyPress(event: KeyboardEvent) {
107 const currentTag = this.form.value['currentTag'];
108
98 // Enter press 109 // Enter press
99 if (event.keyCode === 13) { 110 if (event.keyCode === 13) {
100 // Check if the tag is valid and does not already exist 111 // Check if the tag is valid and does not already exist
101 if ( 112 if (
102 this.currentTag !== '' && 113 currentTag !== '' &&
103 this.videoForm.controls['tags'].valid && 114 this.form.controls['currentTag'].valid &&
104 this.video.tags.indexOf(this.currentTag) === -1 115 this.tags.indexOf(currentTag) === -1
105 ) { 116 ) {
106 this.video.tags.push(this.currentTag); 117 this.tags.push(currentTag);
107 this.currentTag = ''; 118 this.form.patchValue({ currentTag: '' });
119
120 if (this.tags.length >= 3) {
121 this.form.get('currentTag').disable();
122 }
108 } 123 }
109 } 124 }
110 } 125 }
@@ -114,7 +129,7 @@ export class VideoAddComponent implements OnInit {
114 } 129 }
115 130
116 removeTag(tag: string) { 131 removeTag(tag: string) {
117 this.video.tags.splice(this.video.tags.indexOf(tag), 1); 132 this.tags.splice(this.tags.indexOf(tag), 1);
118 } 133 }
119 134
120 upload() { 135 upload() {
diff --git a/client/src/app/videos/video-list/video-list.component.ts b/client/src/app/videos/video-list/video-list.component.ts
index 5691d684e..6b086e938 100644
--- a/client/src/app/videos/video-list/video-list.component.ts
+++ b/client/src/app/videos/video-list/video-list.component.ts
@@ -1,39 +1,30 @@
1import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; 1import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
2import { AsyncPipe } from '@angular/common'; 2import { ActivatedRoute, Router } from '@angular/router';
3import { ActivatedRoute, Router, ROUTER_DIRECTIVES } from '@angular/router';
4import { BehaviorSubject } from 'rxjs/BehaviorSubject'; 3import { BehaviorSubject } from 'rxjs/BehaviorSubject';
5 4
6import { PAGINATION_DIRECTIVES } from 'ng2-bootstrap/components/pagination';
7
8import { 5import {
9 LoaderComponent,
10 Pagination,
11 SortField, 6 SortField,
12 Video, 7 Video,
13 VideoService 8 VideoService
14} from '../shared'; 9} from '../shared';
15import { AuthService, Search, SearchField, User } from '../../shared'; 10import { AuthService, AuthUser, RestPagination, Search, SearchField } from '../../shared';
16import { VideoMiniatureComponent } from './video-miniature.component';
17import { VideoSortComponent } from './video-sort.component';
18import { SearchService } from '../../shared'; 11import { SearchService } from '../../shared';
19 12
20@Component({ 13@Component({
21 selector: 'my-videos-list', 14 selector: 'my-videos-list',
22 styles: [ require('./video-list.component.scss') ], 15 styleUrls: [ './video-list.component.scss' ],
23 pipes: [ AsyncPipe ], 16 templateUrl: './video-list.component.html'
24 template: require('./video-list.component.html'),
25 directives: [ LoaderComponent, PAGINATION_DIRECTIVES, ROUTER_DIRECTIVES, VideoMiniatureComponent, VideoSortComponent ]
26}) 17})
27 18
28export class VideoListComponent implements OnInit, OnDestroy { 19export class VideoListComponent implements OnInit, OnDestroy {
29 loading: BehaviorSubject<boolean> = new BehaviorSubject(false); 20 loading: BehaviorSubject<boolean> = new BehaviorSubject(false);
30 pagination: Pagination = { 21 pagination: RestPagination = {
31 currentPage: 1, 22 currentPage: 1,
32 itemsPerPage: 9, 23 itemsPerPage: 9,
33 totalItems: null 24 totalItems: null
34 }; 25 };
35 sort: SortField; 26 sort: SortField;
36 user: User = null; 27 user: AuthUser = null;
37 videos: Video[] = []; 28 videos: Video[] = [];
38 29
39 private search: Search; 30 private search: Search;
@@ -51,7 +42,7 @@ export class VideoListComponent implements OnInit, OnDestroy {
51 42
52 ngOnInit() { 43 ngOnInit() {
53 if (this.authService.isLoggedIn()) { 44 if (this.authService.isLoggedIn()) {
54 this.user = User.load(); 45 this.user = AuthUser.load();
55 } 46 }
56 47
57 // Subscribe to route changes 48 // Subscribe to route changes
@@ -66,6 +57,8 @@ export class VideoListComponent implements OnInit, OnDestroy {
66 // Subscribe to search changes 57 // Subscribe to search changes
67 this.subSearch = this.searchService.searchUpdated.subscribe(search => { 58 this.subSearch = this.searchService.searchUpdated.subscribe(search => {
68 this.search = search; 59 this.search = search;
60 // Reset pagination
61 this.pagination.currentPage = 1;
69 62
70 this.navigateToNewParams(); 63 this.navigateToNewParams();
71 }); 64 });
@@ -76,7 +69,7 @@ export class VideoListComponent implements OnInit, OnDestroy {
76 this.subSearch.unsubscribe(); 69 this.subSearch.unsubscribe();
77 } 70 }
78 71
79 getVideos(detectChanges = true) { 72 getVideos() {
80 this.loading.next(true); 73 this.loading.next(true);
81 this.videos = []; 74 this.videos = [];
82 75
@@ -97,7 +90,7 @@ export class VideoListComponent implements OnInit, OnDestroy {
97 90
98 this.loading.next(false); 91 this.loading.next(false);
99 }, 92 },
100 error => alert(error) 93 error => alert(error.text)
101 ); 94 );
102 } 95 }
103 96
@@ -153,7 +146,11 @@ export class VideoListComponent implements OnInit, OnDestroy {
153 146
154 this.sort = <SortField>routeParams['sort'] || '-createdDate'; 147 this.sort = <SortField>routeParams['sort'] || '-createdDate';
155 148
156 this.pagination.currentPage = parseInt(routeParams['page']) || 1; 149 if (routeParams['page'] !== undefined) {
150 this.pagination.currentPage = parseInt(routeParams['page']);
151 } else {
152 this.pagination.currentPage = 1;
153 }
157 154
158 this.changeDetector.detectChanges(); 155 this.changeDetector.detectChanges();
159 } 156 }
diff --git a/client/src/app/videos/video-list/video-miniature.component.ts b/client/src/app/videos/video-list/video-miniature.component.ts
index 84bab950e..398d2db75 100644
--- a/client/src/app/videos/video-list/video-miniature.component.ts
+++ b/client/src/app/videos/video-list/video-miniature.component.ts
@@ -1,16 +1,12 @@
1import { DatePipe } from '@angular/common';
2import { Component, Input, Output, EventEmitter } from '@angular/core'; 1import { Component, Input, Output, EventEmitter } from '@angular/core';
3import { ROUTER_DIRECTIVES } from '@angular/router';
4 2
5import { SortField, Video, VideoService } from '../shared'; 3import { SortField, Video, VideoService } from '../shared';
6import { User } from '../../shared'; 4import { User } from '../../shared';
7 5
8@Component({ 6@Component({
9 selector: 'my-video-miniature', 7 selector: 'my-video-miniature',
10 styles: [ require('./video-miniature.component.scss') ], 8 styleUrls: [ './video-miniature.component.scss' ],
11 template: require('./video-miniature.component.html'), 9 templateUrl: './video-miniature.component.html'
12 directives: [ ROUTER_DIRECTIVES ],
13 pipes: [ DatePipe ]
14}) 10})
15 11
16export class VideoMiniatureComponent { 12export class VideoMiniatureComponent {
@@ -40,7 +36,7 @@ export class VideoMiniatureComponent {
40 if (confirm('Do you really want to remove this video?')) { 36 if (confirm('Do you really want to remove this video?')) {
41 this.videoService.removeVideo(id).subscribe( 37 this.videoService.removeVideo(id).subscribe(
42 status => this.removed.emit(true), 38 status => this.removed.emit(true),
43 error => alert(error) 39 error => alert(error.text)
44 ); 40 );
45 } 41 }
46 } 42 }
diff --git a/client/src/app/videos/video-list/video-sort.component.ts b/client/src/app/videos/video-list/video-sort.component.ts
index 0d76b54b7..ca94b07c2 100644
--- a/client/src/app/videos/video-list/video-sort.component.ts
+++ b/client/src/app/videos/video-list/video-sort.component.ts
@@ -4,7 +4,7 @@ import { SortField } from '../shared';
4 4
5@Component({ 5@Component({
6 selector: 'my-video-sort', 6 selector: 'my-video-sort',
7 template: require('./video-sort.component.html') 7 templateUrl: './video-sort.component.html'
8}) 8})
9 9
10export class VideoSortComponent { 10export class VideoSortComponent {
diff --git a/client/src/app/videos/video-watch/video-watch.component.ts b/client/src/app/videos/video-watch/video-watch.component.ts
index 3aaed0487..239e24c99 100644
--- a/client/src/app/videos/video-watch/video-watch.component.ts
+++ b/client/src/app/videos/video-watch/video-watch.component.ts
@@ -1,18 +1,13 @@
1import { Component, ElementRef, OnDestroy, OnInit } from '@angular/core'; 1import { Component, ElementRef, NgZone, OnDestroy, OnInit } from '@angular/core';
2import { ActivatedRoute } from '@angular/router'; 2import { ActivatedRoute } from '@angular/router';
3 3
4import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe'; 4import { Video, VideoService } from '../shared';
5
6import { LoaderComponent, Video, VideoService } from '../shared';
7import { WebTorrentService } from './webtorrent.service'; 5import { WebTorrentService } from './webtorrent.service';
8 6
9@Component({ 7@Component({
10 selector: 'my-video-watch', 8 selector: 'my-video-watch',
11 template: require('./video-watch.component.html'), 9 templateUrl: './video-watch.component.html',
12 styles: [ require('./video-watch.component.scss') ], 10 styleUrls: [ './video-watch.component.scss' ]
13 providers: [ WebTorrentService ],
14 directives: [ LoaderComponent ],
15 pipes: [ BytesPipe ]
16}) 11})
17 12
18export class VideoWatchComponent implements OnInit, OnDestroy { 13export class VideoWatchComponent implements OnInit, OnDestroy {
@@ -31,6 +26,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
31 26
32 constructor( 27 constructor(
33 private elementRef: ElementRef, 28 private elementRef: ElementRef,
29 private ngZone: NgZone,
34 private route: ActivatedRoute, 30 private route: ActivatedRoute,
35 private videoService: VideoService, 31 private videoService: VideoService,
36 private webTorrentService: WebTorrentService 32 private webTorrentService: WebTorrentService
@@ -65,12 +61,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
65 } 61 }
66 }); 62 });
67 63
68 // Refresh each second 64 this.runInProgress(torrent);
69 this.torrentInfosInterval = setInterval(() => {
70 this.downloadSpeed = torrent.downloadSpeed;
71 this.numPeers = torrent.numPeers;
72 this.uploadSpeed = torrent.uploadSpeed;
73 }, 1000);
74 }); 65 });
75 } 66 }
76 67
@@ -91,7 +82,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
91 this.video = video; 82 this.video = video;
92 this.loadVideo(); 83 this.loadVideo();
93 }, 84 },
94 error => alert(error) 85 error => alert(error.text)
95 ); 86 );
96 }); 87 });
97 } 88 }
@@ -100,4 +91,15 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
100 this.error = true; 91 this.error = true;
101 console.error('The video load seems to be abnormally long.'); 92 console.error('The video load seems to be abnormally long.');
102 } 93 }
94
95 private runInProgress(torrent: any) {
96 // Refresh each second
97 this.torrentInfosInterval = setInterval(() => {
98 this.ngZone.run(() => {
99 this.downloadSpeed = torrent.downloadSpeed;
100 this.numPeers = torrent.numPeers;
101 this.uploadSpeed = torrent.uploadSpeed;
102 });
103 }, 1000);
104 }
103} 105}
diff --git a/client/src/app/videos/videos.component.ts b/client/src/app/videos/videos.component.ts
index 76252afbb..591e7523d 100644
--- a/client/src/app/videos/videos.component.ts
+++ b/client/src/app/videos/videos.component.ts
@@ -1,9 +1,7 @@
1import { Component } from '@angular/core'; 1import { Component } from '@angular/core';
2import { ROUTER_DIRECTIVES } from '@angular/router';
3 2
4@Component({ 3@Component({
5 template: '<router-outlet></router-outlet>', 4 template: '<router-outlet></router-outlet>'
6 directives: [ ROUTER_DIRECTIVES ]
7}) 5})
8 6
9export class VideosComponent { 7export class VideosComponent {
diff --git a/client/src/app/videos/videos.routes.ts b/client/src/app/videos/videos.routes.ts
index 1f088b376..074f96596 100644
--- a/client/src/app/videos/videos.routes.ts
+++ b/client/src/app/videos/videos.routes.ts
@@ -1,11 +1,11 @@
1import { RouterConfig } from '@angular/router'; 1import { Routes } from '@angular/router';
2 2
3import { VideoAddComponent } from './video-add'; 3import { VideoAddComponent } from './video-add';
4import { VideoListComponent } from './video-list'; 4import { VideoListComponent } from './video-list';
5import { VideosComponent } from './videos.component'; 5import { VideosComponent } from './videos.component';
6import { VideoWatchComponent } from './video-watch'; 6import { VideoWatchComponent } from './video-watch';
7 7
8export const VideosRoutes: RouterConfig = [ 8export const VideosRoutes: Routes = [
9 { 9 {
10 path: 'videos', 10 path: 'videos',
11 component: VideosComponent, 11 component: VideosComponent,
diff --git a/client/src/custom-typings.d.ts b/client/src/custom-typings.d.ts
index 14c7d8ade..95787181f 100644
--- a/client/src/custom-typings.d.ts
+++ b/client/src/custom-typings.d.ts
@@ -1,15 +1,27 @@
1/* 1/*
2 * Custom Type Definitions 2 * Custom Type Definitions
3 * When including 3rd party modules you also need to include the type definition for the module 3 * When including 3rd party modules you also need to include the type definition for the module
4 * if they don't provide one within the module. You can try to install it with typings 4 * if they don't provide one within the module. You can try to install it with @types
5 5
6typings install node --save 6npm install @types/node
7npm install @types/lodash
7 8
8 * If you can't find the type definition in the registry we can make an ambient definition in 9 * If you can't find the type definition in the registry we can make an ambient/global definition in
9 * this file for now. For example 10 * this file for now. For example
10 11
11declare module "my-module" { 12declare module 'my-module' {
12 export function doesSomething(value: string): string; 13 export function doesSomething(value: string): string;
14}
15
16 * If you are using a CommonJS module that is using module.exports then you will have to write your
17 * types using export = yourObjectOrFunction with a namespace above it
18 * notice how we have to create a namespace that is equal to the function we're
19 * assigning the export to
20
21declare module 'jwt-decode' {
22 function jwtDecode(token: string): any;
23 namespace jwtDecode {}
24 export = jwtDecode;
13} 25}
14 26
15 * 27 *
@@ -17,33 +29,65 @@ declare module "my-module" {
17 * 29 *
18 30
19declare var assert: any; 31declare var assert: any;
32declare var _: any;
33declare var $: any;
20 34
21 * 35 *
22 * If you're importing a module that uses Node.js modules which are CommonJS you need to import as 36 * If you're importing a module that uses Node.js modules which are CommonJS you need to import as
37 * in the files such as main.browser.ts or any file within app/
23 * 38 *
24 39
25import * as _ from 'lodash' 40import * as _ from 'lodash'
26 41
27 * You can include your type definitions in this file until you create one for the typings registry 42 * You can include your type definitions in this file until you create one for the @types
28 * see https://github.com/typings/registry
29 * 43 *
30 */ 44 */
31 45
46// support NodeJS modules without type definitions
47declare module '*';
32 48
33// Extra variables that live on Global that will be replaced by webpack DefinePlugin 49// Extra variables that live on Global that will be replaced by webpack DefinePlugin
34declare var ENV: string; 50declare var ENV: string;
35declare var HMR: boolean; 51declare var HMR: boolean;
52declare var System: SystemJS;
53
54interface SystemJS {
55 import: (path?: string) => Promise<any>;
56}
57
36interface GlobalEnvironment { 58interface GlobalEnvironment {
37 ENV; 59 ENV;
38 HMR; 60 HMR;
61 SystemJS: SystemJS;
62 System: SystemJS;
39} 63}
40 64
65interface Es6PromiseLoader {
66 (id: string): (exportName?: string) => Promise<any>;
67}
68
69type FactoryEs6PromiseLoader = () => Es6PromiseLoader;
70type FactoryPromise = () => Promise<any>;
71
72type AsyncRoutes = {
73 [component: string]: Es6PromiseLoader |
74 Function |
75 FactoryEs6PromiseLoader |
76 FactoryPromise
77};
78
79
80type IdleCallbacks = Es6PromiseLoader |
81 Function |
82 FactoryEs6PromiseLoader |
83 FactoryPromise ;
84
41interface WebpackModule { 85interface WebpackModule {
42 hot: { 86 hot: {
43 data?: any, 87 data?: any,
44 idle: any, 88 idle: any,
45 accept(dependencies?: string | string[], callback?: (updatedDependencies?: any) => void): void; 89 accept(dependencies?: string | string[], callback?: (updatedDependencies?: any) => void): void;
46 decline(dependencies?: string | string[]): void; 90 decline(deps?: any | string | string[]): void;
47 dispose(callback?: (data?: any) => void): void; 91 dispose(callback?: (data?: any) => void): void;
48 addDisposeHandler(callback?: (data?: any) => void): void; 92 addDisposeHandler(callback?: (data?: any) => void): void;
49 removeDisposeHandler(callback?: (data?: any) => void): void; 93 removeDisposeHandler(callback?: (data?: any) => void): void;
@@ -54,66 +98,26 @@ interface WebpackModule {
54 }; 98 };
55} 99}
56 100
101
57interface WebpackRequire { 102interface WebpackRequire {
58 context(file: string, flag?: boolean, exp?: RegExp): any; 103 (id: string): any;
104 (paths: string[], callback: (...modules: any[]) => void): void;
105 ensure(ids: string[], callback: (req: WebpackRequire) => void, chunkName?: string): void;
106 context(directory: string, useSubDirectories?: boolean, regExp?: RegExp): WebpackContext;
59} 107}
60 108
109interface WebpackContext extends WebpackRequire {
110 keys(): string[];
111}
61 112
62interface ErrorStackTraceLimit { 113interface ErrorStackTraceLimit {
63 stackTraceLimit: number; 114 stackTraceLimit: number;
64} 115}
65 116
66 117
67
68// Extend typings 118// Extend typings
69interface NodeRequire extends WebpackRequire {} 119interface NodeRequire extends WebpackRequire {}
70interface ErrorConstructor extends ErrorStackTraceLimit {} 120interface ErrorConstructor extends ErrorStackTraceLimit {}
121interface NodeRequireFunction extends Es6PromiseLoader {}
71interface NodeModule extends WebpackModule {} 122interface NodeModule extends WebpackModule {}
72interface Global extends GlobalEnvironment {} 123interface Global extends GlobalEnvironment {}
73
74
75declare namespace Reflect {
76 function decorate(decorators: ClassDecorator[], target: Function): Function;
77 function decorate(
78 decorators: (PropertyDecorator | MethodDecorator)[],
79 target: Object,
80 targetKey: string | symbol,
81 descriptor?: PropertyDescriptor): PropertyDescriptor;
82
83 function metadata(metadataKey: any, metadataValue: any): {
84 (target: Function): void;
85 (target: Object, propertyKey: string | symbol): void;
86 };
87 function defineMetadata(metadataKey: any, metadataValue: any, target: Object): void;
88 function defineMetadata(
89 metadataKey: any,
90 metadataValue: any,
91 target: Object,
92 targetKey: string | symbol): void;
93 function hasMetadata(metadataKey: any, target: Object): boolean;
94 function hasMetadata(metadataKey: any, target: Object, targetKey: string | symbol): boolean;
95 function hasOwnMetadata(metadataKey: any, target: Object): boolean;
96 function hasOwnMetadata(metadataKey: any, target: Object, targetKey: string | symbol): boolean;
97 function getMetadata(metadataKey: any, target: Object): any;
98 function getMetadata(metadataKey: any, target: Object, targetKey: string | symbol): any;
99 function getOwnMetadata(metadataKey: any, target: Object): any;
100 function getOwnMetadata(metadataKey: any, target: Object, targetKey: string | symbol): any;
101 function getMetadataKeys(target: Object): any[];
102 function getMetadataKeys(target: Object, targetKey: string | symbol): any[];
103 function getOwnMetadataKeys(target: Object): any[];
104 function getOwnMetadataKeys(target: Object, targetKey: string | symbol): any[];
105 function deleteMetadata(metadataKey: any, target: Object): boolean;
106 function deleteMetadata(metadataKey: any, target: Object, targetKey: string | symbol): boolean;
107}
108
109
110// We need this here since there is a problem with Zone.js typings
111interface Thenable<T> {
112 then<U>(
113 onFulfilled?: (value: T) => U | Thenable<U>,
114 onRejected?: (error: any) => U | Thenable<U>): Thenable<U>;
115 then<U>(
116 onFulfilled?: (value: T) => U | Thenable<U>,
117 onRejected?: (error: any) => void): Thenable<U>;
118 catch<U>(onRejected?: (error: any) => U | Thenable<U>): Thenable<U>;
119}
diff --git a/client/src/index.html b/client/src/index.html
index 5cf491221..f39d8d2cf 100644
--- a/client/src/index.html
+++ b/client/src/index.html
@@ -1,3 +1,4 @@
1<!DOCTYPE html>
1<html> 2<html>
2 <head> 3 <head>
3 <base href="/"> 4 <base href="/">
diff --git a/client/src/main.ts b/client/src/main.ts
index a78d275ad..70bf48537 100644
--- a/client/src/main.ts
+++ b/client/src/main.ts
@@ -1,28 +1,20 @@
1import { enableProdMode, provide } from '@angular/core'; 1import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
2import { 2import { decorateModuleRef } from './app/environment';
3 HTTP_PROVIDERS, 3import { bootloader } from '@angularclass/hmr';
4 RequestOptions, 4/*
5 XHRBackend 5 * App Module
6} from '@angular/http'; 6 * our top level module that holds all of our components
7import { bootstrap } from '@angular/platform-browser-dynamic'; 7 */
8import { provideRouter } from '@angular/router'; 8import { AppModule } from './app';
9 9
10import { AppComponent } from './app/app.component'; 10/*
11import { routes } from './app/app.routes'; 11 * Bootstrap our Angular app with a top level NgModule
12import { AuthHttp, AuthService } from './app/shared'; 12 */
13 13export function main(): Promise<any> {
14if (process.env.ENV === 'production') { 14 return platformBrowserDynamic()
15 enableProdMode(); 15 .bootstrapModule(AppModule)
16 .then(decorateModuleRef)
17 .catch(err => console.error(err));
16} 18}
17 19
18bootstrap(AppComponent, [ 20bootloader(main);
19 HTTP_PROVIDERS,
20 provide(AuthHttp, {
21 useFactory: (backend: XHRBackend, defaultOptions: RequestOptions, authService: AuthService) => {
22 return new AuthHttp(backend, defaultOptions, authService);
23 },
24 deps: [ XHRBackend, RequestOptions, AuthService ]
25 }),
26 AuthService,
27 provideRouter(routes)
28]);
diff --git a/client/src/polyfills.ts b/client/src/polyfills.ts
index 740a563bb..65e211459 100644
--- a/client/src/polyfills.ts
+++ b/client/src/polyfills.ts
@@ -6,9 +6,28 @@ require('intl/locale-data/jsonp/en.js');
6import 'ie-shim'; // Internet Explorer 6import 'ie-shim'; // Internet Explorer
7 7
8// Prefer CoreJS over the polyfills above 8// Prefer CoreJS over the polyfills above
9import 'core-js/es6'; 9import 'core-js/es6/symbol';
10import 'core-js/es6/object';
11import 'core-js/es6/function';
12import 'core-js/es6/parse-int';
13import 'core-js/es6/parse-float';
14import 'core-js/es6/number';
15import 'core-js/es6/math';
16import 'core-js/es6/string';
17import 'core-js/es6/date';
18import 'core-js/es6/array';
19import 'core-js/es6/regexp';
20import 'core-js/es6/map';
21import 'core-js/es6/set';
22import 'core-js/es6/weak-map';
23import 'core-js/es6/weak-set';
24import 'core-js/es6/typed';
25import 'core-js/es6/reflect';
26// see issue https://github.com/AngularClass/angular2-webpack-starter/issues/709
27// import 'core-js/es6/promise';
28
10import 'core-js/es7/reflect'; 29import 'core-js/es7/reflect';
11require('zone.js/dist/zone'); 30import 'zone.js/dist/zone';
12 31
13// Typescript emit helpers polyfill 32// Typescript emit helpers polyfill
14import 'ts-helpers'; 33import 'ts-helpers';
diff --git a/client/src/sass/application.scss b/client/src/sass/application.scss
index 9c48b4627..b3bdffe50 100644
--- a/client/src/sass/application.scss
+++ b/client/src/sass/application.scss
@@ -6,6 +6,45 @@ body {
6 } 6 }
7} 7}
8 8
9menu {
10 @media screen and (max-width: 600px) {
11 margin-right: 3px !important;
12 padding: 3px !important;
13 min-height: 400px !important;
14 }
15
16 min-height: 600px;
17 margin-right: 20px;
18 border-right: 1px solid rgba(0, 0, 0, 0.2);
19
20 .panel-block:not(:last-child) {
21 border-bottom: 1px solid rgba(0, 0, 0, 0.1);
22 }
23
24 .panel-button {
25 margin: 8px;
26 cursor: pointer;
27 transition: margin 0.2s;
28
29 &:hover {
30 margin-left: 15px;
31 }
32
33 a {
34 color: #333333;
35 }
36 }
37
38 .glyphicon {
39 margin: 5px;
40 }
41}
42
43.table-column-id {
44 width: 200px;
45}
46
47
9footer { 48footer {
10 border-top: 1px solid rgba(0, 0, 0, 0.2); 49 border-top: 1px solid rgba(0, 0, 0, 0.2);
11 padding-top: 10px; 50 padding-top: 10px;
diff --git a/client/src/vendor.ts b/client/src/vendor.ts
index 8f029191a..95356d9d0 100644
--- a/client/src/vendor.ts
+++ b/client/src/vendor.ts
@@ -8,13 +8,17 @@ import '@angular/platform-browser';
8import '@angular/platform-browser-dynamic'; 8import '@angular/platform-browser-dynamic';
9import '@angular/core'; 9import '@angular/core';
10import '@angular/common'; 10import '@angular/common';
11import '@angular/forms';
11import '@angular/http'; 12import '@angular/http';
12import '@angular/router'; 13import '@angular/router';
13 14
15import '@angularclass/hmr';
16
14// RxJS 17// RxJS
15import 'rxjs/Observable'; 18import 'rxjs/Observable';
16import 'rxjs/Subject'; 19import 'rxjs/Subject';
17import 'rxjs/add/operator/catch'; 20import 'rxjs/add/operator/catch';
21import 'rxjs/add/operator/mergeMap';
18import 'rxjs/add/operator/map'; 22import 'rxjs/add/operator/map';
19import 'rxjs/add/observable/throw'; 23import 'rxjs/add/observable/throw';
20 24
diff --git a/client/tsconfig.json b/client/tsconfig.json
index 67d1fb4f1..10573b8ee 100644
--- a/client/tsconfig.json
+++ b/client/tsconfig.json
@@ -3,74 +3,35 @@
3 "target": "es5", 3 "target": "es5",
4 "module": "commonjs", 4 "module": "commonjs",
5 "moduleResolution": "node", 5 "moduleResolution": "node",
6 "sourceMap": true,
7 "emitDecoratorMetadata": true, 6 "emitDecoratorMetadata": true,
8 "experimentalDecorators": true, 7 "experimentalDecorators": true,
9 "noImplicitAny": false, 8 "allowSyntheticDefaultImports": true,
10 "noEmitHelpers": true 9 "sourceMap": true,
10 "noEmitHelpers": true,
11 "strictNullChecks": false,
12 "baseUrl": "./src",
13 "paths": [
14 ],
15 "lib": [
16 "dom",
17 "es6"
18 ],
19 "types": [
20 "node",
21 "source-map",
22 "uglify-js",
23 "webpack"
24 ]
11 }, 25 },
26 "exclude": [
27 "node_modules",
28 "dist"
29 ],
12 "awesomeTypescriptLoaderOptions": { 30 "awesomeTypescriptLoaderOptions": {
13 "forkChecker": true 31 "forkChecker": true,
32 "useWebpackText": true
14 }, 33 },
15 "compileOnSave": false, 34 "compileOnSave": false,
16 "buildOnSave": false, 35 "buildOnSave": false,
17 "atom": { 36 "atom": { "rewriteTsconfig": false }
18 "rewriteTsconfig": true
19 },
20 "filesGlob": [
21 "**/*.ts",
22 "!node_modules/**"
23 ],
24 "exclude": [
25 "node_modules",
26 "typings/main",
27 "typings/main.d.ts"
28 ],
29 "files": [
30 "src/app/app.component.ts",
31 "src/app/app.routes.ts",
32 "src/app/friends/friend.service.ts",
33 "src/app/friends/index.ts",
34 "src/app/login/index.ts",
35 "src/app/login/login.component.ts",
36 "src/app/login/login.routes.ts",
37 "src/app/shared/auth/auth-http.service.ts",
38 "src/app/shared/auth/auth-status.model.ts",
39 "src/app/shared/auth/auth.service.ts",
40 "src/app/shared/auth/index.ts",
41 "src/app/shared/auth/user.model.ts",
42 "src/app/shared/index.ts",
43 "src/app/shared/search/index.ts",
44 "src/app/shared/search/search-field.type.ts",
45 "src/app/shared/search/search.component.ts",
46 "src/app/shared/search/search.model.ts",
47 "src/app/shared/search/search.service.ts",
48 "src/app/videos/index.ts",
49 "src/app/videos/shared/index.ts",
50 "src/app/videos/shared/loader/index.ts",
51 "src/app/videos/shared/loader/loader.component.ts",
52 "src/app/videos/shared/pagination.model.ts",
53 "src/app/videos/shared/sort-field.type.ts",
54 "src/app/videos/shared/video.model.ts",
55 "src/app/videos/shared/video.service.ts",
56 "src/app/videos/video-add/index.ts",
57 "src/app/videos/video-add/video-add.component.ts",
58 "src/app/videos/video-list/index.ts",
59 "src/app/videos/video-list/video-list.component.ts",
60 "src/app/videos/video-list/video-miniature.component.ts",
61 "src/app/videos/video-list/video-sort.component.ts",
62 "src/app/videos/video-watch/index.ts",
63 "src/app/videos/video-watch/video-watch.component.ts",
64 "src/app/videos/video-watch/webtorrent.service.ts",
65 "src/app/videos/videos.component.ts",
66 "src/app/videos/videos.routes.ts",
67 "src/custom-typings.d.ts",
68 "src/main.ts",
69 "src/polyfills.ts",
70 "src/vendor.ts",
71 "typings/globals/es6-shim/index.d.ts",
72 "typings/globals/jasmine/index.d.ts",
73 "typings/globals/node/index.d.ts",
74 "typings/index.d.ts"
75 ]
76} 37}
diff --git a/client/typings.json b/client/typings.json
deleted file mode 100644
index 9a8891f25..000000000
--- a/client/typings.json
+++ /dev/null
@@ -1,7 +0,0 @@
1{
2 "globalDependencies": {
3 "es6-shim": "registry:dt/es6-shim#0.31.2+20160317120654",
4 "jasmine": "registry:dt/jasmine#2.2.0+20160412134438",
5 "node": "registry:dt/node#4.0.0+20160509154515"
6 }
7}
diff --git a/client/webpack.config.js b/client/webpack.config.js
index 8f54d88e5..3d3af91ad 100644
--- a/client/webpack.config.js
+++ b/client/webpack.config.js
@@ -1,16 +1,16 @@
1switch (process.env.NODE_ENV) { 1switch (process.env.NODE_ENV) {
2 case 'prod': 2 case 'prod':
3 case 'production': 3 case 'production':
4 module.exports = require('./config/webpack.prod') 4 module.exports = require('./config/webpack.prod')({env: 'production'})
5 break 5 break
6 6
7 case 'test': 7 case 'test':
8 case 'testing': 8 case 'testing':
9 module.exports = require('./config/webpack.test') 9 module.exports = require('./config/webpack.test')({env: 'test'})
10 break 10 break
11 11
12 case 'dev': 12 case 'dev':
13 case 'development': 13 case 'development':
14 default: 14 default:
15 module.exports = require('./config/webpack.dev') 15 module.exports = require('./config/webpack.dev')({env: 'development'})
16} 16}
diff --git a/config/default.yaml b/config/default.yaml
index 9a8a57879..b44be31b0 100644
--- a/config/default.yaml
+++ b/config/default.yaml
@@ -19,8 +19,5 @@ storage:
19 thumbnails: 'thumbnails/' 19 thumbnails: 'thumbnails/'
20 torrents: 'torrents/' 20 torrents: 'torrents/'
21 21
22network:
23 friends: []
24
25electron: 22electron:
26 debug: false 23 debug: false
diff --git a/config/production.yaml.example b/config/production.yaml.example
index 6dc79d73f..8bc63ee92 100644
--- a/config/production.yaml.example
+++ b/config/production.yaml.example
@@ -1,4 +1,8 @@
1# Correspond to your reverse proxy "listen" configuration
1webserver: 2webserver:
2 https: false 3 https: false
3 host: 'example.com' 4 host: 'example.com'
4 port: 80 5 port: 80
6
7database:
8 suffix: '-prod'
diff --git a/config/test-1.yaml b/config/test-1.yaml
index 0998eaea1..a59566cc4 100644
--- a/config/test-1.yaml
+++ b/config/test-1.yaml
@@ -15,7 +15,3 @@ storage:
15 logs: 'test1/logs/' 15 logs: 'test1/logs/'
16 thumbnails: 'test1/thumbnails/' 16 thumbnails: 'test1/thumbnails/'
17 torrents: 'test1/torrents/' 17 torrents: 'test1/torrents/'
18
19network:
20 friends:
21 - 'http://localhost:9002'
diff --git a/config/test-2.yaml b/config/test-2.yaml
index ec2cff811..1b937898f 100644
--- a/config/test-2.yaml
+++ b/config/test-2.yaml
@@ -15,7 +15,3 @@ storage:
15 logs: 'test2/logs/' 15 logs: 'test2/logs/'
16 thumbnails: 'test2/thumbnails/' 16 thumbnails: 'test2/thumbnails/'
17 torrents: 'test2/torrents/' 17 torrents: 'test2/torrents/'
18
19network:
20 friends:
21 - 'http://localhost:9003'
diff --git a/config/test-3.yaml b/config/test-3.yaml
index 24f5533e0..e522c13e7 100644
--- a/config/test-3.yaml
+++ b/config/test-3.yaml
@@ -15,7 +15,3 @@ storage:
15 logs: 'test3/logs/' 15 logs: 'test3/logs/'
16 thumbnails: 'test3/thumbnails/' 16 thumbnails: 'test3/thumbnails/'
17 torrents: 'test3/torrents/' 17 torrents: 'test3/torrents/'
18
19network:
20 friends:
21 - 'http://localhost:9001'
diff --git a/config/test-4.yaml b/config/test-4.yaml
index 1f884dbf2..e30cd7978 100644
--- a/config/test-4.yaml
+++ b/config/test-4.yaml
@@ -15,7 +15,3 @@ storage:
15 logs: 'test4/logs/' 15 logs: 'test4/logs/'
16 thumbnails: 'test4/thumbnails/' 16 thumbnails: 'test4/thumbnails/'
17 torrents: 'test4/torrents/' 17 torrents: 'test4/torrents/'
18
19network:
20 friends:
21 - 'http://localhost:9002'
diff --git a/config/test-5.yaml b/config/test-5.yaml
index 08ed9f068..3a54599f5 100644
--- a/config/test-5.yaml
+++ b/config/test-5.yaml
@@ -15,8 +15,3 @@ storage:
15 logs: 'test5/logs/' 15 logs: 'test5/logs/'
16 thumbnails: 'test5/thumbnails/' 16 thumbnails: 'test5/thumbnails/'
17 torrents: 'test5/torrents/' 17 torrents: 'test5/torrents/'
18
19network:
20 friends:
21 - 'http://localhost:9001'
22 - 'http://localhost:9004'
diff --git a/config/test-6.yaml b/config/test-6.yaml
index a57784cca..31608add2 100644
--- a/config/test-6.yaml
+++ b/config/test-6.yaml
@@ -15,9 +15,3 @@ storage:
15 logs: 'test6/logs/' 15 logs: 'test6/logs/'
16 thumbnails: 'test6/thumbnails/' 16 thumbnails: 'test6/thumbnails/'
17 torrents: 'test6/torrents/' 17 torrents: 'test6/torrents/'
18
19network:
20 friends:
21 - 'http://localhost:9001'
22 - 'http://localhost:9002'
23 - 'http://localhost:9003'
diff --git a/package.json b/package.json
index 63d014376..59c7a4332 100644
--- a/package.json
+++ b/package.json
@@ -35,6 +35,7 @@
35 }, 35 },
36 "dependencies": { 36 "dependencies": {
37 "async": "^2.0.0", 37 "async": "^2.0.0",
38 "bcrypt": "^0.8.7",
38 "bittorrent-tracker": "^8.0.0", 39 "bittorrent-tracker": "^8.0.0",
39 "body-parser": "^1.12.4", 40 "body-parser": "^1.12.4",
40 "concurrently": "^2.0.0", 41 "concurrently": "^2.0.0",
@@ -59,7 +60,6 @@
59 "request": "^2.57.0", 60 "request": "^2.57.0",
60 "request-replay": "^1.0.2", 61 "request-replay": "^1.0.2",
61 "scripty": "^1.5.0", 62 "scripty": "^1.5.0",
62 "segfault-handler": "^1.0.0",
63 "ursa": "^0.9.1", 63 "ursa": "^0.9.1",
64 "winston": "^2.1.1", 64 "winston": "^2.1.1",
65 "ws": "^1.1.1" 65 "ws": "^1.1.1"
@@ -67,9 +67,9 @@
67 "devDependencies": { 67 "devDependencies": {
68 "chai": "^3.3.0", 68 "chai": "^3.3.0",
69 "commander": "^2.9.0", 69 "commander": "^2.9.0",
70 "mocha": "^2.3.3", 70 "mocha": "^3.0.1",
71 "standard": "^7.0.1", 71 "standard": "^8.0.0",
72 "supertest": "^1.1.0" 72 "supertest": "^2.0.0"
73 }, 73 },
74 "standard": { 74 "standard": {
75 "ignore": [ 75 "ignore": [
diff --git a/scripts/build/client/prod.sh b/scripts/build/client/prod.sh
index 478cae99e..e090e8082 100755
--- a/scripts/build/client/prod.sh
+++ b/scripts/build/client/prod.sh
@@ -2,4 +2,4 @@
2 2
3cd client || exit -1 3cd client || exit -1
4 4
5npm run webpack -- --config config/webpack.prod.js --progress --profile --colors --display-error-details --display-cached --bail 5npm run webpack -- --config config/webpack.prod.js --progress --profile --bail
diff --git a/server.js b/server.js
index 0033ed1db..5feb21476 100644
--- a/server.js
+++ b/server.js
@@ -32,6 +32,7 @@ if (miss.length !== 0) {
32// ----------- PeerTube modules ----------- 32// ----------- PeerTube modules -----------
33const customValidators = require('./server/helpers/custom-validators') 33const customValidators = require('./server/helpers/custom-validators')
34const installer = require('./server/initializers/installer') 34const installer = require('./server/initializers/installer')
35const migrator = require('./server/initializers/migrator')
35const mongoose = require('mongoose') 36const mongoose = require('mongoose')
36const routes = require('./server/controllers') 37const routes = require('./server/controllers')
37const Request = mongoose.model('Request') 38const Request = mongoose.model('Request')
@@ -46,18 +47,21 @@ const port = config.get('listen.port')
46// For the logger 47// For the logger
47app.use(morgan('combined', { stream: logger.stream })) 48app.use(morgan('combined', { stream: logger.stream }))
48// For body requests 49// For body requests
49app.use(bodyParser.json()) 50app.use(bodyParser.json({ limit: '500kb' }))
50app.use(bodyParser.urlencoded({ extended: false })) 51app.use(bodyParser.urlencoded({ extended: false }))
51// Validate some params for the API 52// Validate some params for the API
52app.use(expressValidator({ 53app.use(expressValidator({
53 customValidators: customValidators 54 customValidators: Object.assign(
55 {},
56 customValidators.misc,
57 customValidators.pods,
58 customValidators.users,
59 customValidators.videos
60 )
54})) 61}))
55 62
56// ----------- Views, routes and static files ----------- 63// ----------- Views, routes and static files -----------
57 64
58// Catch sefaults
59require('segfault-handler').registerHandler()
60
61// API routes 65// API routes
62const apiRoute = '/api/' + constants.API_VERSION 66const apiRoute = '/api/' + constants.API_VERSION
63app.use(apiRoute, routes.api) 67app.use(apiRoute, routes.api)
@@ -125,14 +129,19 @@ app.use(function (err, req, res, next) {
125installer.installApplication(function (err) { 129installer.installApplication(function (err) {
126 if (err) throw err 130 if (err) throw err
127 131
128 // ----------- Make the server listening ----------- 132 // Run the migration scripts if needed
129 server.listen(port, function () { 133 migrator.migrate(function (err) {
130 // Activate the pool requests 134 if (err) throw err
131 Request.activate() 135
136 // ----------- Make the server listening -----------
137 server.listen(port, function () {
138 // Activate the pool requests
139 Request.activate()
132 140
133 logger.info('Seeded all the videos') 141 logger.info('Seeded all the videos')
134 logger.info('Server listening on port %d', port) 142 logger.info('Server listening on port %d', port)
135 app.emit('ready') 143 app.emit('ready')
144 })
136 }) 145 })
137}) 146})
138 147
diff --git a/server/controllers/api/v1/clients.js b/server/controllers/api/v1/clients.js
new file mode 100644
index 000000000..5b460db2e
--- /dev/null
+++ b/server/controllers/api/v1/clients.js
@@ -0,0 +1,41 @@
1'use strict'
2
3const express = require('express')
4const mongoose = require('mongoose')
5
6const constants = require('../../../initializers/constants')
7
8const Client = mongoose.model('OAuthClient')
9
10const router = express.Router()
11
12router.get('/local', getLocalClient)
13
14// Get the client credentials for the PeerTube front end
15function getLocalClient (req, res, next) {
16 const serverHost = constants.CONFIG.WEBSERVER.HOST
17 const serverPort = constants.CONFIG.WEBSERVER.PORT
18 let headerHostShouldBe = serverHost
19 if (serverPort !== 80 && serverPort !== 443) {
20 headerHostShouldBe += ':' + serverPort
21 }
22
23 // Don't make this check if this is a test instance
24 if (process.env.NODE_ENV !== 'test' && req.get('host') !== headerHostShouldBe) {
25 return res.type('json').status(403).end()
26 }
27
28 Client.loadFirstClient(function (err, client) {
29 if (err) return next(err)
30 if (!client) return next(new Error('No client available.'))
31
32 res.json({
33 client_id: client._id,
34 client_secret: client.clientSecret
35 })
36 })
37}
38
39// ---------------------------------------------------------------------------
40
41module.exports = router
diff --git a/server/controllers/api/v1/index.js b/server/controllers/api/v1/index.js
index e0c29a8a2..2e4fb2dab 100644
--- a/server/controllers/api/v1/index.js
+++ b/server/controllers/api/v1/index.js
@@ -4,13 +4,17 @@ const express = require('express')
4 4
5const router = express.Router() 5const router = express.Router()
6 6
7const clientsController = require('./clients')
7const podsController = require('./pods') 8const podsController = require('./pods')
8const remoteController = require('./remote') 9const remoteController = require('./remote')
10const requestsController = require('./requests')
9const usersController = require('./users') 11const usersController = require('./users')
10const videosController = require('./videos') 12const videosController = require('./videos')
11 13
14router.use('/clients', clientsController)
12router.use('/pods', podsController) 15router.use('/pods', podsController)
13router.use('/remote', remoteController) 16router.use('/remote', remoteController)
17router.use('/requests', requestsController)
14router.use('/users', usersController) 18router.use('/users', usersController)
15router.use('/videos', videosController) 19router.use('/videos', videosController)
16router.use('/*', badRequest) 20router.use('/*', badRequest)
diff --git a/server/controllers/api/v1/pods.js b/server/controllers/api/v1/pods.js
index 2bc761fef..8ffade578 100644
--- a/server/controllers/api/v1/pods.js
+++ b/server/controllers/api/v1/pods.js
@@ -8,7 +8,10 @@ const waterfall = require('async/waterfall')
8const logger = require('../../../helpers/logger') 8const logger = require('../../../helpers/logger')
9const friends = require('../../../lib/friends') 9const friends = require('../../../lib/friends')
10const middlewares = require('../../../middlewares') 10const middlewares = require('../../../middlewares')
11const admin = middlewares.admin
11const oAuth = middlewares.oauth 12const oAuth = middlewares.oauth
13const podsMiddleware = middlewares.pods
14const checkSignature = middlewares.secure.checkSignature
12const validators = middlewares.validators.pods 15const validators = middlewares.validators.pods
13const signatureValidator = middlewares.validators.remote.signature 16const signatureValidator = middlewares.validators.remote.signature
14 17
@@ -16,12 +19,30 @@ const router = express.Router()
16const Pod = mongoose.model('Pod') 19const Pod = mongoose.model('Pod')
17const Video = mongoose.model('Video') 20const Video = mongoose.model('Video')
18 21
19router.get('/', listPodsUrl) 22router.get('/', listPods)
20router.post('/', validators.podsAdd, addPods) 23router.post('/',
21router.get('/makefriends', oAuth.authenticate, validators.makeFriends, makeFriends) 24 validators.podsAdd,
22router.get('/quitfriends', oAuth.authenticate, quitFriends) 25 podsMiddleware.setBodyUrlPort,
26 addPods
27)
28router.post('/makefriends',
29 oAuth.authenticate,
30 admin.ensureIsAdmin,
31 validators.makeFriends,
32 podsMiddleware.setBodyUrlsPort,
33 makeFriends
34)
35router.get('/quitfriends',
36 oAuth.authenticate,
37 admin.ensureIsAdmin,
38 quitFriends
39)
23// Post because this is a secured request 40// Post because this is a secured request
24router.post('/remove', signatureValidator, removePods) 41router.post('/remove',
42 signatureValidator,
43 checkSignature,
44 removePods
45)
25 46
26// --------------------------------------------------------------------------- 47// ---------------------------------------------------------------------------
27 48
@@ -64,20 +85,27 @@ function addPods (req, res, next) {
64 }) 85 })
65} 86}
66 87
67function listPodsUrl (req, res, next) { 88function listPods (req, res, next) {
68 Pod.listOnlyUrls(function (err, podsUrlList) { 89 Pod.list(function (err, podsUrlList) {
69 if (err) return next(err) 90 if (err) return next(err)
70 91
71 res.json(podsUrlList) 92 res.json(getFormatedPods(podsUrlList))
72 }) 93 })
73} 94}
74 95
75function makeFriends (req, res, next) { 96function makeFriends (req, res, next) {
76 friends.makeFriends(function (err) { 97 const urls = req.body.urls
77 if (err) return next(err)
78 98
79 res.type('json').status(204).end() 99 friends.makeFriends(urls, function (err) {
100 if (err) {
101 logger.error('Could not make friends.', { error: err })
102 return
103 }
104
105 logger.info('Made friends!')
80 }) 106 })
107
108 res.type('json').status(204).end()
81} 109}
82 110
83function removePods (req, res, next) { 111function removePods (req, res, next) {
@@ -125,3 +153,15 @@ function quitFriends (req, res, next) {
125 res.type('json').status(204).end() 153 res.type('json').status(204).end()
126 }) 154 })
127} 155}
156
157// ---------------------------------------------------------------------------
158
159function getFormatedPods (pods) {
160 const formatedPods = []
161
162 pods.forEach(function (pod) {
163 formatedPods.push(pod.toFormatedJSON())
164 })
165
166 return formatedPods
167}
diff --git a/server/controllers/api/v1/remote.js b/server/controllers/api/v1/remote.js
index f452986b8..a22c5d151 100644
--- a/server/controllers/api/v1/remote.js
+++ b/server/controllers/api/v1/remote.js
@@ -16,6 +16,7 @@ const Video = mongoose.model('Video')
16router.post('/videos', 16router.post('/videos',
17 validators.signature, 17 validators.signature,
18 validators.dataToDecrypt, 18 validators.dataToDecrypt,
19 secureMiddleware.checkSignature,
19 secureMiddleware.decryptBody, 20 secureMiddleware.decryptBody,
20 validators.remoteVideos, 21 validators.remoteVideos,
21 remoteVideos 22 remoteVideos
diff --git a/server/controllers/api/v1/requests.js b/server/controllers/api/v1/requests.js
new file mode 100644
index 000000000..97616424d
--- /dev/null
+++ b/server/controllers/api/v1/requests.js
@@ -0,0 +1,38 @@
1'use strict'
2
3const express = require('express')
4const mongoose = require('mongoose')
5
6const constants = require('../../../initializers/constants')
7const middlewares = require('../../../middlewares')
8const admin = middlewares.admin
9const oAuth = middlewares.oauth
10
11const Request = mongoose.model('Request')
12
13const router = express.Router()
14
15router.get('/stats',
16 oAuth.authenticate,
17 admin.ensureIsAdmin,
18 getStatsRequests
19)
20
21// ---------------------------------------------------------------------------
22
23module.exports = router
24
25// ---------------------------------------------------------------------------
26
27function getStatsRequests (req, res, next) {
28 Request.list(function (err, requests) {
29 if (err) return next(err)
30
31 return res.json({
32 requests: requests,
33 maxRequestsInParallel: constants.REQUESTS_IN_PARALLEL,
34 remainingMilliSeconds: Request.remainingMilliSeconds(),
35 milliSecondsInterval: constants.REQUESTS_INTERVAL
36 })
37 })
38}
diff --git a/server/controllers/api/v1/users.js b/server/controllers/api/v1/users.js
index fbbe6e472..975e25e68 100644
--- a/server/controllers/api/v1/users.js
+++ b/server/controllers/api/v1/users.js
@@ -1,18 +1,59 @@
1'use strict' 1'use strict'
2 2
3const config = require('config') 3const each = require('async/each')
4const mongoose = require('mongoose')
5const express = require('express') 4const express = require('express')
5const mongoose = require('mongoose')
6const waterfall = require('async/waterfall')
6 7
7const oAuth = require('../../../middlewares').oauth 8const constants = require('../../../initializers/constants')
9const friends = require('../../../lib/friends')
10const logger = require('../../../helpers/logger')
11const middlewares = require('../../../middlewares')
12const admin = middlewares.admin
13const oAuth = middlewares.oauth
14const pagination = middlewares.pagination
15const sort = middlewares.sort
16const validatorsPagination = middlewares.validators.pagination
17const validatorsSort = middlewares.validators.sort
18const validatorsUsers = middlewares.validators.users
8 19
9const Client = mongoose.model('OAuthClient') 20const User = mongoose.model('User')
21const Video = mongoose.model('Video')
10 22
11const router = express.Router() 23const router = express.Router()
12 24
13router.get('/client', getAngularClient) 25router.get('/me', oAuth.authenticate, getUserInformation)
26
27router.get('/',
28 validatorsPagination.pagination,
29 validatorsSort.usersSort,
30 sort.setUsersSort,
31 pagination.setPagination,
32 listUsers
33)
34
35router.post('/',
36 oAuth.authenticate,
37 admin.ensureIsAdmin,
38 validatorsUsers.usersAdd,
39 createUser
40)
41
42router.put('/:id',
43 oAuth.authenticate,
44 validatorsUsers.usersUpdate,
45 updateUser
46)
47
48router.delete('/:id',
49 oAuth.authenticate,
50 admin.ensureIsAdmin,
51 validatorsUsers.usersRemove,
52 removeUser
53)
54
14router.post('/token', oAuth.token, success) 55router.post('/token', oAuth.token, success)
15// TODO: Once https://github.com/oauthjs/node-oauth2-server/pull/289 is merged,, implement revoke token route 56// TODO: Once https://github.com/oauthjs/node-oauth2-server/pull/289 is merged, implement revoke token route
16 57
17// --------------------------------------------------------------------------- 58// ---------------------------------------------------------------------------
18 59
@@ -20,26 +61,91 @@ module.exports = router
20 61
21// --------------------------------------------------------------------------- 62// ---------------------------------------------------------------------------
22 63
23function getAngularClient (req, res, next) { 64function createUser (req, res, next) {
24 const serverHost = config.get('webserver.host') 65 const user = new User({
25 const serverPort = config.get('webserver.port') 66 username: req.body.username,
26 let headerHostShouldBe = serverHost 67 password: req.body.password,
27 if (serverPort !== 80 && serverPort !== 443) { 68 role: constants.USER_ROLES.USER
28 headerHostShouldBe += ':' + serverPort 69 })
29 }
30 70
31 // Don't make this check if this is a test instance 71 user.save(function (err, createdUser) {
32 if (process.env.NODE_ENV !== 'test' && req.get('host') !== headerHostShouldBe) { 72 if (err) return next(err)
33 return res.type('json').status(403).end()
34 }
35 73
36 Client.loadFirstClient(function (err, client) { 74 return res.type('json').status(204).end()
75 })
76}
77
78function getUserInformation (req, res, next) {
79 User.loadByUsername(res.locals.oauth.token.user.username, function (err, user) {
37 if (err) return next(err) 80 if (err) return next(err)
38 if (!client) return next(new Error('No client available.'))
39 81
40 res.json({ 82 return res.json(user.toFormatedJSON())
41 client_id: client._id, 83 })
42 client_secret: client.clientSecret 84}
85
86function listUsers (req, res, next) {
87 User.listForApi(req.query.start, req.query.count, req.query.sort, function (err, usersList, usersTotal) {
88 if (err) return next(err)
89
90 res.json(getFormatedUsers(usersList, usersTotal))
91 })
92}
93
94function removeUser (req, res, next) {
95 waterfall([
96 function getUser (callback) {
97 User.loadById(req.params.id, callback)
98 },
99
100 function getVideos (user, callback) {
101 Video.listOwnedByAuthor(user.username, function (err, videos) {
102 return callback(err, user, videos)
103 })
104 },
105
106 function removeVideosFromDB (user, videos, callback) {
107 each(videos, function (video, callbackEach) {
108 video.remove(callbackEach)
109 }, function (err) {
110 return callback(err, user, videos)
111 })
112 },
113
114 function sendInformationToFriends (user, videos, callback) {
115 videos.forEach(function (video) {
116 const params = {
117 name: video.name,
118 magnetUri: video.magnetUri
119 }
120
121 friends.removeVideoToFriends(params)
122 })
123
124 return callback(null, user)
125 },
126
127 function removeUserFromDB (user, callback) {
128 user.remove(callback)
129 }
130 ], function andFinally (err) {
131 if (err) {
132 logger.error('Errors when removed the user.', { error: err })
133 return next(err)
134 }
135
136 return res.sendStatus(204)
137 })
138}
139
140function updateUser (req, res, next) {
141 User.loadByUsername(res.locals.oauth.token.user.username, function (err, user) {
142 if (err) return next(err)
143
144 user.password = req.body.password
145 user.save(function (err) {
146 if (err) return next(err)
147
148 return res.sendStatus(204)
43 }) 149 })
44 }) 150 })
45} 151}
@@ -47,3 +153,18 @@ function getAngularClient (req, res, next) {
47function success (req, res, next) { 153function success (req, res, next) {
48 res.end() 154 res.end()
49} 155}
156
157// ---------------------------------------------------------------------------
158
159function getFormatedUsers (users, usersTotal) {
160 const formatedUsers = []
161
162 users.forEach(function (user) {
163 formatedUsers.push(user.toFormatedJSON())
164 })
165
166 return {
167 total: usersTotal,
168 data: formatedUsers
169 }
170}
diff --git a/server/controllers/api/v1/videos.js b/server/controllers/api/v1/videos.js
index 1f939b077..70d22f139 100644
--- a/server/controllers/api/v1/videos.js
+++ b/server/controllers/api/v1/videos.js
@@ -1,11 +1,11 @@
1'use strict' 1'use strict'
2 2
3const config = require('config')
4const express = require('express') 3const express = require('express')
5const mongoose = require('mongoose') 4const mongoose = require('mongoose')
6const multer = require('multer') 5const multer = require('multer')
7const waterfall = require('async/waterfall') 6const waterfall = require('async/waterfall')
8 7
8const constants = require('../../../initializers/constants')
9const logger = require('../../../helpers/logger') 9const logger = require('../../../helpers/logger')
10const friends = require('../../../lib/friends') 10const friends = require('../../../lib/friends')
11const middlewares = require('../../../middlewares') 11const middlewares = require('../../../middlewares')
@@ -20,13 +20,12 @@ const sort = middlewares.sort
20const utils = require('../../../helpers/utils') 20const utils = require('../../../helpers/utils')
21 21
22const router = express.Router() 22const router = express.Router()
23const uploads = config.get('storage.uploads')
24const Video = mongoose.model('Video') 23const Video = mongoose.model('Video')
25 24
26// multer configuration 25// multer configuration
27const storage = multer.diskStorage({ 26const storage = multer.diskStorage({
28 destination: function (req, file, cb) { 27 destination: function (req, file, cb) {
29 cb(null, uploads) 28 cb(null, constants.CONFIG.STORAGE.UPLOAD_DIR)
30 }, 29 },
31 30
32 filename: function (req, file, cb) { 31 filename: function (req, file, cb) {
@@ -142,7 +141,7 @@ function getVideo (req, res, next) {
142} 141}
143 142
144function listVideos (req, res, next) { 143function listVideos (req, res, next) {
145 Video.list(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) { 144 Video.listForApi(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) {
146 if (err) return next(err) 145 if (err) return next(err)
147 146
148 res.json(getFormatedVideos(videosList, videosTotal)) 147 res.json(getFormatedVideos(videosList, videosTotal))
diff --git a/server/helpers/custom-validators/index.js b/server/helpers/custom-validators/index.js
new file mode 100644
index 000000000..96b5b20b9
--- /dev/null
+++ b/server/helpers/custom-validators/index.js
@@ -0,0 +1,17 @@
1'use strict'
2
3const miscValidators = require('./misc')
4const podsValidators = require('./pods')
5const usersValidators = require('./users')
6const videosValidators = require('./videos')
7
8const validators = {
9 misc: miscValidators,
10 pods: podsValidators,
11 users: usersValidators,
12 videos: videosValidators
13}
14
15// ---------------------------------------------------------------------------
16
17module.exports = validators
diff --git a/server/helpers/custom-validators/misc.js b/server/helpers/custom-validators/misc.js
new file mode 100644
index 000000000..052726241
--- /dev/null
+++ b/server/helpers/custom-validators/misc.js
@@ -0,0 +1,18 @@
1'use strict'
2
3const miscValidators = {
4 exists,
5 isArray
6}
7
8function exists (value) {
9 return value !== undefined && value !== null
10}
11
12function isArray (value) {
13 return Array.isArray(value)
14}
15
16// ---------------------------------------------------------------------------
17
18module.exports = miscValidators
diff --git a/server/helpers/custom-validators/pods.js b/server/helpers/custom-validators/pods.js
new file mode 100644
index 000000000..40f8b5d0b
--- /dev/null
+++ b/server/helpers/custom-validators/pods.js
@@ -0,0 +1,21 @@
1'use strict'
2
3const validator = require('express-validator').validator
4
5const miscValidators = require('./misc')
6
7const podsValidators = {
8 isEachUniqueUrlValid
9}
10
11function isEachUniqueUrlValid (urls) {
12 return miscValidators.isArray(urls) &&
13 urls.length !== 0 &&
14 urls.every(function (url) {
15 return validator.isURL(url) && urls.indexOf(url) === urls.lastIndexOf(url)
16 })
17}
18
19// ---------------------------------------------------------------------------
20
21module.exports = podsValidators
diff --git a/server/helpers/custom-validators/users.js b/server/helpers/custom-validators/users.js
new file mode 100644
index 000000000..88fa1592e
--- /dev/null
+++ b/server/helpers/custom-validators/users.js
@@ -0,0 +1,31 @@
1'use strict'
2
3const validator = require('express-validator').validator
4const values = require('lodash/values')
5
6const constants = require('../../initializers/constants')
7const USERS_CONSTRAINTS_FIELDS = constants.CONSTRAINTS_FIELDS.USERS
8
9const usersValidators = {
10 isUserPasswordValid,
11 isUserRoleValid,
12 isUserUsernameValid
13}
14
15function isUserPasswordValid (value) {
16 return validator.isLength(value, USERS_CONSTRAINTS_FIELDS.PASSWORD)
17}
18
19function isUserRoleValid (value) {
20 return values(constants.USER_ROLES).indexOf(value) !== -1
21}
22
23function isUserUsernameValid (value) {
24 const max = USERS_CONSTRAINTS_FIELDS.USERNAME.max
25 const min = USERS_CONSTRAINTS_FIELDS.USERNAME.min
26 return validator.matches(value, new RegExp(`^[a-zA-Z0-9._]{${min},${max}}$`))
27}
28
29// ---------------------------------------------------------------------------
30
31module.exports = usersValidators
diff --git a/server/helpers/custom-validators.js b/server/helpers/custom-validators/videos.js
index b666644c0..a507ff686 100644
--- a/server/helpers/custom-validators.js
+++ b/server/helpers/custom-validators/videos.js
@@ -2,66 +2,51 @@
2 2
3const validator = require('express-validator').validator 3const validator = require('express-validator').validator
4 4
5const constants = require('../initializers/constants') 5const constants = require('../../initializers/constants')
6const VIDEOS_CONSTRAINTS_FIELDS = constants.VIDEOS_CONSTRAINTS_FIELDS 6const usersValidators = require('./users')
7 7const miscValidators = require('./misc')
8const customValidators = { 8const VIDEOS_CONSTRAINTS_FIELDS = constants.CONSTRAINTS_FIELDS.VIDEOS
9 exists: exists, 9
10 isEachRemoteVideosValid: isEachRemoteVideosValid, 10const videosValidators = {
11 isArray: isArray, 11 isEachRemoteVideosValid,
12 isVideoAuthorValid: isVideoAuthorValid, 12 isVideoAuthorValid,
13 isVideoDateValid: isVideoDateValid, 13 isVideoDateValid,
14 isVideoDescriptionValid: isVideoDescriptionValid, 14 isVideoDescriptionValid,
15 isVideoDurationValid: isVideoDurationValid, 15 isVideoDurationValid,
16 isVideoMagnetUriValid: isVideoMagnetUriValid, 16 isVideoMagnetUriValid,
17 isVideoNameValid: isVideoNameValid, 17 isVideoNameValid,
18 isVideoPodUrlValid: isVideoPodUrlValid, 18 isVideoPodUrlValid,
19 isVideoTagsValid: isVideoTagsValid, 19 isVideoTagsValid,
20 isVideoThumbnailValid: isVideoThumbnailValid, 20 isVideoThumbnailValid,
21 isVideoThumbnail64Valid: isVideoThumbnail64Valid 21 isVideoThumbnail64Valid
22}
23
24function exists (value) {
25 return value !== undefined && value !== null
26} 22}
27 23
28function isEachRemoteVideosValid (requests) { 24function isEachRemoteVideosValid (requests) {
29 return requests.every(function (request) { 25 return miscValidators.isArray(requests) &&
30 const video = request.data 26 requests.every(function (request) {
31 return ( 27 const video = request.data
32 isRequestTypeAddValid(request.type) && 28 return (
33 isVideoAuthorValid(video.author) && 29 isRequestTypeAddValid(request.type) &&
34 isVideoDateValid(video.createdDate) && 30 isVideoAuthorValid(video.author) &&
35 isVideoDescriptionValid(video.description) && 31 isVideoDateValid(video.createdDate) &&
36 isVideoDurationValid(video.duration) && 32 isVideoDescriptionValid(video.description) &&
37 isVideoMagnetUriValid(video.magnetUri) && 33 isVideoDurationValid(video.duration) &&
38 isVideoNameValid(video.name) && 34 isVideoMagnetUriValid(video.magnetUri) &&
39 isVideoPodUrlValid(video.podUrl) && 35 isVideoNameValid(video.name) &&
40 isVideoTagsValid(video.tags) && 36 isVideoPodUrlValid(video.podUrl) &&
41 isVideoThumbnail64Valid(video.thumbnailBase64) 37 isVideoTagsValid(video.tags) &&
42 ) || 38 isVideoThumbnail64Valid(video.thumbnailBase64)
43 ( 39 ) ||
44 isRequestTypeRemoveValid(request.type) && 40 (
45 isVideoNameValid(video.name) && 41 isRequestTypeRemoveValid(request.type) &&
46 isVideoMagnetUriValid(video.magnetUri) 42 isVideoNameValid(video.name) &&
47 ) 43 isVideoMagnetUriValid(video.magnetUri)
48 }) 44 )
49} 45 })
50
51function isArray (value) {
52 return Array.isArray(value)
53}
54
55function isRequestTypeAddValid (value) {
56 return value === 'add'
57}
58
59function isRequestTypeRemoveValid (value) {
60 return value === 'remove'
61} 46}
62 47
63function isVideoAuthorValid (value) { 48function isVideoAuthorValid (value) {
64 return validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.AUTHOR) 49 return usersValidators.isUserUsernameValid(value)
65} 50}
66 51
67function isVideoDateValid (value) { 52function isVideoDateValid (value) {
@@ -90,7 +75,7 @@ function isVideoPodUrlValid (value) {
90} 75}
91 76
92function isVideoTagsValid (tags) { 77function isVideoTagsValid (tags) {
93 return isArray(tags) && 78 return miscValidators.isArray(tags) &&
94 validator.isInt(tags.length, VIDEOS_CONSTRAINTS_FIELDS.TAGS) && 79 validator.isInt(tags.length, VIDEOS_CONSTRAINTS_FIELDS.TAGS) &&
95 tags.every(function (tag) { 80 tags.every(function (tag) {
96 return validator.isAlphanumeric(tag) && 81 return validator.isAlphanumeric(tag) &&
@@ -109,6 +94,14 @@ function isVideoThumbnail64Valid (value) {
109 94
110// --------------------------------------------------------------------------- 95// ---------------------------------------------------------------------------
111 96
112module.exports = customValidators 97module.exports = videosValidators
113 98
114// --------------------------------------------------------------------------- 99// ---------------------------------------------------------------------------
100
101function isRequestTypeAddValid (value) {
102 return value === 'add'
103}
104
105function isRequestTypeRemoveValid (value) {
106 return value === 'remove'
107}
diff --git a/server/helpers/logger.js b/server/helpers/logger.js
index 8ae90a4b2..590ceaeb6 100644
--- a/server/helpers/logger.js
+++ b/server/helpers/logger.js
@@ -1,23 +1,23 @@
1// Thanks http://tostring.it/2014/06/23/advanced-logging-with-nodejs/ 1// Thanks http://tostring.it/2014/06/23/advanced-logging-with-nodejs/
2'use strict' 2'use strict'
3 3
4const config = require('config')
5const mkdirp = require('mkdirp') 4const mkdirp = require('mkdirp')
6const path = require('path') 5const path = require('path')
7const winston = require('winston') 6const winston = require('winston')
8winston.emitErrs = true 7winston.emitErrs = true
9 8
10const logDir = path.join(__dirname, '..', '..', config.get('storage.logs')) 9const constants = require('../initializers/constants')
11const label = config.get('webserver.host') + ':' + config.get('webserver.port') 10
11const label = constants.CONFIG.WEBSERVER.HOST + ':' + constants.CONFIG.WEBSERVER.PORT
12 12
13// Create the directory if it does not exist 13// Create the directory if it does not exist
14mkdirp.sync(logDir) 14mkdirp.sync(constants.CONFIG.STORAGE.LOG_DIR)
15 15
16const logger = new winston.Logger({ 16const logger = new winston.Logger({
17 transports: [ 17 transports: [
18 new winston.transports.File({ 18 new winston.transports.File({
19 level: 'debug', 19 level: 'debug',
20 filename: path.join(logDir, 'all-logs.log'), 20 filename: path.join(constants.CONFIG.STORAGE.LOG_DIR, 'all-logs.log'),
21 handleExceptions: true, 21 handleExceptions: true,
22 json: true, 22 json: true,
23 maxsize: 5242880, 23 maxsize: 5242880,
diff --git a/server/helpers/peertube-crypto.js b/server/helpers/peertube-crypto.js
index 46dff8d03..1ff638b04 100644
--- a/server/helpers/peertube-crypto.js
+++ b/server/helpers/peertube-crypto.js
@@ -1,24 +1,24 @@
1'use strict' 1'use strict'
2 2
3const config = require('config') 3const bcrypt = require('bcrypt')
4const crypto = require('crypto') 4const crypto = require('crypto')
5const fs = require('fs') 5const fs = require('fs')
6const openssl = require('openssl-wrapper') 6const openssl = require('openssl-wrapper')
7const path = require('path')
8const ursa = require('ursa') 7const ursa = require('ursa')
9 8
9const constants = require('../initializers/constants')
10const logger = require('./logger') 10const logger = require('./logger')
11 11
12const certDir = path.join(__dirname, '..', '..', config.get('storage.certs'))
13const algorithm = 'aes-256-ctr' 12const algorithm = 'aes-256-ctr'
14 13
15const peertubeCrypto = { 14const peertubeCrypto = {
16 checkSignature: checkSignature, 15 checkSignature,
17 createCertsIfNotExist: createCertsIfNotExist, 16 comparePassword,
18 decrypt: decrypt, 17 createCertsIfNotExist,
19 encrypt: encrypt, 18 cryptPassword,
20 getCertDir: getCertDir, 19 decrypt,
21 sign: sign 20 encrypt,
21 sign
22} 22}
23 23
24function checkSignature (publicKey, rawData, hexSignature) { 24function checkSignature (publicKey, rawData, hexSignature) {
@@ -27,6 +27,14 @@ function checkSignature (publicKey, rawData, hexSignature) {
27 return isValid 27 return isValid
28} 28}
29 29
30function comparePassword (plainPassword, hashPassword, callback) {
31 bcrypt.compare(plainPassword, hashPassword, function (err, isPasswordMatch) {
32 if (err) return callback(err)
33
34 return callback(null, isPasswordMatch)
35 })
36}
37
30function createCertsIfNotExist (callback) { 38function createCertsIfNotExist (callback) {
31 certsExist(function (exist) { 39 certsExist(function (exist) {
32 if (exist === true) { 40 if (exist === true) {
@@ -39,8 +47,18 @@ function createCertsIfNotExist (callback) {
39 }) 47 })
40} 48}
41 49
50function cryptPassword (password, callback) {
51 bcrypt.genSalt(constants.BCRYPT_SALT_SIZE, function (err, salt) {
52 if (err) return callback(err)
53
54 bcrypt.hash(password, salt, function (err, hash) {
55 return callback(err, hash)
56 })
57 })
58}
59
42function decrypt (key, data, callback) { 60function decrypt (key, data, callback) {
43 fs.readFile(getCertDir() + 'peertube.key.pem', function (err, file) { 61 fs.readFile(constants.CONFIG.STORAGE.CERT_DIR + 'peertube.key.pem', function (err, file) {
44 if (err) return callback(err) 62 if (err) return callback(err)
45 63
46 const myPrivateKey = ursa.createPrivateKey(file) 64 const myPrivateKey = ursa.createPrivateKey(file)
@@ -67,12 +85,8 @@ function encrypt (publicKey, data, callback) {
67 }) 85 })
68} 86}
69 87
70function getCertDir () {
71 return certDir
72}
73
74function sign (data) { 88function sign (data) {
75 const myKey = ursa.createPrivateKey(fs.readFileSync(certDir + 'peertube.key.pem')) 89 const myKey = ursa.createPrivateKey(fs.readFileSync(constants.CONFIG.STORAGE.CERT_DIR + 'peertube.key.pem'))
76 const signature = myKey.hashAndSign('sha256', data, 'utf8', 'hex') 90 const signature = myKey.hashAndSign('sha256', data, 'utf8', 'hex')
77 91
78 return signature 92 return signature
@@ -85,7 +99,7 @@ module.exports = peertubeCrypto
85// --------------------------------------------------------------------------- 99// ---------------------------------------------------------------------------
86 100
87function certsExist (callback) { 101function certsExist (callback) {
88 fs.exists(certDir + 'peertube.key.pem', function (exists) { 102 fs.exists(constants.CONFIG.STORAGE.CERT_DIR + 'peertube.key.pem', function (exists) {
89 return callback(exists) 103 return callback(exists)
90 }) 104 })
91} 105}
@@ -99,15 +113,25 @@ function createCerts (callback) {
99 } 113 }
100 114
101 logger.info('Generating a RSA key...') 115 logger.info('Generating a RSA key...')
102 openssl.exec('genrsa', { 'out': certDir + 'peertube.key.pem', '2048': false }, function (err) { 116
117 let options = {
118 'out': constants.CONFIG.STORAGE.CERT_DIR + 'peertube.key.pem',
119 '2048': false
120 }
121 openssl.exec('genrsa', options, function (err) {
103 if (err) { 122 if (err) {
104 logger.error('Cannot create private key on this pod.') 123 logger.error('Cannot create private key on this pod.')
105 return callback(err) 124 return callback(err)
106 } 125 }
107 logger.info('RSA key generated.') 126 logger.info('RSA key generated.')
108 127
128 options = {
129 'in': constants.CONFIG.STORAGE.CERT_DIR + 'peertube.key.pem',
130 'pubout': true,
131 'out': constants.CONFIG.STORAGE.CERT_DIR + 'peertube.pub'
132 }
109 logger.info('Manage public key...') 133 logger.info('Manage public key...')
110 openssl.exec('rsa', { 'in': certDir + 'peertube.key.pem', 'pubout': true, 'out': certDir + 'peertube.pub' }, function (err) { 134 openssl.exec('rsa', options, function (err) {
111 if (err) { 135 if (err) {
112 logger.error('Cannot create public key on this pod.') 136 logger.error('Cannot create public key on this pod.')
113 return callback(err) 137 return callback(err)
diff --git a/server/helpers/requests.js b/server/helpers/requests.js
index 547230adc..95775c981 100644
--- a/server/helpers/requests.js
+++ b/server/helpers/requests.js
@@ -1,19 +1,14 @@
1'use strict' 1'use strict'
2 2
3const config = require('config')
4const replay = require('request-replay') 3const replay = require('request-replay')
5const request = require('request') 4const request = require('request')
6 5
7const constants = require('../initializers/constants') 6const constants = require('../initializers/constants')
8const peertubeCrypto = require('./peertube-crypto') 7const peertubeCrypto = require('./peertube-crypto')
9 8
10const http = config.get('webserver.https') ? 'https' : 'http'
11const host = config.get('webserver.host')
12const port = config.get('webserver.port')
13
14const requests = { 9const requests = {
15 makeRetryRequest: makeRetryRequest, 10 makeRetryRequest,
16 makeSecureRequest: makeSecureRequest 11 makeSecureRequest
17} 12}
18 13
19function makeRetryRequest (params, callback) { 14function makeRetryRequest (params, callback) {
@@ -29,8 +24,6 @@ function makeRetryRequest (params, callback) {
29} 24}
30 25
31function makeSecureRequest (params, callback) { 26function makeSecureRequest (params, callback) {
32 const myUrl = http + '://' + host + ':' + port
33
34 const requestParams = { 27 const requestParams = {
35 url: params.toPod.url + params.path 28 url: params.toPod.url + params.path
36 } 29 }
@@ -42,8 +35,8 @@ function makeSecureRequest (params, callback) {
42 // Add signature if it is specified in the params 35 // Add signature if it is specified in the params
43 if (params.sign === true) { 36 if (params.sign === true) {
44 requestParams.json.signature = { 37 requestParams.json.signature = {
45 url: myUrl, 38 url: constants.CONFIG.WEBSERVER.URL,
46 signature: peertubeCrypto.sign(myUrl) 39 signature: peertubeCrypto.sign(constants.CONFIG.WEBSERVER.URL)
47 } 40 }
48 } 41 }
49 42
diff --git a/server/helpers/utils.js b/server/helpers/utils.js
index a77116e08..9c2d402e3 100644
--- a/server/helpers/utils.js
+++ b/server/helpers/utils.js
@@ -5,8 +5,8 @@ const crypto = require('crypto')
5const logger = require('./logger') 5const logger = require('./logger')
6 6
7const utils = { 7const utils = {
8 cleanForExit: cleanForExit, 8 cleanForExit,
9 generateRandomString: generateRandomString 9 generateRandomString
10} 10}
11 11
12function generateRandomString (size, callback) { 12function generateRandomString (size, callback) {
diff --git a/server/initializers/checker.js b/server/initializers/checker.js
index 3831efb8d..91fbcfaf9 100644
--- a/server/initializers/checker.js
+++ b/server/initializers/checker.js
@@ -7,9 +7,9 @@ const Client = mongoose.model('OAuthClient')
7const User = mongoose.model('User') 7const User = mongoose.model('User')
8 8
9const checker = { 9const checker = {
10 checkConfig: checkConfig, 10 checkConfig,
11 clientsExist: clientsExist, 11 clientsExist,
12 usersExist: usersExist 12 usersExist
13} 13}
14 14
15// Check the config files 15// Check the config files
@@ -17,8 +17,8 @@ function checkConfig () {
17 const required = [ 'listen.port', 17 const required = [ 'listen.port',
18 'webserver.https', 'webserver.host', 'webserver.port', 18 'webserver.https', 'webserver.host', 'webserver.port',
19 'database.host', 'database.port', 'database.suffix', 19 'database.host', 'database.port', 'database.suffix',
20 'storage.certs', 'storage.uploads', 'storage.logs', 20 'storage.certs', 'storage.uploads', 'storage.logs', 'storage.thumbnails',
21 'network.friends', 'electron.debug' ] 21 'electron.debug' ]
22 const miss = [] 22 const miss = []
23 23
24 for (const key of required) { 24 for (const key of required) {
@@ -39,10 +39,10 @@ function clientsExist (callback) {
39} 39}
40 40
41function usersExist (callback) { 41function usersExist (callback) {
42 User.list(function (err, users) { 42 User.countTotal(function (err, totalUsers) {
43 if (err) return callback(err) 43 if (err) return callback(err)
44 44
45 return callback(null, users.length !== 0) 45 return callback(null, totalUsers !== 0)
46 }) 46 })
47} 47}
48 48
diff --git a/server/initializers/constants.js b/server/initializers/constants.js
index e0ea188af..be2e3e943 100644
--- a/server/initializers/constants.js
+++ b/server/initializers/constants.js
@@ -1,24 +1,103 @@
1'use strict' 1'use strict'
2 2
3// API version of our pod 3const config = require('config')
4const path = require('path')
5
6// ---------------------------------------------------------------------------
7
8// API version
4const API_VERSION = 'v1' 9const API_VERSION = 'v1'
5 10
6// Score a pod has when we create it as a friend 11// Number of results by default for the pagination
7const FRIEND_SCORE = { 12const PAGINATION_COUNT_DEFAULT = 15
8 BASE: 100, 13
9 MAX: 1000 14// Sortable columns per schema
15const SEARCHABLE_COLUMNS = {
16 VIDEOS: [ 'name', 'magnetUri', 'podUrl', 'author', 'tags' ]
10} 17}
11 18
12// Time to wait between requests to the friends (10 min) 19// Sortable columns per schema
13let INTERVAL = 600000 20const SORTABLE_COLUMNS = {
21 USERS: [ 'username', '-username', 'createdDate', '-createdDate' ],
22 VIDEOS: [ 'name', '-name', 'duration', '-duration', 'createdDate', '-createdDate' ]
23}
14 24
15const OAUTH_LIFETIME = { 25const OAUTH_LIFETIME = {
16 ACCESS_TOKEN: 3600 * 4, // 4 hours 26 ACCESS_TOKEN: 3600 * 4, // 4 hours
17 REFRESH_TOKEN: 1209600 // 2 weeks 27 REFRESH_TOKEN: 1209600 // 2 weeks
18} 28}
19 29
20// Number of results by default for the pagination 30// ---------------------------------------------------------------------------
21const PAGINATION_COUNT_DEFAULT = 15 31
32const CONFIG = {
33 DATABASE: {
34 DBNAME: 'peertube' + config.get('database.suffix'),
35 HOST: config.get('database.host'),
36 PORT: config.get('database.port')
37 },
38 ELECTRON: {
39 DEBUG: config.get('electron.debug')
40 },
41 STORAGE: {
42 CERT_DIR: path.join(__dirname, '..', '..', config.get('storage.certs')),
43 LOG_DIR: path.join(__dirname, '..', '..', config.get('storage.logs')),
44 UPLOAD_DIR: path.join(__dirname, '..', '..', config.get('storage.uploads')),
45 THUMBNAILS_DIR: path.join(__dirname, '..', '..', config.get('storage.thumbnails'))
46 },
47 WEBSERVER: {
48 SCHEME: config.get('webserver.https') === true ? 'https' : 'http',
49 HOST: config.get('webserver.host'),
50 PORT: config.get('webserver.port')
51 }
52}
53CONFIG.WEBSERVER.URL = CONFIG.WEBSERVER.SCHEME + '://' + CONFIG.WEBSERVER.HOST + ':' + CONFIG.WEBSERVER.PORT
54
55// ---------------------------------------------------------------------------
56
57const CONSTRAINTS_FIELDS = {
58 USERS: {
59 USERNAME: { min: 3, max: 20 }, // Length
60 PASSWORD: { min: 6, max: 255 } // Length
61 },
62 VIDEOS: {
63 NAME: { min: 3, max: 50 }, // Length
64 DESCRIPTION: { min: 3, max: 250 }, // Length
65 MAGNET_URI: { min: 10 }, // Length
66 DURATION: { min: 1, max: 7200 }, // Number
67 TAGS: { min: 1, max: 3 }, // Number of total tags
68 TAG: { min: 2, max: 10 }, // Length
69 THUMBNAIL: { min: 2, max: 30 },
70 THUMBNAIL64: { min: 0, max: 20000 } // Bytes
71 }
72}
73
74// ---------------------------------------------------------------------------
75
76// Score a pod has when we create it as a friend
77const FRIEND_SCORE = {
78 BASE: 100,
79 MAX: 1000
80}
81
82// ---------------------------------------------------------------------------
83
84const MONGO_MIGRATION_SCRIPTS = [
85 {
86 script: '0005-create-application',
87 version: 5
88 },
89 {
90 script: '0010-users-password',
91 version: 10
92 },
93 {
94 script: '0015-admin-role',
95 version: 15
96 }
97]
98const LAST_MONGO_SCHEMA_VERSION = 15
99
100// ---------------------------------------------------------------------------
22 101
23// Number of points we add/remove from a friend after a successful/bad request 102// Number of points we add/remove from a friend after a successful/bad request
24const PODS_SCORE = { 103const PODS_SCORE = {
@@ -26,28 +105,22 @@ const PODS_SCORE = {
26 BONUS: 10 105 BONUS: 10
27} 106}
28 107
108// Time to wait between requests to the friends (10 min)
109let REQUESTS_INTERVAL = 600000
110
29// Number of requests in parallel we can make 111// Number of requests in parallel we can make
30const REQUESTS_IN_PARALLEL = 10 112const REQUESTS_IN_PARALLEL = 10
31 113
32// How many requests we put in request (request scheduler) 114// How many requests we put in request
33const REQUESTS_LIMIT = 10 115const REQUESTS_LIMIT = 10
34 116
35// Number of requests to retry for replay requests module 117// Number of requests to retry for replay requests module
36const RETRY_REQUESTS = 5 118const RETRY_REQUESTS = 5
37 119
38// Sortable columns per schema 120// ---------------------------------------------------------------------------
39const SEARCHABLE_COLUMNS = {
40 VIDEOS: [ 'name', 'magnetUri', 'podUrl', 'author', 'tags' ]
41}
42
43// Seeds in parallel we send to electron when "seed all"
44// Once a video is in seeding state we seed another video etc
45const SEEDS_IN_PARALLEL = 3
46 121
47// Sortable columns per schema 122// Password encryption
48const SORTABLE_COLUMNS = { 123const BCRYPT_SALT_SIZE = 10
49 VIDEOS: [ 'name', '-name', 'duration', '-duration', 'createdDate', '-createdDate' ]
50}
51 124
52// Express static paths (router) 125// Express static paths (router)
53const STATIC_PATHS = { 126const STATIC_PATHS = {
@@ -59,43 +132,47 @@ const STATIC_PATHS = {
59// Videos thumbnail size 132// Videos thumbnail size
60const THUMBNAILS_SIZE = '200x110' 133const THUMBNAILS_SIZE = '200x110'
61 134
62const VIDEOS_CONSTRAINTS_FIELDS = { 135const USER_ROLES = {
63 NAME: { min: 3, max: 50 }, // Length 136 ADMIN: 'admin',
64 DESCRIPTION: { min: 3, max: 250 }, // Length 137 USER: 'user'
65 MAGNET_URI: { min: 10 }, // Length
66 DURATION: { min: 1, max: 7200 }, // Number
67 AUTHOR: { min: 3, max: 20 }, // Length
68 TAGS: { min: 1, max: 3 }, // Number of total tags
69 TAG: { min: 2, max: 10 }, // Length
70 THUMBNAIL: { min: 2, max: 30 },
71 THUMBNAIL64: { min: 0, max: 20000 } // Bytes
72} 138}
73 139
140// Seeds in parallel we send to electron when "seed all"
141// Once a video is in seeding state we seed another video etc
142const SEEDS_IN_PARALLEL = 3
143
144// ---------------------------------------------------------------------------
145
74// Special constants for a test instance 146// Special constants for a test instance
75if (isTestInstance() === true) { 147if (isTestInstance() === true) {
148 CONSTRAINTS_FIELDS.VIDEOS.DURATION.max = 14
76 FRIEND_SCORE.BASE = 20 149 FRIEND_SCORE.BASE = 20
77 INTERVAL = 10000 150 REQUESTS_INTERVAL = 10000
78 VIDEOS_CONSTRAINTS_FIELDS.DURATION.max = 14
79} 151}
80 152
81// --------------------------------------------------------------------------- 153// ---------------------------------------------------------------------------
82 154
83module.exports = { 155module.exports = {
84 API_VERSION: API_VERSION, 156 API_VERSION,
85 FRIEND_SCORE: FRIEND_SCORE, 157 BCRYPT_SALT_SIZE,
86 INTERVAL: INTERVAL, 158 CONFIG,
87 OAUTH_LIFETIME: OAUTH_LIFETIME, 159 CONSTRAINTS_FIELDS,
88 PAGINATION_COUNT_DEFAULT: PAGINATION_COUNT_DEFAULT, 160 FRIEND_SCORE,
89 PODS_SCORE: PODS_SCORE, 161 LAST_MONGO_SCHEMA_VERSION,
90 REQUESTS_IN_PARALLEL: REQUESTS_IN_PARALLEL, 162 MONGO_MIGRATION_SCRIPTS,
91 REQUESTS_LIMIT: REQUESTS_LIMIT, 163 OAUTH_LIFETIME,
92 RETRY_REQUESTS: RETRY_REQUESTS, 164 PAGINATION_COUNT_DEFAULT,
93 SEARCHABLE_COLUMNS: SEARCHABLE_COLUMNS, 165 PODS_SCORE,
94 SEEDS_IN_PARALLEL: SEEDS_IN_PARALLEL, 166 REQUESTS_IN_PARALLEL,
95 SORTABLE_COLUMNS: SORTABLE_COLUMNS, 167 REQUESTS_INTERVAL,
96 STATIC_PATHS: STATIC_PATHS, 168 REQUESTS_LIMIT,
97 THUMBNAILS_SIZE: THUMBNAILS_SIZE, 169 RETRY_REQUESTS,
98 VIDEOS_CONSTRAINTS_FIELDS: VIDEOS_CONSTRAINTS_FIELDS 170 SEARCHABLE_COLUMNS,
171 SEEDS_IN_PARALLEL,
172 SORTABLE_COLUMNS,
173 STATIC_PATHS,
174 THUMBNAILS_SIZE,
175 USER_ROLES
99} 176}
100 177
101// --------------------------------------------------------------------------- 178// ---------------------------------------------------------------------------
diff --git a/server/initializers/database.js b/server/initializers/database.js
index 8626895ee..45c8a240d 100644
--- a/server/initializers/database.js
+++ b/server/initializers/database.js
@@ -1,30 +1,27 @@
1'use strict' 1'use strict'
2 2
3const config = require('config')
4const mongoose = require('mongoose') 3const mongoose = require('mongoose')
5 4
5const constants = require('../initializers/constants')
6const logger = require('../helpers/logger') 6const logger = require('../helpers/logger')
7 7
8// Bootstrap models 8// Bootstrap models
9require('../models/application')
10require('../models/oauth-token')
9require('../models/user') 11require('../models/user')
10require('../models/oauth-client') 12require('../models/oauth-client')
11require('../models/oauth-token')
12require('../models/pods') 13require('../models/pods')
13require('../models/video') 14require('../models/video')
14// Request model needs Video model 15// Request model needs Video model
15require('../models/request') 16require('../models/request')
16 17
17const dbname = 'peertube' + config.get('database.suffix')
18const host = config.get('database.host')
19const port = config.get('database.port')
20
21const database = { 18const database = {
22 connect: connect 19 connect: connect
23} 20}
24 21
25function connect () { 22function connect () {
26 mongoose.Promise = global.Promise 23 mongoose.Promise = global.Promise
27 mongoose.connect('mongodb://' + host + ':' + port + '/' + dbname) 24 mongoose.connect('mongodb://' + constants.CONFIG.DATABASE.HOST + ':' + constants.CONFIG.DATABASE.PORT + '/' + constants.CONFIG.DATABASE.DBNAME)
28 mongoose.connection.on('error', function () { 25 mongoose.connection.on('error', function () {
29 throw new Error('Mongodb connection error.') 26 throw new Error('Mongodb connection error.')
30 }) 27 })
diff --git a/server/initializers/installer.js b/server/initializers/installer.js
index 32830d4da..1df300ba8 100644
--- a/server/initializers/installer.js
+++ b/server/initializers/installer.js
@@ -9,14 +9,16 @@ const path = require('path')
9const series = require('async/series') 9const series = require('async/series')
10 10
11const checker = require('./checker') 11const checker = require('./checker')
12const constants = require('./constants')
12const logger = require('../helpers/logger') 13const logger = require('../helpers/logger')
13const peertubeCrypto = require('../helpers/peertube-crypto') 14const peertubeCrypto = require('../helpers/peertube-crypto')
14 15
16const Application = mongoose.model('Application')
15const Client = mongoose.model('OAuthClient') 17const Client = mongoose.model('OAuthClient')
16const User = mongoose.model('User') 18const User = mongoose.model('User')
17 19
18const installer = { 20const installer = {
19 installApplication: installApplication 21 installApplication
20} 22}
21 23
22function installApplication (callback) { 24function installApplication (callback) {
@@ -34,7 +36,7 @@ function installApplication (callback) {
34 }, 36 },
35 37
36 function createOAuthUser (callbackAsync) { 38 function createOAuthUser (callbackAsync) {
37 createOAuthUserIfNotExist(callbackAsync) 39 createOAuthAdminIfNotExist(callbackAsync)
38 } 40 }
39 ], callback) 41 ], callback)
40} 42}
@@ -80,7 +82,7 @@ function createOAuthClientIfNotExist (callback) {
80 }) 82 })
81} 83}
82 84
83function createOAuthUserIfNotExist (callback) { 85function createOAuthAdminIfNotExist (callback) {
84 checker.usersExist(function (err, exist) { 86 checker.usersExist(function (err, exist) {
85 if (err) return callback(err) 87 if (err) return callback(err)
86 88
@@ -90,6 +92,7 @@ function createOAuthUserIfNotExist (callback) {
90 logger.info('Creating the administrator.') 92 logger.info('Creating the administrator.')
91 93
92 const username = 'root' 94 const username = 'root'
95 const role = constants.USER_ROLES.ADMIN
93 let password = '' 96 let password = ''
94 97
95 // Do not generate a random password for tests 98 // Do not generate a random password for tests
@@ -104,17 +107,20 @@ function createOAuthUserIfNotExist (callback) {
104 } 107 }
105 108
106 const user = new User({ 109 const user = new User({
107 username: username, 110 username,
108 password: password 111 password,
112 role
109 }) 113 })
110 114
111 user.save(function (err, createdUser) { 115 user.save(function (err, createdUser) {
112 if (err) return callback(err) 116 if (err) return callback(err)
113 117
114 logger.info('Username: ' + createdUser.username) 118 logger.info('Username: ' + username)
115 logger.info('User password: ' + createdUser.password) 119 logger.info('User password: ' + password)
116 120
117 return callback(null) 121 logger.info('Creating Application collection.')
122 const application = new Application({ mongoSchemaVersion: constants.LAST_MONGO_SCHEMA_VERSION })
123 application.save(callback)
118 }) 124 })
119 }) 125 })
120} 126}
diff --git a/server/initializers/migrations/0005-create-application.js b/server/initializers/migrations/0005-create-application.js
new file mode 100644
index 000000000..e99dec019
--- /dev/null
+++ b/server/initializers/migrations/0005-create-application.js
@@ -0,0 +1,17 @@
1/*
2 Create the application collection in MongoDB.
3 Used to store the actual MongoDB scheme version.
4*/
5
6const mongoose = require('mongoose')
7
8const Application = mongoose.model('Application')
9
10exports.up = function (callback) {
11 const application = new Application()
12 application.save(callback)
13}
14
15exports.down = function (callback) {
16 throw new Error('Not implemented.')
17}
diff --git a/server/initializers/migrations/0010-users-password.js b/server/initializers/migrations/0010-users-password.js
new file mode 100644
index 000000000..a0616a269
--- /dev/null
+++ b/server/initializers/migrations/0010-users-password.js
@@ -0,0 +1,22 @@
1/*
2 Convert plain user password to encrypted user password.
3*/
4
5const eachSeries = require('async/eachSeries')
6const mongoose = require('mongoose')
7
8const User = mongoose.model('User')
9
10exports.up = function (callback) {
11 User.list(function (err, users) {
12 if (err) return callback(err)
13
14 eachSeries(users, function (user, callbackEach) {
15 user.save(callbackEach)
16 }, callback)
17 })
18}
19
20exports.down = function (callback) {
21 throw new Error('Not implemented.')
22}
diff --git a/server/initializers/migrations/0015-admin-role.js b/server/initializers/migrations/0015-admin-role.js
new file mode 100644
index 000000000..af06dca9e
--- /dev/null
+++ b/server/initializers/migrations/0015-admin-role.js
@@ -0,0 +1,16 @@
1/*
2 Set the admin role to the root user.
3*/
4
5const constants = require('../constants')
6const mongoose = require('mongoose')
7
8const User = mongoose.model('User')
9
10exports.up = function (callback) {
11 User.update({ username: 'root' }, { role: constants.USER_ROLES.ADMIN }, callback)
12}
13
14exports.down = function (callback) {
15 throw new Error('Not implemented.')
16}
diff --git a/server/initializers/migrator.js b/server/initializers/migrator.js
new file mode 100644
index 000000000..6b31d994f
--- /dev/null
+++ b/server/initializers/migrator.js
@@ -0,0 +1,56 @@
1'use strict'
2
3const eachSeries = require('async/eachSeries')
4const mongoose = require('mongoose')
5const path = require('path')
6
7const constants = require('./constants')
8const logger = require('../helpers/logger')
9
10const Application = mongoose.model('Application')
11
12const migrator = {
13 migrate: migrate
14}
15
16function migrate (callback) {
17 Application.loadMongoSchemaVersion(function (err, actualVersion) {
18 if (err) return callback(err)
19
20 // If there are a new mongo schemas
21 if (!actualVersion || actualVersion < constants.LAST_MONGO_SCHEMA_VERSION) {
22 logger.info('Begin migrations.')
23
24 eachSeries(constants.MONGO_MIGRATION_SCRIPTS, function (entity, callbackEach) {
25 const versionScript = entity.version
26
27 // Do not execute old migration scripts
28 if (versionScript <= actualVersion) return callbackEach(null)
29
30 // Load the migration module and run it
31 const migrationScriptName = entity.script
32 logger.info('Executing %s migration script.', migrationScriptName)
33
34 const migrationScript = require(path.join(__dirname, 'migrations', migrationScriptName))
35 migrationScript.up(function (err) {
36 if (err) return callbackEach(err)
37
38 // Update the new mongo version schema
39 Application.updateMongoSchemaVersion(versionScript, callbackEach)
40 })
41 }, function (err) {
42 if (err) return callback(err)
43
44 logger.info('Migrations finished. New mongo version schema: %s', constants.LAST_MONGO_SCHEMA_VERSION)
45 return callback(null)
46 })
47 } else {
48 return callback(null)
49 }
50 })
51}
52
53// ---------------------------------------------------------------------------
54
55module.exports = migrator
56
diff --git a/server/lib/friends.js b/server/lib/friends.js
index 6e1516b94..556d2e773 100644
--- a/server/lib/friends.js
+++ b/server/lib/friends.js
@@ -1,6 +1,5 @@
1'use strict' 1'use strict'
2 2
3const config = require('config')
4const each = require('async/each') 3const each = require('async/each')
5const eachLimit = require('async/eachLimit') 4const eachLimit = require('async/eachLimit')
6const eachSeries = require('async/eachSeries') 5const eachSeries = require('async/eachSeries')
@@ -11,24 +10,20 @@ const waterfall = require('async/waterfall')
11 10
12const constants = require('../initializers/constants') 11const constants = require('../initializers/constants')
13const logger = require('../helpers/logger') 12const logger = require('../helpers/logger')
14const peertubeCrypto = require('../helpers/peertube-crypto')
15const requests = require('../helpers/requests') 13const requests = require('../helpers/requests')
16 14
17const http = config.get('webserver.https') ? 'https' : 'http'
18const host = config.get('webserver.host')
19const port = config.get('webserver.port')
20const Pod = mongoose.model('Pod') 15const Pod = mongoose.model('Pod')
21const Request = mongoose.model('Request') 16const Request = mongoose.model('Request')
22const Video = mongoose.model('Video') 17const Video = mongoose.model('Video')
23 18
24const friends = { 19const friends = {
25 addVideoToFriends: addVideoToFriends, 20 addVideoToFriends,
26 hasFriends: hasFriends, 21 hasFriends,
27 getMyCertificate: getMyCertificate, 22 getMyCertificate,
28 makeFriends: makeFriends, 23 makeFriends,
29 quitFriends: quitFriends, 24 quitFriends,
30 removeVideoToFriends: removeVideoToFriends, 25 removeVideoToFriends,
31 sendOwnedVideosToPod: sendOwnedVideosToPod 26 sendOwnedVideosToPod
32} 27}
33 28
34function addVideoToFriends (video) { 29function addVideoToFriends (video) {
@@ -45,10 +40,10 @@ function hasFriends (callback) {
45} 40}
46 41
47function getMyCertificate (callback) { 42function getMyCertificate (callback) {
48 fs.readFile(peertubeCrypto.getCertDir() + 'peertube.pub', 'utf8', callback) 43 fs.readFile(constants.CONFIG.STORAGE.CERT_DIR + 'peertube.pub', 'utf8', callback)
49} 44}
50 45
51function makeFriends (callback) { 46function makeFriends (urls, callback) {
52 const podsScore = {} 47 const podsScore = {}
53 48
54 logger.info('Make friends!') 49 logger.info('Make friends!')
@@ -58,8 +53,6 @@ function makeFriends (callback) {
58 return callback(err) 53 return callback(err)
59 } 54 }
60 55
61 const urls = config.get('network.friends')
62
63 eachSeries(urls, function (url, callbackEach) { 56 eachSeries(urls, function (url, callbackEach) {
64 computeForeignPodsList(url, podsScore, callbackEach) 57 computeForeignPodsList(url, podsScore, callbackEach)
65 }, function (err) { 58 }, function (err) {
@@ -205,7 +198,12 @@ function getForeignPodsList (url, callback) {
205 request.get(url + path, function (err, response, body) { 198 request.get(url + path, function (err, response, body) {
206 if (err) return callback(err) 199 if (err) return callback(err)
207 200
208 callback(null, JSON.parse(body)) 201 try {
202 const json = JSON.parse(body)
203 return callback(null, json)
204 } catch (err) {
205 return callback(err)
206 }
209 }) 207 })
210} 208}
211 209
@@ -220,7 +218,7 @@ function makeRequestsToWinningPods (cert, podsList, callback) {
220 url: pod.url + '/api/' + constants.API_VERSION + '/pods/', 218 url: pod.url + '/api/' + constants.API_VERSION + '/pods/',
221 method: 'POST', 219 method: 'POST',
222 json: { 220 json: {
223 url: http + '://' + host + ':' + port, 221 url: constants.CONFIG.WEBSERVER.URL,
224 publicKey: cert 222 publicKey: cert
225 } 223 }
226 } 224 }
diff --git a/server/lib/oauth-model.js b/server/lib/oauth-model.js
index d9f8b175a..45f796796 100644
--- a/server/lib/oauth-model.js
+++ b/server/lib/oauth-model.js
@@ -8,12 +8,12 @@ const User = mongoose.model('User')
8 8
9// See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications 9// See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications
10const OAuthModel = { 10const OAuthModel = {
11 getAccessToken: getAccessToken, 11 getAccessToken,
12 getClient: getClient, 12 getClient,
13 getRefreshToken: getRefreshToken, 13 getRefreshToken,
14 getUser: getUser, 14 getUser,
15 revokeToken: revokeToken, 15 revokeToken,
16 saveToken: saveToken 16 saveToken
17} 17}
18 18
19// --------------------------------------------------------------------------- 19// ---------------------------------------------------------------------------
@@ -41,7 +41,22 @@ function getRefreshToken (refreshToken, callback) {
41function getUser (username, password) { 41function getUser (username, password) {
42 logger.debug('Getting User (username: ' + username + ', password: ' + password + ').') 42 logger.debug('Getting User (username: ' + username + ', password: ' + password + ').')
43 43
44 return User.getByUsernameAndPassword(username, password) 44 return User.getByUsername(username).then(function (user) {
45 if (!user) return null
46
47 // We need to return a promise
48 return new Promise(function (resolve, reject) {
49 return user.isPasswordMatch(password, function (err, isPasswordMatch) {
50 if (err) return reject(err)
51
52 if (isPasswordMatch === true) {
53 return resolve(user)
54 }
55
56 return resolve(null)
57 })
58 })
59 })
45} 60}
46 61
47function revokeToken (token) { 62function revokeToken (token) {
diff --git a/server/middlewares/admin.js b/server/middlewares/admin.js
new file mode 100644
index 000000000..e6d9dc887
--- /dev/null
+++ b/server/middlewares/admin.js
@@ -0,0 +1,22 @@
1'use strict'
2
3const constants = require('../initializers/constants')
4const logger = require('../helpers/logger')
5
6const adminMiddleware = {
7 ensureIsAdmin
8}
9
10function ensureIsAdmin (req, res, next) {
11 const user = res.locals.oauth.token.user
12 if (user.role !== constants.USER_ROLES.ADMIN) {
13 logger.info('A non admin user is trying to access to an admin content.')
14 return res.sendStatus(403)
15 }
16
17 return next()
18}
19
20// ---------------------------------------------------------------------------
21
22module.exports = adminMiddleware
diff --git a/server/middlewares/index.js b/server/middlewares/index.js
index 0a233e701..3f253e31b 100644
--- a/server/middlewares/index.js
+++ b/server/middlewares/index.js
@@ -1,19 +1,23 @@
1'use strict' 1'use strict'
2 2
3const oauth = require('./oauth') 3const adminMiddleware = require('./admin')
4const pagination = require('./pagination') 4const oauthMiddleware = require('./oauth')
5const paginationMiddleware = require('./pagination')
6const podsMiddleware = require('./pods')
5const validatorsMiddleware = require('./validators') 7const validatorsMiddleware = require('./validators')
6const search = require('./search') 8const searchMiddleware = require('./search')
7const sort = require('./sort') 9const sortMiddleware = require('./sort')
8const secureMiddleware = require('./secure') 10const secureMiddleware = require('./secure')
9 11
10const middlewares = { 12const middlewares = {
11 oauth: oauth, 13 admin: adminMiddleware,
12 pagination: pagination, 14 oauth: oauthMiddleware,
13 validators: validatorsMiddleware, 15 pagination: paginationMiddleware,
14 search: search, 16 pods: podsMiddleware,
15 sort: sort, 17 search: searchMiddleware,
16 secure: secureMiddleware 18 secure: secureMiddleware,
19 sort: sortMiddleware,
20 validators: validatorsMiddleware
17} 21}
18 22
19// --------------------------------------------------------------------------- 23// ---------------------------------------------------------------------------
diff --git a/server/middlewares/oauth.js b/server/middlewares/oauth.js
index 91a990509..3a02b9b48 100644
--- a/server/middlewares/oauth.js
+++ b/server/middlewares/oauth.js
@@ -12,8 +12,8 @@ const oAuthServer = new OAuthServer({
12}) 12})
13 13
14const oAuth = { 14const oAuth = {
15 authenticate: authenticate, 15 authenticate,
16 token: token 16 token
17} 17}
18 18
19function authenticate (req, res, next) { 19function authenticate (req, res, next) {
@@ -23,7 +23,7 @@ function authenticate (req, res, next) {
23 return res.sendStatus(500) 23 return res.sendStatus(500)
24 } 24 }
25 25
26 if (res.statusCode === 401 || res.statusCode === 400) return res.end() 26 if (res.statusCode === 401 || res.statusCode === 400 || res.statusCode === 503) return res.end()
27 27
28 return next() 28 return next()
29 }) 29 })
diff --git a/server/middlewares/pagination.js b/server/middlewares/pagination.js
index a571e51f6..a90f60aab 100644
--- a/server/middlewares/pagination.js
+++ b/server/middlewares/pagination.js
@@ -3,7 +3,7 @@
3const constants = require('../initializers/constants') 3const constants = require('../initializers/constants')
4 4
5const paginationMiddleware = { 5const paginationMiddleware = {
6 setPagination: setPagination 6 setPagination
7} 7}
8 8
9function setPagination (req, res, next) { 9function setPagination (req, res, next) {
diff --git a/server/middlewares/pods.js b/server/middlewares/pods.js
new file mode 100644
index 000000000..6e0874a76
--- /dev/null
+++ b/server/middlewares/pods.js
@@ -0,0 +1,62 @@
1'use strict'
2
3const urlModule = require('url')
4
5const logger = require('../helpers/logger')
6
7const podsMiddleware = {
8 setBodyUrlsPort,
9 setBodyUrlPort
10}
11
12function setBodyUrlsPort (req, res, next) {
13 for (let i = 0; i < req.body.urls.length; i++) {
14 const urlWithPort = getUrlWithPort(req.body.urls[i])
15
16 // Problem with the url parsing?
17 if (urlWithPort === null) {
18 return res.sendStatus(500)
19 }
20
21 req.body.urls[i] = urlWithPort
22 }
23
24 return next()
25}
26
27function setBodyUrlPort (req, res, next) {
28 const urlWithPort = getUrlWithPort(req.body.url)
29
30 // Problem with the url parsing?
31 if (urlWithPort === null) {
32 return res.sendStatus(500)
33 }
34
35 req.body.url = urlWithPort
36
37 return next()
38}
39
40// ---------------------------------------------------------------------------
41
42module.exports = podsMiddleware
43
44// ---------------------------------------------------------------------------
45
46function getUrlWithPort (url) {
47 const urlObj = urlModule.parse(url)
48
49 // Add the port if it is not specified
50 if (urlObj.port === null) {
51 if (urlObj.protocol === 'http:') {
52 return url + ':80'
53 } else if (urlObj.protocol === 'https:') {
54 return url + ':443'
55 } else {
56 logger.error('Unknown url protocol: ' + urlObj.protocol)
57 return null
58 }
59 }
60
61 return url
62}
diff --git a/server/middlewares/search.js b/server/middlewares/search.js
index 89302a564..bb88faf54 100644
--- a/server/middlewares/search.js
+++ b/server/middlewares/search.js
@@ -1,7 +1,7 @@
1'use strict' 1'use strict'
2 2
3const searchMiddleware = { 3const searchMiddleware = {
4 setVideosSearch: setVideosSearch 4 setVideosSearch
5} 5}
6 6
7function setVideosSearch (req, res, next) { 7function setVideosSearch (req, res, next) {
diff --git a/server/middlewares/secure.js b/server/middlewares/secure.js
index 9779c14ac..58f824d14 100644
--- a/server/middlewares/secure.js
+++ b/server/middlewares/secure.js
@@ -7,10 +7,11 @@ const peertubeCrypto = require('../helpers/peertube-crypto')
7const Pod = mongoose.model('Pod') 7const Pod = mongoose.model('Pod')
8 8
9const secureMiddleware = { 9const secureMiddleware = {
10 decryptBody: decryptBody 10 checkSignature,
11 decryptBody
11} 12}
12 13
13function decryptBody (req, res, next) { 14function checkSignature (req, res, next) {
14 const url = req.body.signature.url 15 const url = req.body.signature.url
15 Pod.loadByUrl(url, function (err, pod) { 16 Pod.loadByUrl(url, function (err, pod) {
16 if (err) { 17 if (err) {
@@ -28,21 +29,30 @@ function decryptBody (req, res, next) {
28 const signatureOk = peertubeCrypto.checkSignature(pod.publicKey, url, req.body.signature.signature) 29 const signatureOk = peertubeCrypto.checkSignature(pod.publicKey, url, req.body.signature.signature)
29 30
30 if (signatureOk === true) { 31 if (signatureOk === true) {
31 peertubeCrypto.decrypt(req.body.key, req.body.data, function (err, decrypted) { 32 return next()
32 if (err) { 33 }
33 logger.error('Cannot decrypt data.', { error: err }) 34
34 return res.sendStatus(500) 35 logger.error('Signature is not okay in decryptBody for %s.', req.body.signature.url)
35 } 36 return res.sendStatus(403)
36 37 })
37 req.body.data = JSON.parse(decrypted) 38}
38 delete req.body.key 39
39 40function decryptBody (req, res, next) {
40 next() 41 peertubeCrypto.decrypt(req.body.key, req.body.data, function (err, decrypted) {
41 }) 42 if (err) {
42 } else { 43 logger.error('Cannot decrypt data.', { error: err })
43 logger.error('Signature is not okay in decryptBody for %s.', req.body.signature.url) 44 return res.sendStatus(500)
44 return res.sendStatus(403)
45 } 45 }
46
47 try {
48 req.body.data = JSON.parse(decrypted)
49 delete req.body.key
50 } catch (err) {
51 logger.error('Error in JSON.parse', { error: err })
52 return res.sendStatus(500)
53 }
54
55 next()
46 }) 56 })
47} 57}
48 58
diff --git a/server/middlewares/sort.js b/server/middlewares/sort.js
index 9f52290a6..f0b7274eb 100644
--- a/server/middlewares/sort.js
+++ b/server/middlewares/sort.js
@@ -1,7 +1,14 @@
1'use strict' 1'use strict'
2 2
3const sortMiddleware = { 3const sortMiddleware = {
4 setVideosSort: setVideosSort 4 setUsersSort,
5 setVideosSort
6}
7
8function setUsersSort (req, res, next) {
9 if (!req.query.sort) req.query.sort = '-createdDate'
10
11 return next()
5} 12}
6 13
7function setVideosSort (req, res, next) { 14function setVideosSort (req, res, next) {
diff --git a/server/middlewares/validators/index.js b/server/middlewares/validators/index.js
index 0471b3f92..6c3a9c2b4 100644
--- a/server/middlewares/validators/index.js
+++ b/server/middlewares/validators/index.js
@@ -4,6 +4,7 @@ const paginationValidators = require('./pagination')
4const podsValidators = require('./pods') 4const podsValidators = require('./pods')
5const remoteValidators = require('./remote') 5const remoteValidators = require('./remote')
6const sortValidators = require('./sort') 6const sortValidators = require('./sort')
7const usersValidators = require('./users')
7const videosValidators = require('./videos') 8const videosValidators = require('./videos')
8 9
9const validators = { 10const validators = {
@@ -11,6 +12,7 @@ const validators = {
11 pods: podsValidators, 12 pods: podsValidators,
12 remote: remoteValidators, 13 remote: remoteValidators,
13 sort: sortValidators, 14 sort: sortValidators,
15 users: usersValidators,
14 videos: videosValidators 16 videos: videosValidators
15} 17}
16 18
diff --git a/server/middlewares/validators/pagination.js b/server/middlewares/validators/pagination.js
index 8e9a01053..16682696e 100644
--- a/server/middlewares/validators/pagination.js
+++ b/server/middlewares/validators/pagination.js
@@ -4,7 +4,7 @@ const checkErrors = require('./utils').checkErrors
4const logger = require('../../helpers/logger') 4const logger = require('../../helpers/logger')
5 5
6const validatorsPagination = { 6const validatorsPagination = {
7 pagination: pagination 7 pagination
8} 8}
9 9
10function pagination (req, res, next) { 10function pagination (req, res, next) {
diff --git a/server/middlewares/validators/pods.js b/server/middlewares/validators/pods.js
index fda2e865f..fd3d1e2f2 100644
--- a/server/middlewares/validators/pods.js
+++ b/server/middlewares/validators/pods.js
@@ -5,23 +5,29 @@ const friends = require('../../lib/friends')
5const logger = require('../../helpers/logger') 5const logger = require('../../helpers/logger')
6 6
7const validatorsPod = { 7const validatorsPod = {
8 makeFriends: makeFriends, 8 makeFriends,
9 podsAdd: podsAdd 9 podsAdd
10} 10}
11 11
12function makeFriends (req, res, next) { 12function makeFriends (req, res, next) {
13 friends.hasFriends(function (err, hasFriends) { 13 req.checkBody('urls', 'Should have an array of unique urls').isEachUniqueUrlValid()
14 if (err) { 14
15 logger.error('Cannot know if we have friends.', { error: err }) 15 logger.debug('Checking makeFriends parameters', { parameters: req.body })
16 res.sendStatus(500) 16
17 } 17 checkErrors(req, res, function () {
18 18 friends.hasFriends(function (err, hasFriends) {
19 if (hasFriends === true) { 19 if (err) {
20 // We need to quit our friends before make new ones 20 logger.error('Cannot know if we have friends.', { error: err })
21 res.sendStatus(409) 21 res.sendStatus(500)
22 } else { 22 }
23 return next() 23
24 } 24 if (hasFriends === true) {
25 // We need to quit our friends before make new ones
26 res.sendStatus(409)
27 } else {
28 return next()
29 }
30 })
25 }) 31 })
26} 32}
27 33
diff --git a/server/middlewares/validators/remote.js b/server/middlewares/validators/remote.js
index 1be119458..8c29ef8ca 100644
--- a/server/middlewares/validators/remote.js
+++ b/server/middlewares/validators/remote.js
@@ -4,9 +4,9 @@ const checkErrors = require('./utils').checkErrors
4const logger = require('../../helpers/logger') 4const logger = require('../../helpers/logger')
5 5
6const validatorsRemote = { 6const validatorsRemote = {
7 dataToDecrypt: dataToDecrypt, 7 dataToDecrypt,
8 remoteVideos: remoteVideos, 8 remoteVideos,
9 signature: signature 9 signature
10} 10}
11 11
12function dataToDecrypt (req, res, next) { 12function dataToDecrypt (req, res, next) {
@@ -19,7 +19,6 @@ function dataToDecrypt (req, res, next) {
19} 19}
20 20
21function remoteVideos (req, res, next) { 21function remoteVideos (req, res, next) {
22 req.checkBody('data').isArray()
23 req.checkBody('data').isEachRemoteVideosValid() 22 req.checkBody('data').isEachRemoteVideosValid()
24 23
25 logger.debug('Checking remoteVideos parameters', { parameters: req.body }) 24 logger.debug('Checking remoteVideos parameters', { parameters: req.body })
diff --git a/server/middlewares/validators/sort.js b/server/middlewares/validators/sort.js
index 56b63cc8b..431d3fffd 100644
--- a/server/middlewares/validators/sort.js
+++ b/server/middlewares/validators/sort.js
@@ -5,7 +5,18 @@ const constants = require('../../initializers/constants')
5const logger = require('../../helpers/logger') 5const logger = require('../../helpers/logger')
6 6
7const validatorsSort = { 7const validatorsSort = {
8 videosSort: videosSort 8 usersSort,
9 videosSort
10}
11
12function usersSort (req, res, next) {
13 const sortableColumns = constants.SORTABLE_COLUMNS.USERS
14
15 req.checkQuery('sort', 'Should have correct sortable column').optional().isIn(sortableColumns)
16
17 logger.debug('Checking sort parameters', { parameters: req.query })
18
19 checkErrors(req, res, next)
9} 20}
10 21
11function videosSort (req, res, next) { 22function videosSort (req, res, next) {
diff --git a/server/middlewares/validators/users.js b/server/middlewares/validators/users.js
new file mode 100644
index 000000000..d541e9124
--- /dev/null
+++ b/server/middlewares/validators/users.js
@@ -0,0 +1,67 @@
1'use strict'
2
3const mongoose = require('mongoose')
4
5const checkErrors = require('./utils').checkErrors
6const logger = require('../../helpers/logger')
7
8const User = mongoose.model('User')
9
10const validatorsUsers = {
11 usersAdd,
12 usersRemove,
13 usersUpdate
14}
15
16function usersAdd (req, res, next) {
17 req.checkBody('username', 'Should have a valid username').isUserUsernameValid()
18 req.checkBody('password', 'Should have a valid password').isUserPasswordValid()
19
20 logger.debug('Checking usersAdd parameters', { parameters: req.body })
21
22 checkErrors(req, res, function () {
23 User.loadByUsername(req.body.username, function (err, user) {
24 if (err) {
25 logger.error('Error in usersAdd request validator.', { error: err })
26 return res.sendStatus(500)
27 }
28
29 if (user) return res.status(409).send('User already exists.')
30
31 next()
32 })
33 })
34}
35
36function usersRemove (req, res, next) {
37 req.checkParams('id', 'Should have a valid id').notEmpty().isMongoId()
38
39 logger.debug('Checking usersRemove parameters', { parameters: req.params })
40
41 checkErrors(req, res, function () {
42 User.loadById(req.params.id, function (err, user) {
43 if (err) {
44 logger.error('Error in usersRemove request validator.', { error: err })
45 return res.sendStatus(500)
46 }
47
48 if (!user) return res.status(404).send('User not found')
49
50 next()
51 })
52 })
53}
54
55function usersUpdate (req, res, next) {
56 req.checkParams('id', 'Should have a valid id').notEmpty().isMongoId()
57 // Add old password verification
58 req.checkBody('password', 'Should have a valid password').isUserPasswordValid()
59
60 logger.debug('Checking usersUpdate parameters', { parameters: req.body })
61
62 checkErrors(req, res, next)
63}
64
65// ---------------------------------------------------------------------------
66
67module.exports = validatorsUsers
diff --git a/server/middlewares/validators/utils.js b/server/middlewares/validators/utils.js
index f6e5b2b38..3741b84c6 100644
--- a/server/middlewares/validators/utils.js
+++ b/server/middlewares/validators/utils.js
@@ -5,7 +5,7 @@ const util = require('util')
5const logger = require('../../helpers/logger') 5const logger = require('../../helpers/logger')
6 6
7const validatorsUtils = { 7const validatorsUtils = {
8 checkErrors: checkErrors 8 checkErrors
9} 9}
10 10
11function checkErrors (req, res, next, statusCode) { 11function checkErrors (req, res, next, statusCode) {
diff --git a/server/middlewares/validators/videos.js b/server/middlewares/validators/videos.js
index 3e2af06fb..76e943e77 100644
--- a/server/middlewares/validators/videos.js
+++ b/server/middlewares/validators/videos.js
@@ -4,20 +4,21 @@ const mongoose = require('mongoose')
4 4
5const checkErrors = require('./utils').checkErrors 5const checkErrors = require('./utils').checkErrors
6const constants = require('../../initializers/constants') 6const constants = require('../../initializers/constants')
7const customValidators = require('../../helpers/custom-validators') 7const customVideosValidators = require('../../helpers/custom-validators').videos
8const logger = require('../../helpers/logger') 8const logger = require('../../helpers/logger')
9 9
10const Video = mongoose.model('Video') 10const Video = mongoose.model('Video')
11 11
12const validatorsVideos = { 12const validatorsVideos = {
13 videosAdd: videosAdd, 13 videosAdd,
14 videosGet: videosGet, 14 videosGet,
15 videosRemove: videosRemove, 15 videosRemove,
16 videosSearch: videosSearch 16 videosSearch
17} 17}
18 18
19function videosAdd (req, res, next) { 19function videosAdd (req, res, next) {
20 req.checkFiles('videofile[0].originalname', 'Should have an input video').notEmpty() 20 req.checkFiles('videofile[0].originalname', 'Should have an input video').notEmpty()
21 // TODO: move to constants and function
21 req.checkFiles('videofile[0].mimetype', 'Should have a correct mime type').matches(/video\/(webm)|(mp4)|(ogg)/i) 22 req.checkFiles('videofile[0].mimetype', 'Should have a correct mime type').matches(/video\/(webm)|(mp4)|(ogg)/i)
22 req.checkBody('name', 'Should have a valid name').isVideoNameValid() 23 req.checkBody('name', 'Should have a valid name').isVideoNameValid()
23 req.checkBody('description', 'Should have a valid description').isVideoDescriptionValid() 24 req.checkBody('description', 'Should have a valid description').isVideoDescriptionValid()
@@ -33,8 +34,8 @@ function videosAdd (req, res, next) {
33 return res.status(400).send('Cannot retrieve metadata of the file.') 34 return res.status(400).send('Cannot retrieve metadata of the file.')
34 } 35 }
35 36
36 if (!customValidators.isVideoDurationValid(duration)) { 37 if (!customVideosValidators.isVideoDurationValid(duration)) {
37 return res.status(400).send('Duration of the video file is too big (max: ' + constants.VIDEOS_CONSTRAINTS_FIELDS.DURATION.max + 's).') 38 return res.status(400).send('Duration of the video file is too big (max: ' + constants.CONSTRAINTS_FIELDS.VIDEOS.DURATION.max + 's).')
38 } 39 }
39 40
40 videoFile.duration = duration 41 videoFile.duration = duration
@@ -76,6 +77,7 @@ function videosRemove (req, res, next) {
76 77
77 if (!video) return res.status(404).send('Video not found') 78 if (!video) return res.status(404).send('Video not found')
78 else if (video.isOwned() === false) return res.status(403).send('Cannot remove video of another pod') 79 else if (video.isOwned() === false) return res.status(403).send('Cannot remove video of another pod')
80 else if (video.author !== res.locals.oauth.token.user.username) return res.status(403).send('Cannot remove video of another user')
79 81
80 next() 82 next()
81 }) 83 })
diff --git a/server/models/application.js b/server/models/application.js
new file mode 100644
index 000000000..452ac4283
--- /dev/null
+++ b/server/models/application.js
@@ -0,0 +1,31 @@
1const mongoose = require('mongoose')
2
3// ---------------------------------------------------------------------------
4
5const ApplicationSchema = mongoose.Schema({
6 mongoSchemaVersion: {
7 type: Number,
8 default: 0
9 }
10})
11
12ApplicationSchema.statics = {
13 loadMongoSchemaVersion,
14 updateMongoSchemaVersion
15}
16
17mongoose.model('Application', ApplicationSchema)
18
19// ---------------------------------------------------------------------------
20
21function loadMongoSchemaVersion (callback) {
22 return this.findOne({}, { mongoSchemaVersion: 1 }, function (err, data) {
23 const version = data ? data.mongoSchemaVersion : 0
24
25 return callback(err, version)
26 })
27}
28
29function updateMongoSchemaVersion (newVersion, callback) {
30 return this.update({}, { mongoSchemaVersion: newVersion }, callback)
31}
diff --git a/server/models/oauth-client.js b/server/models/oauth-client.js
index 45834c5a5..a1aefa985 100644
--- a/server/models/oauth-client.js
+++ b/server/models/oauth-client.js
@@ -11,9 +11,9 @@ const OAuthClientSchema = mongoose.Schema({
11OAuthClientSchema.path('clientSecret').required(true) 11OAuthClientSchema.path('clientSecret').required(true)
12 12
13OAuthClientSchema.statics = { 13OAuthClientSchema.statics = {
14 getByIdAndSecret: getByIdAndSecret, 14 getByIdAndSecret,
15 list: list, 15 list,
16 loadFirstClient: loadFirstClient 16 loadFirstClient
17} 17}
18 18
19mongoose.model('OAuthClient', OAuthClientSchema) 19mongoose.model('OAuthClient', OAuthClientSchema)
diff --git a/server/models/oauth-token.js b/server/models/oauth-token.js
index f6a814c36..5beb47bed 100644
--- a/server/models/oauth-token.js
+++ b/server/models/oauth-token.js
@@ -18,9 +18,10 @@ OAuthTokenSchema.path('client').required(true)
18OAuthTokenSchema.path('user').required(true) 18OAuthTokenSchema.path('user').required(true)
19 19
20OAuthTokenSchema.statics = { 20OAuthTokenSchema.statics = {
21 getByRefreshTokenAndPopulateClient: getByRefreshTokenAndPopulateClient, 21 getByRefreshTokenAndPopulateClient,
22 getByTokenAndPopulateUser: getByTokenAndPopulateUser, 22 getByTokenAndPopulateUser,
23 getByRefreshToken: getByRefreshToken 23 getByRefreshToken,
24 removeByUserId
24} 25}
25 26
26mongoose.model('OAuthToken', OAuthTokenSchema) 27mongoose.model('OAuthToken', OAuthTokenSchema)
@@ -53,3 +54,7 @@ function getByTokenAndPopulateUser (bearerToken) {
53function getByRefreshToken (refreshToken) { 54function getByRefreshToken (refreshToken) {
54 return this.findOne({ refreshToken: refreshToken }).exec() 55 return this.findOne({ refreshToken: refreshToken }).exec()
55} 56}
57
58function removeByUserId (userId, callback) {
59 return this.remove({ user: userId }, callback)
60}
diff --git a/server/models/pods.js b/server/models/pods.js
index bf43d7b25..4020a9603 100644
--- a/server/models/pods.js
+++ b/server/models/pods.js
@@ -11,7 +11,11 @@ const constants = require('../initializers/constants')
11const PodSchema = mongoose.Schema({ 11const PodSchema = mongoose.Schema({
12 url: String, 12 url: String,
13 publicKey: String, 13 publicKey: String,
14 score: { type: Number, max: constants.FRIEND_SCORE.MAX } 14 score: { type: Number, max: constants.FRIEND_SCORE.MAX },
15 createdDate: {
16 type: Date,
17 default: Date.now
18 }
15}) 19})
16 20
17// TODO: set options (TLD...) 21// TODO: set options (TLD...)
@@ -19,16 +23,19 @@ PodSchema.path('url').validate(validator.isURL)
19PodSchema.path('publicKey').required(true) 23PodSchema.path('publicKey').required(true)
20PodSchema.path('score').validate(function (value) { return !isNaN(value) }) 24PodSchema.path('score').validate(function (value) { return !isNaN(value) })
21 25
26PodSchema.methods = {
27 toFormatedJSON
28}
29
22PodSchema.statics = { 30PodSchema.statics = {
23 countAll: countAll, 31 countAll,
24 incrementScores: incrementScores, 32 incrementScores,
25 list: list, 33 list,
26 listAllIds: listAllIds, 34 listAllIds,
27 listOnlyUrls: listOnlyUrls, 35 listBadPods,
28 listBadPods: listBadPods, 36 load,
29 load: load, 37 loadByUrl,
30 loadByUrl: loadByUrl, 38 removeAll
31 removeAll: removeAll
32} 39}
33 40
34PodSchema.pre('save', function (next) { 41PodSchema.pre('save', function (next) {
@@ -46,6 +53,19 @@ PodSchema.pre('save', function (next) {
46 53
47const Pod = mongoose.model('Pod', PodSchema) 54const Pod = mongoose.model('Pod', PodSchema)
48 55
56// ------------------------------ METHODS ------------------------------
57
58function toFormatedJSON () {
59 const json = {
60 id: this._id,
61 url: this.url,
62 score: this.score,
63 createdDate: this.createdDate
64 }
65
66 return json
67}
68
49// ------------------------------ Statics ------------------------------ 69// ------------------------------ Statics ------------------------------
50 70
51function countAll (callback) { 71function countAll (callback) {
@@ -69,10 +89,6 @@ function listAllIds (callback) {
69 }) 89 })
70} 90}
71 91
72function listOnlyUrls (callback) {
73 return this.find({}, { _id: 0, url: 1 }, callback)
74}
75
76function listBadPods (callback) { 92function listBadPods (callback) {
77 return this.find({ score: 0 }, callback) 93 return this.find({ score: 0 }, callback)
78} 94}
diff --git a/server/models/request.js b/server/models/request.js
index 4d521919a..2d1c5af15 100644
--- a/server/models/request.js
+++ b/server/models/request.js
@@ -14,19 +14,22 @@ const Pod = mongoose.model('Pod')
14const Video = mongoose.model('Video') 14const Video = mongoose.model('Video')
15 15
16let timer = null 16let timer = null
17let lastRequestTimestamp = 0
17 18
18// --------------------------------------------------------------------------- 19// ---------------------------------------------------------------------------
19 20
20const RequestSchema = mongoose.Schema({ 21const RequestSchema = mongoose.Schema({
21 request: mongoose.Schema.Types.Mixed, 22 request: mongoose.Schema.Types.Mixed,
22 to: [ { type: mongoose.Schema.Types.ObjectId, ref: 'users' } ] 23 to: [ { type: mongoose.Schema.Types.ObjectId, ref: 'Pod' } ]
23}) 24})
24 25
25RequestSchema.statics = { 26RequestSchema.statics = {
26 activate, 27 activate,
27 deactivate, 28 deactivate,
28 flush, 29 flush,
29 forceSend 30 forceSend,
31 list,
32 remainingMilliSeconds
30} 33}
31 34
32RequestSchema.pre('save', function (next) { 35RequestSchema.pre('save', function (next) {
@@ -53,12 +56,19 @@ mongoose.model('Request', RequestSchema)
53 56
54function activate () { 57function activate () {
55 logger.info('Requests scheduler activated.') 58 logger.info('Requests scheduler activated.')
56 timer = setInterval(makeRequests.bind(this), constants.INTERVAL) 59 lastRequestTimestamp = Date.now()
60
61 const self = this
62 timer = setInterval(function () {
63 lastRequestTimestamp = Date.now()
64 makeRequests.call(self)
65 }, constants.REQUESTS_INTERVAL)
57} 66}
58 67
59function deactivate () { 68function deactivate () {
60 logger.info('Requests scheduler deactivated.') 69 logger.info('Requests scheduler deactivated.')
61 clearInterval(timer) 70 clearInterval(timer)
71 timer = null
62} 72}
63 73
64function flush () { 74function flush () {
@@ -72,6 +82,16 @@ function forceSend () {
72 makeRequests.call(this) 82 makeRequests.call(this)
73} 83}
74 84
85function list (callback) {
86 this.find({ }, callback)
87}
88
89function remainingMilliSeconds () {
90 if (timer === null) return -1
91
92 return constants.REQUESTS_INTERVAL - (Date.now() - lastRequestTimestamp)
93}
94
75// --------------------------------------------------------------------------- 95// ---------------------------------------------------------------------------
76 96
77// Make a requests to friends of a certain type 97// Make a requests to friends of a certain type
@@ -91,7 +111,13 @@ function makeRequest (toPod, requestsToMake, callback) {
91 // The function fire some useful callbacks 111 // The function fire some useful callbacks
92 requests.makeSecureRequest(params, function (err, res) { 112 requests.makeSecureRequest(params, function (err, res) {
93 if (err || (res.statusCode !== 200 && res.statusCode !== 201 && res.statusCode !== 204)) { 113 if (err || (res.statusCode !== 200 && res.statusCode !== 201 && res.statusCode !== 204)) {
94 logger.error('Error sending secure request to %s pod.', toPod.url, { error: err || new Error('Status code not 20x') }) 114 logger.error(
115 'Error sending secure request to %s pod.',
116 toPod.url,
117 {
118 error: err || new Error('Status code not 20x : ' + res.statusCode)
119 }
120 )
95 121
96 return callback(false) 122 return callback(false)
97 } 123 }
@@ -148,19 +174,14 @@ function makeRequests () {
148 return callbackEach() 174 return callbackEach()
149 } 175 }
150 176
151 // Maybe the pod is not our friend anymore so simply remove them 177 // Maybe the pod is not our friend anymore so simply remove it
152 if (!toPod) { 178 if (!toPod) {
179 logger.info('Removing %d requests of unexisting pod %s.', requestToMake.ids.length, toPodId)
153 removePodOf.call(self, requestToMake.ids, toPodId) 180 removePodOf.call(self, requestToMake.ids, toPodId)
154 return callbackEach() 181 return callbackEach()
155 } 182 }
156 183
157 makeRequest(toPod, requestToMake.datas, function (success) { 184 makeRequest(toPod, requestToMake.datas, function (success) {
158 if (err) {
159 logger.error('Errors when sent request to %s.', toPod.url, { error: err })
160 // Do not stop the process just for one error
161 return callbackEach()
162 }
163
164 if (success === true) { 185 if (success === true) {
165 logger.debug('Removing requests for %s pod.', toPodId, { requestsIds: requestToMake.ids }) 186 logger.debug('Removing requests for %s pod.', toPodId, { requestsIds: requestToMake.ids })
166 187
diff --git a/server/models/user.js b/server/models/user.js
index 14ffecbff..a19de7072 100644
--- a/server/models/user.js
+++ b/server/models/user.js
@@ -1,28 +1,98 @@
1const mongoose = require('mongoose') 1const mongoose = require('mongoose')
2 2
3const customUsersValidators = require('../helpers/custom-validators').users
4const modelUtils = require('./utils')
5const peertubeCrypto = require('../helpers/peertube-crypto')
6
7const OAuthToken = mongoose.model('OAuthToken')
8
3// --------------------------------------------------------------------------- 9// ---------------------------------------------------------------------------
4 10
5const UserSchema = mongoose.Schema({ 11const UserSchema = mongoose.Schema({
12 createdDate: {
13 type: Date,
14 default: Date.now
15 },
6 password: String, 16 password: String,
7 username: String 17 username: String,
18 role: String
8}) 19})
9 20
10UserSchema.path('password').required(true) 21UserSchema.path('password').required(customUsersValidators.isUserPasswordValid)
11UserSchema.path('username').required(true) 22UserSchema.path('username').required(customUsersValidators.isUserUsernameValid)
23UserSchema.path('role').validate(customUsersValidators.isUserRoleValid)
24
25UserSchema.methods = {
26 isPasswordMatch,
27 toFormatedJSON
28}
12 29
13UserSchema.statics = { 30UserSchema.statics = {
14 getByUsernameAndPassword: getByUsernameAndPassword, 31 countTotal,
15 list: list 32 getByUsername,
33 list,
34 listForApi,
35 loadById,
36 loadByUsername
16} 37}
17 38
39UserSchema.pre('save', function (next) {
40 const user = this
41
42 peertubeCrypto.cryptPassword(this.password, function (err, hash) {
43 if (err) return next(err)
44
45 user.password = hash
46
47 return next()
48 })
49})
50
51UserSchema.pre('remove', function (next) {
52 const user = this
53
54 OAuthToken.removeByUserId(user._id, next)
55})
56
18mongoose.model('User', UserSchema) 57mongoose.model('User', UserSchema)
19 58
20// --------------------------------------------------------------------------- 59// ------------------------------ METHODS ------------------------------
60
61function isPasswordMatch (password, callback) {
62 return peertubeCrypto.comparePassword(password, this.password, callback)
63}
64
65function toFormatedJSON () {
66 return {
67 id: this._id,
68 username: this.username,
69 role: this.role,
70 createdDate: this.createdDate
71 }
72}
73// ------------------------------ STATICS ------------------------------
74
75function countTotal (callback) {
76 return this.count(callback)
77}
78
79function getByUsername (username) {
80 return this.findOne({ username: username })
81}
21 82
22function list (callback) { 83function list (callback) {
23 return this.find(callback) 84 return this.find(callback)
24} 85}
25 86
26function getByUsernameAndPassword (username, password) { 87function listForApi (start, count, sort, callback) {
27 return this.findOne({ username: username, password: password }) 88 const query = {}
89 return modelUtils.listForApiWithCount.call(this, query, start, count, sort, callback)
90}
91
92function loadById (id, callback) {
93 return this.findById(id, callback)
94}
95
96function loadByUsername (username, callback) {
97 return this.findOne({ username: username }, callback)
28} 98}
diff --git a/server/models/utils.js b/server/models/utils.js
new file mode 100644
index 000000000..e798aabe6
--- /dev/null
+++ b/server/models/utils.js
@@ -0,0 +1,30 @@
1'use strict'
2
3const parallel = require('async/parallel')
4
5const utils = {
6 listForApiWithCount
7}
8
9function listForApiWithCount (query, start, count, sort, callback) {
10 const self = this
11
12 parallel([
13 function (asyncCallback) {
14 self.find(query).skip(start).limit(count).sort(sort).exec(asyncCallback)
15 },
16 function (asyncCallback) {
17 self.count(query, asyncCallback)
18 }
19 ], function (err, results) {
20 if (err) return callback(err)
21
22 const data = results[0]
23 const total = results[1]
24 return callback(null, data, total)
25 })
26}
27
28// ---------------------------------------------------------------------------
29
30module.exports = utils
diff --git a/server/models/video.js b/server/models/video.js
index 14e0df6f2..7d073cffa 100644
--- a/server/models/video.js
+++ b/server/models/video.js
@@ -11,8 +11,9 @@ const magnet = require('magnet-uri')
11const mongoose = require('mongoose') 11const mongoose = require('mongoose')
12 12
13const constants = require('../initializers/constants') 13const constants = require('../initializers/constants')
14const customValidators = require('../helpers/custom-validators') 14const customVideosValidators = require('../helpers/custom-validators').videos
15const logger = require('../helpers/logger') 15const logger = require('../helpers/logger')
16const modelUtils = require('./utils')
16const utils = require('../helpers/utils') 17const utils = require('../helpers/utils')
17 18
18const http = config.get('webserver.https') === true ? 'https' : 'http' 19const http = config.get('webserver.https') === true ? 'https' : 'http'
@@ -42,34 +43,35 @@ const VideoSchema = mongoose.Schema({
42 } 43 }
43}) 44})
44 45
45VideoSchema.path('name').validate(customValidators.isVideoNameValid) 46VideoSchema.path('name').validate(customVideosValidators.isVideoNameValid)
46VideoSchema.path('description').validate(customValidators.isVideoDescriptionValid) 47VideoSchema.path('description').validate(customVideosValidators.isVideoDescriptionValid)
47VideoSchema.path('magnetUri').validate(customValidators.isVideoMagnetUriValid) 48VideoSchema.path('magnetUri').validate(customVideosValidators.isVideoMagnetUriValid)
48VideoSchema.path('podUrl').validate(customValidators.isVideoPodUrlValid) 49VideoSchema.path('podUrl').validate(customVideosValidators.isVideoPodUrlValid)
49VideoSchema.path('author').validate(customValidators.isVideoAuthorValid) 50VideoSchema.path('author').validate(customVideosValidators.isVideoAuthorValid)
50VideoSchema.path('duration').validate(customValidators.isVideoDurationValid) 51VideoSchema.path('duration').validate(customVideosValidators.isVideoDurationValid)
51// The tumbnail can be the path or the data in base 64 52// The tumbnail can be the path or the data in base 64
52// The pre save hook will convert the base 64 data in a file on disk and replace the thumbnail key by the filename 53// The pre save hook will convert the base 64 data in a file on disk and replace the thumbnail key by the filename
53VideoSchema.path('thumbnail').validate(function (value) { 54VideoSchema.path('thumbnail').validate(function (value) {
54 return customValidators.isVideoThumbnailValid(value) || customValidators.isVideoThumbnail64Valid(value) 55 return customVideosValidators.isVideoThumbnailValid(value) || customVideosValidators.isVideoThumbnail64Valid(value)
55}) 56})
56VideoSchema.path('tags').validate(customValidators.isVideoTagsValid) 57VideoSchema.path('tags').validate(customVideosValidators.isVideoTagsValid)
57 58
58VideoSchema.methods = { 59VideoSchema.methods = {
59 isOwned: isOwned, 60 isOwned,
60 toFormatedJSON: toFormatedJSON, 61 toFormatedJSON,
61 toRemoteJSON: toRemoteJSON 62 toRemoteJSON
62} 63}
63 64
64VideoSchema.statics = { 65VideoSchema.statics = {
65 getDurationFromFile: getDurationFromFile, 66 getDurationFromFile,
66 list: list, 67 listForApi,
67 listByUrlAndMagnet: listByUrlAndMagnet, 68 listByUrlAndMagnet,
68 listByUrls: listByUrls, 69 listByUrls,
69 listOwned: listOwned, 70 listOwned,
70 listRemotes: listRemotes, 71 listOwnedByAuthor,
71 load: load, 72 listRemotes,
72 search: search 73 load,
74 search
73} 75}
74 76
75VideoSchema.pre('remove', function (next) { 77VideoSchema.pre('remove', function (next) {
@@ -101,8 +103,8 @@ VideoSchema.pre('save', function (next) {
101 const tasks = [] 103 const tasks = []
102 104
103 if (video.isOwned()) { 105 if (video.isOwned()) {
104 const videoPath = pathUtils.join(uploadsDir, video.filename) 106 const videoPath = pathUtils.join(constants.CONFIG.STORAGE.UPLOAD_DIR, video.filename)
105 this.podUrl = http + '://' + host + ':' + port 107 this.podUrl = constants.CONFIG.WEBSERVER.URL
106 108
107 tasks.push( 109 tasks.push(
108 // TODO: refractoring 110 // TODO: refractoring
@@ -174,7 +176,7 @@ function toRemoteJSON (callback) {
174 const self = this 176 const self = this
175 177
176 // Convert thumbnail to base64 178 // Convert thumbnail to base64
177 fs.readFile(pathUtils.join(thumbnailsDir, this.thumbnail), function (err, thumbnailData) { 179 fs.readFile(pathUtils.join(constants.CONFIG.STORAGE.THUMBNAILS_DIR, this.thumbnail), function (err, thumbnailData) {
178 if (err) { 180 if (err) {
179 logger.error('Cannot read the thumbnail of the video') 181 logger.error('Cannot read the thumbnail of the video')
180 return callback(err) 182 return callback(err)
@@ -207,9 +209,9 @@ function getDurationFromFile (videoPath, callback) {
207 }) 209 })
208} 210}
209 211
210function list (start, count, sort, callback) { 212function listForApi (start, count, sort, callback) {
211 const query = {} 213 const query = {}
212 return findWithCount.call(this, query, start, count, sort, callback) 214 return modelUtils.listForApiWithCount.call(this, query, start, count, sort, callback)
213} 215}
214 216
215function listByUrlAndMagnet (fromUrl, magnetUri, callback) { 217function listByUrlAndMagnet (fromUrl, magnetUri, callback) {
@@ -225,6 +227,10 @@ function listOwned (callback) {
225 this.find({ filename: { $ne: null } }, callback) 227 this.find({ filename: { $ne: null } }, callback)
226} 228}
227 229
230function listOwnedByAuthor (author, callback) {
231 this.find({ filename: { $ne: null }, author: author }, callback)
232}
233
228function listRemotes (callback) { 234function listRemotes (callback) {
229 this.find({ filename: null }, callback) 235 this.find({ filename: null }, callback)
230} 236}
@@ -242,36 +248,17 @@ function search (value, field, start, count, sort, callback) {
242 query[field] = new RegExp(value) 248 query[field] = new RegExp(value)
243 } 249 }
244 250
245 findWithCount.call(this, query, start, count, sort, callback) 251 modelUtils.listForApiWithCount.call(this, query, start, count, sort, callback)
246} 252}
247 253
248// --------------------------------------------------------------------------- 254// ---------------------------------------------------------------------------
249 255
250function findWithCount (query, start, count, sort, callback) {
251 const self = this
252
253 parallel([
254 function (asyncCallback) {
255 self.find(query).skip(start).limit(count).sort(sort).exec(asyncCallback)
256 },
257 function (asyncCallback) {
258 self.count(query, asyncCallback)
259 }
260 ], function (err, results) {
261 if (err) return callback(err)
262
263 const videos = results[0]
264 const totalVideos = results[1]
265 return callback(null, videos, totalVideos)
266 })
267}
268
269function removeThumbnail (video, callback) { 256function removeThumbnail (video, callback) {
270 fs.unlink(thumbnailsDir + video.thumbnail, callback) 257 fs.unlink(constants.CONFIG.STORAGE.THUMBNAILS_DIR + video.thumbnail, callback)
271} 258}
272 259
273function removeFile (video, callback) { 260function removeFile (video, callback) {
274 fs.unlink(uploadsDir + video.filename, callback) 261 fs.unlink(constants.CONFIG.STORAGE.UPLOAD_DIR + video.filename, callback)
275} 262}
276 263
277// Maybe the torrent is not seeded, but we catch the error to don't stop the removing process 264// 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) {
288 }) 275 })
289 .thumbnail({ 276 .thumbnail({
290 count: 1, 277 count: 1,
291 folder: thumbnailsDir, 278 folder: constants.CONFIG.STORAGE.THUMBNAILS_DIR,
292 size: constants.THUMBNAILS_SIZE, 279 size: constants.THUMBNAILS_SIZE,
293 filename: filename 280 filename: filename
294 }) 281 })
@@ -300,7 +287,7 @@ function generateThumbnailFromBase64 (data, callback) {
300 if (err) return callback(err) 287 if (err) return callback(err)
301 288
302 const thumbnailName = randomString + '.jpg' 289 const thumbnailName = randomString + '.jpg'
303 fs.writeFile(thumbnailsDir + thumbnailName, data, { encoding: 'base64' }, function (err) { 290 fs.writeFile(constants.CONFIG.STORAGE.THUMBNAILS_DIR + thumbnailName, data, { encoding: 'base64' }, function (err) {
304 if (err) return callback(err) 291 if (err) return callback(err)
305 292
306 return callback(null, thumbnailName) 293 return callback(null, thumbnailName)
diff --git a/server/tests/api/check-params.js b/server/tests/api/check-params.js
new file mode 100644
index 000000000..57b5ca024
--- /dev/null
+++ b/server/tests/api/check-params.js
@@ -0,0 +1,746 @@
1'use strict'
2
3const chai = require('chai')
4const expect = chai.expect
5const pathUtils = require('path')
6const request = require('supertest')
7const series = require('async/series')
8
9const loginUtils = require('../utils/login')
10const requestsUtils = require('../utils/requests')
11const serversUtils = require('../utils/servers')
12const usersUtils = require('../utils/users')
13
14describe('Test parameters validator', function () {
15 let server = null
16 let userAccessToken = null
17
18 // ---------------------------------------------------------------
19
20 before(function (done) {
21 this.timeout(20000)
22
23 series([
24 function (next) {
25 serversUtils.flushTests(next)
26 },
27 function (next) {
28 serversUtils.runServer(1, function (server1) {
29 server = server1
30
31 next()
32 })
33 },
34 function (next) {
35 loginUtils.loginAndGetAccessToken(server, function (err, token) {
36 if (err) throw err
37 server.accessToken = token
38
39 next()
40 })
41 }
42 ], done)
43 })
44
45 describe('Of the pods API', function () {
46 const path = '/api/v1/pods/'
47
48 describe('When making friends', function () {
49 let userAccessToken = null
50
51 before(function (done) {
52 usersUtils.createUser(server.url, server.accessToken, 'user1', 'password', function () {
53 server.user = {
54 username: 'user1',
55 password: 'password'
56 }
57
58 loginUtils.loginAndGetAccessToken(server, function (err, accessToken) {
59 if (err) throw err
60
61 userAccessToken = accessToken
62
63 done()
64 })
65 })
66 })
67
68 describe('When making friends', function () {
69 const body = {
70 urls: [ 'http://localhost:9002' ]
71 }
72
73 it('Should fail without urls', function (done) {
74 request(server.url)
75 .post(path + '/makefriends')
76 .set('Authorization', 'Bearer ' + server.accessToken)
77 .set('Accept', 'application/json')
78 .expect(400, done)
79 })
80
81 it('Should fail with urls is not an array', function (done) {
82 request(server.url)
83 .post(path + '/makefriends')
84 .send({ urls: 'http://localhost:9002' })
85 .set('Authorization', 'Bearer ' + server.accessToken)
86 .set('Accept', 'application/json')
87 .expect(400, done)
88 })
89
90 it('Should fail if the array is not composed by urls', function (done) {
91 request(server.url)
92 .post(path + '/makefriends')
93 .send({ urls: [ 'http://localhost:9002', 'localhost:coucou' ] })
94 .set('Authorization', 'Bearer ' + server.accessToken)
95 .set('Accept', 'application/json')
96 .expect(400, done)
97 })
98
99 it('Should fail if urls are not unique', function (done) {
100 request(server.url)
101 .post(path + '/makefriends')
102 .send({ urls: [ 'http://localhost:9002', 'http://localhost:9002' ] })
103 .set('Authorization', 'Bearer ' + server.accessToken)
104 .set('Accept', 'application/json')
105 .expect(400, done)
106 })
107
108 it('Should fail with a invalid token', function (done) {
109 request(server.url)
110 .post(path + '/makefriends')
111 .send(body)
112 .set('Authorization', 'Bearer faketoken')
113 .set('Accept', 'application/json')
114 .expect(401, done)
115 })
116
117 it('Should fail if the user is not an administrator', function (done) {
118 request(server.url)
119 .post(path + '/makefriends')
120 .send(body)
121 .set('Authorization', 'Bearer ' + userAccessToken)
122 .set('Accept', 'application/json')
123 .expect(403, done)
124 })
125 })
126
127 describe('When quitting friends', function () {
128 it('Should fail with a invalid token', function (done) {
129 request(server.url)
130 .get(path + '/quitfriends')
131 .query({ start: 'hello' })
132 .set('Authorization', 'Bearer faketoken')
133 .set('Accept', 'application/json')
134 .expect(401, done)
135 })
136
137 it('Should fail if the user is not an administrator', function (done) {
138 request(server.url)
139 .get(path + '/quitfriends')
140 .query({ start: 'hello' })
141 .set('Authorization', 'Bearer ' + userAccessToken)
142 .set('Accept', 'application/json')
143 .expect(403, done)
144 })
145 })
146 })
147
148 describe('When adding a pod', function () {
149 it('Should fail with nothing', function (done) {
150 const data = {}
151 requestsUtils.makePostBodyRequest(server.url, path, null, data, done)
152 })
153
154 it('Should fail without public key', function (done) {
155 const data = {
156 url: 'http://coucou.com'
157 }
158 requestsUtils.makePostBodyRequest(server.url, path, null, data, done)
159 })
160
161 it('Should fail without an url', function (done) {
162 const data = {
163 publicKey: 'mysuperpublickey'
164 }
165 requestsUtils.makePostBodyRequest(server.url, path, null, data, done)
166 })
167
168 it('Should fail with an incorrect url', function (done) {
169 const data = {
170 url: 'coucou.com',
171 publicKey: 'mysuperpublickey'
172 }
173 requestsUtils.makePostBodyRequest(server.url, path, null, data, function () {
174 data.url = 'http://coucou'
175 requestsUtils.makePostBodyRequest(server.url, path, null, data, function () {
176 data.url = 'coucou'
177 requestsUtils.makePostBodyRequest(server.url, path, null, data, done)
178 })
179 })
180 })
181
182 it('Should succeed with the correct parameters', function (done) {
183 const data = {
184 url: 'http://coucou.com',
185 publicKey: 'mysuperpublickey'
186 }
187 requestsUtils.makePostBodyRequest(server.url, path, null, data, done, 200)
188 })
189 })
190 })
191
192 describe('Of the videos API', function () {
193 const path = '/api/v1/videos/'
194
195 describe('When listing a video', function () {
196 it('Should fail with a bad start pagination', function (done) {
197 request(server.url)
198 .get(path)
199 .query({ start: 'hello' })
200 .set('Accept', 'application/json')
201 .expect(400, done)
202 })
203
204 it('Should fail with a bad count pagination', function (done) {
205 request(server.url)
206 .get(path)
207 .query({ count: 'hello' })
208 .set('Accept', 'application/json')
209 .expect(400, done)
210 })
211
212 it('Should fail with an incorrect sort', function (done) {
213 request(server.url)
214 .get(path)
215 .query({ sort: 'hello' })
216 .set('Accept', 'application/json')
217 .expect(400, done)
218 })
219 })
220
221 describe('When searching a video', function () {
222 it('Should fail with nothing', function (done) {
223 request(server.url)
224 .get(pathUtils.join(path, 'search'))
225 .set('Accept', 'application/json')
226 .expect(400, done)
227 })
228
229 it('Should fail with a bad start pagination', function (done) {
230 request(server.url)
231 .get(pathUtils.join(path, 'search', 'test'))
232 .query({ start: 'hello' })
233 .set('Accept', 'application/json')
234 .expect(400, done)
235 })
236
237 it('Should fail with a bad count pagination', function (done) {
238 request(server.url)
239 .get(pathUtils.join(path, 'search', 'test'))
240 .query({ count: 'hello' })
241 .set('Accept', 'application/json')
242 .expect(400, done)
243 })
244
245 it('Should fail with an incorrect sort', function (done) {
246 request(server.url)
247 .get(pathUtils.join(path, 'search', 'test'))
248 .query({ sort: 'hello' })
249 .set('Accept', 'application/json')
250 .expect(400, done)
251 })
252 })
253
254 describe('When adding a video', function () {
255 it('Should fail with nothing', function (done) {
256 const data = {}
257 const attach = {}
258 requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done)
259 })
260
261 it('Should fail without name', function (done) {
262 const data = {
263 description: 'my super description',
264 tags: [ 'tag1', 'tag2' ]
265 }
266 const attach = {
267 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
268 }
269 requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done)
270 })
271
272 it('Should fail with a long name', function (done) {
273 const data = {
274 name: 'My very very very very very very very very very very very very very very very very long name',
275 description: 'my super description',
276 tags: [ 'tag1', 'tag2' ]
277 }
278 const attach = {
279 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
280 }
281 requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done)
282 })
283
284 it('Should fail without description', function (done) {
285 const data = {
286 name: 'my super name',
287 tags: [ 'tag1', 'tag2' ]
288 }
289 const attach = {
290 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
291 }
292 requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done)
293 })
294
295 it('Should fail with a long description', function (done) {
296 const data = {
297 name: 'my super name',
298 description: 'my super description which is very very very very very very very very very very very very very very' +
299 'very very very very very very very very very very very very very very very very very very very very very' +
300 'very very very very very very very very very very very very very very very long',
301 tags: [ 'tag1', 'tag2' ]
302 }
303 const attach = {
304 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
305 }
306 requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done)
307 })
308
309 it('Should fail without tags', function (done) {
310 const data = {
311 name: 'my super name',
312 description: 'my super description'
313 }
314 const attach = {
315 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
316 }
317 requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done)
318 })
319
320 it('Should fail with too many tags', function (done) {
321 const data = {
322 name: 'my super name',
323 description: 'my super description',
324 tags: [ 'tag1', 'tag2', 'tag3', 'tag4' ]
325 }
326 const attach = {
327 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
328 }
329 requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done)
330 })
331
332 it('Should fail with not enough tags', function (done) {
333 const data = {
334 name: 'my super name',
335 description: 'my super description',
336 tags: [ ]
337 }
338 const attach = {
339 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
340 }
341 requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done)
342 })
343
344 it('Should fail with a tag length too low', function (done) {
345 const data = {
346 name: 'my super name',
347 description: 'my super description',
348 tags: [ 'tag1', 't' ]
349 }
350 const attach = {
351 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
352 }
353 requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done)
354 })
355
356 it('Should fail with a tag length too big', function (done) {
357 const data = {
358 name: 'my super name',
359 description: 'my super description',
360 tags: [ 'mysupertagtoolong', 'tag1' ]
361 }
362 const attach = {
363 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
364 }
365 requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done)
366 })
367
368 it('Should fail with malformed tags', function (done) {
369 const data = {
370 name: 'my super name',
371 description: 'my super description',
372 tags: [ 'my tag' ]
373 }
374 const attach = {
375 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
376 }
377 requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done)
378 })
379
380 it('Should fail without an input file', function (done) {
381 const data = {
382 name: 'my super name',
383 description: 'my super description',
384 tags: [ 'tag1', 'tag2' ]
385 }
386 const attach = {}
387 requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done)
388 })
389
390 it('Should fail without an incorrect input file', function (done) {
391 const data = {
392 name: 'my super name',
393 description: 'my super description',
394 tags: [ 'tag1', 'tag2' ]
395 }
396 const attach = {
397 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short_fake.webm')
398 }
399 requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done)
400 })
401
402 it('Should fail with a too big duration', function (done) {
403 const data = {
404 name: 'my super name',
405 description: 'my super description',
406 tags: [ 'tag1', 'tag2' ]
407 }
408 const attach = {
409 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_too_long.webm')
410 }
411 requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done)
412 })
413
414 it('Should succeed with the correct parameters', function (done) {
415 const data = {
416 name: 'my super name',
417 description: 'my super description',
418 tags: [ 'tag1', 'tag2' ]
419 }
420 const attach = {
421 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
422 }
423 requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, function () {
424 attach.videofile = pathUtils.join(__dirname, 'fixtures', 'video_short.mp4')
425 requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, function () {
426 attach.videofile = pathUtils.join(__dirname, 'fixtures', 'video_short.ogv')
427 requestsUtils.makePostUploadRequest(server.url, path, server.accessToken, data, attach, done, 204)
428 }, false)
429 }, false)
430 })
431 })
432
433 describe('When getting a video', function () {
434 it('Should return the list of the videos with nothing', function (done) {
435 request(server.url)
436 .get(path)
437 .set('Accept', 'application/json')
438 .expect(200)
439 .expect('Content-Type', /json/)
440 .end(function (err, res) {
441 if (err) throw err
442
443 expect(res.body.data).to.be.an('array')
444 expect(res.body.data.length).to.equal(3)
445
446 done()
447 })
448 })
449
450 it('Should fail without a mongodb id', function (done) {
451 request(server.url)
452 .get(path + 'coucou')
453 .set('Accept', 'application/json')
454 .expect(400, done)
455 })
456
457 it('Should return 404 with an incorrect video', function (done) {
458 request(server.url)
459 .get(path + '123456789012345678901234')
460 .set('Accept', 'application/json')
461 .expect(404, done)
462 })
463
464 it('Should succeed with the correct parameters')
465 })
466
467 describe('When removing a video', function () {
468 it('Should have 404 with nothing', function (done) {
469 request(server.url)
470 .delete(path)
471 .set('Authorization', 'Bearer ' + server.accessToken)
472 .expect(400, done)
473 })
474
475 it('Should fail without a mongodb id', function (done) {
476 request(server.url)
477 .delete(path + 'hello')
478 .set('Authorization', 'Bearer ' + server.accessToken)
479 .expect(400, done)
480 })
481
482 it('Should fail with a video which does not exist', function (done) {
483 request(server.url)
484 .delete(path + '123456789012345678901234')
485 .set('Authorization', 'Bearer ' + server.accessToken)
486 .expect(404, done)
487 })
488
489 it('Should fail with a video of another user')
490
491 it('Should fail with a video of another pod')
492
493 it('Should succeed with the correct parameters')
494 })
495 })
496
497 describe('Of the users API', function () {
498 const path = '/api/v1/users/'
499 let userId = null
500
501 describe('When listing users', function () {
502 it('Should fail with a bad start pagination', function (done) {
503 request(server.url)
504 .get(path)
505 .query({ start: 'hello' })
506 .set('Accept', 'application/json')
507 .expect(400, done)
508 })
509
510 it('Should fail with a bad count pagination', function (done) {
511 request(server.url)
512 .get(path)
513 .query({ count: 'hello' })
514 .set('Accept', 'application/json')
515 .expect(400, done)
516 })
517
518 it('Should fail with an incorrect sort', function (done) {
519 request(server.url)
520 .get(path)
521 .query({ sort: 'hello' })
522 .set('Accept', 'application/json')
523 .expect(400, done)
524 })
525 })
526
527 describe('When adding a new user', function () {
528 it('Should fail with a too small username', function (done) {
529 const data = {
530 username: 'ji',
531 password: 'mysuperpassword'
532 }
533
534 requestsUtils.makePostBodyRequest(server.url, path, server.accessToken, data, done)
535 })
536
537 it('Should fail with a too long username', function (done) {
538 const data = {
539 username: 'mysuperusernamewhichisverylong',
540 password: 'mysuperpassword'
541 }
542
543 requestsUtils.makePostBodyRequest(server.url, path, server.accessToken, data, done)
544 })
545
546 it('Should fail with an incorrect username', function (done) {
547 const data = {
548 username: 'my username',
549 password: 'mysuperpassword'
550 }
551
552 requestsUtils.makePostBodyRequest(server.url, path, server.accessToken, data, done)
553 })
554
555 it('Should fail with a too small password', function (done) {
556 const data = {
557 username: 'myusername',
558 password: 'bla'
559 }
560
561 requestsUtils.makePostBodyRequest(server.url, path, server.accessToken, data, done)
562 })
563
564 it('Should fail with a too long password', function (done) {
565 const data = {
566 username: 'myusername',
567 password: 'my super long password which is very very very very very very very very very very very very very very' +
568 'very very very very very very very very very very very very very very very veryv very very very very' +
569 'very very very very very very very very very very very very very very very very very very very very long'
570 }
571
572 requestsUtils.makePostBodyRequest(server.url, path, server.accessToken, data, done)
573 })
574
575 it('Should fail with an non authenticated user', function (done) {
576 const data = {
577 username: 'myusername',
578 password: 'my super password'
579 }
580
581 requestsUtils.makePostBodyRequest(server.url, path, 'super token', data, done, 401)
582 })
583
584 it('Should fail if we add a user with the same username', function (done) {
585 const data = {
586 username: 'user1',
587 password: 'my super password'
588 }
589
590 requestsUtils.makePostBodyRequest(server.url, path, server.accessToken, data, done, 409)
591 })
592
593 it('Should succeed with the correct params', function (done) {
594 const data = {
595 username: 'user2',
596 password: 'my super password'
597 }
598
599 requestsUtils.makePostBodyRequest(server.url, path, server.accessToken, data, done, 204)
600 })
601
602 it('Should fail with a non admin user', function (done) {
603 server.user = {
604 username: 'user1',
605 password: 'password'
606 }
607
608 loginUtils.loginAndGetAccessToken(server, function (err, accessToken) {
609 if (err) throw err
610
611 userAccessToken = accessToken
612
613 const data = {
614 username: 'user3',
615 password: 'my super password'
616 }
617
618 requestsUtils.makePostBodyRequest(server.url, path, userAccessToken, data, done, 403)
619 })
620 })
621 })
622
623 describe('When updating a user', function () {
624 before(function (done) {
625 usersUtils.getUsersList(server.url, function (err, res) {
626 if (err) throw err
627
628 userId = res.body.data[1].id
629 done()
630 })
631 })
632
633 it('Should fail with a too small password', function (done) {
634 const data = {
635 password: 'bla'
636 }
637
638 requestsUtils.makePutBodyRequest(server.url, path + userId, userAccessToken, data, done)
639 })
640
641 it('Should fail with a too long password', function (done) {
642 const data = {
643 password: 'my super long password which is very very very very very very very very very very very very very very' +
644 'very very very very very very very very very very very very very very very veryv very very very very' +
645 'very very very very very very very very very very very very very very very very very very very very long'
646 }
647
648 requestsUtils.makePutBodyRequest(server.url, path + userId, userAccessToken, data, done)
649 })
650
651 it('Should fail with an non authenticated user', function (done) {
652 const data = {
653 password: 'my super password'
654 }
655
656 requestsUtils.makePutBodyRequest(server.url, path + userId, 'super token', data, done, 401)
657 })
658
659 it('Should succeed with the correct params', function (done) {
660 const data = {
661 password: 'my super password'
662 }
663
664 requestsUtils.makePutBodyRequest(server.url, path + userId, userAccessToken, data, done, 204)
665 })
666 })
667
668 describe('When getting my information', function () {
669 it('Should fail with a non authenticated user', function (done) {
670 request(server.url)
671 .get(path + 'me')
672 .set('Authorization', 'Bearer faketoken')
673 .set('Accept', 'application/json')
674 .expect(401, done)
675 })
676
677 it('Should success with the correct parameters', function (done) {
678 request(server.url)
679 .get(path + 'me')
680 .set('Authorization', 'Bearer ' + userAccessToken)
681 .set('Accept', 'application/json')
682 .expect(200, done)
683 })
684 })
685
686 describe('When removing an user', function () {
687 it('Should fail with an incorrect id', function (done) {
688 request(server.url)
689 .delete(path + 'bla-bla')
690 .set('Authorization', 'Bearer ' + server.accessToken)
691 .expect(400, done)
692 })
693
694 it('Should return 404 with a non existing id', function (done) {
695 request(server.url)
696 .delete(path + '579f982228c99c221d8092b8')
697 .set('Authorization', 'Bearer ' + server.accessToken)
698 .expect(404, done)
699 })
700 })
701 })
702
703 describe('Of the remote videos API', function () {
704 describe('When making a secure request', function () {
705 it('Should check a secure request')
706 })
707
708 describe('When adding a video', function () {
709 it('Should check when adding a video')
710 })
711
712 describe('When removing a video', function () {
713 it('Should check when removing a video')
714 })
715 })
716
717 describe('Of the requests API', function () {
718 const path = '/api/v1/requests/stats'
719
720 it('Should fail with an non authenticated user', function (done) {
721 request(server.url)
722 .get(path)
723 .set('Accept', 'application/json')
724 .expect(401, done)
725 })
726
727 it('Should fail with a non admin user', function (done) {
728 request(server.url)
729 .get(path)
730 .set('Authorization', 'Bearer ' + userAccessToken)
731 .set('Accept', 'application/json')
732 .expect(403, done)
733 })
734 })
735
736 after(function (done) {
737 process.kill(-server.app.pid)
738
739 // Keep the logs if the test failed
740 if (this.ok) {
741 serversUtils.flushTests(done)
742 } else {
743 done()
744 }
745 })
746})
diff --git a/server/tests/api/checkParams.js b/server/tests/api/checkParams.js
deleted file mode 100644
index c1ba9c2c0..000000000
--- a/server/tests/api/checkParams.js
+++ /dev/null
@@ -1,456 +0,0 @@
1'use strict'
2
3const chai = require('chai')
4const expect = chai.expect
5const pathUtils = require('path')
6const request = require('supertest')
7const series = require('async/series')
8
9const utils = require('./utils')
10
11describe('Test parameters validator', function () {
12 let server = null
13
14 function makePostRequest (path, token, fields, attaches, done, fail) {
15 let statusCode = 400
16 if (fail !== undefined && fail === false) statusCode = 204
17
18 const req = request(server.url)
19 .post(path)
20 .set('Accept', 'application/json')
21
22 if (token) req.set('Authorization', 'Bearer ' + token)
23
24 Object.keys(fields).forEach(function (field) {
25 const value = fields[field]
26
27 if (Array.isArray(value)) {
28 for (let i = 0; i < value.length; i++) {
29 req.field(field + '[' + i + ']', value[i])
30 }
31 } else {
32 req.field(field, value)
33 }
34 })
35
36 Object.keys(attaches).forEach(function (attach) {
37 const value = attaches[attach]
38 req.attach(attach, value)
39 })
40
41 req.expect(statusCode, done)
42 }
43
44 function makePostBodyRequest (path, fields, done, fail) {
45 let statusCode = 400
46 if (fail !== undefined && fail === false) statusCode = 200
47
48 request(server.url)
49 .post(path)
50 .set('Accept', 'application/json')
51 .send(fields)
52 .expect(statusCode, done)
53 }
54
55 // ---------------------------------------------------------------
56
57 before(function (done) {
58 this.timeout(20000)
59
60 series([
61 function (next) {
62 utils.flushTests(next)
63 },
64 function (next) {
65 utils.runServer(1, function (server1) {
66 server = server1
67
68 next()
69 })
70 },
71 function (next) {
72 utils.loginAndGetAccessToken(server, function (err, token) {
73 if (err) throw err
74 server.accessToken = token
75
76 next()
77 })
78 }
79 ], done)
80 })
81
82 describe('Of the pods API', function () {
83 const path = '/api/v1/pods/'
84
85 describe('When adding a pod', function () {
86 it('Should fail with nothing', function (done) {
87 const data = {}
88 makePostBodyRequest(path, data, done)
89 })
90
91 it('Should fail without public key', function (done) {
92 const data = {
93 url: 'http://coucou.com'
94 }
95 makePostBodyRequest(path, data, done)
96 })
97
98 it('Should fail without an url', function (done) {
99 const data = {
100 publicKey: 'mysuperpublickey'
101 }
102 makePostBodyRequest(path, data, done)
103 })
104
105 it('Should fail with an incorrect url', function (done) {
106 const data = {
107 url: 'coucou.com',
108 publicKey: 'mysuperpublickey'
109 }
110 makePostBodyRequest(path, data, function () {
111 data.url = 'http://coucou'
112 makePostBodyRequest(path, data, function () {
113 data.url = 'coucou'
114 makePostBodyRequest(path, data, done)
115 })
116 })
117 })
118
119 it('Should succeed with the correct parameters', function (done) {
120 const data = {
121 url: 'http://coucou.com',
122 publicKey: 'mysuperpublickey'
123 }
124 makePostBodyRequest(path, data, done, false)
125 })
126 })
127 })
128
129 describe('Of the videos API', function () {
130 const path = '/api/v1/videos/'
131
132 describe('When listing a video', function () {
133 it('Should fail with a bad start pagination', function (done) {
134 request(server.url)
135 .get(path)
136 .query({ start: 'hello' })
137 .set('Accept', 'application/json')
138 .expect(400, done)
139 })
140
141 it('Should fail with a bad count pagination', function (done) {
142 request(server.url)
143 .get(path)
144 .query({ count: 'hello' })
145 .set('Accept', 'application/json')
146 .expect(400, done)
147 })
148
149 it('Should fail with an incorrect sort', function (done) {
150 request(server.url)
151 .get(path)
152 .query({ sort: 'hello' })
153 .set('Accept', 'application/json')
154 .expect(400, done)
155 })
156 })
157
158 describe('When searching a video', function () {
159 it('Should fail with nothing', function (done) {
160 request(server.url)
161 .get(pathUtils.join(path, 'search'))
162 .set('Accept', 'application/json')
163 .expect(400, done)
164 })
165
166 it('Should fail with a bad start pagination', function (done) {
167 request(server.url)
168 .get(pathUtils.join(path, 'search', 'test'))
169 .query({ start: 'hello' })
170 .set('Accept', 'application/json')
171 .expect(400, done)
172 })
173
174 it('Should fail with a bad count pagination', function (done) {
175 request(server.url)
176 .get(pathUtils.join(path, 'search', 'test'))
177 .query({ count: 'hello' })
178 .set('Accept', 'application/json')
179 .expect(400, done)
180 })
181
182 it('Should fail with an incorrect sort', function (done) {
183 request(server.url)
184 .get(pathUtils.join(path, 'search', 'test'))
185 .query({ sort: 'hello' })
186 .set('Accept', 'application/json')
187 .expect(400, done)
188 })
189 })
190
191 describe('When adding a video', function () {
192 it('Should fail with nothing', function (done) {
193 const data = {}
194 const attach = {}
195 makePostRequest(path, server.accessToken, data, attach, done)
196 })
197
198 it('Should fail without name', function (done) {
199 const data = {
200 description: 'my super description',
201 tags: [ 'tag1', 'tag2' ]
202 }
203 const attach = {
204 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
205 }
206 makePostRequest(path, server.accessToken, data, attach, done)
207 })
208
209 it('Should fail with a long name', function (done) {
210 const data = {
211 name: 'My very very very very very very very very very very very very very very very very long name',
212 description: 'my super description',
213 tags: [ 'tag1', 'tag2' ]
214 }
215 const attach = {
216 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
217 }
218 makePostRequest(path, server.accessToken, data, attach, done)
219 })
220
221 it('Should fail without description', function (done) {
222 const data = {
223 name: 'my super name',
224 tags: [ 'tag1', 'tag2' ]
225 }
226 const attach = {
227 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
228 }
229 makePostRequest(path, server.accessToken, data, attach, done)
230 })
231
232 it('Should fail with a long description', function (done) {
233 const data = {
234 name: 'my super name',
235 description: 'my super description which is very very very very very very very very very very very very very very' +
236 'very very very very very very very very very very very very very very very very very very very very very' +
237 'very very very very very very very very very very very very very very very long',
238 tags: [ 'tag1', 'tag2' ]
239 }
240 const attach = {
241 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
242 }
243 makePostRequest(path, server.accessToken, data, attach, done)
244 })
245
246 it('Should fail without tags', function (done) {
247 const data = {
248 name: 'my super name',
249 description: 'my super description'
250 }
251 const attach = {
252 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
253 }
254 makePostRequest(path, server.accessToken, data, attach, done)
255 })
256
257 it('Should fail with too many tags', function (done) {
258 const data = {
259 name: 'my super name',
260 description: 'my super description',
261 tags: [ 'tag1', 'tag2', 'tag3', 'tag4' ]
262 }
263 const attach = {
264 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
265 }
266 makePostRequest(path, server.accessToken, data, attach, done)
267 })
268
269 it('Should fail with not enough tags', function (done) {
270 const data = {
271 name: 'my super name',
272 description: 'my super description',
273 tags: [ ]
274 }
275 const attach = {
276 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
277 }
278 makePostRequest(path, server.accessToken, data, attach, done)
279 })
280
281 it('Should fail with a tag length too low', function (done) {
282 const data = {
283 name: 'my super name',
284 description: 'my super description',
285 tags: [ 'tag1', 't' ]
286 }
287 const attach = {
288 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
289 }
290 makePostRequest(path, server.accessToken, data, attach, done)
291 })
292
293 it('Should fail with a tag length too big', function (done) {
294 const data = {
295 name: 'my super name',
296 description: 'my super description',
297 tags: [ 'mysupertagtoolong', 'tag1' ]
298 }
299 const attach = {
300 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
301 }
302 makePostRequest(path, server.accessToken, data, attach, done)
303 })
304
305 it('Should fail with malformed tags', function (done) {
306 const data = {
307 name: 'my super name',
308 description: 'my super description',
309 tags: [ 'my tag' ]
310 }
311 const attach = {
312 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
313 }
314 makePostRequest(path, server.accessToken, data, attach, done)
315 })
316
317 it('Should fail without an input file', function (done) {
318 const data = {
319 name: 'my super name',
320 description: 'my super description',
321 tags: [ 'tag1', 'tag2' ]
322 }
323 const attach = {}
324 makePostRequest(path, server.accessToken, data, attach, done)
325 })
326
327 it('Should fail without an incorrect input file', function (done) {
328 const data = {
329 name: 'my super name',
330 description: 'my super description',
331 tags: [ 'tag1', 'tag2' ]
332 }
333 const attach = {
334 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short_fake.webm')
335 }
336 makePostRequest(path, server.accessToken, data, attach, done)
337 })
338
339 it('Should fail with a too big duration', function (done) {
340 const data = {
341 name: 'my super name',
342 description: 'my super description',
343 tags: [ 'tag1', 'tag2' ]
344 }
345 const attach = {
346 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_too_long.webm')
347 }
348 makePostRequest(path, server.accessToken, data, attach, done)
349 })
350
351 it('Should succeed with the correct parameters', function (done) {
352 const data = {
353 name: 'my super name',
354 description: 'my super description',
355 tags: [ 'tag1', 'tag2' ]
356 }
357 const attach = {
358 'videofile': pathUtils.join(__dirname, 'fixtures', 'video_short.webm')
359 }
360 makePostRequest(path, server.accessToken, data, attach, function () {
361 attach.videofile = pathUtils.join(__dirname, 'fixtures', 'video_short.mp4')
362 makePostRequest(path, server.accessToken, data, attach, function () {
363 attach.videofile = pathUtils.join(__dirname, 'fixtures', 'video_short.ogv')
364 makePostRequest(path, server.accessToken, data, attach, done, false)
365 }, false)
366 }, false)
367 })
368 })
369
370 describe('When getting a video', function () {
371 it('Should return the list of the videos with nothing', function (done) {
372 request(server.url)
373 .get(path)
374 .set('Accept', 'application/json')
375 .expect(200)
376 .expect('Content-Type', /json/)
377 .end(function (err, res) {
378 if (err) throw err
379
380 expect(res.body.data).to.be.an('array')
381 expect(res.body.data.length).to.equal(3)
382
383 done()
384 })
385 })
386
387 it('Should fail without a mongodb id', function (done) {
388 request(server.url)
389 .get(path + 'coucou')
390 .set('Accept', 'application/json')
391 .expect(400, done)
392 })
393
394 it('Should return 404 with an incorrect video', function (done) {
395 request(server.url)
396 .get(path + '123456789012345678901234')
397 .set('Accept', 'application/json')
398 .expect(404, done)
399 })
400
401 it('Should succeed with the correct parameters')
402 })
403
404 describe('When removing a video', function () {
405 it('Should have 404 with nothing', function (done) {
406 request(server.url)
407 .delete(path)
408 .set('Authorization', 'Bearer ' + server.accessToken)
409 .expect(400, done)
410 })
411
412 it('Should fail without a mongodb id', function (done) {
413 request(server.url)
414 .delete(path + 'hello')
415 .set('Authorization', 'Bearer ' + server.accessToken)
416 .expect(400, done)
417 })
418
419 it('Should fail with a video which does not exist', function (done) {
420 request(server.url)
421 .delete(path + '123456789012345678901234')
422 .set('Authorization', 'Bearer ' + server.accessToken)
423 .expect(404, done)
424 })
425
426 it('Should fail with a video of another pod')
427
428 it('Should succeed with the correct parameters')
429 })
430 })
431
432 describe('Of the remote videos API', function () {
433 describe('When making a secure request', function () {
434 it('Should check a secure request')
435 })
436
437 describe('When adding a video', function () {
438 it('Should check when adding a video')
439 })
440
441 describe('When removing a video', function () {
442 it('Should check when removing a video')
443 })
444 })
445
446 after(function (done) {
447 process.kill(-server.app.pid)
448
449 // Keep the logs if the test failed
450 if (this.ok) {
451 utils.flushTests(done)
452 } else {
453 done()
454 }
455 })
456})
diff --git a/server/tests/api/friendsAdvanced.js b/server/tests/api/friends-advanced.js
index 603fbc16b..0d24481ef 100644
--- a/server/tests/api/friendsAdvanced.js
+++ b/server/tests/api/friends-advanced.js
@@ -5,24 +5,27 @@ const each = require('async/each')
5const expect = chai.expect 5const expect = chai.expect
6const series = require('async/series') 6const series = require('async/series')
7 7
8const utils = require('./utils') 8const loginUtils = require('../utils/login')
9const podsUtils = require('../utils/pods')
10const serversUtils = require('../utils/servers')
11const videosUtils = require('../utils/videos')
9 12
10describe('Test advanced friends', function () { 13describe('Test advanced friends', function () {
11 let servers = [] 14 let servers = []
12 15
13 function makeFriends (podNumber, callback) { 16 function makeFriends (podNumber, callback) {
14 const server = servers[podNumber - 1] 17 const server = servers[podNumber - 1]
15 return utils.makeFriends(server.url, server.accessToken, callback) 18 return podsUtils.makeFriends(server.url, server.accessToken, callback)
16 } 19 }
17 20
18 function quitFriends (podNumber, callback) { 21 function quitFriends (podNumber, callback) {
19 const server = servers[podNumber - 1] 22 const server = servers[podNumber - 1]
20 return utils.quitFriends(server.url, server.accessToken, callback) 23 return podsUtils.quitFriends(server.url, server.accessToken, callback)
21 } 24 }
22 25
23 function getFriendsList (podNumber, end) { 26 function getFriendsList (podNumber, end) {
24 const server = servers[podNumber - 1] 27 const server = servers[podNumber - 1]
25 return utils.getFriendsList(server.url, end) 28 return podsUtils.getFriendsList(server.url, end)
26 } 29 }
27 30
28 function uploadVideo (podNumber, callback) { 31 function uploadVideo (podNumber, callback) {
@@ -32,22 +35,22 @@ describe('Test advanced friends', function () {
32 const fixture = 'video_short.webm' 35 const fixture = 'video_short.webm'
33 const server = servers[podNumber - 1] 36 const server = servers[podNumber - 1]
34 37
35 return utils.uploadVideo(server.url, server.accessToken, name, description, tags, fixture, callback) 38 return videosUtils.uploadVideo(server.url, server.accessToken, name, description, tags, fixture, callback)
36 } 39 }
37 40
38 function getVideos (podNumber, callback) { 41 function getVideos (podNumber, callback) {
39 return utils.getVideosList(servers[podNumber - 1].url, callback) 42 return videosUtils.getVideosList(servers[podNumber - 1].url, callback)
40 } 43 }
41 44
42 // --------------------------------------------------------------- 45 // ---------------------------------------------------------------
43 46
44 before(function (done) { 47 before(function (done) {
45 this.timeout(30000) 48 this.timeout(30000)
46 utils.flushAndRunMultipleServers(6, function (serversRun, urlsRun) { 49 serversUtils.flushAndRunMultipleServers(6, function (serversRun, urlsRun) {
47 servers = serversRun 50 servers = serversRun
48 51
49 each(servers, function (server, callbackEach) { 52 each(servers, function (server, callbackEach) {
50 utils.loginAndGetAccessToken(server, function (err, accessToken) { 53 loginUtils.loginAndGetAccessToken(server, function (err, accessToken) {
51 if (err) return callbackEach(err) 54 if (err) return callbackEach(err)
52 55
53 server.accessToken = accessToken 56 server.accessToken = accessToken
@@ -169,7 +172,7 @@ describe('Test advanced friends', function () {
169 }, 172 },
170 // Rerun server 4 173 // Rerun server 4
171 function (next) { 174 function (next) {
172 utils.runServer(4, function (server) { 175 serversUtils.runServer(4, function (server) {
173 servers[3].app = server.app 176 servers[3].app = server.app
174 next() 177 next()
175 }) 178 })
@@ -273,7 +276,7 @@ describe('Test advanced friends', function () {
273 }) 276 })
274 277
275 if (this.ok) { 278 if (this.ok) {
276 utils.flushTests(done) 279 serversUtils.flushTests(done)
277 } else { 280 } else {
278 done() 281 done()
279 } 282 }
diff --git a/server/tests/api/friendsBasic.js b/server/tests/api/friends-basic.js
index c74a7f224..f1393b5ec 100644
--- a/server/tests/api/friendsBasic.js
+++ b/server/tests/api/friends-basic.js
@@ -5,14 +5,17 @@ const each = require('async/each')
5const expect = chai.expect 5const expect = chai.expect
6const series = require('async/series') 6const series = require('async/series')
7 7
8const utils = require('./utils') 8const loginUtils = require('../utils/login')
9const miscsUtils = require('../utils/miscs')
10const podsUtils = require('../utils/pods')
11const serversUtils = require('../utils/servers')
9 12
10describe('Test basic friends', function () { 13describe('Test basic friends', function () {
11 let servers = [] 14 let servers = []
12 15
13 function makeFriends (podNumber, callback) { 16 function makeFriends (podNumber, callback) {
14 const server = servers[podNumber - 1] 17 const server = servers[podNumber - 1]
15 return utils.makeFriends(server.url, server.accessToken, callback) 18 return podsUtils.makeFriends(server.url, server.accessToken, callback)
16 } 19 }
17 20
18 function testMadeFriends (servers, serverToTest, callback) { 21 function testMadeFriends (servers, serverToTest, callback) {
@@ -22,7 +25,7 @@ describe('Test basic friends', function () {
22 friends.push(servers[i].url) 25 friends.push(servers[i].url)
23 } 26 }
24 27
25 utils.getFriendsList(serverToTest.url, function (err, res) { 28 podsUtils.getFriendsList(serverToTest.url, function (err, res) {
26 if (err) throw err 29 if (err) throw err
27 30
28 const result = res.body 31 const result = res.body
@@ -43,11 +46,11 @@ describe('Test basic friends', function () {
43 46
44 before(function (done) { 47 before(function (done) {
45 this.timeout(20000) 48 this.timeout(20000)
46 utils.flushAndRunMultipleServers(3, function (serversRun, urlsRun) { 49 serversUtils.flushAndRunMultipleServers(3, function (serversRun, urlsRun) {
47 servers = serversRun 50 servers = serversRun
48 51
49 each(servers, function (server, callbackEach) { 52 each(servers, function (server, callbackEach) {
50 utils.loginAndGetAccessToken(server, function (err, accessToken) { 53 loginUtils.loginAndGetAccessToken(server, function (err, accessToken) {
51 if (err) return callbackEach(err) 54 if (err) return callbackEach(err)
52 55
53 server.accessToken = accessToken 56 server.accessToken = accessToken
@@ -59,7 +62,7 @@ describe('Test basic friends', function () {
59 62
60 it('Should not have friends', function (done) { 63 it('Should not have friends', function (done) {
61 each(servers, function (server, callback) { 64 each(servers, function (server, callback) {
62 utils.getFriendsList(server.url, function (err, res) { 65 podsUtils.getFriendsList(server.url, function (err, res) {
63 if (err) throw err 66 if (err) throw err
64 67
65 const result = res.body 68 const result = res.body
@@ -71,7 +74,7 @@ describe('Test basic friends', function () {
71 }) 74 })
72 75
73 it('Should make friends', function (done) { 76 it('Should make friends', function (done) {
74 this.timeout(10000) 77 this.timeout(40000)
75 78
76 series([ 79 series([
77 // The second pod make friend with the third 80 // The second pod make friend with the third
@@ -80,30 +83,38 @@ describe('Test basic friends', function () {
80 }, 83 },
81 // Wait for the request between pods 84 // Wait for the request between pods
82 function (next) { 85 function (next) {
83 setTimeout(next, 1000) 86 setTimeout(next, 11000)
84 }, 87 },
85 // The second pod should have the third as a friend 88 // The second pod should have the third as a friend
86 function (next) { 89 function (next) {
87 utils.getFriendsList(servers[1].url, function (err, res) { 90 podsUtils.getFriendsList(servers[1].url, function (err, res) {
88 if (err) throw err 91 if (err) throw err
89 92
90 const result = res.body 93 const result = res.body
91 expect(result).to.be.an('array') 94 expect(result).to.be.an('array')
92 expect(result.length).to.equal(1) 95 expect(result.length).to.equal(1)
93 expect(result[0].url).to.be.equal(servers[2].url) 96
97 const pod = result[0]
98 expect(pod.url).to.equal(servers[2].url)
99 expect(pod.score).to.equal(20)
100 expect(miscsUtils.dateIsValid(pod.createdDate)).to.be.true
94 101
95 next() 102 next()
96 }) 103 })
97 }, 104 },
98 // Same here, the third pod should have the second pod as a friend 105 // Same here, the third pod should have the second pod as a friend
99 function (next) { 106 function (next) {
100 utils.getFriendsList(servers[2].url, function (err, res) { 107 podsUtils.getFriendsList(servers[2].url, function (err, res) {
101 if (err) throw err 108 if (err) throw err
102 109
103 const result = res.body 110 const result = res.body
104 expect(result).to.be.an('array') 111 expect(result).to.be.an('array')
105 expect(result.length).to.equal(1) 112 expect(result.length).to.equal(1)
106 expect(result[0].url).to.be.equal(servers[1].url) 113
114 const pod = result[0]
115 expect(pod.url).to.equal(servers[1].url)
116 expect(pod.score).to.equal(20)
117 expect(miscsUtils.dateIsValid(pod.createdDate)).to.be.true
107 118
108 next() 119 next()
109 }) 120 })
@@ -114,7 +125,7 @@ describe('Test basic friends', function () {
114 }, 125 },
115 // Wait for the request between pods 126 // Wait for the request between pods
116 function (next) { 127 function (next) {
117 setTimeout(next, 1000) 128 setTimeout(next, 11000)
118 } 129 }
119 ], 130 ],
120 // Now each pod should be friend with the other ones 131 // Now each pod should be friend with the other ones
@@ -128,7 +139,7 @@ describe('Test basic friends', function () {
128 139
129 it('Should not be allowed to make friend again', function (done) { 140 it('Should not be allowed to make friend again', function (done) {
130 const server = servers[1] 141 const server = servers[1]
131 utils.makeFriends(server.url, server.accessToken, 409, done) 142 podsUtils.makeFriends(server.url, server.accessToken, 409, done)
132 }) 143 })
133 144
134 it('Should quit friends of pod 2', function (done) { 145 it('Should quit friends of pod 2', function (done) {
@@ -136,11 +147,11 @@ describe('Test basic friends', function () {
136 // Pod 1 quit friends 147 // Pod 1 quit friends
137 function (next) { 148 function (next) {
138 const server = servers[1] 149 const server = servers[1]
139 utils.quitFriends(server.url, server.accessToken, next) 150 podsUtils.quitFriends(server.url, server.accessToken, next)
140 }, 151 },
141 // Pod 1 should not have friends anymore 152 // Pod 1 should not have friends anymore
142 function (next) { 153 function (next) {
143 utils.getFriendsList(servers[1].url, function (err, res) { 154 podsUtils.getFriendsList(servers[1].url, function (err, res) {
144 if (err) throw err 155 if (err) throw err
145 156
146 const result = res.body 157 const result = res.body
@@ -153,7 +164,7 @@ describe('Test basic friends', function () {
153 // Other pods shouldn't have pod 1 too 164 // Other pods shouldn't have pod 1 too
154 function (next) { 165 function (next) {
155 each([ servers[0].url, servers[2].url ], function (url, callback) { 166 each([ servers[0].url, servers[2].url ], function (url, callback) {
156 utils.getFriendsList(url, function (err, res) { 167 podsUtils.getFriendsList(url, function (err, res) {
157 if (err) throw err 168 if (err) throw err
158 169
159 const result = res.body 170 const result = res.body
@@ -168,11 +179,15 @@ describe('Test basic friends', function () {
168 }) 179 })
169 180
170 it('Should allow pod 2 to make friend again', function (done) { 181 it('Should allow pod 2 to make friend again', function (done) {
182 this.timeout(20000)
183
171 const server = servers[1] 184 const server = servers[1]
172 utils.makeFriends(server.url, server.accessToken, function () { 185 podsUtils.makeFriends(server.url, server.accessToken, function () {
173 each(servers, function (server, callback) { 186 setTimeout(function () {
174 testMadeFriends(servers, server, callback) 187 each(servers, function (server, callback) {
175 }, done) 188 testMadeFriends(servers, server, callback)
189 }, done)
190 }, 11000)
176 }) 191 })
177 }) 192 })
178 193
@@ -182,7 +197,7 @@ describe('Test basic friends', function () {
182 }) 197 })
183 198
184 if (this.ok) { 199 if (this.ok) {
185 utils.flushTests(done) 200 serversUtils.flushTests(done)
186 } else { 201 } else {
187 done() 202 done()
188 } 203 }
diff --git a/server/tests/api/index.js b/server/tests/api/index.js
index 61c9a7aca..11f49e1e2 100644
--- a/server/tests/api/index.js
+++ b/server/tests/api/index.js
@@ -1,9 +1,9 @@
1'use strict' 1'use strict'
2 2
3// Order of the tests we want to execute 3// Order of the tests we want to execute
4require('./checkParams') 4require('./check-params')
5require('./friendsBasic') 5require('./friends-basic')
6require('./users') 6require('./users')
7require('./singlePod') 7require('./single-pod')
8require('./multiplePods') 8require('./multiple-pods')
9require('./friendsAdvanced') 9require('./friends-advanced')
diff --git a/server/tests/api/multiplePods.js b/server/tests/api/multiple-pods.js
index ac140f6bb..b86f88c22 100644
--- a/server/tests/api/multiplePods.js
+++ b/server/tests/api/multiple-pods.js
@@ -6,7 +6,11 @@ const expect = chai.expect
6const pathUtils = require('path') 6const pathUtils = require('path')
7const series = require('async/series') 7const series = require('async/series')
8 8
9const utils = require('./utils') 9const loginUtils = require('../utils/login')
10const miscsUtils = require('../utils/miscs')
11const podsUtils = require('../utils/pods')
12const serversUtils = require('../utils/servers')
13const videosUtils = require('../utils/videos')
10const webtorrent = require(pathUtils.join(__dirname, '../../lib/webtorrent')) 14const webtorrent = require(pathUtils.join(__dirname, '../../lib/webtorrent'))
11webtorrent.silent = true 15webtorrent.silent = true
12 16
@@ -20,7 +24,7 @@ describe('Test multiple pods', function () {
20 series([ 24 series([
21 // Run servers 25 // Run servers
22 function (next) { 26 function (next) {
23 utils.flushAndRunMultipleServers(3, function (serversRun) { 27 serversUtils.flushAndRunMultipleServers(3, function (serversRun) {
24 servers = serversRun 28 servers = serversRun
25 next() 29 next()
26 }) 30 })
@@ -28,7 +32,7 @@ describe('Test multiple pods', function () {
28 // Get the access tokens 32 // Get the access tokens
29 function (next) { 33 function (next) {
30 each(servers, function (server, callbackEach) { 34 each(servers, function (server, callbackEach) {
31 utils.loginAndGetAccessToken(server, function (err, accessToken) { 35 loginUtils.loginAndGetAccessToken(server, function (err, accessToken) {
32 if (err) return callbackEach(err) 36 if (err) return callbackEach(err)
33 37
34 server.accessToken = accessToken 38 server.accessToken = accessToken
@@ -39,7 +43,7 @@ describe('Test multiple pods', function () {
39 // The second pod make friend with the third 43 // The second pod make friend with the third
40 function (next) { 44 function (next) {
41 const server = servers[1] 45 const server = servers[1]
42 utils.makeFriends(server.url, server.accessToken, next) 46 podsUtils.makeFriends(server.url, server.accessToken, next)
43 }, 47 },
44 // Wait for the request between pods 48 // Wait for the request between pods
45 function (next) { 49 function (next) {
@@ -48,7 +52,7 @@ describe('Test multiple pods', function () {
48 // Pod 1 make friends too 52 // Pod 1 make friends too
49 function (next) { 53 function (next) {
50 const server = servers[0] 54 const server = servers[0]
51 utils.makeFriends(server.url, server.accessToken, next) 55 podsUtils.makeFriends(server.url, server.accessToken, next)
52 }, 56 },
53 function (next) { 57 function (next) {
54 webtorrent.create({ host: 'client', port: '1' }, next) 58 webtorrent.create({ host: 'client', port: '1' }, next)
@@ -58,7 +62,7 @@ describe('Test multiple pods', function () {
58 62
59 it('Should not have videos for all pods', function (done) { 63 it('Should not have videos for all pods', function (done) {
60 each(servers, function (server, callback) { 64 each(servers, function (server, callback) {
61 utils.getVideosList(server.url, function (err, res) { 65 videosUtils.getVideosList(server.url, function (err, res) {
62 if (err) throw err 66 if (err) throw err
63 67
64 const videos = res.body.data 68 const videos = res.body.data
@@ -80,7 +84,7 @@ describe('Test multiple pods', function () {
80 const description = 'my super description for pod 1' 84 const description = 'my super description for pod 1'
81 const tags = [ 'tag1p1', 'tag2p1' ] 85 const tags = [ 'tag1p1', 'tag2p1' ]
82 const file = 'video_short1.webm' 86 const file = 'video_short1.webm'
83 utils.uploadVideo(servers[0].url, servers[0].accessToken, name, description, tags, file, next) 87 videosUtils.uploadVideo(servers[0].url, servers[0].accessToken, name, description, tags, file, next)
84 }, 88 },
85 function (next) { 89 function (next) {
86 setTimeout(next, 11000) 90 setTimeout(next, 11000)
@@ -92,7 +96,7 @@ describe('Test multiple pods', function () {
92 each(servers, function (server, callback) { 96 each(servers, function (server, callback) {
93 let baseMagnet = null 97 let baseMagnet = null
94 98
95 utils.getVideosList(server.url, function (err, res) { 99 videosUtils.getVideosList(server.url, function (err, res) {
96 if (err) throw err 100 if (err) throw err
97 101
98 const videos = res.body.data 102 const videos = res.body.data
@@ -105,7 +109,7 @@ describe('Test multiple pods', function () {
105 expect(video.magnetUri).to.exist 109 expect(video.magnetUri).to.exist
106 expect(video.duration).to.equal(10) 110 expect(video.duration).to.equal(10)
107 expect(video.tags).to.deep.equal([ 'tag1p1', 'tag2p1' ]) 111 expect(video.tags).to.deep.equal([ 'tag1p1', 'tag2p1' ])
108 expect(utils.dateIsValid(video.createdDate)).to.be.true 112 expect(miscsUtils.dateIsValid(video.createdDate)).to.be.true
109 expect(video.author).to.equal('root') 113 expect(video.author).to.equal('root')
110 114
111 if (server.url !== 'http://localhost:9001') { 115 if (server.url !== 'http://localhost:9001') {
@@ -121,7 +125,7 @@ describe('Test multiple pods', function () {
121 expect(video.magnetUri).to.equal.magnetUri 125 expect(video.magnetUri).to.equal.magnetUri
122 } 126 }
123 127
124 utils.testImage(server.url, 'video_short1.webm', video.thumbnailPath, function (err, test) { 128 videosUtils.testVideoImage(server.url, 'video_short1.webm', video.thumbnailPath, function (err, test) {
125 if (err) throw err 129 if (err) throw err
126 expect(test).to.equal(true) 130 expect(test).to.equal(true)
127 131
@@ -142,7 +146,7 @@ describe('Test multiple pods', function () {
142 const description = 'my super description for pod 2' 146 const description = 'my super description for pod 2'
143 const tags = [ 'tag1p2', 'tag2p2', 'tag3p2' ] 147 const tags = [ 'tag1p2', 'tag2p2', 'tag3p2' ]
144 const file = 'video_short2.webm' 148 const file = 'video_short2.webm'
145 utils.uploadVideo(servers[1].url, servers[1].accessToken, name, description, tags, file, next) 149 videosUtils.uploadVideo(servers[1].url, servers[1].accessToken, name, description, tags, file, next)
146 }, 150 },
147 function (next) { 151 function (next) {
148 setTimeout(next, 11000) 152 setTimeout(next, 11000)
@@ -154,7 +158,7 @@ describe('Test multiple pods', function () {
154 each(servers, function (server, callback) { 158 each(servers, function (server, callback) {
155 let baseMagnet = null 159 let baseMagnet = null
156 160
157 utils.getVideosList(server.url, function (err, res) { 161 videosUtils.getVideosList(server.url, function (err, res) {
158 if (err) throw err 162 if (err) throw err
159 163
160 const videos = res.body.data 164 const videos = res.body.data
@@ -167,7 +171,7 @@ describe('Test multiple pods', function () {
167 expect(video.magnetUri).to.exist 171 expect(video.magnetUri).to.exist
168 expect(video.duration).to.equal(5) 172 expect(video.duration).to.equal(5)
169 expect(video.tags).to.deep.equal([ 'tag1p2', 'tag2p2', 'tag3p2' ]) 173 expect(video.tags).to.deep.equal([ 'tag1p2', 'tag2p2', 'tag3p2' ])
170 expect(utils.dateIsValid(video.createdDate)).to.be.true 174 expect(miscsUtils.dateIsValid(video.createdDate)).to.be.true
171 expect(video.author).to.equal('root') 175 expect(video.author).to.equal('root')
172 176
173 if (server.url !== 'http://localhost:9002') { 177 if (server.url !== 'http://localhost:9002') {
@@ -183,7 +187,7 @@ describe('Test multiple pods', function () {
183 expect(video.magnetUri).to.equal.magnetUri 187 expect(video.magnetUri).to.equal.magnetUri
184 } 188 }
185 189
186 utils.testImage(server.url, 'video_short2.webm', video.thumbnailPath, function (err, test) { 190 videosUtils.testVideoImage(server.url, 'video_short2.webm', video.thumbnailPath, function (err, test) {
187 if (err) throw err 191 if (err) throw err
188 expect(test).to.equal(true) 192 expect(test).to.equal(true)
189 193
@@ -204,14 +208,14 @@ describe('Test multiple pods', function () {
204 const description = 'my super description for pod 3' 208 const description = 'my super description for pod 3'
205 const tags = [ 'tag1p3' ] 209 const tags = [ 'tag1p3' ]
206 const file = 'video_short3.webm' 210 const file = 'video_short3.webm'
207 utils.uploadVideo(servers[2].url, servers[2].accessToken, name, description, tags, file, next) 211 videosUtils.uploadVideo(servers[2].url, servers[2].accessToken, name, description, tags, file, next)
208 }, 212 },
209 function (next) { 213 function (next) {
210 const name = 'my super name for pod 3-2' 214 const name = 'my super name for pod 3-2'
211 const description = 'my super description for pod 3-2' 215 const description = 'my super description for pod 3-2'
212 const tags = [ 'tag2p3', 'tag3p3', 'tag4p3' ] 216 const tags = [ 'tag2p3', 'tag3p3', 'tag4p3' ]
213 const file = 'video_short.webm' 217 const file = 'video_short.webm'
214 utils.uploadVideo(servers[2].url, servers[2].accessToken, name, description, tags, file, next) 218 videosUtils.uploadVideo(servers[2].url, servers[2].accessToken, name, description, tags, file, next)
215 }, 219 },
216 function (next) { 220 function (next) {
217 setTimeout(next, 22000) 221 setTimeout(next, 22000)
@@ -222,7 +226,7 @@ describe('Test multiple pods', function () {
222 let baseMagnet = null 226 let baseMagnet = null
223 // All pods should have this video 227 // All pods should have this video
224 each(servers, function (server, callback) { 228 each(servers, function (server, callback) {
225 utils.getVideosList(server.url, function (err, res) { 229 videosUtils.getVideosList(server.url, function (err, res) {
226 if (err) throw err 230 if (err) throw err
227 231
228 const videos = res.body.data 232 const videos = res.body.data
@@ -247,7 +251,7 @@ describe('Test multiple pods', function () {
247 expect(video1.duration).to.equal(5) 251 expect(video1.duration).to.equal(5)
248 expect(video1.tags).to.deep.equal([ 'tag1p3' ]) 252 expect(video1.tags).to.deep.equal([ 'tag1p3' ])
249 expect(video1.author).to.equal('root') 253 expect(video1.author).to.equal('root')
250 expect(utils.dateIsValid(video1.createdDate)).to.be.true 254 expect(miscsUtils.dateIsValid(video1.createdDate)).to.be.true
251 255
252 expect(video2.name).to.equal('my super name for pod 3-2') 256 expect(video2.name).to.equal('my super name for pod 3-2')
253 expect(video2.description).to.equal('my super description for pod 3-2') 257 expect(video2.description).to.equal('my super description for pod 3-2')
@@ -256,7 +260,7 @@ describe('Test multiple pods', function () {
256 expect(video2.duration).to.equal(5) 260 expect(video2.duration).to.equal(5)
257 expect(video2.tags).to.deep.equal([ 'tag2p3', 'tag3p3', 'tag4p3' ]) 261 expect(video2.tags).to.deep.equal([ 'tag2p3', 'tag3p3', 'tag4p3' ])
258 expect(video2.author).to.equal('root') 262 expect(video2.author).to.equal('root')
259 expect(utils.dateIsValid(video2.createdDate)).to.be.true 263 expect(miscsUtils.dateIsValid(video2.createdDate)).to.be.true
260 264
261 if (server.url !== 'http://localhost:9003') { 265 if (server.url !== 'http://localhost:9003') {
262 expect(video1.isLocal).to.be.false 266 expect(video1.isLocal).to.be.false
@@ -273,11 +277,11 @@ describe('Test multiple pods', function () {
273 expect(video2.magnetUri).to.equal.magnetUri 277 expect(video2.magnetUri).to.equal.magnetUri
274 } 278 }
275 279
276 utils.testImage(server.url, 'video_short3.webm', video1.thumbnailPath, function (err, test) { 280 videosUtils.testVideoImage(server.url, 'video_short3.webm', video1.thumbnailPath, function (err, test) {
277 if (err) throw err 281 if (err) throw err
278 expect(test).to.equal(true) 282 expect(test).to.equal(true)
279 283
280 utils.testImage(server.url, 'video_short.webm', video2.thumbnailPath, function (err, test) { 284 videosUtils.testVideoImage(server.url, 'video_short.webm', video2.thumbnailPath, function (err, test) {
281 if (err) throw err 285 if (err) throw err
282 expect(test).to.equal(true) 286 expect(test).to.equal(true)
283 287
@@ -296,7 +300,7 @@ describe('Test multiple pods', function () {
296 // Yes, this could be long 300 // Yes, this could be long
297 this.timeout(200000) 301 this.timeout(200000)
298 302
299 utils.getVideosList(servers[2].url, function (err, res) { 303 videosUtils.getVideosList(servers[2].url, function (err, res) {
300 if (err) throw err 304 if (err) throw err
301 305
302 const video = res.body.data[0] 306 const video = res.body.data[0]
@@ -317,7 +321,7 @@ describe('Test multiple pods', function () {
317 // Yes, this could be long 321 // Yes, this could be long
318 this.timeout(200000) 322 this.timeout(200000)
319 323
320 utils.getVideosList(servers[0].url, function (err, res) { 324 videosUtils.getVideosList(servers[0].url, function (err, res) {
321 if (err) throw err 325 if (err) throw err
322 326
323 const video = res.body.data[1] 327 const video = res.body.data[1]
@@ -336,7 +340,7 @@ describe('Test multiple pods', function () {
336 // Yes, this could be long 340 // Yes, this could be long
337 this.timeout(200000) 341 this.timeout(200000)
338 342
339 utils.getVideosList(servers[1].url, function (err, res) { 343 videosUtils.getVideosList(servers[1].url, function (err, res) {
340 if (err) throw err 344 if (err) throw err
341 345
342 const video = res.body.data[2] 346 const video = res.body.data[2]
@@ -355,7 +359,7 @@ describe('Test multiple pods', function () {
355 // Yes, this could be long 359 // Yes, this could be long
356 this.timeout(200000) 360 this.timeout(200000)
357 361
358 utils.getVideosList(servers[0].url, function (err, res) { 362 videosUtils.getVideosList(servers[0].url, function (err, res) {
359 if (err) throw err 363 if (err) throw err
360 364
361 const video = res.body.data[3] 365 const video = res.body.data[3]
@@ -375,10 +379,10 @@ describe('Test multiple pods', function () {
375 379
376 series([ 380 series([
377 function (next) { 381 function (next) {
378 utils.removeVideo(servers[2].url, servers[2].accessToken, toRemove[0], next) 382 videosUtils.removeVideo(servers[2].url, servers[2].accessToken, toRemove[0], next)
379 }, 383 },
380 function (next) { 384 function (next) {
381 utils.removeVideo(servers[2].url, servers[2].accessToken, toRemove[1], next) 385 videosUtils.removeVideo(servers[2].url, servers[2].accessToken, toRemove[1], next)
382 }], 386 }],
383 function (err) { 387 function (err) {
384 if (err) throw err 388 if (err) throw err
@@ -389,7 +393,7 @@ describe('Test multiple pods', function () {
389 393
390 it('Should have videos 1 and 3 on each pod', function (done) { 394 it('Should have videos 1 and 3 on each pod', function (done) {
391 each(servers, function (server, callback) { 395 each(servers, function (server, callback) {
392 utils.getVideosList(server.url, function (err, res) { 396 videosUtils.getVideosList(server.url, function (err, res) {
393 if (err) throw err 397 if (err) throw err
394 398
395 const videos = res.body.data 399 const videos = res.body.data
@@ -415,7 +419,7 @@ describe('Test multiple pods', function () {
415 419
416 // Keep the logs if the test failed 420 // Keep the logs if the test failed
417 if (this.ok) { 421 if (this.ok) {
418 utils.flushTests(done) 422 serversUtils.flushTests(done)
419 } else { 423 } else {
420 done() 424 done()
421 } 425 }
diff --git a/server/tests/api/requests.js b/server/tests/api/requests.js
new file mode 100644
index 000000000..af36f6e34
--- /dev/null
+++ b/server/tests/api/requests.js
@@ -0,0 +1,128 @@
1'use strict'
2
3const chai = require('chai')
4const each = require('async/each')
5const expect = chai.expect
6const request = require('supertest')
7
8const loginUtils = require('../utils/login')
9const podsUtils = require('../utils/pods')
10const serversUtils = require('../utils/servers')
11const videosUtils = require('../utils/videos')
12
13describe('Test requests stats', function () {
14 const path = '/api/v1/requests/stats'
15 let servers = []
16
17 function uploadVideo (server, callback) {
18 const name = 'my super video'
19 const description = 'my super description'
20 const tags = [ 'tag1', 'tag2' ]
21 const fixture = 'video_short.webm'
22
23 videosUtils.uploadVideo(server.url, server.accessToken, name, description, tags, fixture, callback)
24 }
25
26 function getRequestsStats (server, callback) {
27 request(server.url)
28 .get(path)
29 .set('Accept', 'application/json')
30 .set('Authorization', 'Bearer ' + server.accessToken)
31 .expect(200)
32 .end(callback)
33 }
34
35 // ---------------------------------------------------------------
36
37 before(function (done) {
38 this.timeout(20000)
39 serversUtils.flushAndRunMultipleServers(2, function (serversRun, urlsRun) {
40 servers = serversRun
41
42 each(servers, function (server, callbackEach) {
43 loginUtils.loginAndGetAccessToken(server, function (err, accessToken) {
44 if (err) return callbackEach(err)
45
46 server.accessToken = accessToken
47 callbackEach()
48 })
49 }, function (err) {
50 if (err) throw err
51
52 const server1 = servers[0]
53 podsUtils.makeFriends(server1.url, server1.accessToken, done)
54 })
55 })
56 })
57
58 it('Should have a correct timer', function (done) {
59 const server = servers[0]
60
61 getRequestsStats(server, function (err, res) {
62 if (err) throw err
63
64 const body = res.body
65 expect(body.remainingMilliSeconds).to.be.at.least(0)
66 expect(body.remainingMilliSeconds).to.be.at.most(10000)
67
68 done()
69 })
70 })
71
72 it('Should have the correct request', function (done) {
73 this.timeout(15000)
74
75 const server = servers[0]
76 // Ensure the requests of pod 1 won't be made
77 servers[1].app.kill()
78
79 uploadVideo(server, function (err) {
80 if (err) throw err
81
82 getRequestsStats(server, function (err, res) {
83 if (err) throw err
84
85 const body = res.body
86 expect(body.requests).to.have.lengthOf(1)
87
88 const request = body.requests[0]
89 expect(request.to).to.have.lengthOf(1)
90 expect(request.request.type).to.equal('add')
91
92 // Wait one cycle
93 setTimeout(done, 10000)
94 })
95 })
96 })
97
98 it('Should have the correct requests', function (done) {
99 const server = servers[0]
100
101 uploadVideo(server, function (err) {
102 if (err) throw err
103
104 getRequestsStats(server, function (err, res) {
105 if (err) throw err
106
107 const body = res.body
108 expect(body.requests).to.have.lengthOf(2)
109
110 const request = body.requests[1]
111 expect(request.to).to.have.lengthOf(1)
112 expect(request.request.type).to.equal('add')
113
114 done()
115 })
116 })
117 })
118
119 after(function (done) {
120 process.kill(-servers[0].app.pid)
121
122 if (this.ok) {
123 serversUtils.flushTests(done)
124 } else {
125 done()
126 }
127 })
128})
diff --git a/server/tests/api/singlePod.js b/server/tests/api/single-pod.js
index 6ed719f87..bdaaee46c 100644
--- a/server/tests/api/singlePod.js
+++ b/server/tests/api/single-pod.js
@@ -8,11 +8,13 @@ const keyBy = require('lodash/keyBy')
8const pathUtils = require('path') 8const pathUtils = require('path')
9const series = require('async/series') 9const series = require('async/series')
10 10
11const loginUtils = require('../utils/login')
12const miscsUtils = require('../utils/miscs')
13const serversUtils = require('../utils/servers')
14const videosUtils = require('../utils/videos')
11const webtorrent = require(pathUtils.join(__dirname, '../../lib/webtorrent')) 15const webtorrent = require(pathUtils.join(__dirname, '../../lib/webtorrent'))
12webtorrent.silent = true 16webtorrent.silent = true
13 17
14const utils = require('./utils')
15
16describe('Test a single pod', function () { 18describe('Test a single pod', function () {
17 let server = null 19 let server = null
18 let videoId = -1 20 let videoId = -1
@@ -23,16 +25,16 @@ describe('Test a single pod', function () {
23 25
24 series([ 26 series([
25 function (next) { 27 function (next) {
26 utils.flushTests(next) 28 serversUtils.flushTests(next)
27 }, 29 },
28 function (next) { 30 function (next) {
29 utils.runServer(1, function (server1) { 31 serversUtils.runServer(1, function (server1) {
30 server = server1 32 server = server1
31 next() 33 next()
32 }) 34 })
33 }, 35 },
34 function (next) { 36 function (next) {
35 utils.loginAndGetAccessToken(server, function (err, token) { 37 loginUtils.loginAndGetAccessToken(server, function (err, token) {
36 if (err) throw err 38 if (err) throw err
37 server.accessToken = token 39 server.accessToken = token
38 next() 40 next()
@@ -45,7 +47,7 @@ describe('Test a single pod', function () {
45 }) 47 })
46 48
47 it('Should not have videos', function (done) { 49 it('Should not have videos', function (done) {
48 utils.getVideosList(server.url, function (err, res) { 50 videosUtils.getVideosList(server.url, function (err, res) {
49 if (err) throw err 51 if (err) throw err
50 52
51 expect(res.body.total).to.equal(0) 53 expect(res.body.total).to.equal(0)
@@ -62,14 +64,14 @@ describe('Test a single pod', function () {
62 const description = 'my super description' 64 const description = 'my super description'
63 const tags = [ 'tag1', 'tag2', 'tag3' ] 65 const tags = [ 'tag1', 'tag2', 'tag3' ]
64 const file = 'video_short.webm' 66 const file = 'video_short.webm'
65 utils.uploadVideo(server.url, server.accessToken, name, description, tags, file, done) 67 videosUtils.uploadVideo(server.url, server.accessToken, name, description, tags, file, done)
66 }) 68 })
67 69
68 it('Should seed the uploaded video', function (done) { 70 it('Should seed the uploaded video', function (done) {
69 // Yes, this could be long 71 // Yes, this could be long
70 this.timeout(60000) 72 this.timeout(60000)
71 73
72 utils.getVideosList(server.url, function (err, res) { 74 videosUtils.getVideosList(server.url, function (err, res) {
73 if (err) throw err 75 if (err) throw err
74 76
75 expect(res.body.total).to.equal(1) 77 expect(res.body.total).to.equal(1)
@@ -84,9 +86,9 @@ describe('Test a single pod', function () {
84 expect(video.author).to.equal('root') 86 expect(video.author).to.equal('root')
85 expect(video.isLocal).to.be.true 87 expect(video.isLocal).to.be.true
86 expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ]) 88 expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ])
87 expect(utils.dateIsValid(video.createdDate)).to.be.true 89 expect(miscsUtils.dateIsValid(video.createdDate)).to.be.true
88 90
89 utils.testImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) { 91 videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) {
90 if (err) throw err 92 if (err) throw err
91 expect(test).to.equal(true) 93 expect(test).to.equal(true)
92 94
@@ -97,8 +99,7 @@ describe('Test a single pod', function () {
97 expect(torrent.files.length).to.equal(1) 99 expect(torrent.files.length).to.equal(1)
98 expect(torrent.files[0].path).to.exist.and.to.not.equal('') 100 expect(torrent.files[0].path).to.exist.and.to.not.equal('')
99 101
100 // We remove it because we'll add it again 102 done()
101 webtorrent.remove(video.magnetUri, done)
102 }) 103 })
103 }) 104 })
104 }) 105 })
@@ -108,7 +109,7 @@ describe('Test a single pod', function () {
108 // Yes, this could be long 109 // Yes, this could be long
109 this.timeout(60000) 110 this.timeout(60000)
110 111
111 utils.getVideo(server.url, videoId, function (err, res) { 112 videosUtils.getVideo(server.url, videoId, function (err, res) {
112 if (err) throw err 113 if (err) throw err
113 114
114 const video = res.body 115 const video = res.body
@@ -119,25 +120,19 @@ describe('Test a single pod', function () {
119 expect(video.author).to.equal('root') 120 expect(video.author).to.equal('root')
120 expect(video.isLocal).to.be.true 121 expect(video.isLocal).to.be.true
121 expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ]) 122 expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ])
122 expect(utils.dateIsValid(video.createdDate)).to.be.true 123 expect(miscsUtils.dateIsValid(video.createdDate)).to.be.true
123 124
124 utils.testImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) { 125 videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) {
125 if (err) throw err 126 if (err) throw err
126 expect(test).to.equal(true) 127 expect(test).to.equal(true)
127 128
128 webtorrent.add(video.magnetUri, function (torrent) { 129 done()
129 expect(torrent.files).to.exist
130 expect(torrent.files.length).to.equal(1)
131 expect(torrent.files[0].path).to.exist.and.to.not.equal('')
132
133 done()
134 })
135 }) 130 })
136 }) 131 })
137 }) 132 })
138 133
139 it('Should search the video by name by default', function (done) { 134 it('Should search the video by name by default', function (done) {
140 utils.searchVideo(server.url, 'my', function (err, res) { 135 videosUtils.searchVideo(server.url, 'my', function (err, res) {
141 if (err) throw err 136 if (err) throw err
142 137
143 expect(res.body.total).to.equal(1) 138 expect(res.body.total).to.equal(1)
@@ -151,9 +146,9 @@ describe('Test a single pod', function () {
151 expect(video.author).to.equal('root') 146 expect(video.author).to.equal('root')
152 expect(video.isLocal).to.be.true 147 expect(video.isLocal).to.be.true
153 expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ]) 148 expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ])
154 expect(utils.dateIsValid(video.createdDate)).to.be.true 149 expect(miscsUtils.dateIsValid(video.createdDate)).to.be.true
155 150
156 utils.testImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) { 151 videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) {
157 if (err) throw err 152 if (err) throw err
158 expect(test).to.equal(true) 153 expect(test).to.equal(true)
159 154
@@ -163,7 +158,7 @@ describe('Test a single pod', function () {
163 }) 158 })
164 159
165 it('Should search the video by podUrl', function (done) { 160 it('Should search the video by podUrl', function (done) {
166 utils.searchVideo(server.url, '9001', 'podUrl', function (err, res) { 161 videosUtils.searchVideo(server.url, '9001', 'podUrl', function (err, res) {
167 if (err) throw err 162 if (err) throw err
168 163
169 expect(res.body.total).to.equal(1) 164 expect(res.body.total).to.equal(1)
@@ -177,9 +172,9 @@ describe('Test a single pod', function () {
177 expect(video.author).to.equal('root') 172 expect(video.author).to.equal('root')
178 expect(video.isLocal).to.be.true 173 expect(video.isLocal).to.be.true
179 expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ]) 174 expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ])
180 expect(utils.dateIsValid(video.createdDate)).to.be.true 175 expect(miscsUtils.dateIsValid(video.createdDate)).to.be.true
181 176
182 utils.testImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) { 177 videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) {
183 if (err) throw err 178 if (err) throw err
184 expect(test).to.equal(true) 179 expect(test).to.equal(true)
185 180
@@ -189,7 +184,7 @@ describe('Test a single pod', function () {
189 }) 184 })
190 185
191 it('Should search the video by tag', function (done) { 186 it('Should search the video by tag', function (done) {
192 utils.searchVideo(server.url, 'tag1', 'tags', function (err, res) { 187 videosUtils.searchVideo(server.url, 'tag1', 'tags', function (err, res) {
193 if (err) throw err 188 if (err) throw err
194 189
195 expect(res.body.total).to.equal(1) 190 expect(res.body.total).to.equal(1)
@@ -203,9 +198,9 @@ describe('Test a single pod', function () {
203 expect(video.author).to.equal('root') 198 expect(video.author).to.equal('root')
204 expect(video.isLocal).to.be.true 199 expect(video.isLocal).to.be.true
205 expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ]) 200 expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ])
206 expect(utils.dateIsValid(video.createdDate)).to.be.true 201 expect(miscsUtils.dateIsValid(video.createdDate)).to.be.true
207 202
208 utils.testImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) { 203 videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) {
209 if (err) throw err 204 if (err) throw err
210 expect(test).to.equal(true) 205 expect(test).to.equal(true)
211 206
@@ -215,7 +210,7 @@ describe('Test a single pod', function () {
215 }) 210 })
216 211
217 it('Should not find a search by name by default', function (done) { 212 it('Should not find a search by name by default', function (done) {
218 utils.searchVideo(server.url, 'hello', function (err, res) { 213 videosUtils.searchVideo(server.url, 'hello', function (err, res) {
219 if (err) throw err 214 if (err) throw err
220 215
221 expect(res.body.total).to.equal(0) 216 expect(res.body.total).to.equal(0)
@@ -227,7 +222,7 @@ describe('Test a single pod', function () {
227 }) 222 })
228 223
229 it('Should not find a search by author', function (done) { 224 it('Should not find a search by author', function (done) {
230 utils.searchVideo(server.url, 'hello', 'author', function (err, res) { 225 videosUtils.searchVideo(server.url, 'hello', 'author', function (err, res) {
231 if (err) throw err 226 if (err) throw err
232 227
233 expect(res.body.total).to.equal(0) 228 expect(res.body.total).to.equal(0)
@@ -239,7 +234,7 @@ describe('Test a single pod', function () {
239 }) 234 })
240 235
241 it('Should not find a search by tag', function (done) { 236 it('Should not find a search by tag', function (done) {
242 utils.searchVideo(server.url, 'tag', 'tags', function (err, res) { 237 videosUtils.searchVideo(server.url, 'tag', 'tags', function (err, res) {
243 if (err) throw err 238 if (err) throw err
244 239
245 expect(res.body.total).to.equal(0) 240 expect(res.body.total).to.equal(0)
@@ -251,7 +246,7 @@ describe('Test a single pod', function () {
251 }) 246 })
252 247
253 it('Should remove the video', function (done) { 248 it('Should remove the video', function (done) {
254 utils.removeVideo(server.url, server.accessToken, videoId, function (err) { 249 videosUtils.removeVideo(server.url, server.accessToken, videoId, function (err) {
255 if (err) throw err 250 if (err) throw err
256 251
257 fs.readdir(pathUtils.join(__dirname, '../../../test1/uploads/'), function (err, files) { 252 fs.readdir(pathUtils.join(__dirname, '../../../test1/uploads/'), function (err, files) {
@@ -264,7 +259,7 @@ describe('Test a single pod', function () {
264 }) 259 })
265 260
266 it('Should not have videos', function (done) { 261 it('Should not have videos', function (done) {
267 utils.getVideosList(server.url, function (err, res) { 262 videosUtils.getVideosList(server.url, function (err, res) {
268 if (err) throw err 263 if (err) throw err
269 264
270 expect(res.body.total).to.equal(0) 265 expect(res.body.total).to.equal(0)
@@ -286,12 +281,12 @@ describe('Test a single pod', function () {
286 const description = video + ' description' 281 const description = video + ' description'
287 const tags = [ 'tag1', 'tag2', 'tag3' ] 282 const tags = [ 'tag1', 'tag2', 'tag3' ]
288 283
289 utils.uploadVideo(server.url, server.accessToken, name, description, tags, video, callbackEach) 284 videosUtils.uploadVideo(server.url, server.accessToken, name, description, tags, video, callbackEach)
290 }, done) 285 }, done)
291 }) 286 })
292 287
293 it('Should have the correct durations', function (done) { 288 it('Should have the correct durations', function (done) {
294 utils.getVideosList(server.url, function (err, res) { 289 videosUtils.getVideosList(server.url, function (err, res) {
295 if (err) throw err 290 if (err) throw err
296 291
297 expect(res.body.total).to.equal(6) 292 expect(res.body.total).to.equal(6)
@@ -312,7 +307,7 @@ describe('Test a single pod', function () {
312 }) 307 })
313 308
314 it('Should have the correct thumbnails', function (done) { 309 it('Should have the correct thumbnails', function (done) {
315 utils.getVideosList(server.url, function (err, res) { 310 videosUtils.getVideosList(server.url, function (err, res) {
316 if (err) throw err 311 if (err) throw err
317 312
318 const videos = res.body.data 313 const videos = res.body.data
@@ -323,7 +318,7 @@ describe('Test a single pod', function () {
323 if (err) throw err 318 if (err) throw err
324 const videoName = video.name.replace(' name', '') 319 const videoName = video.name.replace(' name', '')
325 320
326 utils.testImage(server.url, videoName, video.thumbnailPath, function (err, test) { 321 videosUtils.testVideoImage(server.url, videoName, video.thumbnailPath, function (err, test) {
327 if (err) throw err 322 if (err) throw err
328 323
329 expect(test).to.equal(true) 324 expect(test).to.equal(true)
@@ -334,7 +329,7 @@ describe('Test a single pod', function () {
334 }) 329 })
335 330
336 it('Should list only the two first videos', function (done) { 331 it('Should list only the two first videos', function (done) {
337 utils.getVideosListPagination(server.url, 0, 2, function (err, res) { 332 videosUtils.getVideosListPagination(server.url, 0, 2, function (err, res) {
338 if (err) throw err 333 if (err) throw err
339 334
340 const videos = res.body.data 335 const videos = res.body.data
@@ -348,7 +343,7 @@ describe('Test a single pod', function () {
348 }) 343 })
349 344
350 it('Should list only the next three videos', function (done) { 345 it('Should list only the next three videos', function (done) {
351 utils.getVideosListPagination(server.url, 2, 3, function (err, res) { 346 videosUtils.getVideosListPagination(server.url, 2, 3, function (err, res) {
352 if (err) throw err 347 if (err) throw err
353 348
354 const videos = res.body.data 349 const videos = res.body.data
@@ -363,7 +358,7 @@ describe('Test a single pod', function () {
363 }) 358 })
364 359
365 it('Should list the last video', function (done) { 360 it('Should list the last video', function (done) {
366 utils.getVideosListPagination(server.url, 5, 6, function (err, res) { 361 videosUtils.getVideosListPagination(server.url, 5, 6, function (err, res) {
367 if (err) throw err 362 if (err) throw err
368 363
369 const videos = res.body.data 364 const videos = res.body.data
@@ -376,7 +371,7 @@ describe('Test a single pod', function () {
376 }) 371 })
377 372
378 it('Should search the first video', function (done) { 373 it('Should search the first video', function (done) {
379 utils.searchVideoWithPagination(server.url, 'webm', 'name', 0, 1, function (err, res) { 374 videosUtils.searchVideoWithPagination(server.url, 'webm', 'name', 0, 1, function (err, res) {
380 if (err) throw err 375 if (err) throw err
381 376
382 const videos = res.body.data 377 const videos = res.body.data
@@ -389,7 +384,7 @@ describe('Test a single pod', function () {
389 }) 384 })
390 385
391 it('Should search the last two videos', function (done) { 386 it('Should search the last two videos', function (done) {
392 utils.searchVideoWithPagination(server.url, 'webm', 'name', 2, 2, function (err, res) { 387 videosUtils.searchVideoWithPagination(server.url, 'webm', 'name', 2, 2, function (err, res) {
393 if (err) throw err 388 if (err) throw err
394 389
395 const videos = res.body.data 390 const videos = res.body.data
@@ -403,7 +398,7 @@ describe('Test a single pod', function () {
403 }) 398 })
404 399
405 it('Should search all the webm videos', function (done) { 400 it('Should search all the webm videos', function (done) {
406 utils.searchVideoWithPagination(server.url, 'webm', 'name', 0, 15, function (err, res) { 401 videosUtils.searchVideoWithPagination(server.url, 'webm', 'name', 0, 15, function (err, res) {
407 if (err) throw err 402 if (err) throw err
408 403
409 const videos = res.body.data 404 const videos = res.body.data
@@ -415,7 +410,7 @@ describe('Test a single pod', function () {
415 }) 410 })
416 411
417 it('Should search all the root author videos', function (done) { 412 it('Should search all the root author videos', function (done) {
418 utils.searchVideoWithPagination(server.url, 'root', 'author', 0, 15, function (err, res) { 413 videosUtils.searchVideoWithPagination(server.url, 'root', 'author', 0, 15, function (err, res) {
419 if (err) throw err 414 if (err) throw err
420 415
421 const videos = res.body.data 416 const videos = res.body.data
@@ -427,7 +422,7 @@ describe('Test a single pod', function () {
427 }) 422 })
428 423
429 it('Should search all the 9001 port videos', function (done) { 424 it('Should search all the 9001 port videos', function (done) {
430 utils.searchVideoWithPagination(server.url, '9001', 'podUrl', 0, 15, function (err, res) { 425 videosUtils.searchVideoWithPagination(server.url, '9001', 'podUrl', 0, 15, function (err, res) {
431 if (err) throw err 426 if (err) throw err
432 427
433 const videos = res.body.data 428 const videos = res.body.data
@@ -439,7 +434,7 @@ describe('Test a single pod', function () {
439 }) 434 })
440 435
441 it('Should search all the localhost videos', function (done) { 436 it('Should search all the localhost videos', function (done) {
442 utils.searchVideoWithPagination(server.url, 'localhost', 'podUrl', 0, 15, function (err, res) { 437 videosUtils.searchVideoWithPagination(server.url, 'localhost', 'podUrl', 0, 15, function (err, res) {
443 if (err) throw err 438 if (err) throw err
444 439
445 const videos = res.body.data 440 const videos = res.body.data
@@ -452,7 +447,7 @@ describe('Test a single pod', function () {
452 447
453 it('Should search the good magnetUri video', function (done) { 448 it('Should search the good magnetUri video', function (done) {
454 const video = videosListBase[0] 449 const video = videosListBase[0]
455 utils.searchVideoWithPagination(server.url, encodeURIComponent(video.magnetUri), 'magnetUri', 0, 15, function (err, res) { 450 videosUtils.searchVideoWithPagination(server.url, encodeURIComponent(video.magnetUri), 'magnetUri', 0, 15, function (err, res) {
456 if (err) throw err 451 if (err) throw err
457 452
458 const videos = res.body.data 453 const videos = res.body.data
@@ -465,7 +460,7 @@ describe('Test a single pod', function () {
465 }) 460 })
466 461
467 it('Should list and sort by name in descending order', function (done) { 462 it('Should list and sort by name in descending order', function (done) {
468 utils.getVideosListSort(server.url, '-name', function (err, res) { 463 videosUtils.getVideosListSort(server.url, '-name', function (err, res) {
469 if (err) throw err 464 if (err) throw err
470 465
471 const videos = res.body.data 466 const videos = res.body.data
@@ -483,7 +478,7 @@ describe('Test a single pod', function () {
483 }) 478 })
484 479
485 it('Should search and sort by name in ascending order', function (done) { 480 it('Should search and sort by name in ascending order', function (done) {
486 utils.searchVideoWithSort(server.url, 'webm', 'name', function (err, res) { 481 videosUtils.searchVideoWithSort(server.url, 'webm', 'name', function (err, res) {
487 if (err) throw err 482 if (err) throw err
488 483
489 const videos = res.body.data 484 const videos = res.body.data
@@ -505,7 +500,7 @@ describe('Test a single pod', function () {
505 500
506 // Keep the logs if the test failed 501 // Keep the logs if the test failed
507 if (this.ok) { 502 if (this.ok) {
508 utils.flushTests(done) 503 serversUtils.flushTests(done)
509 } else { 504 } else {
510 done() 505 done()
511 } 506 }
diff --git a/server/tests/api/users.js b/server/tests/api/users.js
index 68ba9de33..c6c892bf2 100644
--- a/server/tests/api/users.js
+++ b/server/tests/api/users.js
@@ -5,25 +5,30 @@ const expect = chai.expect
5const pathUtils = require('path') 5const pathUtils = require('path')
6const series = require('async/series') 6const series = require('async/series')
7 7
8const loginUtils = require('../utils/login')
9const podsUtils = require('../utils/pods')
10const serversUtils = require('../utils/servers')
11const usersUtils = require('../utils/users')
12const videosUtils = require('../utils/videos')
8const webtorrent = require(pathUtils.join(__dirname, '../../lib/webtorrent')) 13const webtorrent = require(pathUtils.join(__dirname, '../../lib/webtorrent'))
9webtorrent.silent = true 14webtorrent.silent = true
10 15
11const utils = require('./utils')
12
13describe('Test users', function () { 16describe('Test users', function () {
14 let server = null 17 let server = null
15 let accessToken = null 18 let accessToken = null
16 let videoId 19 let accessTokenUser = null
20 let videoId = null
21 let userId = null
17 22
18 before(function (done) { 23 before(function (done) {
19 this.timeout(20000) 24 this.timeout(20000)
20 25
21 series([ 26 series([
22 function (next) { 27 function (next) {
23 utils.flushTests(next) 28 serversUtils.flushTests(next)
24 }, 29 },
25 function (next) { 30 function (next) {
26 utils.runServer(1, function (server1) { 31 serversUtils.runServer(1, function (server1) {
27 server = server1 32 server = server1
28 next() 33 next()
29 }) 34 })
@@ -39,7 +44,7 @@ describe('Test users', function () {
39 44
40 it('Should not login with an invalid client id', function (done) { 45 it('Should not login with an invalid client id', function (done) {
41 const client = { id: 'client', password: server.client.secret } 46 const client = { id: 'client', password: server.client.secret }
42 utils.login(server.url, client, server.user, 400, function (err, res) { 47 loginUtils.login(server.url, client, server.user, 400, function (err, res) {
43 if (err) throw err 48 if (err) throw err
44 49
45 expect(res.body.error).to.equal('invalid_client') 50 expect(res.body.error).to.equal('invalid_client')
@@ -49,7 +54,7 @@ describe('Test users', function () {
49 54
50 it('Should not login with an invalid client password', function (done) { 55 it('Should not login with an invalid client password', function (done) {
51 const client = { id: server.client.id, password: 'coucou' } 56 const client = { id: server.client.id, password: 'coucou' }
52 utils.login(server.url, client, server.user, 400, function (err, res) { 57 loginUtils.login(server.url, client, server.user, 400, function (err, res) {
53 if (err) throw err 58 if (err) throw err
54 59
55 expect(res.body.error).to.equal('invalid_client') 60 expect(res.body.error).to.equal('invalid_client')
@@ -59,7 +64,7 @@ describe('Test users', function () {
59 64
60 it('Should not login with an invalid username', function (done) { 65 it('Should not login with an invalid username', function (done) {
61 const user = { username: 'captain crochet', password: server.user.password } 66 const user = { username: 'captain crochet', password: server.user.password }
62 utils.login(server.url, server.client, user, 400, function (err, res) { 67 loginUtils.login(server.url, server.client, user, 400, function (err, res) {
63 if (err) throw err 68 if (err) throw err
64 69
65 expect(res.body.error).to.equal('invalid_grant') 70 expect(res.body.error).to.equal('invalid_grant')
@@ -69,7 +74,7 @@ describe('Test users', function () {
69 74
70 it('Should not login with an invalid password', function (done) { 75 it('Should not login with an invalid password', function (done) {
71 const user = { username: server.user.username, password: 'mewthree' } 76 const user = { username: server.user.username, password: 'mewthree' }
72 utils.login(server.url, server.client, user, 400, function (err, res) { 77 loginUtils.login(server.url, server.client, user, 400, function (err, res) {
73 if (err) throw err 78 if (err) throw err
74 79
75 expect(res.body.error).to.equal('invalid_grant') 80 expect(res.body.error).to.equal('invalid_grant')
@@ -84,21 +89,21 @@ describe('Test users', function () {
84 const description = 'my super description' 89 const description = 'my super description'
85 const tags = [ 'tag1', 'tag2' ] 90 const tags = [ 'tag1', 'tag2' ]
86 const video = 'video_short.webm' 91 const video = 'video_short.webm'
87 utils.uploadVideo(server.url, accessToken, name, description, tags, video, 401, done) 92 videosUtils.uploadVideo(server.url, accessToken, name, description, tags, video, 401, done)
88 }) 93 })
89 94
90 it('Should not be able to make friends', function (done) { 95 it('Should not be able to make friends', function (done) {
91 accessToken = 'mysupertoken' 96 accessToken = 'mysupertoken'
92 utils.makeFriends(server.url, accessToken, 401, done) 97 podsUtils.makeFriends(server.url, accessToken, 401, done)
93 }) 98 })
94 99
95 it('Should not be able to quit friends', function (done) { 100 it('Should not be able to quit friends', function (done) {
96 accessToken = 'mysupertoken' 101 accessToken = 'mysupertoken'
97 utils.quitFriends(server.url, accessToken, 401, done) 102 podsUtils.quitFriends(server.url, accessToken, 401, done)
98 }) 103 })
99 104
100 it('Should be able to login', function (done) { 105 it('Should be able to login', function (done) {
101 utils.login(server.url, server.client, server.user, 200, function (err, res) { 106 loginUtils.login(server.url, server.client, server.user, 200, function (err, res) {
102 if (err) throw err 107 if (err) throw err
103 108
104 accessToken = res.body.access_token 109 accessToken = res.body.access_token
@@ -111,10 +116,10 @@ describe('Test users', function () {
111 const description = 'my super description' 116 const description = 'my super description'
112 const tags = [ 'tag1', 'tag2' ] 117 const tags = [ 'tag1', 'tag2' ]
113 const video = 'video_short.webm' 118 const video = 'video_short.webm'
114 utils.uploadVideo(server.url, accessToken, name, description, tags, video, 204, function (err, res) { 119 videosUtils.uploadVideo(server.url, accessToken, name, description, tags, video, 204, function (err, res) {
115 if (err) throw err 120 if (err) throw err
116 121
117 utils.getVideosList(server.url, function (err, res) { 122 videosUtils.getVideosList(server.url, function (err, res) {
118 if (err) throw err 123 if (err) throw err
119 124
120 const video = res.body.data[0] 125 const video = res.body.data[0]
@@ -131,17 +136,17 @@ describe('Test users', function () {
131 const description = 'my super description 2' 136 const description = 'my super description 2'
132 const tags = [ 'tag1' ] 137 const tags = [ 'tag1' ]
133 const video = 'video_short.webm' 138 const video = 'video_short.webm'
134 utils.uploadVideo(server.url, accessToken, name, description, tags, video, 204, done) 139 videosUtils.uploadVideo(server.url, accessToken, name, description, tags, video, 204, done)
135 }) 140 })
136 141
137 it('Should not be able to remove the video with an incorrect token', function (done) { 142 it('Should not be able to remove the video with an incorrect token', function (done) {
138 utils.removeVideo(server.url, 'bad_token', videoId, 401, done) 143 videosUtils.removeVideo(server.url, 'bad_token', videoId, 401, done)
139 }) 144 })
140 145
141 it('Should not be able to remove the video with the token of another account') 146 it('Should not be able to remove the video with the token of another account')
142 147
143 it('Should be able to remove the video with the correct token', function (done) { 148 it('Should be able to remove the video with the correct token', function (done) {
144 utils.removeVideo(server.url, accessToken, videoId, done) 149 videosUtils.removeVideo(server.url, accessToken, videoId, done)
145 }) 150 })
146 151
147 it('Should logout (revoke token)') 152 it('Should logout (revoke token)')
@@ -158,12 +163,179 @@ describe('Test users', function () {
158 163
159 it('Should be able to upload a video again') 164 it('Should be able to upload a video again')
160 165
166 it('Should be able to create a new user', function (done) {
167 usersUtils.createUser(server.url, accessToken, 'user_1', 'super password', done)
168 })
169
170 it('Should be able to login with this user', function (done) {
171 server.user = {
172 username: 'user_1',
173 password: 'super password'
174 }
175
176 loginUtils.loginAndGetAccessToken(server, function (err, token) {
177 if (err) throw err
178
179 accessTokenUser = token
180
181 done()
182 })
183 })
184
185 it('Should be able to get the user informations', function (done) {
186 usersUtils.getUserInformation(server.url, accessTokenUser, function (err, res) {
187 if (err) throw err
188
189 const user = res.body
190
191 expect(user.username).to.equal('user_1')
192 expect(user.id).to.exist
193
194 done()
195 })
196 })
197
198 it('Should be able to upload a video with this user', function (done) {
199 this.timeout(5000)
200
201 const name = 'my super name'
202 const description = 'my super description'
203 const tags = [ 'tag1', 'tag2', 'tag3' ]
204 const file = 'video_short.webm'
205 videosUtils.uploadVideo(server.url, accessTokenUser, name, description, tags, file, done)
206 })
207
208 it('Should list all the users', function (done) {
209 usersUtils.getUsersList(server.url, function (err, res) {
210 if (err) throw err
211
212 const result = res.body
213 const total = result.total
214 const users = result.data
215
216 expect(total).to.equal(2)
217 expect(users).to.be.an('array')
218 expect(users.length).to.equal(2)
219
220 const user = users[0]
221 expect(user.username).to.equal('user_1')
222
223 const rootUser = users[1]
224 expect(rootUser.username).to.equal('root')
225 userId = user.id
226
227 done()
228 })
229 })
230
231 it('Should list only the first user by username asc', function (done) {
232 usersUtils.getUsersListPaginationAndSort(server.url, 0, 1, 'username', function (err, res) {
233 if (err) throw err
234
235 const result = res.body
236 const total = result.total
237 const users = result.data
238
239 expect(total).to.equal(2)
240 expect(users.length).to.equal(1)
241
242 const user = users[0]
243 expect(user.username).to.equal('root')
244
245 done()
246 })
247 })
248
249 it('Should list only the first user by username desc', function (done) {
250 usersUtils.getUsersListPaginationAndSort(server.url, 0, 1, '-username', function (err, res) {
251 if (err) throw err
252
253 const result = res.body
254 const total = result.total
255 const users = result.data
256
257 expect(total).to.equal(2)
258 expect(users.length).to.equal(1)
259
260 const user = users[0]
261 expect(user.username).to.equal('user_1')
262
263 done()
264 })
265 })
266
267 it('Should list only the second user by createdDate desc', function (done) {
268 usersUtils.getUsersListPaginationAndSort(server.url, 0, 1, '-createdDate', function (err, res) {
269 if (err) throw err
270
271 const result = res.body
272 const total = result.total
273 const users = result.data
274
275 expect(total).to.equal(2)
276 expect(users.length).to.equal(1)
277
278 const user = users[0]
279 expect(user.username).to.equal('user_1')
280
281 done()
282 })
283 })
284
285 it('Should list all the users by createdDate asc', function (done) {
286 usersUtils.getUsersListPaginationAndSort(server.url, 0, 2, 'createdDate', function (err, res) {
287 if (err) throw err
288
289 const result = res.body
290 const total = result.total
291 const users = result.data
292
293 expect(total).to.equal(2)
294 expect(users.length).to.equal(2)
295
296 expect(users[0].username).to.equal('root')
297 expect(users[1].username).to.equal('user_1')
298
299 done()
300 })
301 })
302
303 it('Should update the user password', function (done) {
304 usersUtils.updateUser(server.url, userId, accessTokenUser, 'new password', function (err, res) {
305 if (err) throw err
306
307 server.user.password = 'new password'
308 loginUtils.login(server.url, server.client, server.user, 200, done)
309 })
310 })
311
312 it('Should be able to remove this user', function (done) {
313 usersUtils.removeUser(server.url, userId, accessToken, done)
314 })
315
316 it('Should not be able to login with this user', function (done) {
317 // server.user is already set to user 1
318 loginUtils.login(server.url, server.client, server.user, 400, done)
319 })
320
321 it('Should not have videos of this user', function (done) {
322 videosUtils.getVideosList(server.url, function (err, res) {
323 if (err) throw err
324
325 expect(res.body.total).to.equal(1)
326 const video = res.body.data[0]
327 expect(video.author).to.equal('root')
328
329 done()
330 })
331 })
332
161 after(function (done) { 333 after(function (done) {
162 process.kill(-server.app.pid) 334 process.kill(-server.app.pid)
163 335
164 // Keep the logs if the test failed 336 // Keep the logs if the test failed
165 if (this.ok) { 337 if (this.ok) {
166 utils.flushTests(done) 338 serversUtils.flushTests(done)
167 } else { 339 } else {
168 done() 340 done()
169 } 341 }
diff --git a/server/tests/api/utils.js b/server/tests/api/utils.js
deleted file mode 100644
index 3cc769f26..000000000
--- a/server/tests/api/utils.js
+++ /dev/null
@@ -1,419 +0,0 @@
1'use strict'
2
3const childProcess = require('child_process')
4const exec = childProcess.exec
5const fork = childProcess.fork
6const fs = require('fs')
7const pathUtils = require('path')
8const request = require('supertest')
9
10const testUtils = {
11 dateIsValid: dateIsValid,
12 flushTests: flushTests,
13 getAllVideosListBy: getAllVideosListBy,
14 getClient: getClient,
15 getFriendsList: getFriendsList,
16 getVideo: getVideo,
17 getVideosList: getVideosList,
18 getVideosListPagination: getVideosListPagination,
19 getVideosListSort: getVideosListSort,
20 login: login,
21 loginAndGetAccessToken: loginAndGetAccessToken,
22 makeFriends: makeFriends,
23 quitFriends: quitFriends,
24 removeVideo: removeVideo,
25 flushAndRunMultipleServers: flushAndRunMultipleServers,
26 runServer: runServer,
27 searchVideo: searchVideo,
28 searchVideoWithPagination: searchVideoWithPagination,
29 searchVideoWithSort: searchVideoWithSort,
30 testImage: testImage,
31 uploadVideo: uploadVideo
32}
33
34// ---------------------- Export functions --------------------
35
36function dateIsValid (dateString) {
37 const dateToCheck = new Date(dateString)
38 const now = new Date()
39
40 // Check if the interval is more than 2 minutes
41 if (now - dateToCheck > 120000) return false
42
43 return true
44}
45
46function flushTests (callback) {
47 exec('npm run clean:server:test', callback)
48}
49
50function getAllVideosListBy (url, end) {
51 const path = '/api/v1/videos'
52
53 request(url)
54 .get(path)
55 .query({ sort: 'createdDate' })
56 .query({ start: 0 })
57 .query({ count: 10000 })
58 .set('Accept', 'application/json')
59 .expect(200)
60 .expect('Content-Type', /json/)
61 .end(end)
62}
63
64function getClient (url, end) {
65 const path = '/api/v1/users/client'
66
67 request(url)
68 .get(path)
69 .set('Accept', 'application/json')
70 .expect(200)
71 .expect('Content-Type', /json/)
72 .end(end)
73}
74
75function getFriendsList (url, end) {
76 const path = '/api/v1/pods/'
77
78 request(url)
79 .get(path)
80 .set('Accept', 'application/json')
81 .expect(200)
82 .expect('Content-Type', /json/)
83 .end(end)
84}
85
86function getVideo (url, id, end) {
87 const path = '/api/v1/videos/' + id
88
89 request(url)
90 .get(path)
91 .set('Accept', 'application/json')
92 .expect(200)
93 .expect('Content-Type', /json/)
94 .end(end)
95}
96
97function getVideosList (url, end) {
98 const path = '/api/v1/videos'
99
100 request(url)
101 .get(path)
102 .query({ sort: 'name' })
103 .set('Accept', 'application/json')
104 .expect(200)
105 .expect('Content-Type', /json/)
106 .end(end)
107}
108
109function getVideosListPagination (url, start, count, end) {
110 const path = '/api/v1/videos'
111
112 request(url)
113 .get(path)
114 .query({ start: start })
115 .query({ count: count })
116 .set('Accept', 'application/json')
117 .expect(200)
118 .expect('Content-Type', /json/)
119 .end(end)
120}
121
122function getVideosListSort (url, sort, end) {
123 const path = '/api/v1/videos'
124
125 request(url)
126 .get(path)
127 .query({ sort: sort })
128 .set('Accept', 'application/json')
129 .expect(200)
130 .expect('Content-Type', /json/)
131 .end(end)
132}
133
134function login (url, client, user, expectedStatus, end) {
135 if (!end) {
136 end = expectedStatus
137 expectedStatus = 200
138 }
139
140 const path = '/api/v1/users/token'
141
142 const body = {
143 client_id: client.id,
144 client_secret: client.secret,
145 username: user.username,
146 password: user.password,
147 response_type: 'code',
148 grant_type: 'password',
149 scope: 'upload'
150 }
151
152 request(url)
153 .post(path)
154 .type('form')
155 .send(body)
156 .expect(expectedStatus)
157 .end(end)
158}
159
160function loginAndGetAccessToken (server, callback) {
161 login(server.url, server.client, server.user, 200, function (err, res) {
162 if (err) return callback(err)
163
164 return callback(null, res.body.access_token)
165 })
166}
167
168function makeFriends (url, accessToken, expectedStatus, callback) {
169 if (!callback) {
170 callback = expectedStatus
171 expectedStatus = 204
172 }
173
174 const path = '/api/v1/pods/makefriends'
175
176 // The first pod make friend with the third
177 request(url)
178 .get(path)
179 .set('Accept', 'application/json')
180 .set('Authorization', 'Bearer ' + accessToken)
181 .expect(expectedStatus)
182 .end(function (err, res) {
183 if (err) throw err
184
185 // Wait for the request between pods
186 setTimeout(callback, 1000)
187 })
188}
189
190function quitFriends (url, accessToken, expectedStatus, callback) {
191 if (!callback) {
192 callback = expectedStatus
193 expectedStatus = 204
194 }
195
196 const path = '/api/v1/pods/quitfriends'
197
198 // The first pod make friend with the third
199 request(url)
200 .get(path)
201 .set('Accept', 'application/json')
202 .set('Authorization', 'Bearer ' + accessToken)
203 .expect(expectedStatus)
204 .end(function (err, res) {
205 if (err) throw err
206
207 // Wait for the request between pods
208 setTimeout(callback, 1000)
209 })
210}
211
212function removeVideo (url, token, id, expectedStatus, end) {
213 if (!end) {
214 end = expectedStatus
215 expectedStatus = 204
216 }
217
218 const path = '/api/v1/videos'
219
220 request(url)
221 .delete(path + '/' + id)
222 .set('Accept', 'application/json')
223 .set('Authorization', 'Bearer ' + token)
224 .expect(expectedStatus)
225 .end(end)
226}
227
228function flushAndRunMultipleServers (totalServers, serversRun) {
229 let apps = []
230 let urls = []
231 let i = 0
232
233 function anotherServerDone (number, app, url) {
234 apps[number - 1] = app
235 urls[number - 1] = url
236 i++
237 if (i === totalServers) {
238 serversRun(apps, urls)
239 }
240 }
241
242 flushTests(function () {
243 for (let j = 1; j <= totalServers; j++) {
244 // For the virtual buffer
245 setTimeout(function () {
246 runServer(j, function (app, url) {
247 anotherServerDone(j, app, url)
248 })
249 }, 1000 * j)
250 }
251 })
252}
253
254function runServer (number, callback) {
255 const server = {
256 app: null,
257 url: `http://localhost:${9000 + number}`,
258 client: {
259 id: null,
260 secret: null
261 },
262 user: {
263 username: null,
264 password: null
265 }
266 }
267
268 // These actions are async so we need to be sure that they have both been done
269 const serverRunString = {
270 'Connected to mongodb': false,
271 'Server listening on port': false
272 }
273
274 const regexps = {
275 client_id: 'Client id: ([a-f0-9]+)',
276 client_secret: 'Client secret: (.+)',
277 user_username: 'Username: (.+)',
278 user_password: 'User password: (.+)'
279 }
280
281 // Share the environment
282 const env = Object.create(process.env)
283 env.NODE_ENV = 'test'
284 env.NODE_APP_INSTANCE = number
285 const options = {
286 silent: true,
287 env: env,
288 detached: true
289 }
290
291 server.app = fork(pathUtils.join(__dirname, '../../../server.js'), [], options)
292 server.app.stdout.on('data', function onStdout (data) {
293 let dontContinue = false
294
295 // Capture things if we want to
296 for (const key of Object.keys(regexps)) {
297 const regexp = regexps[key]
298 const matches = data.toString().match(regexp)
299 if (matches !== null) {
300 if (key === 'client_id') server.client.id = matches[1]
301 else if (key === 'client_secret') server.client.secret = matches[1]
302 else if (key === 'user_username') server.user.username = matches[1]
303 else if (key === 'user_password') server.user.password = matches[1]
304 }
305 }
306
307 // Check if all required sentences are here
308 for (const key of Object.keys(serverRunString)) {
309 if (data.toString().indexOf(key) !== -1) serverRunString[key] = true
310 if (serverRunString[key] === false) dontContinue = true
311 }
312
313 // If no, there is maybe one thing not already initialized (mongodb...)
314 if (dontContinue === true) return
315
316 server.app.stdout.removeListener('data', onStdout)
317 callback(server)
318 })
319}
320
321function searchVideo (url, search, field, end) {
322 if (!end) {
323 end = field
324 field = null
325 }
326
327 const path = '/api/v1/videos'
328 const req = request(url)
329 .get(path + '/search/' + search)
330 .set('Accept', 'application/json')
331
332 if (field) req.query({ field: field })
333 req.expect(200)
334 .expect('Content-Type', /json/)
335 .end(end)
336}
337
338function searchVideoWithPagination (url, search, field, start, count, end) {
339 const path = '/api/v1/videos'
340
341 request(url)
342 .get(path + '/search/' + search)
343 .query({ start: start })
344 .query({ count: count })
345 .query({ field: field })
346 .set('Accept', 'application/json')
347 .expect(200)
348 .expect('Content-Type', /json/)
349 .end(end)
350}
351
352function searchVideoWithSort (url, search, sort, end) {
353 const path = '/api/v1/videos'
354
355 request(url)
356 .get(path + '/search/' + search)
357 .query({ sort: sort })
358 .set('Accept', 'application/json')
359 .expect(200)
360 .expect('Content-Type', /json/)
361 .end(end)
362}
363
364function testImage (url, videoName, imagePath, callback) {
365 // Don't test images if the node env is not set
366 // Because we need a special ffmpeg version for this test
367 if (process.env.NODE_TEST_IMAGE) {
368 request(url)
369 .get(imagePath)
370 .expect(200)
371 .end(function (err, res) {
372 if (err) return callback(err)
373
374 fs.readFile(pathUtils.join(__dirname, 'fixtures', videoName + '.jpg'), function (err, data) {
375 if (err) return callback(err)
376
377 callback(null, data.equals(res.body))
378 })
379 })
380 } else {
381 console.log('Do not test images. Enable it by setting NODE_TEST_IMAGE env variable.')
382 callback(null, true)
383 }
384}
385
386function uploadVideo (url, accessToken, name, description, tags, fixture, specialStatus, end) {
387 if (!end) {
388 end = specialStatus
389 specialStatus = 204
390 }
391
392 const path = '/api/v1/videos'
393
394 const req = request(url)
395 .post(path)
396 .set('Accept', 'application/json')
397 .set('Authorization', 'Bearer ' + accessToken)
398 .field('name', name)
399 .field('description', description)
400
401 for (let i = 0; i < tags.length; i++) {
402 req.field('tags[' + i + ']', tags[i])
403 }
404
405 let filepath = ''
406 if (pathUtils.isAbsolute(fixture)) {
407 filepath = fixture
408 } else {
409 filepath = pathUtils.join(__dirname, 'fixtures', fixture)
410 }
411
412 req.attach('videofile', filepath)
413 .expect(specialStatus)
414 .end(end)
415}
416
417// ---------------------------------------------------------------------------
418
419module.exports = testUtils
diff --git a/server/tests/real-world/real-world.js b/server/tests/real-world/real-world.js
index b28796852..dba1970c5 100644
--- a/server/tests/real-world/real-world.js
+++ b/server/tests/real-world/real-world.js
@@ -1,6 +1,6 @@
1'use strict' 1'use strict'
2 2
3const each = require('each') 3const each = require('async/each')
4const isEqual = require('lodash/isEqual') 4const isEqual = require('lodash/isEqual')
5const program = require('commander') 5const program = require('commander')
6const series = require('async/series') 6const series = require('async/series')
@@ -8,7 +8,10 @@ const series = require('async/series')
8process.env.NODE_ENV = 'test' 8process.env.NODE_ENV = 'test'
9const constants = require('../../initializers/constants') 9const constants = require('../../initializers/constants')
10 10
11const utils = require('../api/utils') 11const loginUtils = require('../utils/login')
12const podsUtils = require('../utils/pods')
13const serversUtils = require('../utils/servers')
14const videosUtils = require('../utils/videos')
12 15
13program 16program
14 .option('-c, --create [weight]', 'Weight for creating videos') 17 .option('-c, --create [weight]', 'Weight for creating videos')
@@ -97,7 +100,7 @@ function runServers (numberOfPods, callback) {
97 series([ 100 series([
98 // Run servers 101 // Run servers
99 function (next) { 102 function (next) {
100 utils.flushAndRunMultipleServers(numberOfPods, function (serversRun) { 103 serversUtils.flushAndRunMultipleServers(numberOfPods, function (serversRun) {
101 servers = serversRun 104 servers = serversRun
102 next() 105 next()
103 }) 106 })
@@ -105,7 +108,7 @@ function runServers (numberOfPods, callback) {
105 // Get the access tokens 108 // Get the access tokens
106 function (next) { 109 function (next) {
107 each(servers, function (server, callbackEach) { 110 each(servers, function (server, callbackEach) {
108 utils.loginAndGetAccessToken(server, function (err, accessToken) { 111 loginUtils.loginAndGetAccessToken(server, function (err, accessToken) {
109 if (err) return callbackEach(err) 112 if (err) return callbackEach(err)
110 113
111 server.accessToken = accessToken 114 server.accessToken = accessToken
@@ -115,26 +118,26 @@ function runServers (numberOfPods, callback) {
115 }, 118 },
116 function (next) { 119 function (next) {
117 const server = servers[1] 120 const server = servers[1]
118 utils.makeFriends(server.url, server.accessToken, next) 121 podsUtils.makeFriends(server.url, server.accessToken, next)
119 }, 122 },
120 function (next) { 123 function (next) {
121 const server = servers[0] 124 const server = servers[0]
122 utils.makeFriends(server.url, server.accessToken, next) 125 podsUtils.makeFriends(server.url, server.accessToken, next)
123 }, 126 },
124 function (next) { 127 function (next) {
125 setTimeout(next, 1000) 128 setTimeout(next, 1000)
126 }, 129 },
127 function (next) { 130 function (next) {
128 const server = servers[3] 131 const server = servers[3]
129 utils.makeFriends(server.url, server.accessToken, next) 132 podsUtils.makeFriends(server.url, server.accessToken, next)
130 }, 133 },
131 function (next) { 134 function (next) {
132 const server = servers[5] 135 const server = servers[5]
133 utils.makeFriends(server.url, server.accessToken, next) 136 podsUtils.makeFriends(server.url, server.accessToken, next)
134 }, 137 },
135 function (next) { 138 function (next) {
136 const server = servers[4] 139 const server = servers[4]
137 utils.makeFriends(server.url, server.accessToken, next) 140 podsUtils.makeFriends(server.url, server.accessToken, next)
138 }, 141 },
139 function (next) { 142 function (next) {
140 setTimeout(next, 1000) 143 setTimeout(next, 1000)
@@ -151,7 +154,7 @@ function exitServers (servers, callback) {
151 if (server.app) process.kill(-server.app.pid) 154 if (server.app) process.kill(-server.app.pid)
152 }) 155 })
153 156
154 if (flushAtExit) utils.flushTests(callback) 157 if (flushAtExit) serversUtils.flushTests(callback)
155} 158}
156 159
157function upload (servers, numServer, callback) { 160function upload (servers, numServer, callback) {
@@ -164,13 +167,13 @@ function upload (servers, numServer, callback) {
164 167
165 console.log('Upload video to server ' + numServer) 168 console.log('Upload video to server ' + numServer)
166 169
167 utils.uploadVideo(servers[numServer].url, servers[numServer].accessToken, name, description, tags, file, callback) 170 videosUtils.uploadVideo(servers[numServer].url, servers[numServer].accessToken, name, description, tags, file, callback)
168} 171}
169 172
170function remove (servers, numServer, callback) { 173function remove (servers, numServer, callback) {
171 if (!callback) callback = function () {} 174 if (!callback) callback = function () {}
172 175
173 utils.getVideosList(servers[numServer].url, function (err, res) { 176 videosUtils.getVideosList(servers[numServer].url, function (err, res) {
174 if (err) throw err 177 if (err) throw err
175 178
176 const videos = res.body.data 179 const videos = res.body.data
@@ -179,14 +182,14 @@ function remove (servers, numServer, callback) {
179 const toRemove = videos[getRandomInt(0, videos.length)].id 182 const toRemove = videos[getRandomInt(0, videos.length)].id
180 183
181 console.log('Removing video from server ' + numServer) 184 console.log('Removing video from server ' + numServer)
182 utils.removeVideo(servers[numServer].url, servers[numServer].accessToken, toRemove, callback) 185 videosUtils.removeVideo(servers[numServer].url, servers[numServer].accessToken, toRemove, callback)
183 }) 186 })
184} 187}
185 188
186function checkIntegrity (servers, callback) { 189function checkIntegrity (servers, callback) {
187 const videos = [] 190 const videos = []
188 each(servers, function (server, callback) { 191 each(servers, function (server, callback) {
189 utils.getAllVideosListBy(server.url, function (err, res) { 192 videosUtils.getAllVideosListBy(server.url, function (err, res) {
190 if (err) throw err 193 if (err) throw err
191 const serverVideos = res.body.data 194 const serverVideos = res.body.data
192 for (const serverVideo of serverVideos) { 195 for (const serverVideo of serverVideos) {
diff --git a/server/tests/utils/clients.js b/server/tests/utils/clients.js
new file mode 100644
index 000000000..e3ded493e
--- /dev/null
+++ b/server/tests/utils/clients.js
@@ -0,0 +1,24 @@
1'use strict'
2
3const request = require('supertest')
4
5const clientsUtils = {
6 getClient: getClient
7}
8
9// ---------------------- Export functions --------------------
10
11function getClient (url, end) {
12 const path = '/api/v1/users/client'
13
14 request(url)
15 .get(path)
16 .set('Accept', 'application/json')
17 .expect(200)
18 .expect('Content-Type', /json/)
19 .end(end)
20}
21
22// ---------------------------------------------------------------------------
23
24module.exports = clientsUtils
diff --git a/server/tests/utils/login.js b/server/tests/utils/login.js
new file mode 100644
index 000000000..465564e14
--- /dev/null
+++ b/server/tests/utils/login.js
@@ -0,0 +1,48 @@
1'use strict'
2
3const request = require('supertest')
4
5const loginUtils = {
6 login,
7 loginAndGetAccessToken
8}
9
10// ---------------------- Export functions --------------------
11
12function login (url, client, user, expectedStatus, end) {
13 if (!end) {
14 end = expectedStatus
15 expectedStatus = 200
16 }
17
18 const path = '/api/v1/users/token'
19
20 const body = {
21 client_id: client.id,
22 client_secret: client.secret,
23 username: user.username,
24 password: user.password,
25 response_type: 'code',
26 grant_type: 'password',
27 scope: 'upload'
28 }
29
30 request(url)
31 .post(path)
32 .type('form')
33 .send(body)
34 .expect(expectedStatus)
35 .end(end)
36}
37
38function loginAndGetAccessToken (server, callback) {
39 login(server.url, server.client, server.user, 200, function (err, res) {
40 if (err) return callback(err)
41
42 return callback(null, res.body.access_token)
43 })
44}
45
46// ---------------------------------------------------------------------------
47
48module.exports = loginUtils
diff --git a/server/tests/utils/miscs.js b/server/tests/utils/miscs.js
new file mode 100644
index 000000000..4ceff65df
--- /dev/null
+++ b/server/tests/utils/miscs.js
@@ -0,0 +1,21 @@
1'use strict'
2
3const miscsUtils = {
4 dateIsValid
5}
6
7// ---------------------- Export functions --------------------
8
9function dateIsValid (dateString) {
10 const dateToCheck = new Date(dateString)
11 const now = new Date()
12
13 // Check if the interval is more than 2 minutes
14 if (now - dateToCheck > 120000) return false
15
16 return true
17}
18
19// ---------------------------------------------------------------------------
20
21module.exports = miscsUtils
diff --git a/server/tests/utils/pods.js b/server/tests/utils/pods.js
new file mode 100644
index 000000000..a8551a49d
--- /dev/null
+++ b/server/tests/utils/pods.js
@@ -0,0 +1,95 @@
1'use strict'
2
3const request = require('supertest')
4
5const podsUtils = {
6 getFriendsList,
7 makeFriends,
8 quitFriends
9}
10
11// ---------------------- Export functions --------------------
12
13function getFriendsList (url, end) {
14 const path = '/api/v1/pods/'
15
16 request(url)
17 .get(path)
18 .set('Accept', 'application/json')
19 .expect(200)
20 .expect('Content-Type', /json/)
21 .end(end)
22}
23
24function makeFriends (url, accessToken, expectedStatus, end) {
25 if (!end) {
26 end = expectedStatus
27 expectedStatus = 204
28 }
29
30 // Which pod makes friends with which pod
31 const friendsMatrix = {
32 'http://localhost:9001': [
33 'http://localhost:9002'
34 ],
35 'http://localhost:9002': [
36 'http://localhost:9003'
37 ],
38 'http://localhost:9003': [
39 'http://localhost:9001'
40 ],
41 'http://localhost:9004': [
42 'http://localhost:9002'
43 ],
44 'http://localhost:9005': [
45 'http://localhost:9001',
46 'http://localhost:9004'
47 ],
48 'http://localhost:9006': [
49 'http://localhost:9001',
50 'http://localhost:9002',
51 'http://localhost:9003'
52 ]
53 }
54 const path = '/api/v1/pods/makefriends'
55
56 // The first pod make friend with the third
57 request(url)
58 .post(path)
59 .set('Accept', 'application/json')
60 .set('Authorization', 'Bearer ' + accessToken)
61 .send({ 'urls': friendsMatrix[url] })
62 .expect(expectedStatus)
63 .end(function (err, res) {
64 if (err) throw err
65
66 // Wait for the request between pods
67 setTimeout(end, 1000)
68 })
69}
70
71function quitFriends (url, accessToken, expectedStatus, end) {
72 if (!end) {
73 end = expectedStatus
74 expectedStatus = 204
75 }
76
77 const path = '/api/v1/pods/quitfriends'
78
79 // The first pod make friend with the third
80 request(url)
81 .get(path)
82 .set('Accept', 'application/json')
83 .set('Authorization', 'Bearer ' + accessToken)
84 .expect(expectedStatus)
85 .end(function (err, res) {
86 if (err) throw err
87
88 // Wait for the request between pods
89 setTimeout(end, 1000)
90 })
91}
92
93// ---------------------------------------------------------------------------
94
95module.exports = podsUtils
diff --git a/server/tests/utils/requests.js b/server/tests/utils/requests.js
new file mode 100644
index 000000000..b1470814d
--- /dev/null
+++ b/server/tests/utils/requests.js
@@ -0,0 +1,68 @@
1'use strict'
2
3const request = require('supertest')
4
5const requestsUtils = {
6 makePostUploadRequest,
7 makePostBodyRequest,
8 makePutBodyRequest
9}
10
11// ---------------------- Export functions --------------------
12
13function makePostUploadRequest (url, path, token, fields, attaches, done, statusCodeExpected) {
14 if (!statusCodeExpected) statusCodeExpected = 400
15
16 const req = request(url)
17 .post(path)
18 .set('Accept', 'application/json')
19
20 if (token) req.set('Authorization', 'Bearer ' + token)
21
22 Object.keys(fields).forEach(function (field) {
23 const value = fields[field]
24
25 if (Array.isArray(value)) {
26 for (let i = 0; i < value.length; i++) {
27 req.field(field + '[' + i + ']', value[i])
28 }
29 } else {
30 req.field(field, value)
31 }
32 })
33
34 Object.keys(attaches).forEach(function (attach) {
35 const value = attaches[attach]
36 req.attach(attach, value)
37 })
38
39 req.expect(statusCodeExpected, done)
40}
41
42function makePostBodyRequest (url, path, token, fields, done, statusCodeExpected) {
43 if (!statusCodeExpected) statusCodeExpected = 400
44
45 const req = request(url)
46 .post(path)
47 .set('Accept', 'application/json')
48
49 if (token) req.set('Authorization', 'Bearer ' + token)
50
51 req.send(fields).expect(statusCodeExpected, done)
52}
53
54function makePutBodyRequest (url, path, token, fields, done, statusCodeExpected) {
55 if (!statusCodeExpected) statusCodeExpected = 400
56
57 const req = request(url)
58 .put(path)
59 .set('Accept', 'application/json')
60
61 if (token) req.set('Authorization', 'Bearer ' + token)
62
63 req.send(fields).expect(statusCodeExpected, done)
64}
65
66// ---------------------------------------------------------------------------
67
68module.exports = requestsUtils
diff --git a/server/tests/utils/servers.js b/server/tests/utils/servers.js
new file mode 100644
index 000000000..d62838bc7
--- /dev/null
+++ b/server/tests/utils/servers.js
@@ -0,0 +1,115 @@
1'use strict'
2
3const childProcess = require('child_process')
4const exec = childProcess.exec
5const fork = childProcess.fork
6const pathUtils = require('path')
7
8const serversUtils = {
9 flushAndRunMultipleServers,
10 flushTests,
11 runServer
12}
13
14// ---------------------- Export functions --------------------
15
16function flushAndRunMultipleServers (totalServers, serversRun) {
17 let apps = []
18 let urls = []
19 let i = 0
20
21 function anotherServerDone (number, app, url) {
22 apps[number - 1] = app
23 urls[number - 1] = url
24 i++
25 if (i === totalServers) {
26 serversRun(apps, urls)
27 }
28 }
29
30 flushTests(function () {
31 for (let j = 1; j <= totalServers; j++) {
32 // For the virtual buffer
33 setTimeout(function () {
34 runServer(j, function (app, url) {
35 anotherServerDone(j, app, url)
36 })
37 }, 1000 * j)
38 }
39 })
40}
41
42function flushTests (callback) {
43 exec('npm run clean:server:test', callback)
44}
45
46function runServer (number, callback) {
47 const server = {
48 app: null,
49 url: `http://localhost:${9000 + number}`,
50 client: {
51 id: null,
52 secret: null
53 },
54 user: {
55 username: null,
56 password: null
57 }
58 }
59
60 // These actions are async so we need to be sure that they have both been done
61 const serverRunString = {
62 'Connected to mongodb': false,
63 'Server listening on port': false
64 }
65
66 const regexps = {
67 client_id: 'Client id: ([a-f0-9]+)',
68 client_secret: 'Client secret: (.+)',
69 user_username: 'Username: (.+)',
70 user_password: 'User password: (.+)'
71 }
72
73 // Share the environment
74 const env = Object.create(process.env)
75 env.NODE_ENV = 'test'
76 env.NODE_APP_INSTANCE = number
77 const options = {
78 silent: true,
79 env: env,
80 detached: true
81 }
82
83 server.app = fork(pathUtils.join(__dirname, '../../../server.js'), [], options)
84 server.app.stdout.on('data', function onStdout (data) {
85 let dontContinue = false
86
87 // Capture things if we want to
88 for (const key of Object.keys(regexps)) {
89 const regexp = regexps[key]
90 const matches = data.toString().match(regexp)
91 if (matches !== null) {
92 if (key === 'client_id') server.client.id = matches[1]
93 else if (key === 'client_secret') server.client.secret = matches[1]
94 else if (key === 'user_username') server.user.username = matches[1]
95 else if (key === 'user_password') server.user.password = matches[1]
96 }
97 }
98
99 // Check if all required sentences are here
100 for (const key of Object.keys(serverRunString)) {
101 if (data.toString().indexOf(key) !== -1) serverRunString[key] = true
102 if (serverRunString[key] === false) dontContinue = true
103 }
104
105 // If no, there is maybe one thing not already initialized (mongodb...)
106 if (dontContinue === true) return
107
108 server.app.stdout.removeListener('data', onStdout)
109 callback(server)
110 })
111}
112
113// ---------------------------------------------------------------------------
114
115module.exports = serversUtils
diff --git a/server/tests/utils/users.js b/server/tests/utils/users.js
new file mode 100644
index 000000000..2bf9c6e3e
--- /dev/null
+++ b/server/tests/utils/users.js
@@ -0,0 +1,100 @@
1'use strict'
2
3const request = require('supertest')
4
5const usersUtils = {
6 createUser,
7 getUserInformation,
8 getUsersList,
9 getUsersListPaginationAndSort,
10 removeUser,
11 updateUser
12}
13
14// ---------------------- Export functions --------------------
15
16function createUser (url, accessToken, username, password, specialStatus, end) {
17 if (!end) {
18 end = specialStatus
19 specialStatus = 204
20 }
21
22 const path = '/api/v1/users'
23
24 request(url)
25 .post(path)
26 .set('Accept', 'application/json')
27 .set('Authorization', 'Bearer ' + accessToken)
28 .send({ username: username, password: password })
29 .expect(specialStatus)
30 .end(end)
31}
32
33function getUserInformation (url, accessToken, end) {
34 const path = '/api/v1/users/me'
35
36 request(url)
37 .get(path)
38 .set('Accept', 'application/json')
39 .set('Authorization', 'Bearer ' + accessToken)
40 .expect(200)
41 .expect('Content-Type', /json/)
42 .end(end)
43}
44
45function getUsersList (url, end) {
46 const path = '/api/v1/users'
47
48 request(url)
49 .get(path)
50 .set('Accept', 'application/json')
51 .expect(200)
52 .expect('Content-Type', /json/)
53 .end(end)
54}
55
56function getUsersListPaginationAndSort (url, start, count, sort, end) {
57 const path = '/api/v1/users'
58
59 request(url)
60 .get(path)
61 .query({ start: start })
62 .query({ count: count })
63 .query({ sort: sort })
64 .set('Accept', 'application/json')
65 .expect(200)
66 .expect('Content-Type', /json/)
67 .end(end)
68}
69
70function removeUser (url, userId, accessToken, expectedStatus, end) {
71 if (!end) {
72 end = expectedStatus
73 expectedStatus = 204
74 }
75
76 const path = '/api/v1/users'
77
78 request(url)
79 .delete(path + '/' + userId)
80 .set('Accept', 'application/json')
81 .set('Authorization', 'Bearer ' + accessToken)
82 .expect(expectedStatus)
83 .end(end)
84}
85
86function updateUser (url, userId, accessToken, newPassword, end) {
87 const path = '/api/v1/users/' + userId
88
89 request(url)
90 .put(path)
91 .set('Accept', 'application/json')
92 .set('Authorization', 'Bearer ' + accessToken)
93 .send({ password: newPassword })
94 .expect(204)
95 .end(end)
96}
97
98// ---------------------------------------------------------------------------
99
100module.exports = usersUtils
diff --git a/server/tests/utils/videos.js b/server/tests/utils/videos.js
new file mode 100644
index 000000000..536093db1
--- /dev/null
+++ b/server/tests/utils/videos.js
@@ -0,0 +1,199 @@
1'use strict'
2
3const fs = require('fs')
4const pathUtils = require('path')
5const request = require('supertest')
6
7const videosUtils = {
8 getAllVideosListBy,
9 getVideo,
10 getVideosList,
11 getVideosListPagination,
12 getVideosListSort,
13 removeVideo,
14 searchVideo,
15 searchVideoWithPagination,
16 searchVideoWithSort,
17 testVideoImage,
18 uploadVideo
19}
20
21// ---------------------- Export functions --------------------
22
23function getAllVideosListBy (url, end) {
24 const path = '/api/v1/videos'
25
26 request(url)
27 .get(path)
28 .query({ sort: 'createdDate' })
29 .query({ start: 0 })
30 .query({ count: 10000 })
31 .set('Accept', 'application/json')
32 .expect(200)
33 .expect('Content-Type', /json/)
34 .end(end)
35}
36
37function getVideo (url, id, end) {
38 const path = '/api/v1/videos/' + id
39
40 request(url)
41 .get(path)
42 .set('Accept', 'application/json')
43 .expect(200)
44 .expect('Content-Type', /json/)
45 .end(end)
46}
47
48function getVideosList (url, end) {
49 const path = '/api/v1/videos'
50
51 request(url)
52 .get(path)
53 .query({ sort: 'name' })
54 .set('Accept', 'application/json')
55 .expect(200)
56 .expect('Content-Type', /json/)
57 .end(end)
58}
59
60function getVideosListPagination (url, start, count, end) {
61 const path = '/api/v1/videos'
62
63 request(url)
64 .get(path)
65 .query({ start: start })
66 .query({ count: count })
67 .set('Accept', 'application/json')
68 .expect(200)
69 .expect('Content-Type', /json/)
70 .end(end)
71}
72
73function getVideosListSort (url, sort, end) {
74 const path = '/api/v1/videos'
75
76 request(url)
77 .get(path)
78 .query({ sort: sort })
79 .set('Accept', 'application/json')
80 .expect(200)
81 .expect('Content-Type', /json/)
82 .end(end)
83}
84
85function removeVideo (url, token, id, expectedStatus, end) {
86 if (!end) {
87 end = expectedStatus
88 expectedStatus = 204
89 }
90
91 const path = '/api/v1/videos'
92
93 request(url)
94 .delete(path + '/' + id)
95 .set('Accept', 'application/json')
96 .set('Authorization', 'Bearer ' + token)
97 .expect(expectedStatus)
98 .end(end)
99}
100
101function searchVideo (url, search, field, end) {
102 if (!end) {
103 end = field
104 field = null
105 }
106
107 const path = '/api/v1/videos'
108 const req = request(url)
109 .get(path + '/search/' + search)
110 .set('Accept', 'application/json')
111
112 if (field) req.query({ field: field })
113 req.expect(200)
114 .expect('Content-Type', /json/)
115 .end(end)
116}
117
118function searchVideoWithPagination (url, search, field, start, count, end) {
119 const path = '/api/v1/videos'
120
121 request(url)
122 .get(path + '/search/' + search)
123 .query({ start: start })
124 .query({ count: count })
125 .query({ field: field })
126 .set('Accept', 'application/json')
127 .expect(200)
128 .expect('Content-Type', /json/)
129 .end(end)
130}
131
132function searchVideoWithSort (url, search, sort, end) {
133 const path = '/api/v1/videos'
134
135 request(url)
136 .get(path + '/search/' + search)
137 .query({ sort: sort })
138 .set('Accept', 'application/json')
139 .expect(200)
140 .expect('Content-Type', /json/)
141 .end(end)
142}
143
144function testVideoImage (url, videoName, imagePath, callback) {
145 // Don't test images if the node env is not set
146 // Because we need a special ffmpeg version for this test
147 if (process.env.NODE_TEST_IMAGE) {
148 request(url)
149 .get(imagePath)
150 .expect(200)
151 .end(function (err, res) {
152 if (err) return callback(err)
153
154 fs.readFile(pathUtils.join(__dirname, '..', 'api', 'fixtures', videoName + '.jpg'), function (err, data) {
155 if (err) return callback(err)
156
157 callback(null, data.equals(res.body))
158 })
159 })
160 } else {
161 console.log('Do not test images. Enable it by setting NODE_TEST_IMAGE env variable.')
162 callback(null, true)
163 }
164}
165
166function uploadVideo (url, accessToken, name, description, tags, fixture, specialStatus, end) {
167 if (!end) {
168 end = specialStatus
169 specialStatus = 204
170 }
171
172 const path = '/api/v1/videos'
173
174 const req = request(url)
175 .post(path)
176 .set('Accept', 'application/json')
177 .set('Authorization', 'Bearer ' + accessToken)
178 .field('name', name)
179 .field('description', description)
180
181 for (let i = 0; i < tags.length; i++) {
182 req.field('tags[' + i + ']', tags[i])
183 }
184
185 let filepath = ''
186 if (pathUtils.isAbsolute(fixture)) {
187 filepath = fixture
188 } else {
189 filepath = pathUtils.join(__dirname, '..', 'api', 'fixtures', fixture)
190 }
191
192 req.attach('videofile', filepath)
193 .expect(specialStatus)
194 .end(end)
195}
196
197// ---------------------------------------------------------------------------
198
199module.exports = videosUtils