Merge branch 'master' into webseed-merged
This commit is contained in:
commit
a6375e6966
|
@ -14,4 +14,11 @@ uploads
|
||||||
thumbnails
|
thumbnails
|
||||||
config/production.yaml
|
config/production.yaml
|
||||||
ffmpeg
|
ffmpeg
|
||||||
|
<<<<<<< HEAD
|
||||||
torrents
|
torrents
|
||||||
|
=======
|
||||||
|
.tags
|
||||||
|
*.sublime-project
|
||||||
|
*.sublime-workspace
|
||||||
|
torrents/
|
||||||
|
>>>>>>> master
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
language: node_js
|
language: node_js
|
||||||
|
|
||||||
node_js:
|
node_js:
|
||||||
- "4.4"
|
- "4.5"
|
||||||
- "6.2"
|
- "6.6"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
- CXX=g++-4.8
|
- CXX=g++-4.8
|
||||||
|
@ -19,8 +19,10 @@ sudo: false
|
||||||
services:
|
services:
|
||||||
- mongodb
|
- mongodb
|
||||||
|
|
||||||
|
before_install: if [[ `npm -v` != 3* ]]; then npm i -g npm@3; fi
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- npm install electron-prebuilt -g
|
- npm install electron -g
|
||||||
- npm run build
|
- npm run build
|
||||||
- wget --no-check-certificate "https://download.cpy.re/ffmpeg/ffmpeg-release-3.0.2-64bit-static.tar.xz"
|
- wget --no-check-certificate "https://download.cpy.re/ffmpeg/ffmpeg-release-3.0.2-64bit-static.tar.xz"
|
||||||
- tar xf ffmpeg-release-3.0.2-64bit-static.tar.xz
|
- tar xf ffmpeg-release-3.0.2-64bit-static.tar.xz
|
||||||
|
|
19
README.md
19
README.md
|
@ -60,6 +60,7 @@ Want to see in action?
|
||||||
|
|
||||||
* You can directly test in your browser with this [demo server](http://peertube.cpy.re). Don't forget to use the latest version of Firefox/Chromium/(Opera?) and check your firewall configuration (for WebRTC)
|
* You can directly test in your browser with this [demo server](http://peertube.cpy.re). Don't forget to use the latest version of Firefox/Chromium/(Opera?) and check your firewall configuration (for WebRTC)
|
||||||
* You can find [a video](https://vimeo.com/164881662 "Yes Vimeo, please don't judge me") to see how the "decentralization feature" looks like
|
* You can find [a video](https://vimeo.com/164881662 "Yes Vimeo, please don't judge me") to see how the "decentralization feature" looks like
|
||||||
|
* Experimental demo servers that share videos (they are in the same network): [peertube2](http://peertube2.cpy.re), [peertube3](http://peertube3.cpy.re). Since I do experiments with them, sometimes they might not work correctly.
|
||||||
|
|
||||||
## Why
|
## Why
|
||||||
|
|
||||||
|
@ -95,10 +96,12 @@ Thanks to [WebTorrent](https://github.com/feross/webtorrent), we can make P2P (t
|
||||||
- [ ] Validate the prototype (test PeerTube in a real world with many pods and videos)
|
- [ ] Validate the prototype (test PeerTube in a real world with many pods and videos)
|
||||||
- [ ] Manage API breaks
|
- [ ] Manage API breaks
|
||||||
- [ ] Add "DDOS" security (check if a pod don't send too many requests for example)
|
- [ ] Add "DDOS" security (check if a pod don't send too many requests for example)
|
||||||
- [ ] Admin panel
|
- [X] Admin panel
|
||||||
- [ ] Stats about the network (how many friends, how many requests per hour...)
|
- [X] Stats
|
||||||
- [ ] Stats about videos
|
- [X] Friends list
|
||||||
- [ ] Manage users (create/remove)
|
- [X] Manage users (create/remove)
|
||||||
|
- [ ] User playlists
|
||||||
|
- [ ] User subscriptions (by tags, author...)
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
@ -111,6 +114,7 @@ Thanks to [WebTorrent](https://github.com/feross/webtorrent), we can make P2P (t
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
* **NodeJS >= 4.2**
|
* **NodeJS >= 4.2**
|
||||||
|
* **npm >= 3.0**
|
||||||
* OpenSSL (cli)
|
* OpenSSL (cli)
|
||||||
* MongoDB
|
* MongoDB
|
||||||
* ffmpeg xvfb-run libgtk2.0-0 libgconf-2-4 libnss3 libasound2 libxtst6 libxss1 libnotify-bin (for electron)
|
* ffmpeg xvfb-run libgtk2.0-0 libgconf-2-4 libnss3 libasound2 libxtst6 libxss1 libnotify-bin (for electron)
|
||||||
|
@ -123,7 +127,8 @@ Thanks to [WebTorrent](https://github.com/feross/webtorrent), we can make P2P (t
|
||||||
|
|
||||||
# apt-get update
|
# apt-get update
|
||||||
# apt-get install ffmpeg mongodb openssl xvfb curl sudo git build-essential libgtk2.0-0 libgconf-2-4 libnss3 libasound2 libxtst6 libxss1 libnotify-bin
|
# apt-get install ffmpeg mongodb openssl xvfb curl sudo git build-essential libgtk2.0-0 libgconf-2-4 libnss3 libasound2 libxtst6 libxss1 libnotify-bin
|
||||||
# npm install -g electron-prebuilt
|
# npm install -g npm@3
|
||||||
|
# npm install -g electron
|
||||||
|
|
||||||
#### Other distribution... (PR welcome)
|
#### Other distribution... (PR welcome)
|
||||||
|
|
||||||
|
@ -160,6 +165,10 @@ Finally, run the server with the `production` `NODE_ENV` variable set.
|
||||||
|
|
||||||
$ NODE_ENV=production npm start
|
$ NODE_ENV=production npm start
|
||||||
|
|
||||||
|
**Nginx template** (reverse proxy): https://github.com/Chocobozzz/PeerTube/tree/master/support/nginx
|
||||||
|
|
||||||
|
**Systemd template**: https://github.com/Chocobozzz/PeerTube/tree/master/support/systemd
|
||||||
|
|
||||||
### Other commands
|
### Other commands
|
||||||
|
|
||||||
To print all available command run:
|
To print all available command run:
|
||||||
|
|
|
@ -8,10 +8,15 @@ function hasProcessFlag (flag) {
|
||||||
return process.argv.join('').indexOf(flag) > -1
|
return process.argv.join('').indexOf(flag) > -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isWebpackDevServer () {
|
||||||
|
return process.argv[1] && !!(/webpack-dev-server$/.exec(process.argv[1]))
|
||||||
|
}
|
||||||
|
|
||||||
function root (args) {
|
function root (args) {
|
||||||
args = Array.prototype.slice.call(arguments, 0)
|
args = Array.prototype.slice.call(arguments, 0)
|
||||||
return path.join.apply(path, [ROOT].concat(args))
|
return path.join.apply(path, [ROOT].concat(args))
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.hasProcessFlag = hasProcessFlag
|
exports.hasProcessFlag = hasProcessFlag
|
||||||
|
exports.isWebpackDevServer = isWebpackDevServer
|
||||||
exports.root = root
|
exports.root = root
|
||||||
|
|
|
@ -5,9 +5,11 @@ const helpers = require('./helpers')
|
||||||
* Webpack Plugins
|
* Webpack Plugins
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var CopyWebpackPlugin = (CopyWebpackPlugin = require('copy-webpack-plugin'), CopyWebpackPlugin.default || CopyWebpackPlugin)
|
const CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||||
const ForkCheckerPlugin = require('awesome-typescript-loader').ForkCheckerPlugin
|
const ForkCheckerPlugin = require('awesome-typescript-loader').ForkCheckerPlugin
|
||||||
|
const AssetsPlugin = require('assets-webpack-plugin')
|
||||||
|
const ContextReplacementPlugin = require('webpack/lib/ContextReplacementPlugin')
|
||||||
const WebpackNotifierPlugin = require('webpack-notifier')
|
const WebpackNotifierPlugin = require('webpack-notifier')
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -15,7 +17,8 @@ const WebpackNotifierPlugin = require('webpack-notifier')
|
||||||
*/
|
*/
|
||||||
const METADATA = {
|
const METADATA = {
|
||||||
title: 'PeerTube',
|
title: 'PeerTube',
|
||||||
baseUrl: '/'
|
baseUrl: '/',
|
||||||
|
isDevServer: helpers.isWebpackDevServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -23,247 +26,241 @@ const METADATA = {
|
||||||
*
|
*
|
||||||
* See: http://webpack.github.io/docs/configuration.html#cli
|
* See: http://webpack.github.io/docs/configuration.html#cli
|
||||||
*/
|
*/
|
||||||
module.exports = {
|
module.exports = function (options) {
|
||||||
/*
|
var isProd = options.env === 'production'
|
||||||
* Static metadata for index.html
|
|
||||||
*
|
|
||||||
* See: (custom attribute)
|
|
||||||
*/
|
|
||||||
metadata: METADATA,
|
|
||||||
|
|
||||||
/*
|
return {
|
||||||
* Cache generated modules and chunks to improve performance for multiple incremental builds.
|
|
||||||
* This is enabled by default in watch mode.
|
|
||||||
* You can pass false to disable it.
|
|
||||||
*
|
|
||||||
* See: http://webpack.github.io/docs/configuration.html#cache
|
|
||||||
*/
|
|
||||||
// cache: false,
|
|
||||||
|
|
||||||
/*
|
|
||||||
* The entry point for the bundle
|
|
||||||
* Our Angular.js app
|
|
||||||
*
|
|
||||||
* See: http://webpack.github.io/docs/configuration.html#entry
|
|
||||||
*/
|
|
||||||
entry: {
|
|
||||||
'polyfills': './src/polyfills.ts',
|
|
||||||
'vendor': './src/vendor.ts',
|
|
||||||
'main': './src/main.ts'
|
|
||||||
},
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Options affecting the resolving of modules.
|
|
||||||
*
|
|
||||||
* See: http://webpack.github.io/docs/configuration.html#resolve
|
|
||||||
*/
|
|
||||||
resolve: {
|
|
||||||
/*
|
/*
|
||||||
* An array of extensions that should be used to resolve modules.
|
* Static metadata for index.html
|
||||||
*
|
*
|
||||||
* See: http://webpack.github.io/docs/configuration.html#resolve-extensions
|
* See: (custom attribute)
|
||||||
*/
|
*/
|
||||||
extensions: [ '', '.ts', '.js', '.scss' ],
|
metadata: METADATA,
|
||||||
|
|
||||||
// Make sure root is src
|
|
||||||
root: helpers.root('src'),
|
|
||||||
|
|
||||||
// remove other default values
|
|
||||||
modulesDirectories: [ 'node_modules' ],
|
|
||||||
|
|
||||||
packageAlias: 'browser'
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
output: {
|
|
||||||
publicPath: '/client/'
|
|
||||||
},
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Options affecting the normal modules.
|
|
||||||
*
|
|
||||||
* See: http://webpack.github.io/docs/configuration.html#module
|
|
||||||
*/
|
|
||||||
module: {
|
|
||||||
/*
|
/*
|
||||||
* An array of applied pre and post loaders.
|
* Cache generated modules and chunks to improve performance for multiple incremental builds.
|
||||||
|
* This is enabled by default in watch mode.
|
||||||
|
* You can pass false to disable it.
|
||||||
*
|
*
|
||||||
* See: http://webpack.github.io/docs/configuration.html#module-preloaders-module-postloaders
|
* See: http://webpack.github.io/docs/configuration.html#cache
|
||||||
*/
|
*/
|
||||||
preLoaders: [
|
// cache: false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The entry point for the bundle
|
||||||
|
* Our Angular.js app
|
||||||
|
*
|
||||||
|
* See: http://webpack.github.io/docs/configuration.html#entry
|
||||||
|
*/
|
||||||
|
entry: {
|
||||||
|
'polyfills': './src/polyfills.ts',
|
||||||
|
'vendor': './src/vendor.ts',
|
||||||
|
'main': './src/main.ts'
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Options affecting the resolving of modules.
|
||||||
|
*
|
||||||
|
* See: http://webpack.github.io/docs/configuration.html#resolve
|
||||||
|
*/
|
||||||
|
resolve: {
|
||||||
|
/*
|
||||||
|
* An array of extensions that should be used to resolve modules.
|
||||||
|
*
|
||||||
|
* See: http://webpack.github.io/docs/configuration.html#resolve-extensions
|
||||||
|
*/
|
||||||
|
extensions: [ '', '.ts', '.js', '.scss' ],
|
||||||
|
|
||||||
|
// Make sure root is src
|
||||||
|
root: helpers.root('src'),
|
||||||
|
|
||||||
|
// remove other default values
|
||||||
|
modulesDirectories: [ 'node_modules' ]
|
||||||
|
},
|
||||||
|
|
||||||
|
output: {
|
||||||
|
publicPath: '/client/'
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Options affecting the normal modules.
|
||||||
|
*
|
||||||
|
* See: http://webpack.github.io/docs/configuration.html#module
|
||||||
|
*/
|
||||||
|
module: {
|
||||||
|
/*
|
||||||
|
* An array of applied pre and post loaders.
|
||||||
|
*
|
||||||
|
* See: http://webpack.github.io/docs/configuration.html#module-preloaders-module-postloaders
|
||||||
|
*/
|
||||||
|
preLoaders: [
|
||||||
|
{
|
||||||
|
test: /\.ts$/,
|
||||||
|
loader: 'string-replace-loader',
|
||||||
|
query: {
|
||||||
|
search: '(System|SystemJS)(.*[\\n\\r]\\s*\\.|\\.)import\\((.+)\\)',
|
||||||
|
replace: '$1.import($3).then(mod => (mod.__esModule && mod.default) ? mod.default : mod)',
|
||||||
|
flags: 'g'
|
||||||
|
},
|
||||||
|
include: [helpers.root('src')]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Tslint loader support for *.ts files
|
* An array of automatically applied loaders.
|
||||||
*
|
*
|
||||||
* See: https://github.com/wbuchwalter/tslint-loader
|
* IMPORTANT: The loaders here are resolved relative to the resource which they are applied to.
|
||||||
|
* This means they are not resolved relative to the configuration file.
|
||||||
|
*
|
||||||
|
* See: http://webpack.github.io/docs/configuration.html#module-loaders
|
||||||
*/
|
*/
|
||||||
// { test: /\.ts$/, loader: 'tslint-loader', exclude: [ helpers.root('node_modules') ] },
|
loaders: [
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Typescript loader support for .ts and Angular 2 async routes via .async.ts
|
||||||
|
*
|
||||||
|
* See: https://github.com/s-panferov/awesome-typescript-loader
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
test: /\.ts$/,
|
||||||
|
loaders: [
|
||||||
|
'@angularclass/hmr-loader?pretty=' + !isProd + '&prod=' + isProd,
|
||||||
|
'awesome-typescript-loader',
|
||||||
|
'angular2-template-loader'
|
||||||
|
],
|
||||||
|
exclude: [/\.(spec|e2e)\.ts$/]
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Json loader support for *.json files.
|
||||||
|
*
|
||||||
|
* See: https://github.com/webpack/json-loader
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
test: /\.json$/,
|
||||||
|
loader: 'json-loader'
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
test: /\.(sass|scss)$/,
|
||||||
|
loaders: ['css-to-string-loader', 'css-loader?sourceMap', 'resolve-url', 'sass-loader?sourceMap']
|
||||||
|
},
|
||||||
|
{ test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'url?limit=10000&minetype=application/font-woff' },
|
||||||
|
{ test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'file' },
|
||||||
|
|
||||||
|
/* Raw loader support for *.html
|
||||||
|
* Returns file content as string
|
||||||
|
*
|
||||||
|
* See: https://github.com/webpack/raw-loader
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
test: /\.html$/,
|
||||||
|
loader: 'raw-loader',
|
||||||
|
exclude: [ helpers.root('src/index.html') ]
|
||||||
|
}
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
sassLoader: {
|
||||||
|
precision: 10
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add additional plugins to the compiler.
|
||||||
|
*
|
||||||
|
* See: http://webpack.github.io/docs/configuration.html#plugins
|
||||||
|
*/
|
||||||
|
plugins: [
|
||||||
|
new AssetsPlugin({
|
||||||
|
path: helpers.root('dist'),
|
||||||
|
filename: 'webpack-assets.json',
|
||||||
|
prettyPrint: true
|
||||||
|
}),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Source map loader support for *.js files
|
* Plugin: ForkCheckerPlugin
|
||||||
* Extracts SourceMaps for source files that as added as sourceMappingURL comment.
|
* Description: Do type checking in a separate process, so webpack don't need to wait.
|
||||||
*
|
*
|
||||||
* See: https://github.com/webpack/source-map-loader
|
* See: https://github.com/s-panferov/awesome-typescript-loader#forkchecker-boolean-defaultfalse
|
||||||
*/
|
*/
|
||||||
{
|
new ForkCheckerPlugin(),
|
||||||
test: /\.js$/,
|
|
||||||
loader: 'source-map-loader',
|
|
||||||
exclude: [
|
|
||||||
// these packages have problems with their sourcemaps
|
|
||||||
helpers.root('node_modules/rxjs'),
|
|
||||||
helpers.root('node_modules/@angular')
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Plugin: CommonsChunkPlugin
|
||||||
|
* Description: Shares common code between the pages.
|
||||||
|
* It identifies common modules and put them into a commons chunk.
|
||||||
|
*
|
||||||
|
* See: https://webpack.github.io/docs/list-of-plugins.html#commonschunkplugin
|
||||||
|
* See: https://github.com/webpack/docs/wiki/optimization#multi-page-app
|
||||||
|
*/
|
||||||
|
new webpack.optimize.CommonsChunkPlugin({
|
||||||
|
name: [ 'polyfills', 'vendor' ].reverse()
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin: ContextReplacementPlugin
|
||||||
|
* Description: Provides context to Angular's use of System.import
|
||||||
|
*
|
||||||
|
* See: https://webpack.github.io/docs/list-of-plugins.html#contextreplacementplugin
|
||||||
|
* See: https://github.com/angular/angular/issues/11580
|
||||||
|
*/
|
||||||
|
new ContextReplacementPlugin(
|
||||||
|
// The (\\|\/) piece accounts for path separators in *nix and Windows
|
||||||
|
/angular(\\|\/)core(\\|\/)(esm(\\|\/)src|src)(\\|\/)linker/,
|
||||||
|
helpers.root('src') // location of your src
|
||||||
|
),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Plugin: CopyWebpackPlugin
|
||||||
|
* Description: Copy files and directories in webpack.
|
||||||
|
*
|
||||||
|
* Copies project static assets.
|
||||||
|
*
|
||||||
|
* See: https://www.npmjs.com/package/copy-webpack-plugin
|
||||||
|
*/
|
||||||
|
new CopyWebpackPlugin([
|
||||||
|
{
|
||||||
|
from: 'src/assets',
|
||||||
|
to: 'assets'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: 'node_modules/webtorrent/webtorrent.min.js',
|
||||||
|
to: 'assets/webtorrent'
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Plugin: HtmlWebpackPlugin
|
||||||
|
* Description: Simplifies creation of HTML files to serve your webpack bundles.
|
||||||
|
* This is especially useful for webpack bundles that include a hash in the filename
|
||||||
|
* which changes every compilation.
|
||||||
|
*
|
||||||
|
* See: https://github.com/ampedandwired/html-webpack-plugin
|
||||||
|
*/
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
template: 'src/index.html',
|
||||||
|
chunksSortMode: 'dependency'
|
||||||
|
}),
|
||||||
|
|
||||||
|
new WebpackNotifierPlugin({ alwaysNotify: true })
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* An array of automatically applied loaders.
|
* Include polyfills or mocks for various node stuff
|
||||||
|
* Description: Node configuration
|
||||||
*
|
*
|
||||||
* IMPORTANT: The loaders here are resolved relative to the resource which they are applied to.
|
* See: https://webpack.github.io/docs/configuration.html#node
|
||||||
* This means they are not resolved relative to the configuration file.
|
|
||||||
*
|
|
||||||
* See: http://webpack.github.io/docs/configuration.html#module-loaders
|
|
||||||
*/
|
*/
|
||||||
loaders: [
|
node: {
|
||||||
|
global: 'window',
|
||||||
/*
|
crypto: 'empty',
|
||||||
* Typescript loader support for .ts and Angular 2 async routes via .async.ts
|
fs: 'empty',
|
||||||
*
|
events: true,
|
||||||
* See: https://github.com/s-panferov/awesome-typescript-loader
|
module: false,
|
||||||
*/
|
clearImmediate: false,
|
||||||
{
|
setImmediate: false
|
||||||
test: /\.ts$/,
|
}
|
||||||
loader: 'awesome-typescript-loader',
|
|
||||||
exclude: [/\.(spec|e2e)\.ts$/]
|
|
||||||
},
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Json loader support for *.json files.
|
|
||||||
*
|
|
||||||
* See: https://github.com/webpack/json-loader
|
|
||||||
*/
|
|
||||||
{
|
|
||||||
test: /\.json$/,
|
|
||||||
loader: 'json-loader'
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
test: /\.scss$/,
|
|
||||||
exclude: /node_modules/,
|
|
||||||
loaders: [ 'raw-loader', 'sass-loader' ]
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
test: /\.(woff2?|ttf|eot|svg)$/,
|
|
||||||
loader: 'url?limit=10000&name=assets/fonts/[hash].[ext]'
|
|
||||||
},
|
|
||||||
|
|
||||||
/* Raw loader support for *.html
|
|
||||||
* Returns file content as string
|
|
||||||
*
|
|
||||||
* See: https://github.com/webpack/raw-loader
|
|
||||||
*/
|
|
||||||
{
|
|
||||||
test: /\.html$/,
|
|
||||||
loader: 'raw-loader',
|
|
||||||
exclude: [ helpers.root('src/index.html') ]
|
|
||||||
}
|
|
||||||
|
|
||||||
]
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
sassLoader: {
|
|
||||||
precision: 10
|
|
||||||
},
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Add additional plugins to the compiler.
|
|
||||||
*
|
|
||||||
* See: http://webpack.github.io/docs/configuration.html#plugins
|
|
||||||
*/
|
|
||||||
plugins: [
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Plugin: ForkCheckerPlugin
|
|
||||||
* Description: Do type checking in a separate process, so webpack don't need to wait.
|
|
||||||
*
|
|
||||||
* See: https://github.com/s-panferov/awesome-typescript-loader#forkchecker-boolean-defaultfalse
|
|
||||||
*/
|
|
||||||
new ForkCheckerPlugin(),
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Plugin: OccurenceOrderPlugin
|
|
||||||
* Description: Varies the distribution of the ids to get the smallest id length
|
|
||||||
* for often used ids.
|
|
||||||
*
|
|
||||||
* See: https://webpack.github.io/docs/list-of-plugins.html#occurrenceorderplugin
|
|
||||||
* See: https://github.com/webpack/docs/wiki/optimization#minimize
|
|
||||||
*/
|
|
||||||
new webpack.optimize.OccurenceOrderPlugin(true),
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Plugin: CommonsChunkPlugin
|
|
||||||
* Description: Shares common code between the pages.
|
|
||||||
* It identifies common modules and put them into a commons chunk.
|
|
||||||
*
|
|
||||||
* See: https://webpack.github.io/docs/list-of-plugins.html#commonschunkplugin
|
|
||||||
* See: https://github.com/webpack/docs/wiki/optimization#multi-page-app
|
|
||||||
*/
|
|
||||||
new webpack.optimize.CommonsChunkPlugin({
|
|
||||||
name: [ 'polyfills', 'vendor' ].reverse()
|
|
||||||
}),
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Plugin: CopyWebpackPlugin
|
|
||||||
* Description: Copy files and directories in webpack.
|
|
||||||
*
|
|
||||||
* Copies project static assets.
|
|
||||||
*
|
|
||||||
* See: https://www.npmjs.com/package/copy-webpack-plugin
|
|
||||||
*/
|
|
||||||
new CopyWebpackPlugin([
|
|
||||||
{
|
|
||||||
from: 'src/assets',
|
|
||||||
to: 'assets'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
from: 'node_modules/webtorrent/webtorrent.min.js',
|
|
||||||
to: 'assets/webtorrent'
|
|
||||||
}
|
|
||||||
]),
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Plugin: HtmlWebpackPlugin
|
|
||||||
* Description: Simplifies creation of HTML files to serve your webpack bundles.
|
|
||||||
* This is especially useful for webpack bundles that include a hash in the filename
|
|
||||||
* which changes every compilation.
|
|
||||||
*
|
|
||||||
* See: https://github.com/ampedandwired/html-webpack-plugin
|
|
||||||
*/
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
template: 'src/index.html',
|
|
||||||
chunksSortMode: 'dependency'
|
|
||||||
}),
|
|
||||||
|
|
||||||
new WebpackNotifierPlugin({ alwaysNotify: true })
|
|
||||||
],
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Include polyfills or mocks for various node stuff
|
|
||||||
* Description: Node configuration
|
|
||||||
*
|
|
||||||
* See: https://webpack.github.io/docs/configuration.html#node
|
|
||||||
*/
|
|
||||||
node: {
|
|
||||||
global: 'window',
|
|
||||||
crypto: 'empty',
|
|
||||||
fs: 'empty',
|
|
||||||
events: true,
|
|
||||||
module: false,
|
|
||||||
clearImmediate: false,
|
|
||||||
setImmediate: false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,15 +6,18 @@ const commonConfig = require('./webpack.common.js') // the settings that are com
|
||||||
* Webpack Plugins
|
* Webpack Plugins
|
||||||
*/
|
*/
|
||||||
const DefinePlugin = require('webpack/lib/DefinePlugin')
|
const DefinePlugin = require('webpack/lib/DefinePlugin')
|
||||||
|
const NamedModulesPlugin = require('webpack/lib/NamedModulesPlugin')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Webpack Constants
|
* Webpack Constants
|
||||||
*/
|
*/
|
||||||
const ENV = process.env.ENV = process.env.NODE_ENV = 'development'
|
const ENV = process.env.ENV = process.env.NODE_ENV = 'development'
|
||||||
|
const HOST = process.env.HOST || 'localhost'
|
||||||
|
const PORT = process.env.PORT || 3000
|
||||||
const HMR = helpers.hasProcessFlag('hot')
|
const HMR = helpers.hasProcessFlag('hot')
|
||||||
const METADATA = webpackMerge(commonConfig.metadata, {
|
const METADATA = webpackMerge(commonConfig({env: ENV}).metadata, {
|
||||||
host: 'localhost',
|
host: HOST,
|
||||||
port: 3000,
|
port: PORT,
|
||||||
ENV: ENV,
|
ENV: ENV,
|
||||||
HMR: HMR
|
HMR: HMR
|
||||||
})
|
})
|
||||||
|
@ -24,119 +27,136 @@ const METADATA = webpackMerge(commonConfig.metadata, {
|
||||||
*
|
*
|
||||||
* See: http://webpack.github.io/docs/configuration.html#cli
|
* See: http://webpack.github.io/docs/configuration.html#cli
|
||||||
*/
|
*/
|
||||||
module.exports = webpackMerge(commonConfig, {
|
module.exports = function (env) {
|
||||||
/**
|
return webpackMerge(commonConfig({env: ENV}), {
|
||||||
* Merged metadata from webpack.common.js for index.html
|
|
||||||
*
|
|
||||||
* See: (custom attribute)
|
|
||||||
*/
|
|
||||||
metadata: METADATA,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Switch loaders to debug mode.
|
|
||||||
*
|
|
||||||
* See: http://webpack.github.io/docs/configuration.html#debug
|
|
||||||
*/
|
|
||||||
debug: true,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Developer tool to enhance debugging
|
|
||||||
*
|
|
||||||
* See: http://webpack.github.io/docs/configuration.html#devtool
|
|
||||||
* See: https://github.com/webpack/docs/wiki/build-performance#sourcemaps
|
|
||||||
*/
|
|
||||||
devtool: 'cheap-module-source-map',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Options affecting the output of the compilation.
|
|
||||||
*
|
|
||||||
* See: http://webpack.github.io/docs/configuration.html#output
|
|
||||||
*/
|
|
||||||
output: {
|
|
||||||
/**
|
/**
|
||||||
* The output directory as absolute path (required).
|
* Merged metadata from webpack.common.js for index.html
|
||||||
*
|
*
|
||||||
* See: http://webpack.github.io/docs/configuration.html#output-path
|
* See: (custom attribute)
|
||||||
*/
|
*/
|
||||||
path: helpers.root('dist'),
|
metadata: METADATA,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Specifies the name of each output file on disk.
|
* Switch loaders to debug mode.
|
||||||
* IMPORTANT: You must not specify an absolute path here!
|
|
||||||
*
|
*
|
||||||
* See: http://webpack.github.io/docs/configuration.html#output-filename
|
* See: http://webpack.github.io/docs/configuration.html#debug
|
||||||
*/
|
*/
|
||||||
filename: '[name].bundle.js',
|
debug: true,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The filename of the SourceMaps for the JavaScript files.
|
* Developer tool to enhance debugging
|
||||||
* They are inside the output.path directory.
|
|
||||||
*
|
*
|
||||||
* See: http://webpack.github.io/docs/configuration.html#output-sourcemapfilename
|
* See: http://webpack.github.io/docs/configuration.html#devtool
|
||||||
|
* See: https://github.com/webpack/docs/wiki/build-performance#sourcemaps
|
||||||
*/
|
*/
|
||||||
sourceMapFilename: '[name].map',
|
devtool: 'cheap-module-source-map',
|
||||||
|
|
||||||
/** The filename of non-entry chunks as relative path
|
|
||||||
* inside the output.path directory.
|
|
||||||
*
|
|
||||||
* See: http://webpack.github.io/docs/configuration.html#output-chunkfilename
|
|
||||||
*/
|
|
||||||
chunkFilename: '[id].chunk.js'
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
externals: {
|
|
||||||
webtorrent: 'WebTorrent'
|
|
||||||
},
|
|
||||||
|
|
||||||
plugins: [
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plugin: DefinePlugin
|
* Options affecting the output of the compilation.
|
||||||
* Description: Define free variables.
|
|
||||||
* Useful for having development builds with debug logging or adding global constants.
|
|
||||||
*
|
*
|
||||||
* Environment helpers
|
* See: http://webpack.github.io/docs/configuration.html#output
|
||||||
*
|
|
||||||
* See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin
|
|
||||||
*/
|
*/
|
||||||
// NOTE: when adding more properties, make sure you include them in custom-typings.d.ts
|
output: {
|
||||||
new DefinePlugin({
|
/**
|
||||||
'ENV': JSON.stringify(METADATA.ENV),
|
* The output directory as absolute path (required).
|
||||||
'HMR': METADATA.HMR,
|
*
|
||||||
'process.env': {
|
* See: http://webpack.github.io/docs/configuration.html#output-path
|
||||||
|
*/
|
||||||
|
path: helpers.root('dist'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the name of each output file on disk.
|
||||||
|
* IMPORTANT: You must not specify an absolute path here!
|
||||||
|
*
|
||||||
|
* See: http://webpack.github.io/docs/configuration.html#output-filename
|
||||||
|
*/
|
||||||
|
filename: '[name].bundle.js',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The filename of the SourceMaps for the JavaScript files.
|
||||||
|
* They are inside the output.path directory.
|
||||||
|
*
|
||||||
|
* See: http://webpack.github.io/docs/configuration.html#output-sourcemapfilename
|
||||||
|
*/
|
||||||
|
sourceMapFilename: '[name].map',
|
||||||
|
|
||||||
|
/** The filename of non-entry chunks as relative path
|
||||||
|
* inside the output.path directory.
|
||||||
|
*
|
||||||
|
* See: http://webpack.github.io/docs/configuration.html#output-chunkfilename
|
||||||
|
*/
|
||||||
|
chunkFilename: '[id].chunk.js',
|
||||||
|
|
||||||
|
library: 'ac_[name]',
|
||||||
|
libraryTarget: 'var'
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
externals: {
|
||||||
|
webtorrent: 'WebTorrent'
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin: DefinePlugin
|
||||||
|
* Description: Define free variables.
|
||||||
|
* Useful for having development builds with debug logging or adding global constants.
|
||||||
|
*
|
||||||
|
* Environment helpers
|
||||||
|
*
|
||||||
|
* See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin
|
||||||
|
*/
|
||||||
|
// NOTE: when adding more properties, make sure you include them in custom-typings.d.ts
|
||||||
|
new DefinePlugin({
|
||||||
'ENV': JSON.stringify(METADATA.ENV),
|
'ENV': JSON.stringify(METADATA.ENV),
|
||||||
'NODE_ENV': JSON.stringify(METADATA.ENV),
|
'HMR': METADATA.HMR,
|
||||||
'HMR': METADATA.HMR
|
'process.env': {
|
||||||
}
|
'ENV': JSON.stringify(METADATA.ENV),
|
||||||
})
|
'NODE_ENV': JSON.stringify(METADATA.ENV),
|
||||||
],
|
'HMR': METADATA.HMR
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
/**
|
new NamedModulesPlugin()
|
||||||
* Static analysis linter for TypeScript advanced options configuration
|
],
|
||||||
* Description: An extensible linter for the TypeScript language.
|
|
||||||
*
|
|
||||||
* See: https://github.com/wbuchwalter/tslint-loader
|
|
||||||
*/
|
|
||||||
tslint: {
|
|
||||||
emitErrors: false,
|
|
||||||
failOnHint: false,
|
|
||||||
resourcePath: 'src'
|
|
||||||
},
|
|
||||||
|
|
||||||
/*
|
/**
|
||||||
* Include polyfills or mocks for various node stuff
|
* Static analysis linter for TypeScript advanced options configuration
|
||||||
* Description: Node configuration
|
* Description: An extensible linter for the TypeScript language.
|
||||||
*
|
*
|
||||||
* See: https://webpack.github.io/docs/configuration.html#node
|
* See: https://github.com/wbuchwalter/tslint-loader
|
||||||
*/
|
*/
|
||||||
node: {
|
tslint: {
|
||||||
global: 'window',
|
emitErrors: false,
|
||||||
crypto: 'empty',
|
failOnHint: false,
|
||||||
process: true,
|
resourcePath: 'src'
|
||||||
module: false,
|
},
|
||||||
clearImmediate: false,
|
|
||||||
setImmediate: false
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
devServer: {
|
||||||
|
port: METADATA.port,
|
||||||
|
host: METADATA.host,
|
||||||
|
historyApiFallback: true,
|
||||||
|
watchOptions: {
|
||||||
|
aggregateTimeout: 300,
|
||||||
|
poll: 1000
|
||||||
|
},
|
||||||
|
outputPath: helpers.root('dist')
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Include polyfills or mocks for various node stuff
|
||||||
|
* Description: Node configuration
|
||||||
|
*
|
||||||
|
* See: https://webpack.github.io/docs/configuration.html#node
|
||||||
|
*/
|
||||||
|
node: {
|
||||||
|
global: 'window',
|
||||||
|
crypto: 'empty',
|
||||||
|
process: true,
|
||||||
|
module: false,
|
||||||
|
clearImmediate: false,
|
||||||
|
setImmediate: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -9,10 +9,12 @@ const commonConfig = require('./webpack.common.js') // the settings that are com
|
||||||
/**
|
/**
|
||||||
* Webpack Plugins
|
* Webpack Plugins
|
||||||
*/
|
*/
|
||||||
|
// const ProvidePlugin = require('webpack/lib/ProvidePlugin')
|
||||||
const DefinePlugin = require('webpack/lib/DefinePlugin')
|
const DefinePlugin = require('webpack/lib/DefinePlugin')
|
||||||
const DedupePlugin = require('webpack/lib/optimize/DedupePlugin')
|
const NormalModuleReplacementPlugin = require('webpack/lib/NormalModuleReplacementPlugin')
|
||||||
|
// const IgnorePlugin = require('webpack/lib/IgnorePlugin')
|
||||||
|
// const DedupePlugin = require('webpack/lib/optimize/DedupePlugin')
|
||||||
const UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin')
|
const UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin')
|
||||||
const CompressionPlugin = require('compression-webpack-plugin')
|
|
||||||
const WebpackMd5Hash = require('webpack-md5-hash')
|
const WebpackMd5Hash = require('webpack-md5-hash')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -21,211 +23,210 @@ const WebpackMd5Hash = require('webpack-md5-hash')
|
||||||
const ENV = process.env.NODE_ENV = process.env.ENV = 'production'
|
const ENV = process.env.NODE_ENV = process.env.ENV = 'production'
|
||||||
const HOST = process.env.HOST || 'localhost'
|
const HOST = process.env.HOST || 'localhost'
|
||||||
const PORT = process.env.PORT || 8080
|
const PORT = process.env.PORT || 8080
|
||||||
const METADATA = webpackMerge(commonConfig.metadata, {
|
const METADATA = webpackMerge(commonConfig({env: ENV}).metadata, {
|
||||||
host: HOST,
|
host: HOST,
|
||||||
port: PORT,
|
port: PORT,
|
||||||
ENV: ENV,
|
ENV: ENV,
|
||||||
HMR: false
|
HMR: false
|
||||||
})
|
})
|
||||||
|
|
||||||
module.exports = webpackMerge(commonConfig, {
|
module.exports = function (env) {
|
||||||
/**
|
return webpackMerge(commonConfig({env: ENV}), {
|
||||||
* Switch loaders to debug mode.
|
|
||||||
*
|
|
||||||
* See: http://webpack.github.io/docs/configuration.html#debug
|
|
||||||
*/
|
|
||||||
debug: false,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Developer tool to enhance debugging
|
|
||||||
*
|
|
||||||
* See: http://webpack.github.io/docs/configuration.html#devtool
|
|
||||||
* See: https://github.com/webpack/docs/wiki/build-performance#sourcemaps
|
|
||||||
*/
|
|
||||||
devtool: 'source-map',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Options affecting the output of the compilation.
|
|
||||||
*
|
|
||||||
* See: http://webpack.github.io/docs/configuration.html#output
|
|
||||||
*/
|
|
||||||
output: {
|
|
||||||
/**
|
/**
|
||||||
* The output directory as absolute path (required).
|
* Switch loaders to debug mode.
|
||||||
*
|
*
|
||||||
* See: http://webpack.github.io/docs/configuration.html#output-path
|
* See: http://webpack.github.io/docs/configuration.html#debug
|
||||||
*/
|
*/
|
||||||
path: helpers.root('dist'),
|
debug: false,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Specifies the name of each output file on disk.
|
* Developer tool to enhance debugging
|
||||||
* IMPORTANT: You must not specify an absolute path here!
|
|
||||||
*
|
*
|
||||||
* See: http://webpack.github.io/docs/configuration.html#output-filename
|
* See: http://webpack.github.io/docs/configuration.html#devtool
|
||||||
|
* See: https://github.com/webpack/docs/wiki/build-performance#sourcemaps
|
||||||
*/
|
*/
|
||||||
filename: '[name].[chunkhash].bundle.js',
|
devtool: 'source-map',
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The filename of the SourceMaps for the JavaScript files.
|
* Options affecting the output of the compilation.
|
||||||
* They are inside the output.path directory.
|
|
||||||
*
|
*
|
||||||
* See: http://webpack.github.io/docs/configuration.html#output-sourcemapfilename
|
* See: http://webpack.github.io/docs/configuration.html#output
|
||||||
*/
|
*/
|
||||||
sourceMapFilename: '[name].[chunkhash].bundle.map',
|
output: {
|
||||||
|
/**
|
||||||
|
* The output directory as absolute path (required).
|
||||||
|
*
|
||||||
|
* See: http://webpack.github.io/docs/configuration.html#output-path
|
||||||
|
*/
|
||||||
|
path: helpers.root('dist'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the name of each output file on disk.
|
||||||
|
* IMPORTANT: You must not specify an absolute path here!
|
||||||
|
*
|
||||||
|
* See: http://webpack.github.io/docs/configuration.html#output-filename
|
||||||
|
*/
|
||||||
|
filename: '[name].[chunkhash].bundle.js',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The filename of the SourceMaps for the JavaScript files.
|
||||||
|
* They are inside the output.path directory.
|
||||||
|
*
|
||||||
|
* See: http://webpack.github.io/docs/configuration.html#output-sourcemapfilename
|
||||||
|
*/
|
||||||
|
sourceMapFilename: '[name].[chunkhash].bundle.map',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The filename of non-entry chunks as relative path
|
||||||
|
* inside the output.path directory.
|
||||||
|
*
|
||||||
|
* See: http://webpack.github.io/docs/configuration.html#output-chunkfilename
|
||||||
|
*/
|
||||||
|
chunkFilename: '[id].[chunkhash].chunk.js'
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
externals: {
|
||||||
|
webtorrent: 'WebTorrent'
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The filename of non-entry chunks as relative path
|
* Add additional plugins to the compiler.
|
||||||
* inside the output.path directory.
|
|
||||||
*
|
*
|
||||||
* See: http://webpack.github.io/docs/configuration.html#output-chunkfilename
|
* See: http://webpack.github.io/docs/configuration.html#plugins
|
||||||
*/
|
*/
|
||||||
chunkFilename: '[id].[chunkhash].chunk.js'
|
plugins: [
|
||||||
|
|
||||||
},
|
/**
|
||||||
|
* Plugin: WebpackMd5Hash
|
||||||
|
* Description: Plugin to replace a standard webpack chunkhash with md5.
|
||||||
|
*
|
||||||
|
* See: https://www.npmjs.com/package/webpack-md5-hash
|
||||||
|
*/
|
||||||
|
new WebpackMd5Hash(),
|
||||||
|
|
||||||
externals: {
|
/**
|
||||||
webtorrent: 'WebTorrent'
|
* Plugin: DedupePlugin
|
||||||
},
|
* Description: Prevents the inclusion of duplicate code into your bundle
|
||||||
|
* and instead applies a copy of the function at runtime.
|
||||||
|
*
|
||||||
|
* See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin
|
||||||
|
* See: https://github.com/webpack/docs/wiki/optimization#deduplication
|
||||||
|
*/
|
||||||
|
// new DedupePlugin(),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add additional plugins to the compiler.
|
* Plugin: DefinePlugin
|
||||||
*
|
* Description: Define free variables.
|
||||||
* See: http://webpack.github.io/docs/configuration.html#plugins
|
* Useful for having development builds with debug logging or adding global constants.
|
||||||
*/
|
*
|
||||||
plugins: [
|
* Environment helpers
|
||||||
|
*
|
||||||
/**
|
* See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin
|
||||||
* Plugin: WebpackMd5Hash
|
*/
|
||||||
* Description: Plugin to replace a standard webpack chunkhash with md5.
|
// NOTE: when adding more properties make sure you include them in custom-typings.d.ts
|
||||||
*
|
new DefinePlugin({
|
||||||
* See: https://www.npmjs.com/package/webpack-md5-hash
|
|
||||||
*/
|
|
||||||
new WebpackMd5Hash(),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Plugin: DedupePlugin
|
|
||||||
* Description: Prevents the inclusion of duplicate code into your bundle
|
|
||||||
* and instead applies a copy of the function at runtime.
|
|
||||||
*
|
|
||||||
* See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin
|
|
||||||
* See: https://github.com/webpack/docs/wiki/optimization#deduplication
|
|
||||||
*/
|
|
||||||
new DedupePlugin(),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Plugin: DefinePlugin
|
|
||||||
* Description: Define free variables.
|
|
||||||
* Useful for having development builds with debug logging or adding global constants.
|
|
||||||
*
|
|
||||||
* Environment helpers
|
|
||||||
*
|
|
||||||
* See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin
|
|
||||||
*/
|
|
||||||
// NOTE: when adding more properties make sure you include them in custom-typings.d.ts
|
|
||||||
new DefinePlugin({
|
|
||||||
'ENV': JSON.stringify(METADATA.ENV),
|
|
||||||
'HMR': METADATA.HMR,
|
|
||||||
'process.env': {
|
|
||||||
'ENV': JSON.stringify(METADATA.ENV),
|
'ENV': JSON.stringify(METADATA.ENV),
|
||||||
'NODE_ENV': JSON.stringify(METADATA.ENV),
|
'HMR': METADATA.HMR,
|
||||||
'HMR': METADATA.HMR
|
'process.env': {
|
||||||
}
|
'ENV': JSON.stringify(METADATA.ENV),
|
||||||
}),
|
'NODE_ENV': JSON.stringify(METADATA.ENV),
|
||||||
|
'HMR': METADATA.HMR
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plugin: UglifyJsPlugin
|
* Plugin: UglifyJsPlugin
|
||||||
* Description: Minimize all JavaScript output of chunks.
|
* Description: Minimize all JavaScript output of chunks.
|
||||||
* Loaders are switched into minimizing mode.
|
* Loaders are switched into minimizing mode.
|
||||||
*
|
*
|
||||||
* See: https://webpack.github.io/docs/list-of-plugins.html#uglifyjsplugin
|
* See: https://webpack.github.io/docs/list-of-plugins.html#uglifyjsplugin
|
||||||
*/
|
*/
|
||||||
// NOTE: To debug prod builds uncomment //debug lines and comment //prod lines
|
// NOTE: To debug prod builds uncomment //debug lines and comment //prod lines
|
||||||
new UglifyJsPlugin({
|
new UglifyJsPlugin({
|
||||||
// beautify: true, //debug
|
// beautify: true, //debug
|
||||||
// mangle: false, //debug
|
// mangle: false, //debug
|
||||||
// dead_code: false, //debug
|
// dead_code: false, //debug
|
||||||
// unused: false, //debug
|
// unused: false, //debug
|
||||||
// deadCode: false, //debug
|
// deadCode: false, //debug
|
||||||
// compress: {
|
// compress: {
|
||||||
// screw_ie8: true,
|
// screw_ie8: true,
|
||||||
// keep_fnames: true,
|
// keep_fnames: true,
|
||||||
// drop_debugger: false,
|
// drop_debugger: false,
|
||||||
// dead_code: false,
|
// dead_code: false,
|
||||||
// unused: false
|
// unused: false
|
||||||
// }, // debug
|
// }, // debug
|
||||||
// comments: true, //debug
|
// comments: true, //debug
|
||||||
|
|
||||||
beautify: false, // prod
|
beautify: false, // prod
|
||||||
|
mangle: { screw_ie8: true, keep_fnames: true }, // prod
|
||||||
|
compress: { screw_ie8: true }, // prod
|
||||||
|
comments: false // prod
|
||||||
|
}),
|
||||||
|
|
||||||
mangle: {
|
new NormalModuleReplacementPlugin(
|
||||||
screw_ie8: true,
|
/angular2-hmr/,
|
||||||
keep_fnames: true
|
helpers.root('config/modules/angular2-hmr-prod.js')
|
||||||
}, // prod
|
)
|
||||||
|
|
||||||
compress: {
|
/**
|
||||||
screw_ie8: true
|
* Plugin: CompressionPlugin
|
||||||
}, // prod
|
* Description: Prepares compressed versions of assets to serve
|
||||||
|
* them with Content-Encoding
|
||||||
|
*
|
||||||
|
* See: https://github.com/webpack/compression-webpack-plugin
|
||||||
|
*/
|
||||||
|
// new CompressionPlugin({
|
||||||
|
// regExp: /\.css$|\.html$|\.js$|\.map$/,
|
||||||
|
// threshold: 2 * 1024
|
||||||
|
// })
|
||||||
|
|
||||||
comments: false // prod
|
|
||||||
}),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Plugin: CompressionPlugin
|
|
||||||
* Description: Prepares compressed versions of assets to serve
|
|
||||||
* them with Content-Encoding
|
|
||||||
*
|
|
||||||
* See: https://github.com/webpack/compression-webpack-plugin
|
|
||||||
*/
|
|
||||||
new CompressionPlugin({
|
|
||||||
regExp: /\.css$|\.html$|\.js$|\.map$/,
|
|
||||||
threshold: 2 * 1024
|
|
||||||
})
|
|
||||||
|
|
||||||
],
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Static analysis linter for TypeScript advanced options configuration
|
|
||||||
* Description: An extensible linter for the TypeScript language.
|
|
||||||
*
|
|
||||||
* See: https://github.com/wbuchwalter/tslint-loader
|
|
||||||
*/
|
|
||||||
tslint: {
|
|
||||||
emitErrors: true,
|
|
||||||
failOnHint: true,
|
|
||||||
resourcePath: 'src'
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Html loader advanced options
|
|
||||||
*
|
|
||||||
* See: https://github.com/webpack/html-loader#advanced-options
|
|
||||||
*/
|
|
||||||
// TODO: Need to workaround Angular 2's html syntax => #id [bind] (event) *ngFor
|
|
||||||
htmlLoader: {
|
|
||||||
minimize: true,
|
|
||||||
removeAttributeQuotes: false,
|
|
||||||
caseSensitive: true,
|
|
||||||
customAttrSurround: [
|
|
||||||
[/#/, /(?:)/],
|
|
||||||
[/\*/, /(?:)/],
|
|
||||||
[/\[?\(?/, /(?:)/]
|
|
||||||
],
|
],
|
||||||
customAttrAssign: [/\)?\]?=/]
|
|
||||||
},
|
|
||||||
|
|
||||||
/*
|
/**
|
||||||
* Include polyfills or mocks for various node stuff
|
* Static analysis linter for TypeScript advanced options configuration
|
||||||
* Description: Node configuration
|
* Description: An extensible linter for the TypeScript language.
|
||||||
*
|
*
|
||||||
* See: https://webpack.github.io/docs/configuration.html#node
|
* See: https://github.com/wbuchwalter/tslint-loader
|
||||||
*/
|
*/
|
||||||
node: {
|
tslint: {
|
||||||
global: 'window',
|
emitErrors: true,
|
||||||
crypto: 'empty',
|
failOnHint: true,
|
||||||
process: false,
|
resourcePath: 'src'
|
||||||
module: false,
|
},
|
||||||
clearImmediate: false,
|
|
||||||
setImmediate: false
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
/**
|
||||||
|
* Html loader advanced options
|
||||||
|
*
|
||||||
|
* See: https://github.com/webpack/html-loader#advanced-options
|
||||||
|
*/
|
||||||
|
// TODO: Need to workaround Angular 2's html syntax => #id [bind] (event) *ngFor
|
||||||
|
htmlLoader: {
|
||||||
|
minimize: true,
|
||||||
|
removeAttributeQuotes: false,
|
||||||
|
caseSensitive: true,
|
||||||
|
customAttrSurround: [
|
||||||
|
[/#/, /(?:)/],
|
||||||
|
[/\*/, /(?:)/],
|
||||||
|
[/\[?\(?/, /(?:)/]
|
||||||
|
],
|
||||||
|
customAttrAssign: [/\)?\]?=/]
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Include polyfills or mocks for various node stuff
|
||||||
|
* Description: Node configuration
|
||||||
|
*
|
||||||
|
* See: https://webpack.github.io/docs/configuration.html#node
|
||||||
|
*/
|
||||||
|
node: {
|
||||||
|
global: 'window',
|
||||||
|
crypto: 'empty',
|
||||||
|
process: false,
|
||||||
|
module: false,
|
||||||
|
clearImmediate: false,
|
||||||
|
setImmediate: false
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -13,61 +13,72 @@
|
||||||
"url": "git://github.com/Chocobozzz/PeerTube.git"
|
"url": "git://github.com/Chocobozzz/PeerTube.git"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "typings install",
|
|
||||||
"test": "standard && tslint -c ./tslint.json src/**/*.ts",
|
"test": "standard && tslint -c ./tslint.json src/**/*.ts",
|
||||||
"webpack": "webpack"
|
"webpack": "webpack"
|
||||||
},
|
},
|
||||||
"license": "GPLv3",
|
"license": "GPLv3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/common": "2.0.0-rc.4",
|
"@angular/common": "^2.0.0",
|
||||||
"@angular/compiler": "2.0.0-rc.4",
|
"@angular/compiler": "^2.0.0",
|
||||||
"@angular/core": "2.0.0-rc.4",
|
"@angular/core": "^2.0.0",
|
||||||
"@angular/http": "2.0.0-rc.4",
|
"@angular/forms": "^2.0.0",
|
||||||
"@angular/platform-browser": "2.0.0-rc.4",
|
"@angular/http": "^2.0.0",
|
||||||
"@angular/platform-browser-dynamic": "2.0.0-rc.4",
|
"@angular/platform-browser": "^2.0.0",
|
||||||
"@angular/router": "3.0.0-beta.2",
|
"@angular/platform-browser-dynamic": "^2.0.0",
|
||||||
"angular-pipes": "^2.0.0",
|
"@angular/router": "^3.0.0",
|
||||||
"awesome-typescript-loader": "^0.17.0",
|
"@angularclass/hmr": "^1.2.0",
|
||||||
"bootstrap-loader": "^1.0.8",
|
"@angularclass/hmr-loader": "^3.0.2",
|
||||||
|
"@types/core-js": "^0.9.28",
|
||||||
|
"@types/node": "^6.0.38",
|
||||||
|
"@types/source-map": "^0.1.26",
|
||||||
|
"@types/uglify-js": "^2.0.27",
|
||||||
|
"@types/webpack": "^1.12.29",
|
||||||
|
"angular-pipes": "^3.0.0",
|
||||||
|
"angular2-template-loader": "^0.5.0",
|
||||||
|
"assets-webpack-plugin": "^3.4.0",
|
||||||
|
"awesome-typescript-loader": "^2.2.1",
|
||||||
|
"bootstrap-loader": "^2.0.0-beta.11",
|
||||||
"bootstrap-sass": "^3.3.6",
|
"bootstrap-sass": "^3.3.6",
|
||||||
"compression-webpack-plugin": "^0.3.1",
|
"compression-webpack-plugin": "^0.3.1",
|
||||||
"copy-webpack-plugin": "^3.0.1",
|
"copy-webpack-plugin": "^3.0.1",
|
||||||
"core-js": "^2.4.0",
|
"core-js": "^2.4.1",
|
||||||
"css-loader": "^0.23.1",
|
"css-loader": "^0.25.0",
|
||||||
|
"css-to-string-loader": "https://github.com/Chocobozzz/css-to-string-loader#patch-1",
|
||||||
"es6-promise": "^3.0.2",
|
"es6-promise": "^3.0.2",
|
||||||
"es6-promise-loader": "^1.0.1",
|
"es6-promise-loader": "^1.0.1",
|
||||||
"es6-shim": "^0.35.0",
|
"es6-shim": "^0.35.0",
|
||||||
"file-loader": "^0.8.5",
|
"extract-text-webpack-plugin": "^2.0.0-beta.4",
|
||||||
|
"file-loader": "^0.9.0",
|
||||||
"html-webpack-plugin": "^2.19.0",
|
"html-webpack-plugin": "^2.19.0",
|
||||||
"ie-shim": "^0.1.0",
|
"ie-shim": "^0.1.0",
|
||||||
"intl": "^1.2.4",
|
"intl": "^1.2.4",
|
||||||
"json-loader": "^0.5.4",
|
"json-loader": "^0.5.4",
|
||||||
"ng2-bootstrap": "1.0.16",
|
"ng2-bootstrap": "^1.1.5",
|
||||||
"ng2-file-upload": "^1.0.3",
|
"ng2-file-upload": "^1.0.3",
|
||||||
"node-sass": "^3.7.0",
|
"node-sass": "^3.10.0",
|
||||||
"normalize.css": "^4.1.1",
|
"normalize.css": "^4.1.1",
|
||||||
"raw-loader": "^0.5.1",
|
"raw-loader": "^0.5.1",
|
||||||
"reflect-metadata": "0.1.3",
|
"reflect-metadata": "0.1.3",
|
||||||
"resolve-url-loader": "^1.4.3",
|
"resolve-url-loader": "^1.6.0",
|
||||||
"rxjs": "5.0.0-beta.6",
|
"rxjs": "5.0.0-beta.12",
|
||||||
"sass-loader": "^3.2.0",
|
"sass-loader": "^4.0.2",
|
||||||
"source-map-loader": "^0.1.5",
|
"source-map-loader": "^0.1.5",
|
||||||
|
"string-replace-loader": "^1.0.3",
|
||||||
"style-loader": "^0.13.1",
|
"style-loader": "^0.13.1",
|
||||||
"ts-helpers": "^1.1.1",
|
"ts-helpers": "^1.1.1",
|
||||||
"tslint": "^3.7.4",
|
"tslint": "3.15.1",
|
||||||
"tslint-loader": "^2.1.4",
|
"tslint-loader": "^2.1.4",
|
||||||
"typescript": "^1.8.10",
|
"typescript": "^2.0.0",
|
||||||
"typings": "^1.0.4",
|
|
||||||
"url-loader": "^0.5.7",
|
"url-loader": "^0.5.7",
|
||||||
"webpack": "^1.13.1",
|
"webpack": "2.1.0-beta.22",
|
||||||
"webpack-md5-hash": "0.0.5",
|
"webpack-md5-hash": "0.0.5",
|
||||||
"webpack-merge": "^0.13.0",
|
"webpack-merge": "^0.14.1",
|
||||||
"webpack-notifier": "^1.3.0",
|
"webpack-notifier": "^1.3.0",
|
||||||
"webtorrent": "^0.95.2",
|
"webtorrent": "^0.96.0",
|
||||||
"zone.js": "0.6.12"
|
"zone.js": "0.6.23"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"codelyzer": "0.0.19",
|
"codelyzer": "0.0.28",
|
||||||
"standard": "^7.0.1"
|
"standard": "^8.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
<h3>Account</h3>
|
||||||
|
|
||||||
|
<div *ngIf="information" class="alert alert-success">{{ information }}</div>
|
||||||
|
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
|
||||||
|
|
||||||
|
<form role="form" (ngSubmit)="changePassword()" [formGroup]="form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="new-password">New password</label>
|
||||||
|
<input
|
||||||
|
type="password" class="form-control" id="new-password"
|
||||||
|
formControlName="new-password"
|
||||||
|
>
|
||||||
|
<div *ngIf="formErrors['new-password']" class="alert alert-danger">
|
||||||
|
{{ formErrors['new-password'] }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name">Confirm new password</label>
|
||||||
|
<input
|
||||||
|
type="password" class="form-control" id="new-confirmed-password"
|
||||||
|
formControlName="new-confirmed-password"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="submit" value="Change password" class="btn btn-default" [disabled]="!form.valid">
|
||||||
|
</form>
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { } from '@angular/common';
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
import { AccountService } from './account.service';
|
||||||
|
import { FormReactive, USER_PASSWORD } from '../shared';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-account',
|
||||||
|
templateUrl: './account.component.html'
|
||||||
|
})
|
||||||
|
|
||||||
|
export class AccountComponent extends FormReactive implements OnInit {
|
||||||
|
information: string = null;
|
||||||
|
error: string = null;
|
||||||
|
|
||||||
|
form: FormGroup;
|
||||||
|
formErrors = {
|
||||||
|
'new-password': '',
|
||||||
|
'new-confirmed-password': ''
|
||||||
|
};
|
||||||
|
validationMessages = {
|
||||||
|
'new-password': USER_PASSWORD.MESSAGES,
|
||||||
|
'new-confirmed-password': USER_PASSWORD.MESSAGES
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private accountService: AccountService,
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private router: Router
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
buildForm() {
|
||||||
|
this.form = this.formBuilder.group({
|
||||||
|
'new-password': [ '', USER_PASSWORD.VALIDATORS ],
|
||||||
|
'new-confirmed-password': [ '', USER_PASSWORD.VALIDATORS ],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.form.valueChanges.subscribe(data => this.onValueChanged(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.buildForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
changePassword() {
|
||||||
|
const newPassword = this.form.value['new-password'];
|
||||||
|
const newConfirmedPassword = this.form.value['new-confirmed-password'];
|
||||||
|
|
||||||
|
this.information = null;
|
||||||
|
this.error = null;
|
||||||
|
|
||||||
|
if (newPassword !== newConfirmedPassword) {
|
||||||
|
this.error = 'The new password and the confirmed password do not correspond.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.accountService.changePassword(newPassword).subscribe(
|
||||||
|
ok => this.information = 'Password updated.',
|
||||||
|
|
||||||
|
err => this.error = err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { AccountComponent } from './account.component';
|
||||||
|
|
||||||
|
export const AccountRoutes = [
|
||||||
|
{ path: 'account', component: AccountComponent }
|
||||||
|
];
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { AuthHttp, AuthService, RestExtractor } from '../shared';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AccountService {
|
||||||
|
private static BASE_USERS_URL = '/api/v1/users/';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private authHttp: AuthHttp,
|
||||||
|
private authService: AuthService,
|
||||||
|
private restExtractor: RestExtractor
|
||||||
|
) {}
|
||||||
|
|
||||||
|
changePassword(newPassword: string) {
|
||||||
|
const url = AccountService.BASE_USERS_URL + this.authService.getUser().id;
|
||||||
|
const body = {
|
||||||
|
password: newPassword
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.authHttp.put(url, body)
|
||||||
|
.map(this.restExtractor.extractDataBool)
|
||||||
|
.catch((res) => this.restExtractor.handleError(res));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './account.component';
|
||||||
|
export * from './account.routes';
|
||||||
|
export * from './account.service';
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: '<router-outlet></router-outlet>'
|
||||||
|
})
|
||||||
|
|
||||||
|
export class AdminComponent {
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { Routes } from '@angular/router';
|
||||||
|
|
||||||
|
import { AdminComponent } from './admin.component';
|
||||||
|
import { FriendsRoutes } from './friends';
|
||||||
|
import { RequestsRoutes } from './requests';
|
||||||
|
import { UsersRoutes } from './users';
|
||||||
|
|
||||||
|
export const AdminRoutes: Routes = [
|
||||||
|
{
|
||||||
|
path: 'admin',
|
||||||
|
component: AdminComponent,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
redirectTo: 'users',
|
||||||
|
pathMatch: 'full'
|
||||||
|
},
|
||||||
|
...FriendsRoutes,
|
||||||
|
...RequestsRoutes,
|
||||||
|
...UsersRoutes
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
|
@ -0,0 +1,26 @@
|
||||||
|
<h3>Make friends</h3>
|
||||||
|
|
||||||
|
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
|
||||||
|
|
||||||
|
<form (ngSubmit)="makeFriends()" [formGroup]="form">
|
||||||
|
<div class="form-group" *ngFor="let url of urls; let id = index; trackBy:customTrackBy">
|
||||||
|
<label for="username">Url</label>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
type="text" class="form-control" placeholder="http://domain.com"
|
||||||
|
[id]="'url-' + id" [formControlName]="'url-' + id"
|
||||||
|
/>
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button *ngIf="displayAddField(id)" (click)="addField()" class="btn btn-default" type="button">+</button>
|
||||||
|
<button *ngIf="displayRemoveField(id)" (click)="removeField(id)" class="btn btn-default" type="button">-</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div [hidden]="form.controls['url-' + id].valid || form.controls['url-' + id].pristine" class="alert alert-warning">
|
||||||
|
It should be a valid url.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="submit" value="Make friends" class="btn btn-default" [disabled]="!isFormValid()">
|
||||||
|
</form>
|
|
@ -0,0 +1,7 @@
|
||||||
|
table {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group-btn button {
|
||||||
|
width: 35px;
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { FormControl, FormGroup } from '@angular/forms';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
import { validateUrl } from '../../../shared';
|
||||||
|
import { FriendService } from '../shared';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-friend-add',
|
||||||
|
templateUrl: './friend-add.component.html',
|
||||||
|
styleUrls: [ './friend-add.component.scss' ]
|
||||||
|
})
|
||||||
|
export class FriendAddComponent implements OnInit {
|
||||||
|
form: FormGroup;
|
||||||
|
urls = [ ];
|
||||||
|
error: string = null;
|
||||||
|
|
||||||
|
constructor(private router: Router, private friendService: FriendService) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.form = new FormGroup({});
|
||||||
|
this.addField();
|
||||||
|
}
|
||||||
|
|
||||||
|
addField() {
|
||||||
|
this.form.addControl(`url-${this.urls.length}`, new FormControl('', [ validateUrl ]));
|
||||||
|
this.urls.push('');
|
||||||
|
}
|
||||||
|
|
||||||
|
customTrackBy(index: number, obj: any): any {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
displayAddField(index: number) {
|
||||||
|
return index === (this.urls.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
displayRemoveField(index: number) {
|
||||||
|
return (index !== 0 || this.urls.length > 1) && index !== (this.urls.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
isFormValid() {
|
||||||
|
// Do not check the last input
|
||||||
|
for (let i = 0; i < this.urls.length - 1; i++) {
|
||||||
|
if (!this.form.controls[`url-${i}`].valid) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastIndex = this.urls.length - 1;
|
||||||
|
// If the last input (which is not the first) is empty, it's ok
|
||||||
|
if (this.urls[lastIndex] === '' && lastIndex !== 0) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return this.form.controls[`url-${lastIndex}`].valid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeField(index: number) {
|
||||||
|
// Remove the last control
|
||||||
|
this.form.removeControl(`url-${this.urls.length - 1}`);
|
||||||
|
this.urls.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
makeFriends() {
|
||||||
|
this.error = '';
|
||||||
|
|
||||||
|
const notEmptyUrls = this.getNotEmptyUrls();
|
||||||
|
if (notEmptyUrls.length === 0) {
|
||||||
|
this.error = 'You need to specify at less 1 url.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isUrlsUnique(notEmptyUrls)) {
|
||||||
|
this.error = 'Urls need to be unique.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmMessage = 'Are you sure to make friends with:\n - ' + notEmptyUrls.join('\n - ');
|
||||||
|
if (!confirm(confirmMessage)) return;
|
||||||
|
|
||||||
|
this.friendService.makeFriends(notEmptyUrls).subscribe(
|
||||||
|
status => {
|
||||||
|
// TODO: extractdatastatus
|
||||||
|
// if (status === 409) {
|
||||||
|
// alert('Already made friends!');
|
||||||
|
// } else {
|
||||||
|
alert('Make friends request sent!');
|
||||||
|
this.router.navigate([ '/admin/friends/list' ]);
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
error => alert(error.text)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getNotEmptyUrls() {
|
||||||
|
const notEmptyUrls = [];
|
||||||
|
|
||||||
|
Object.keys(this.form.value).forEach((urlKey) => {
|
||||||
|
const url = this.form.value[urlKey];
|
||||||
|
if (url !== '') notEmptyUrls.push(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
return notEmptyUrls;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isUrlsUnique(urls: string[]) {
|
||||||
|
return urls.every(url => urls.indexOf(url) === urls.lastIndexOf(url));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './friend-add.component';
|
|
@ -0,0 +1,29 @@
|
||||||
|
<h3>Friends list</h3>
|
||||||
|
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="table-column-id">ID</th>
|
||||||
|
<th>Url</th>
|
||||||
|
<th>Score</th>
|
||||||
|
<th>Created Date</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let friend of friends">
|
||||||
|
<td>{{ friend.id }}</td>
|
||||||
|
<td>{{ friend.url }}</td>
|
||||||
|
<td>{{ friend.score }}</td>
|
||||||
|
<td>{{ friend.createdDate | date: 'medium' }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<a *ngIf="friends?.length !== 0" class="add-user btn btn-danger pull-left" (click)="quitFriends()">
|
||||||
|
Quit friends
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a *ngIf="friends?.length === 0" class="add-user btn btn-success pull-right" [routerLink]="['/admin/friends/add']">
|
||||||
|
Make friends
|
||||||
|
</a>
|
|
@ -0,0 +1,3 @@
|
||||||
|
table {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
import { Friend, FriendService } from '../shared';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-friend-list',
|
||||||
|
templateUrl: './friend-list.component.html',
|
||||||
|
styleUrls: [ './friend-list.component.scss' ]
|
||||||
|
})
|
||||||
|
export class FriendListComponent implements OnInit {
|
||||||
|
friends: Friend[];
|
||||||
|
|
||||||
|
constructor(private friendService: FriendService) { }
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.getFriends();
|
||||||
|
}
|
||||||
|
|
||||||
|
quitFriends() {
|
||||||
|
if (!confirm('Are you sure?')) return;
|
||||||
|
|
||||||
|
this.friendService.quitFriends().subscribe(
|
||||||
|
status => {
|
||||||
|
alert('Quit friends!');
|
||||||
|
this.getFriends();
|
||||||
|
},
|
||||||
|
error => alert(error.text)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFriends() {
|
||||||
|
this.friendService.getFriends().subscribe(
|
||||||
|
friends => this.friends = friends,
|
||||||
|
|
||||||
|
err => alert(err.text)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './friend-list.component';
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: '<router-outlet></router-outlet>'
|
||||||
|
})
|
||||||
|
|
||||||
|
export class FriendsComponent {
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { Routes } from '@angular/router';
|
||||||
|
|
||||||
|
import { FriendsComponent } from './friends.component';
|
||||||
|
import { FriendAddComponent } from './friend-add';
|
||||||
|
import { FriendListComponent } from './friend-list';
|
||||||
|
|
||||||
|
export const FriendsRoutes: Routes = [
|
||||||
|
{
|
||||||
|
path: 'friends',
|
||||||
|
component: FriendsComponent,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
redirectTo: 'list',
|
||||||
|
pathMatch: 'full'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'list',
|
||||||
|
component: FriendListComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'add',
|
||||||
|
component: FriendAddComponent
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
|
@ -0,0 +1,5 @@
|
||||||
|
export * from './friend-add';
|
||||||
|
export * from './friend-list';
|
||||||
|
export * from './shared';
|
||||||
|
export * from './friends.component';
|
||||||
|
export * from './friends.routes';
|
|
@ -0,0 +1,6 @@
|
||||||
|
export interface Friend {
|
||||||
|
id: string;
|
||||||
|
url: string;
|
||||||
|
score: number;
|
||||||
|
createdDate: Date;
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
|
||||||
|
import { Friend } from './friend.model';
|
||||||
|
import { AuthHttp, RestExtractor } from '../../../shared';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class FriendService {
|
||||||
|
private static BASE_FRIEND_URL: string = '/api/v1/pods/';
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
private authHttp: AuthHttp,
|
||||||
|
private restExtractor: RestExtractor
|
||||||
|
) {}
|
||||||
|
|
||||||
|
getFriends(): Observable<Friend[]> {
|
||||||
|
return this.authHttp.get(FriendService.BASE_FRIEND_URL)
|
||||||
|
// Not implemented as a data list by the server yet
|
||||||
|
// .map(this.restExtractor.extractDataList)
|
||||||
|
.map((res) => res.json())
|
||||||
|
.catch((res) => this.restExtractor.handleError(res));
|
||||||
|
}
|
||||||
|
|
||||||
|
makeFriends(notEmptyUrls) {
|
||||||
|
const body = {
|
||||||
|
urls: notEmptyUrls
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.authHttp.post(FriendService.BASE_FRIEND_URL + 'makefriends', body)
|
||||||
|
.map(this.restExtractor.extractDataBool)
|
||||||
|
.catch((res) => this.restExtractor.handleError(res));
|
||||||
|
}
|
||||||
|
|
||||||
|
quitFriends() {
|
||||||
|
return this.authHttp.get(FriendService.BASE_FRIEND_URL + 'quitfriends')
|
||||||
|
.map(res => res.status)
|
||||||
|
.catch((res) => this.restExtractor.handleError(res));
|
||||||
|
}
|
||||||
|
}
|
|
@ -1 +1,2 @@
|
||||||
|
export * from './friend.model';
|
||||||
export * from './friend.service';
|
export * from './friend.service';
|
|
@ -0,0 +1,6 @@
|
||||||
|
export * from './friends';
|
||||||
|
export * from './requests';
|
||||||
|
export * from './users';
|
||||||
|
export * from './admin.component';
|
||||||
|
export * from './admin.routes';
|
||||||
|
export * from './menu-admin.component';
|
|
@ -0,0 +1,26 @@
|
||||||
|
<menu class="col-md-2 col-sm-3 col-xs-3">
|
||||||
|
|
||||||
|
<div class="panel-block">
|
||||||
|
<div id="panel-users" class="panel-button">
|
||||||
|
<span class="hidden-xs glyphicon glyphicon-user"></span>
|
||||||
|
<a [routerLink]="['/admin/users/list']">List users</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="panel-friends" class="panel-button">
|
||||||
|
<span class="hidden-xs glyphicon glyphicon-cloud"></span>
|
||||||
|
<a [routerLink]="['/admin/friends/list']">List friends</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="panel-request-stats" class="panel-button">
|
||||||
|
<span class="hidden-xs glyphicon glyphicon-stats"></span>
|
||||||
|
<a [routerLink]="['/admin/requests/stats']">Request stats</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel-block">
|
||||||
|
<div id="panel-quit-administration" class="panel-button">
|
||||||
|
<span class="hidden-xs glyphicon glyphicon-cog"></span>
|
||||||
|
<a [routerLink]="['/videos/list']">Quit admin.</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</menu>
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-menu-admin',
|
||||||
|
templateUrl: './menu-admin.component.html'
|
||||||
|
})
|
||||||
|
export class MenuAdminComponent { }
|
|
@ -0,0 +1,4 @@
|
||||||
|
export * from './request-stats';
|
||||||
|
export * from './shared';
|
||||||
|
export * from './requests.component';
|
||||||
|
export * from './requests.routes';
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './request-stats.component';
|
|
@ -0,0 +1,23 @@
|
||||||
|
<h3>Requests stats</h3>
|
||||||
|
|
||||||
|
<div *ngIf="stats !== null">
|
||||||
|
<div>
|
||||||
|
<span class="label-description">Interval seconds between requests:</span>
|
||||||
|
{{ stats.secondsInterval }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span class="label-description">Remaining time before the scheduled request:</span>
|
||||||
|
{{ stats.remainingSeconds }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span class="label-description">Maximum number of requests per interval:</span>
|
||||||
|
{{ stats.maxRequestsInParallel }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span class="label-description">Remaining requests:</span>
|
||||||
|
{{ stats.requests.length }}
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,6 @@
|
||||||
|
.label-description {
|
||||||
|
display: inline-block;
|
||||||
|
width: 350px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: black;
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||||
|
|
||||||
|
import { RequestService, RequestStats } from '../shared';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-request-stats',
|
||||||
|
templateUrl: './request-stats.component.html',
|
||||||
|
styleUrls: [ './request-stats.component.scss' ]
|
||||||
|
})
|
||||||
|
export class RequestStatsComponent implements OnInit, OnDestroy {
|
||||||
|
stats: RequestStats = null;
|
||||||
|
|
||||||
|
private interval: NodeJS.Timer = null;
|
||||||
|
|
||||||
|
constructor(private requestService: RequestService) { }
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.getStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
if (this.stats.secondsInterval !== null) {
|
||||||
|
clearInterval(this.interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getStats() {
|
||||||
|
this.requestService.getStats().subscribe(
|
||||||
|
stats => {
|
||||||
|
console.log(stats);
|
||||||
|
this.stats = stats;
|
||||||
|
this.runInterval();
|
||||||
|
},
|
||||||
|
|
||||||
|
err => alert(err.text)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private runInterval() {
|
||||||
|
this.interval = setInterval(() => {
|
||||||
|
this.stats.remainingMilliSeconds -= 1000;
|
||||||
|
|
||||||
|
if (this.stats.remainingMilliSeconds <= 0) {
|
||||||
|
setTimeout(() => this.getStats(), this.stats.remainingMilliSeconds + 100);
|
||||||
|
clearInterval(this.interval);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: '<router-outlet></router-outlet>'
|
||||||
|
})
|
||||||
|
|
||||||
|
export class RequestsComponent {
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Routes } from '@angular/router';
|
||||||
|
|
||||||
|
import { RequestsComponent } from './requests.component';
|
||||||
|
import { RequestStatsComponent } from './request-stats';
|
||||||
|
|
||||||
|
export const RequestsRoutes: Routes = [
|
||||||
|
{
|
||||||
|
path: 'requests',
|
||||||
|
component: RequestsComponent,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
redirectTo: 'stats',
|
||||||
|
pathMatch: 'full'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'stats',
|
||||||
|
component: RequestStatsComponent
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './request-stats.model';
|
||||||
|
export * from './request.service';
|
|
@ -0,0 +1,32 @@
|
||||||
|
export interface Request {
|
||||||
|
request: any;
|
||||||
|
to: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RequestStats {
|
||||||
|
maxRequestsInParallel: number;
|
||||||
|
milliSecondsInterval: number;
|
||||||
|
remainingMilliSeconds: number;
|
||||||
|
requests: Request[];
|
||||||
|
|
||||||
|
constructor(hash: {
|
||||||
|
maxRequestsInParallel: number,
|
||||||
|
milliSecondsInterval: number,
|
||||||
|
remainingMilliSeconds: number,
|
||||||
|
requests: Request[];
|
||||||
|
}) {
|
||||||
|
this.maxRequestsInParallel = hash.maxRequestsInParallel;
|
||||||
|
this.milliSecondsInterval = hash.milliSecondsInterval;
|
||||||
|
this.remainingMilliSeconds = hash.remainingMilliSeconds;
|
||||||
|
this.requests = hash.requests;
|
||||||
|
}
|
||||||
|
|
||||||
|
get remainingSeconds() {
|
||||||
|
return Math.floor(this.remainingMilliSeconds / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
get secondsInterval() {
|
||||||
|
return Math.floor(this.milliSecondsInterval / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
|
||||||
|
import { RequestStats } from './request-stats.model';
|
||||||
|
import { AuthHttp, RestExtractor } from '../../../shared';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RequestService {
|
||||||
|
private static BASE_REQUEST_URL: string = '/api/v1/requests/';
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
private authHttp: AuthHttp,
|
||||||
|
private restExtractor: RestExtractor
|
||||||
|
) {}
|
||||||
|
|
||||||
|
getStats(): Observable<RequestStats> {
|
||||||
|
return this.authHttp.get(RequestService.BASE_REQUEST_URL + 'stats')
|
||||||
|
.map(this.restExtractor.extractDataGet)
|
||||||
|
.map((data) => new RequestStats(data))
|
||||||
|
.catch((res) => this.restExtractor.handleError(res));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
export * from './shared';
|
||||||
|
export * from './user-add';
|
||||||
|
export * from './user-list';
|
||||||
|
export * from './users.component';
|
||||||
|
export * from './users.routes';
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './user.service';
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { AuthHttp, RestExtractor, ResultList, User } from '../../../shared';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserService {
|
||||||
|
// TODO: merge this constant with account
|
||||||
|
private static BASE_USERS_URL = '/api/v1/users/';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private authHttp: AuthHttp,
|
||||||
|
private restExtractor: RestExtractor
|
||||||
|
) {}
|
||||||
|
|
||||||
|
addUser(username: string, password: string) {
|
||||||
|
const body = {
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.authHttp.post(UserService.BASE_USERS_URL, body)
|
||||||
|
.map(this.restExtractor.extractDataBool)
|
||||||
|
.catch(this.restExtractor.handleError);
|
||||||
|
}
|
||||||
|
|
||||||
|
getUsers() {
|
||||||
|
return this.authHttp.get(UserService.BASE_USERS_URL)
|
||||||
|
.map(this.restExtractor.extractDataList)
|
||||||
|
.map(this.extractUsers)
|
||||||
|
.catch((res) => this.restExtractor.handleError(res));
|
||||||
|
}
|
||||||
|
|
||||||
|
removeUser(user: User) {
|
||||||
|
return this.authHttp.delete(UserService.BASE_USERS_URL + user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractUsers(result: ResultList) {
|
||||||
|
const usersJson = result.data;
|
||||||
|
const totalUsers = result.total;
|
||||||
|
const users = [];
|
||||||
|
for (const userJson of usersJson) {
|
||||||
|
users.push(new User(userJson));
|
||||||
|
}
|
||||||
|
|
||||||
|
return { users, totalUsers };
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './user-add.component';
|
|
@ -0,0 +1,29 @@
|
||||||
|
<h3>Add user</h3>
|
||||||
|
|
||||||
|
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
|
||||||
|
|
||||||
|
<form role="form" (ngSubmit)="addUser()" [formGroup]="form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<input
|
||||||
|
type="text" class="form-control" id="username" placeholder="Username"
|
||||||
|
formControlName="username"
|
||||||
|
>
|
||||||
|
<div *ngIf="formErrors.username" class="alert alert-danger">
|
||||||
|
{{ formErrors.username }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input
|
||||||
|
type="password" class="form-control" id="password" placeholder="Password"
|
||||||
|
formControlName="password"
|
||||||
|
>
|
||||||
|
<div *ngIf="formErrors.password" class="alert alert-danger">
|
||||||
|
{{ formErrors.password }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="submit" value="Add user" class="btn btn-default" [disabled]="!form.valid">
|
||||||
|
</form>
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
import { UserService } from '../shared';
|
||||||
|
import { FormReactive, USER_USERNAME, USER_PASSWORD } from '../../../shared';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-user-add',
|
||||||
|
templateUrl: './user-add.component.html'
|
||||||
|
})
|
||||||
|
export class UserAddComponent extends FormReactive implements OnInit {
|
||||||
|
error: string = null;
|
||||||
|
|
||||||
|
form: FormGroup;
|
||||||
|
formErrors = {
|
||||||
|
'username': '',
|
||||||
|
'password': ''
|
||||||
|
};
|
||||||
|
validationMessages = {
|
||||||
|
'username': USER_USERNAME.MESSAGES,
|
||||||
|
'password': USER_PASSWORD.MESSAGES,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private router: Router,
|
||||||
|
private userService: UserService
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
buildForm() {
|
||||||
|
this.form = this.formBuilder.group({
|
||||||
|
username: [ '', USER_USERNAME.VALIDATORS ],
|
||||||
|
password: [ '', USER_PASSWORD.VALIDATORS ],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.form.valueChanges.subscribe(data => this.onValueChanged(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.buildForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
addUser() {
|
||||||
|
this.error = null;
|
||||||
|
|
||||||
|
const { username, password } = this.form.value;
|
||||||
|
|
||||||
|
this.userService.addUser(username, password).subscribe(
|
||||||
|
ok => this.router.navigate([ '/admin/users/list' ]),
|
||||||
|
|
||||||
|
err => this.error = err.text
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './user-list.component';
|
|
@ -0,0 +1,28 @@
|
||||||
|
<h3>Users list</h3>
|
||||||
|
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="table-column-id">ID</th>
|
||||||
|
<th>Username</th>
|
||||||
|
<th>Created Date</th>
|
||||||
|
<th class="text-right">Remove</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let user of users">
|
||||||
|
<td>{{ user.id }}</td>
|
||||||
|
<td>{{ user.username }}</td>
|
||||||
|
<td>{{ user.createdDate | date: 'medium' }}</td>
|
||||||
|
<td class="text-right">
|
||||||
|
<span class="glyphicon glyphicon-remove" *ngIf="!user.isAdmin()" (click)="removeUser(user)"></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<a class="add-user btn btn-success pull-right" [routerLink]="['/admin/users/add']">
|
||||||
|
<span class="glyphicon glyphicon-plus"></span>
|
||||||
|
Add user
|
||||||
|
</a>
|
|
@ -0,0 +1,7 @@
|
||||||
|
.glyphicon-remove {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-user {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
import { User } from '../../../shared';
|
||||||
|
import { UserService } from '../shared';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-user-list',
|
||||||
|
templateUrl: './user-list.component.html',
|
||||||
|
styleUrls: [ './user-list.component.scss' ]
|
||||||
|
})
|
||||||
|
export class UserListComponent implements OnInit {
|
||||||
|
totalUsers: number;
|
||||||
|
users: User[];
|
||||||
|
|
||||||
|
constructor(private userService: UserService) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.getUsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
getUsers() {
|
||||||
|
this.userService.getUsers().subscribe(
|
||||||
|
({ users, totalUsers }) => {
|
||||||
|
this.users = users;
|
||||||
|
this.totalUsers = totalUsers;
|
||||||
|
},
|
||||||
|
|
||||||
|
err => alert(err.text)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
removeUser(user: User) {
|
||||||
|
if (confirm('Are you sure?')) {
|
||||||
|
this.userService.removeUser(user).subscribe(
|
||||||
|
() => this.getUsers(),
|
||||||
|
|
||||||
|
err => alert(err.text)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: '<router-outlet></router-outlet>'
|
||||||
|
})
|
||||||
|
|
||||||
|
export class UsersComponent {
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { Routes } from '@angular/router';
|
||||||
|
|
||||||
|
import { UsersComponent } from './users.component';
|
||||||
|
import { UserAddComponent } from './user-add';
|
||||||
|
import { UserListComponent } from './user-list';
|
||||||
|
|
||||||
|
export const UsersRoutes: Routes = [
|
||||||
|
{
|
||||||
|
path: 'users',
|
||||||
|
component: UsersComponent,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
redirectTo: 'list',
|
||||||
|
pathMatch: 'full'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'list',
|
||||||
|
component: UserListComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'add',
|
||||||
|
component: UserAddComponent
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
|
@ -14,48 +14,14 @@
|
||||||
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
<my-menu *ngIf="isInAdmin() === false"></my-menu>
|
||||||
<menu class="col-md-2 col-sm-3 col-xs-3">
|
<my-menu-admin *ngIf="isInAdmin() === true"></my-menu-admin>
|
||||||
<div class="panel-block">
|
|
||||||
<div id="panel-user-login" class="panel-button">
|
|
||||||
<span class="hidden-xs glyphicon glyphicon-user"></span>
|
|
||||||
<a *ngIf="!isLoggedIn" [routerLink]="['/login']">Login</a>
|
|
||||||
<a *ngIf="isLoggedIn" (click)="logout()">Logout</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="panel-block">
|
|
||||||
<div id="panel-get-videos" class="panel-button">
|
|
||||||
<span class="hidden-xs glyphicon glyphicon-list"></span>
|
|
||||||
<a [routerLink]="['/videos/list']">Get videos</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="panel-upload-video" class="panel-button" *ngIf="isLoggedIn">
|
|
||||||
<span class="hidden-xs glyphicon glyphicon-cloud-upload"></span>
|
|
||||||
<a [routerLink]="['/videos/add']">Upload a video</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="panel-block" *ngIf="isLoggedIn">
|
|
||||||
<div id="panel-make-friends" class="panel-button">
|
|
||||||
<span class="hidden-xs glyphicon glyphicon-cloud"></span>
|
|
||||||
<a (click)='makeFriends()'>Make friends</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="panel-quit-friends" class="panel-button">
|
|
||||||
<span class="hidden-xs glyphicon glyphicon-plane"></span>
|
|
||||||
<a (click)='quitFriends()'>Quit friends</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</menu>
|
|
||||||
|
|
||||||
<div class="col-md-9 col-sm-8 col-xs-8 router-outlet-container">
|
<div class="col-md-9 col-sm-8 col-xs-8 router-outlet-container">
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
PeerTube, CopyLeft 2015-2016
|
PeerTube, CopyLeft 2015-2016
|
||||||
</footer>
|
</footer>
|
||||||
|
|
|
@ -12,40 +12,6 @@ header div {
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
menu {
|
|
||||||
@media screen and (max-width: 600px) {
|
|
||||||
margin-right: 3px !important;
|
|
||||||
padding: 3px !important;
|
|
||||||
min-height: 400px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
min-height: 600px;
|
|
||||||
margin-right: 20px;
|
|
||||||
border-right: 1px solid rgba(0, 0, 0, 0.2);
|
|
||||||
|
|
||||||
.panel-button {
|
|
||||||
margin: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: margin 0.2s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
margin-left: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: #333333;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.glyphicon {
|
|
||||||
margin: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-block:not(:last-child) {
|
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.router-outlet-container {
|
.router-outlet-container {
|
||||||
@media screen and (max-width: 400px) {
|
@media screen and (max-width: 400px) {
|
||||||
padding: 0 3px 0 3px;
|
padding: 0 3px 0 3px;
|
||||||
|
|
|
@ -1,73 +1,16 @@
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { ActivatedRoute, Router, ROUTER_DIRECTIVES } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
import { FriendService } from './friends';
|
|
||||||
import {
|
|
||||||
AuthService,
|
|
||||||
AuthStatus,
|
|
||||||
SearchComponent,
|
|
||||||
SearchService
|
|
||||||
} from './shared';
|
|
||||||
import { VideoService } from './videos';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-app',
|
selector: 'my-app',
|
||||||
template: require('./app.component.html'),
|
templateUrl: './app.component.html',
|
||||||
styles: [ require('./app.component.scss') ],
|
styleUrls: [ './app.component.scss' ]
|
||||||
directives: [ ROUTER_DIRECTIVES, SearchComponent ],
|
|
||||||
providers: [ FriendService, VideoService, SearchService ]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
choices = [];
|
constructor(private router: Router) {}
|
||||||
isLoggedIn: boolean;
|
|
||||||
|
|
||||||
constructor(
|
isInAdmin() {
|
||||||
private authService: AuthService,
|
return this.router.url.indexOf('/admin/') !== -1;
|
||||||
private friendService: FriendService,
|
|
||||||
private route: ActivatedRoute,
|
|
||||||
private router: Router
|
|
||||||
) {
|
|
||||||
this.isLoggedIn = this.authService.isLoggedIn();
|
|
||||||
|
|
||||||
this.authService.loginChangedSource.subscribe(
|
|
||||||
status => {
|
|
||||||
if (status === AuthStatus.LoggedIn) {
|
|
||||||
this.isLoggedIn = true;
|
|
||||||
console.log('Logged in.');
|
|
||||||
} else if (status === AuthStatus.LoggedOut) {
|
|
||||||
this.isLoggedIn = false;
|
|
||||||
console.log('Logged out.');
|
|
||||||
} else {
|
|
||||||
console.error('Unknown auth status: ' + status);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
logout() {
|
|
||||||
this.authService.logout();
|
|
||||||
}
|
|
||||||
|
|
||||||
makeFriends() {
|
|
||||||
this.friendService.makeFriends().subscribe(
|
|
||||||
status => {
|
|
||||||
if (status === 409) {
|
|
||||||
alert('Already made friends!');
|
|
||||||
} else {
|
|
||||||
alert('Made friends!');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error => alert(error)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
quitFriends() {
|
|
||||||
this.friendService.quitFriends().subscribe(
|
|
||||||
status => {
|
|
||||||
alert('Quit friends!');
|
|
||||||
},
|
|
||||||
error => alert(error)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,146 @@
|
||||||
|
import { ApplicationRef, NgModule } from '@angular/core';
|
||||||
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { HttpModule, RequestOptions, XHRBackend } from '@angular/http';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { removeNgStyles, createNewHosts } from '@angularclass/hmr';
|
||||||
|
|
||||||
|
import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe';
|
||||||
|
import { ProgressbarModule } from 'ng2-bootstrap/components/progressbar';
|
||||||
|
import { PaginationModule } from 'ng2-bootstrap/components/pagination';
|
||||||
|
import { FileUploadModule } from 'ng2-file-upload/ng2-file-upload';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Platform and Environment providers/directives/pipes
|
||||||
|
*/
|
||||||
|
import { ENV_PROVIDERS } from './environment';
|
||||||
|
import { routes } from './app.routes';
|
||||||
|
// App is our top level component
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
import { AppState } from './app.service';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AdminComponent,
|
||||||
|
FriendsComponent,
|
||||||
|
FriendAddComponent,
|
||||||
|
FriendListComponent,
|
||||||
|
FriendService,
|
||||||
|
MenuAdminComponent,
|
||||||
|
RequestsComponent,
|
||||||
|
RequestStatsComponent,
|
||||||
|
RequestService,
|
||||||
|
UsersComponent,
|
||||||
|
UserAddComponent,
|
||||||
|
UserListComponent,
|
||||||
|
UserService
|
||||||
|
} from './admin';
|
||||||
|
import { AccountComponent, AccountService } from './account';
|
||||||
|
import { LoginComponent } from './login';
|
||||||
|
import { MenuComponent } from './menu.component';
|
||||||
|
import { AuthService, AuthHttp, RestExtractor, RestService, SearchComponent, SearchService } from './shared';
|
||||||
|
import {
|
||||||
|
LoaderComponent,
|
||||||
|
VideosComponent,
|
||||||
|
VideoAddComponent,
|
||||||
|
VideoListComponent,
|
||||||
|
VideoMiniatureComponent,
|
||||||
|
VideoSortComponent,
|
||||||
|
VideoWatchComponent,
|
||||||
|
VideoService,
|
||||||
|
WebTorrentService
|
||||||
|
} from './videos';
|
||||||
|
|
||||||
|
// Application wide providers
|
||||||
|
const APP_PROVIDERS = [
|
||||||
|
AppState,
|
||||||
|
|
||||||
|
{
|
||||||
|
provide: AuthHttp,
|
||||||
|
useFactory: (backend: XHRBackend, defaultOptions: RequestOptions, authService: AuthService) => {
|
||||||
|
return new AuthHttp(backend, defaultOptions, authService);
|
||||||
|
},
|
||||||
|
deps: [ XHRBackend, RequestOptions, AuthService ]
|
||||||
|
},
|
||||||
|
|
||||||
|
AuthService,
|
||||||
|
RestExtractor,
|
||||||
|
RestService,
|
||||||
|
|
||||||
|
VideoService,
|
||||||
|
SearchService,
|
||||||
|
FriendService,
|
||||||
|
RequestService,
|
||||||
|
UserService,
|
||||||
|
AccountService,
|
||||||
|
WebTorrentService
|
||||||
|
];
|
||||||
|
/**
|
||||||
|
* `AppModule` is the main entry point into Angular2's bootstraping process
|
||||||
|
*/
|
||||||
|
@NgModule({
|
||||||
|
bootstrap: [ AppComponent ],
|
||||||
|
declarations: [
|
||||||
|
AccountComponent,
|
||||||
|
AdminComponent,
|
||||||
|
AppComponent,
|
||||||
|
BytesPipe,
|
||||||
|
FriendAddComponent,
|
||||||
|
FriendListComponent,
|
||||||
|
FriendsComponent,
|
||||||
|
LoaderComponent,
|
||||||
|
LoginComponent,
|
||||||
|
MenuAdminComponent,
|
||||||
|
MenuComponent,
|
||||||
|
RequestsComponent,
|
||||||
|
RequestStatsComponent,
|
||||||
|
SearchComponent,
|
||||||
|
UserAddComponent,
|
||||||
|
UserListComponent,
|
||||||
|
UsersComponent,
|
||||||
|
VideoAddComponent,
|
||||||
|
VideoListComponent,
|
||||||
|
VideoMiniatureComponent,
|
||||||
|
VideosComponent,
|
||||||
|
VideoSortComponent,
|
||||||
|
VideoWatchComponent,
|
||||||
|
],
|
||||||
|
imports: [ // import Angular's modules
|
||||||
|
BrowserModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
HttpModule,
|
||||||
|
RouterModule.forRoot(routes),
|
||||||
|
|
||||||
|
ProgressbarModule,
|
||||||
|
PaginationModule,
|
||||||
|
FileUploadModule
|
||||||
|
],
|
||||||
|
providers: [ // expose our Services and Providers into Angular's dependency injection
|
||||||
|
ENV_PROVIDERS,
|
||||||
|
APP_PROVIDERS
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AppModule {
|
||||||
|
constructor(public appRef: ApplicationRef, public appState: AppState) {}
|
||||||
|
hmrOnInit(store) {
|
||||||
|
if (!store || !store.state) return;
|
||||||
|
console.log('HMR store', store);
|
||||||
|
this.appState._state = store.state;
|
||||||
|
this.appRef.tick();
|
||||||
|
delete store.state;
|
||||||
|
}
|
||||||
|
hmrOnDestroy(store) {
|
||||||
|
const cmpLocation = this.appRef.components.map(cmp => cmp.location.nativeElement);
|
||||||
|
// recreate elements
|
||||||
|
const state = this.appState._state;
|
||||||
|
store.state = state;
|
||||||
|
store.disposeOldHosts = createNewHosts(cmpLocation);
|
||||||
|
// remove styles
|
||||||
|
removeNgStyles();
|
||||||
|
}
|
||||||
|
hmrAfterDestroy(store) {
|
||||||
|
// display new elements
|
||||||
|
store.disposeOldHosts();
|
||||||
|
delete store.disposeOldHosts;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,15 +1,18 @@
|
||||||
import { RouterConfig } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
|
|
||||||
|
import { AccountRoutes } from './account';
|
||||||
import { LoginRoutes } from './login';
|
import { LoginRoutes } from './login';
|
||||||
|
import { AdminRoutes } from './admin';
|
||||||
import { VideosRoutes } from './videos';
|
import { VideosRoutes } from './videos';
|
||||||
|
|
||||||
export const routes: RouterConfig = [
|
export const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
redirectTo: '/videos/list',
|
redirectTo: '/videos/list',
|
||||||
pathMatch: 'full'
|
pathMatch: 'full'
|
||||||
},
|
},
|
||||||
|
...AdminRoutes,
|
||||||
|
...AccountRoutes,
|
||||||
...LoginRoutes,
|
...LoginRoutes,
|
||||||
...VideosRoutes
|
...VideosRoutes
|
||||||
];
|
];
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AppState {
|
||||||
|
_state = { };
|
||||||
|
|
||||||
|
constructor() { ; }
|
||||||
|
|
||||||
|
// already return a clone of the current state
|
||||||
|
get state() {
|
||||||
|
return this._state = this._clone(this._state);
|
||||||
|
}
|
||||||
|
// never allow mutation
|
||||||
|
set state(value) {
|
||||||
|
throw new Error('do not mutate the `.state` directly');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
get(prop?: any) {
|
||||||
|
// use our state getter for the clone
|
||||||
|
const state = this.state;
|
||||||
|
return state.hasOwnProperty(prop) ? state[prop] : state;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(prop: string, value: any) {
|
||||||
|
// internally mutate our state
|
||||||
|
return this._state[prop] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_clone(object) {
|
||||||
|
// simple object clone
|
||||||
|
return JSON.parse(JSON.stringify( object ));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
|
||||||
|
// Angular 2
|
||||||
|
// rc2 workaround
|
||||||
|
import { enableDebugTools, disableDebugTools } from '@angular/platform-browser';
|
||||||
|
import { enableProdMode, ApplicationRef } from '@angular/core';
|
||||||
|
// Environment Providers
|
||||||
|
let PROVIDERS = [
|
||||||
|
// common env directives
|
||||||
|
];
|
||||||
|
|
||||||
|
// Angular debug tools in the dev console
|
||||||
|
// https://github.com/angular/angular/blob/86405345b781a9dc2438c0fbe3e9409245647019/TOOLS_JS.md
|
||||||
|
let _decorateModuleRef = function identity(value) { return value; };
|
||||||
|
|
||||||
|
if ('production' === ENV) {
|
||||||
|
// Production
|
||||||
|
disableDebugTools();
|
||||||
|
enableProdMode();
|
||||||
|
|
||||||
|
PROVIDERS = [
|
||||||
|
...PROVIDERS,
|
||||||
|
// custom providers in production
|
||||||
|
];
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
_decorateModuleRef = (modRef: any) => {
|
||||||
|
const appRef = modRef.injector.get(ApplicationRef);
|
||||||
|
const cmpRef = appRef.components[0];
|
||||||
|
|
||||||
|
let _ng = (<any>window).ng;
|
||||||
|
enableDebugTools(cmpRef);
|
||||||
|
(<any>window).ng.probe = _ng.probe;
|
||||||
|
(<any>window).ng.coreTokens = _ng.coreTokens;
|
||||||
|
return modRef;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Development
|
||||||
|
PROVIDERS = [
|
||||||
|
...PROVIDERS,
|
||||||
|
// custom providers in development
|
||||||
|
];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const decorateModuleRef = _decorateModuleRef;
|
||||||
|
|
||||||
|
export const ENV_PROVIDERS = [
|
||||||
|
...PROVIDERS
|
||||||
|
];
|
|
@ -1,29 +0,0 @@
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { Response } from '@angular/http';
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
|
||||||
|
|
||||||
import { AuthHttp, AuthService } from '../shared';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class FriendService {
|
|
||||||
private static BASE_FRIEND_URL: string = '/api/v1/pods/';
|
|
||||||
|
|
||||||
constructor (private authHttp: AuthHttp, private authService: AuthService) {}
|
|
||||||
|
|
||||||
makeFriends() {
|
|
||||||
return this.authHttp.get(FriendService.BASE_FRIEND_URL + 'makefriends')
|
|
||||||
.map(res => res.status)
|
|
||||||
.catch(this.handleError);
|
|
||||||
}
|
|
||||||
|
|
||||||
quitFriends() {
|
|
||||||
return this.authHttp.get(FriendService.BASE_FRIEND_URL + 'quitfriends')
|
|
||||||
.map(res => res.status)
|
|
||||||
.catch(this.handleError);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleError (error: Response): Observable<number> {
|
|
||||||
console.error(error);
|
|
||||||
return Observable.throw(error.json().error || 'Server error');
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './app.module';
|
|
@ -1,17 +1,16 @@
|
||||||
<h3>Login</h3>
|
<h3>Login</h3>
|
||||||
|
|
||||||
|
|
||||||
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
|
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
|
||||||
|
|
||||||
<form role="form" (ngSubmit)="login(username.value, password.value)" #loginForm="ngForm">
|
<form role="form" (ngSubmit)="login()" [formGroup]="form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="username">Username</label>
|
<label for="username">Username</label>
|
||||||
<input
|
<input
|
||||||
type="text" class="form-control" name="username" id="username" placeholder="Username" required
|
type="text" class="form-control" id="username" placeholder="Username" required
|
||||||
ngControl="username" #username="ngForm"
|
formControlName="username"
|
||||||
>
|
>
|
||||||
<div [hidden]="username.valid || username.pristine" class="alert alert-danger">
|
<div *ngIf="formErrors.username" class="alert alert-danger">
|
||||||
Username is required
|
{{ formErrors.username }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -19,12 +18,12 @@
|
||||||
<label for="password">Password</label>
|
<label for="password">Password</label>
|
||||||
<input
|
<input
|
||||||
type="password" class="form-control" name="password" id="password" placeholder="Password" required
|
type="password" class="form-control" name="password" id="password" placeholder="Password" required
|
||||||
ngControl="password" #password="ngForm"
|
formControlName="password"
|
||||||
>
|
>
|
||||||
<div [hidden]="password.valid || password.pristine" class="alert alert-danger">
|
<div *ngIf="formErrors.password" class="alert alert-danger">
|
||||||
Password is required
|
{{ formErrors.password }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input type="submit" value="Login" class="btn btn-default" [disabled]="!loginForm.form.valid">
|
<input type="submit" value="Login" class="btn btn-default" [disabled]="!form.valid">
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -1,35 +1,67 @@
|
||||||
import { Component } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
import { AuthService } from '../shared';
|
import { AuthService, FormReactive } from '../shared';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-login',
|
selector: 'my-login',
|
||||||
template: require('./login.component.html')
|
templateUrl: './login.component.html'
|
||||||
})
|
})
|
||||||
|
|
||||||
export class LoginComponent {
|
export class LoginComponent extends FormReactive implements OnInit {
|
||||||
error: string = null;
|
error: string = null;
|
||||||
|
|
||||||
|
form: FormGroup;
|
||||||
|
formErrors = {
|
||||||
|
'username': '',
|
||||||
|
'password': ''
|
||||||
|
};
|
||||||
|
validationMessages = {
|
||||||
|
'username': {
|
||||||
|
'required': 'Username is required.',
|
||||||
|
},
|
||||||
|
'password': {
|
||||||
|
'required': 'Password is required.'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
private router: Router
|
private router: Router
|
||||||
) {}
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
buildForm() {
|
||||||
|
this.form = this.formBuilder.group({
|
||||||
|
username: [ '', Validators.required ],
|
||||||
|
password: [ '', Validators.required ],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.form.valueChanges.subscribe(data => this.onValueChanged(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.buildForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
login() {
|
||||||
|
this.error = null;
|
||||||
|
|
||||||
|
const { username, password } = this.form.value;
|
||||||
|
|
||||||
login(username: string, password: string) {
|
|
||||||
this.authService.login(username, password).subscribe(
|
this.authService.login(username, password).subscribe(
|
||||||
result => {
|
result => this.router.navigate(['/videos/list']),
|
||||||
this.error = null;
|
|
||||||
|
|
||||||
this.router.navigate(['/videos/list']);
|
|
||||||
},
|
|
||||||
error => {
|
error => {
|
||||||
console.error(error);
|
console.error(error.json);
|
||||||
|
|
||||||
if (error.error === 'invalid_grant') {
|
if (error.json.error === 'invalid_grant') {
|
||||||
this.error = 'Credentials are invalid.';
|
this.error = 'Credentials are invalid.';
|
||||||
} else {
|
} else {
|
||||||
this.error = `${error.error}: ${error.error_description}`;
|
this.error = `${error.json.error}: ${error.json.error_description}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
<menu class="col-md-2 col-sm-3 col-xs-3">
|
||||||
|
<div class="panel-block">
|
||||||
|
<div id="panel-user-login" class="panel-button">
|
||||||
|
<span *ngIf="!isLoggedIn" >
|
||||||
|
<span class="hidden-xs glyphicon glyphicon-log-in"></span>
|
||||||
|
<a [routerLink]="['/login']">Login</a>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span *ngIf="isLoggedIn">
|
||||||
|
<span class="hidden-xs glyphicon glyphicon-log-out"></span>
|
||||||
|
<a *ngIf="isLoggedIn" (click)="logout()">Logout</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="isLoggedIn" id="panel-user-account" class="panel-button">
|
||||||
|
<span class="hidden-xs glyphicon glyphicon-user"></span>
|
||||||
|
<a [routerLink]="['/account']">My account</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel-block">
|
||||||
|
<div id="panel-get-videos" class="panel-button">
|
||||||
|
<span class="hidden-xs glyphicon glyphicon-list"></span>
|
||||||
|
<a [routerLink]="['/videos/list']">Get videos</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="panel-upload-video" class="panel-button" *ngIf="isLoggedIn">
|
||||||
|
<span class="hidden-xs glyphicon glyphicon-cloud-upload"></span>
|
||||||
|
<a [routerLink]="['/videos/add']">Upload a video</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel-block" *ngIf="isUserAdmin()">
|
||||||
|
<div id="panel-get-videos" class="panel-button">
|
||||||
|
<span class="hidden-xs glyphicon glyphicon-cog"></span>
|
||||||
|
<a [routerLink]="['/admin']">Administration</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</menu>
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
import { AuthService, AuthStatus } from './shared';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-menu',
|
||||||
|
templateUrl: './menu.component.html'
|
||||||
|
})
|
||||||
|
export class MenuComponent implements OnInit {
|
||||||
|
isLoggedIn: boolean;
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
private authService: AuthService,
|
||||||
|
private router: Router
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.isLoggedIn = this.authService.isLoggedIn();
|
||||||
|
|
||||||
|
this.authService.loginChangedSource.subscribe(
|
||||||
|
status => {
|
||||||
|
if (status === AuthStatus.LoggedIn) {
|
||||||
|
this.isLoggedIn = true;
|
||||||
|
console.log('Logged in.');
|
||||||
|
} else if (status === AuthStatus.LoggedOut) {
|
||||||
|
this.isLoggedIn = false;
|
||||||
|
console.log('Logged out.');
|
||||||
|
} else {
|
||||||
|
console.error('Unknown auth status: ' + status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
isUserAdmin() {
|
||||||
|
return this.authService.isAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
logout() {
|
||||||
|
this.authService.logout();
|
||||||
|
// Redirect to home page
|
||||||
|
this.router.navigate(['/videos/list']);
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,7 +28,7 @@ export class AuthHttp extends Http {
|
||||||
return super.request(url, options)
|
return super.request(url, options)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (err.status === 401) {
|
if (err.status === 401) {
|
||||||
return this.handleTokenExpired(err, url, options);
|
return this.handleTokenExpired(url, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Observable.throw(err);
|
return Observable.throw(err);
|
||||||
|
@ -49,26 +49,29 @@ export class AuthHttp extends Http {
|
||||||
return this.request(url, options);
|
return this.request(url, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
post(url: string, options?: RequestOptionsArgs): Observable<Response> {
|
post(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
|
||||||
if (!options) options = {};
|
if (!options) options = {};
|
||||||
options.method = RequestMethod.Post;
|
options.method = RequestMethod.Post;
|
||||||
|
options.body = body;
|
||||||
|
|
||||||
return this.request(url, options);
|
return this.request(url, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
put(url: string, options?: RequestOptionsArgs): Observable<Response> {
|
put(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
|
||||||
if (!options) options = {};
|
if (!options) options = {};
|
||||||
options.method = RequestMethod.Put;
|
options.method = RequestMethod.Put;
|
||||||
|
options.body = body;
|
||||||
|
|
||||||
return this.request(url, options);
|
return this.request(url, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleTokenExpired(err: Response, url: string | Request, options: RequestOptionsArgs) {
|
private handleTokenExpired(url: string | Request, options: RequestOptionsArgs) {
|
||||||
return this.authService.refreshAccessToken().flatMap(() => {
|
return this.authService.refreshAccessToken()
|
||||||
this.setAuthorizationHeader(options.headers);
|
.flatMap(() => {
|
||||||
|
this.setAuthorizationHeader(options.headers);
|
||||||
|
|
||||||
return super.request(url, options);
|
return super.request(url, options);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private setAuthorizationHeader(headers: Headers) {
|
private setAuthorizationHeader(headers: Headers) {
|
||||||
|
|
|
@ -1,15 +1,28 @@
|
||||||
export class User {
|
import { User } from '../users';
|
||||||
|
|
||||||
|
export class AuthUser extends User {
|
||||||
private static KEYS = {
|
private static KEYS = {
|
||||||
|
ID: 'id',
|
||||||
|
ROLE: 'role',
|
||||||
USERNAME: 'username'
|
USERNAME: 'username'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
id: string;
|
||||||
|
role: string;
|
||||||
username: string;
|
username: string;
|
||||||
tokens: Tokens;
|
tokens: Tokens;
|
||||||
|
|
||||||
static load() {
|
static load() {
|
||||||
const usernameLocalStorage = localStorage.getItem(this.KEYS.USERNAME);
|
const usernameLocalStorage = localStorage.getItem(this.KEYS.USERNAME);
|
||||||
if (usernameLocalStorage) {
|
if (usernameLocalStorage) {
|
||||||
return new User(localStorage.getItem(this.KEYS.USERNAME), Tokens.load());
|
return new AuthUser(
|
||||||
|
{
|
||||||
|
id: localStorage.getItem(this.KEYS.ID),
|
||||||
|
username: localStorage.getItem(this.KEYS.USERNAME),
|
||||||
|
role: localStorage.getItem(this.KEYS.ROLE)
|
||||||
|
},
|
||||||
|
Tokens.load()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -17,12 +30,14 @@ export class User {
|
||||||
|
|
||||||
static flush() {
|
static flush() {
|
||||||
localStorage.removeItem(this.KEYS.USERNAME);
|
localStorage.removeItem(this.KEYS.USERNAME);
|
||||||
|
localStorage.removeItem(this.KEYS.ID);
|
||||||
|
localStorage.removeItem(this.KEYS.ROLE);
|
||||||
Tokens.flush();
|
Tokens.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(username: string, hash_tokens: any) {
|
constructor(userHash: { id: string, username: string, role: string }, hashTokens: any) {
|
||||||
this.username = username;
|
super(userHash);
|
||||||
this.tokens = new Tokens(hash_tokens);
|
this.tokens = new Tokens(hashTokens);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAccessToken() {
|
getAccessToken() {
|
||||||
|
@ -43,12 +58,14 @@ export class User {
|
||||||
}
|
}
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
localStorage.setItem('username', this.username);
|
localStorage.setItem(AuthUser.KEYS.ID, this.id);
|
||||||
|
localStorage.setItem(AuthUser.KEYS.USERNAME, this.username);
|
||||||
|
localStorage.setItem(AuthUser.KEYS.ROLE, this.role);
|
||||||
this.tokens.save();
|
this.tokens.save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Private class used only by User
|
// Private class only used by User
|
||||||
class Tokens {
|
class Tokens {
|
||||||
private static KEYS = {
|
private static KEYS = {
|
||||||
ACCESS_TOKEN: 'access_token',
|
ACCESS_TOKEN: 'access_token',
|
|
@ -1,32 +1,39 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Headers, Http, Response, URLSearchParams } from '@angular/http';
|
import { Headers, Http, Response, URLSearchParams } from '@angular/http';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { Subject } from 'rxjs/Subject';
|
import { Subject } from 'rxjs/Subject';
|
||||||
|
|
||||||
import { AuthStatus } from './auth-status.model';
|
import { AuthStatus } from './auth-status.model';
|
||||||
import { User } from './user.model';
|
import { AuthUser } from './auth-user.model';
|
||||||
|
import { RestExtractor } from '../rest';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
private static BASE_CLIENT_URL = '/api/v1/users/client';
|
private static BASE_CLIENT_URL = '/api/v1/clients/local';
|
||||||
private static BASE_TOKEN_URL = '/api/v1/users/token';
|
private static BASE_TOKEN_URL = '/api/v1/users/token';
|
||||||
|
private static BASE_USER_INFORMATIONS_URL = '/api/v1/users/me';
|
||||||
|
|
||||||
loginChangedSource: Observable<AuthStatus>;
|
loginChangedSource: Observable<AuthStatus>;
|
||||||
|
|
||||||
private clientId: string;
|
private clientId: string;
|
||||||
private clientSecret: string;
|
private clientSecret: string;
|
||||||
private loginChanged: Subject<AuthStatus>;
|
private loginChanged: Subject<AuthStatus>;
|
||||||
private user: User = null;
|
private user: AuthUser = null;
|
||||||
|
|
||||||
constructor(private http: Http) {
|
constructor(
|
||||||
|
private http: Http,
|
||||||
|
private restExtractor: RestExtractor,
|
||||||
|
private router: Router
|
||||||
|
) {
|
||||||
this.loginChanged = new Subject<AuthStatus>();
|
this.loginChanged = new Subject<AuthStatus>();
|
||||||
this.loginChangedSource = this.loginChanged.asObservable();
|
this.loginChangedSource = this.loginChanged.asObservable();
|
||||||
|
|
||||||
// Fetch the client_id/client_secret
|
// Fetch the client_id/client_secret
|
||||||
// FIXME: save in local storage?
|
// FIXME: save in local storage?
|
||||||
this.http.get(AuthService.BASE_CLIENT_URL)
|
this.http.get(AuthService.BASE_CLIENT_URL)
|
||||||
.map(res => res.json())
|
.map(this.restExtractor.extractDataGet)
|
||||||
.catch(this.handleError)
|
.catch((res) => this.restExtractor.handleError(res))
|
||||||
.subscribe(
|
.subscribe(
|
||||||
result => {
|
result => {
|
||||||
this.clientId = result.client_id;
|
this.clientId = result.client_id;
|
||||||
|
@ -34,12 +41,15 @@ export class AuthService {
|
||||||
console.log('Client credentials loaded.');
|
console.log('Client credentials loaded.');
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
alert(error);
|
alert(
|
||||||
|
`Cannot retrieve OAuth Client credentials: ${error.text}. \n` +
|
||||||
|
'Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Return null if there is nothing to load
|
// Return null if there is nothing to load
|
||||||
this.user = User.load();
|
this.user = AuthUser.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
getRefreshToken() {
|
getRefreshToken() {
|
||||||
|
@ -64,10 +74,16 @@ export class AuthService {
|
||||||
return this.user.getTokenType();
|
return this.user.getTokenType();
|
||||||
}
|
}
|
||||||
|
|
||||||
getUser(): User {
|
getUser(): AuthUser {
|
||||||
return this.user;
|
return this.user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isAdmin() {
|
||||||
|
if (this.user === null) return false;
|
||||||
|
|
||||||
|
return this.user.isAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
isLoggedIn() {
|
isLoggedIn() {
|
||||||
if (this.getAccessToken()) {
|
if (this.getAccessToken()) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -94,21 +110,23 @@ export class AuthService {
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options)
|
return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options)
|
||||||
.map(res => res.json())
|
.map(this.restExtractor.extractDataGet)
|
||||||
.map(res => {
|
.map(res => {
|
||||||
res.username = username;
|
res.username = username;
|
||||||
return res;
|
return res;
|
||||||
})
|
})
|
||||||
|
.flatMap(res => this.fetchUserInformations(res))
|
||||||
.map(res => this.handleLogin(res))
|
.map(res => this.handleLogin(res))
|
||||||
.catch(this.handleError);
|
.catch((res) => this.restExtractor.handleError(res));
|
||||||
}
|
}
|
||||||
|
|
||||||
logout() {
|
logout() {
|
||||||
// TODO: make an HTTP request to revoke the tokens
|
// TODO: make an HTTP request to revoke the tokens
|
||||||
this.user = null;
|
this.user = null;
|
||||||
User.flush();
|
|
||||||
|
|
||||||
this.setStatus(AuthStatus.LoggedIn);
|
AuthUser.flush();
|
||||||
|
|
||||||
|
this.setStatus(AuthStatus.LoggedOut);
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshAccessToken() {
|
refreshAccessToken() {
|
||||||
|
@ -131,36 +149,64 @@ export class AuthService {
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options)
|
return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options)
|
||||||
.map(res => res.json())
|
.map(this.restExtractor.extractDataGet)
|
||||||
.map(res => this.handleRefreshToken(res))
|
.map(res => this.handleRefreshToken(res))
|
||||||
.catch(this.handleError);
|
.catch((res: Response) => {
|
||||||
|
// The refresh token is invalid?
|
||||||
|
if (res.status === 400 && res.json() && res.json().error === 'invalid_grant') {
|
||||||
|
console.error('Cannot refresh token -> logout...');
|
||||||
|
this.logout();
|
||||||
|
this.router.navigate(['/login']);
|
||||||
|
|
||||||
|
return Observable.throw({
|
||||||
|
json: '',
|
||||||
|
text: 'You need to reconnect.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.restExtractor.handleError(res);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private setStatus(status: AuthStatus) {
|
private fetchUserInformations (obj: any) {
|
||||||
this.loginChanged.next(status);
|
// Do not call authHttp here to avoid circular dependencies headaches
|
||||||
|
|
||||||
|
const headers = new Headers();
|
||||||
|
headers.set('Authorization', `Bearer ${obj.access_token}`);
|
||||||
|
|
||||||
|
return this.http.get(AuthService.BASE_USER_INFORMATIONS_URL, { headers })
|
||||||
|
.map(res => res.json())
|
||||||
|
.map(res => {
|
||||||
|
obj.id = res.id;
|
||||||
|
obj.role = res.role;
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleLogin (obj: any) {
|
private handleLogin (obj: any) {
|
||||||
|
const id = obj.id;
|
||||||
const username = obj.username;
|
const username = obj.username;
|
||||||
const hash_tokens = {
|
const role = obj.role;
|
||||||
|
const hashTokens = {
|
||||||
access_token: obj.access_token,
|
access_token: obj.access_token,
|
||||||
token_type: obj.token_type,
|
token_type: obj.token_type,
|
||||||
refresh_token: obj.refresh_token
|
refresh_token: obj.refresh_token
|
||||||
};
|
};
|
||||||
|
|
||||||
this.user = new User(username, hash_tokens);
|
this.user = new AuthUser({ id, username, role }, hashTokens);
|
||||||
this.user.save();
|
this.user.save();
|
||||||
|
|
||||||
this.setStatus(AuthStatus.LoggedIn);
|
this.setStatus(AuthStatus.LoggedIn);
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleError (error: Response) {
|
|
||||||
console.error(error);
|
|
||||||
return Observable.throw(error.json() || { error: 'Server error' });
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleRefreshToken (obj: any) {
|
private handleRefreshToken (obj: any) {
|
||||||
this.user.refreshTokens(obj.access_token, obj.refresh_token);
|
this.user.refreshTokens(obj.access_token, obj.refresh_token);
|
||||||
this.user.save();
|
this.user.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setStatus(status: AuthStatus) {
|
||||||
|
this.loginChanged.next(status);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export * from './auth-http.service';
|
export * from './auth-http.service';
|
||||||
export * from './auth-status.model';
|
export * from './auth-status.model';
|
||||||
export * from './auth.service';
|
export * from './auth.service';
|
||||||
export * from './user.model';
|
export * from './auth-user.model';
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { FormGroup } from '@angular/forms';
|
||||||
|
|
||||||
|
export abstract class FormReactive {
|
||||||
|
abstract form: FormGroup;
|
||||||
|
abstract formErrors: Object;
|
||||||
|
abstract validationMessages: Object;
|
||||||
|
|
||||||
|
abstract buildForm(): void;
|
||||||
|
|
||||||
|
protected onValueChanged(data?: any) {
|
||||||
|
for (const field in this.formErrors) {
|
||||||
|
// clear previous error message (if any)
|
||||||
|
this.formErrors[field] = '';
|
||||||
|
const control = this.form.get(field);
|
||||||
|
|
||||||
|
if (control && control.dirty && !control.valid) {
|
||||||
|
const messages = this.validationMessages[field];
|
||||||
|
for (const key in control.errors) {
|
||||||
|
this.formErrors[field] += messages[key] + ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './url.validator';
|
||||||
|
export * from './user';
|
||||||
|
export * from './video';
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { FormControl } from '@angular/forms';
|
||||||
|
|
||||||
|
export function validateUrl(c: FormControl) {
|
||||||
|
let URL_REGEXP = new RegExp('^https?://(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$');
|
||||||
|
|
||||||
|
return URL_REGEXP.test(c.value) ? null : {
|
||||||
|
validateUrl: {
|
||||||
|
valid: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { Validators } from '@angular/forms';
|
||||||
|
|
||||||
|
export const USER_USERNAME = {
|
||||||
|
VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(20) ],
|
||||||
|
MESSAGES: {
|
||||||
|
'required': 'Username is required.',
|
||||||
|
'minlength': 'Username must be at least 3 characters long.',
|
||||||
|
'maxlength': 'Username cannot be more than 20 characters long.'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export const USER_PASSWORD = {
|
||||||
|
VALIDATORS: [ Validators.required, Validators.minLength(6) ],
|
||||||
|
MESSAGES: {
|
||||||
|
'required': 'Password is required.',
|
||||||
|
'minlength': 'Password must be at least 6 characters long.',
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { Validators } from '@angular/forms';
|
||||||
|
|
||||||
|
export const VIDEO_NAME = {
|
||||||
|
VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(50) ],
|
||||||
|
MESSAGES: {
|
||||||
|
'required': 'Video name is required.',
|
||||||
|
'minlength': 'Video name must be at least 3 characters long.',
|
||||||
|
'maxlength': 'Video name cannot be more than 50 characters long.'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export const VIDEO_DESCRIPTION = {
|
||||||
|
VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(250) ],
|
||||||
|
MESSAGES: {
|
||||||
|
'required': 'Video description is required.',
|
||||||
|
'minlength': 'Video description must be at least 3 characters long.',
|
||||||
|
'maxlength': 'Video description cannot be more than 250 characters long.'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const VIDEO_TAGS = {
|
||||||
|
VALIDATORS: [ Validators.pattern('^[a-zA-Z0-9]{2,10}$') ],
|
||||||
|
MESSAGES: {
|
||||||
|
'pattern': 'A tag should be between 2 and 10 alphanumeric characters long.'
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './form-validators';
|
||||||
|
export * from './form-reactive';
|
|
@ -1,2 +1,5 @@
|
||||||
export * from './auth';
|
export * from './auth';
|
||||||
|
export * from './forms';
|
||||||
|
export * from './rest';
|
||||||
export * from './search';
|
export * from './search';
|
||||||
|
export * from './users';
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './rest-extractor.service';
|
||||||
|
export * from './rest-pagination';
|
||||||
|
export * from './rest.service';
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Response } from '@angular/http';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
|
||||||
|
export interface ResultList {
|
||||||
|
data: any[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RestExtractor {
|
||||||
|
|
||||||
|
constructor () { ; }
|
||||||
|
|
||||||
|
extractDataBool(res: Response) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
extractDataList(res: Response) {
|
||||||
|
const body = res.json();
|
||||||
|
|
||||||
|
const ret: ResultList = {
|
||||||
|
data: body.data,
|
||||||
|
total: body.total
|
||||||
|
};
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
extractDataGet(res: Response) {
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleError(res: Response) {
|
||||||
|
let text = 'Server error: ';
|
||||||
|
text += res.text();
|
||||||
|
let json = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
json = res.json();
|
||||||
|
} catch (err) { ; }
|
||||||
|
|
||||||
|
const error = {
|
||||||
|
json,
|
||||||
|
text
|
||||||
|
};
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
return Observable.throw(error);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
export interface Pagination {
|
export interface RestPagination {
|
||||||
currentPage: number;
|
currentPage: number;
|
||||||
itemsPerPage: number;
|
itemsPerPage: number;
|
||||||
totalItems: number;
|
totalItems: number;
|
||||||
}
|
};
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { URLSearchParams } from '@angular/http';
|
||||||
|
|
||||||
|
import { RestPagination } from './rest-pagination';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RestService {
|
||||||
|
|
||||||
|
buildRestGetParams(pagination?: RestPagination, sort?: string) {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
if (pagination) {
|
||||||
|
const start: number = (pagination.currentPage - 1) * pagination.itemsPerPage;
|
||||||
|
const count: number = pagination.itemsPerPage;
|
||||||
|
|
||||||
|
params.set('start', start.toString());
|
||||||
|
params.set('count', count.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sort) {
|
||||||
|
params.set('sort', sort);
|
||||||
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,15 +1,13 @@
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
import { DROPDOWN_DIRECTIVES} from 'ng2-bootstrap/components/dropdown';
|
|
||||||
|
|
||||||
import { Search } from './search.model';
|
import { Search } from './search.model';
|
||||||
import { SearchField } from './search-field.type';
|
import { SearchField } from './search-field.type';
|
||||||
import { SearchService } from './search.service';
|
import { SearchService } from './search.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-search',
|
selector: 'my-search',
|
||||||
template: require('./search.component.html'),
|
templateUrl: './search.component.html'
|
||||||
directives: [ DROPDOWN_DIRECTIVES ]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export class SearchComponent implements OnInit {
|
export class SearchComponent implements OnInit {
|
||||||
|
@ -25,10 +23,10 @@ export class SearchComponent implements OnInit {
|
||||||
value: ''
|
value: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(private searchService: SearchService) {}
|
constructor(private searchService: SearchService, private router: Router) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
// Subscribe is the search changed
|
// Subscribe if the search changed
|
||||||
// Usually changed by videos list component
|
// Usually changed by videos list component
|
||||||
this.searchService.updateSearch.subscribe(
|
this.searchService.updateSearch.subscribe(
|
||||||
newSearchCriterias => {
|
newSearchCriterias => {
|
||||||
|
@ -58,6 +56,10 @@ export class SearchComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
doSearch() {
|
doSearch() {
|
||||||
|
if (this.router.url.indexOf('/videos/list') === -1) {
|
||||||
|
this.router.navigate([ '/videos/list' ]);
|
||||||
|
}
|
||||||
|
|
||||||
this.searchService.searchUpdated.next(this.searchCriterias);
|
this.searchService.searchUpdated.next(this.searchCriterias);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Subject } from 'rxjs/Subject';
|
import { Subject } from 'rxjs/Subject';
|
||||||
|
import { ReplaySubject } from 'rxjs/ReplaySubject';
|
||||||
|
|
||||||
import { Search } from './search.model';
|
import { Search } from './search.model';
|
||||||
|
|
||||||
|
@ -12,6 +13,6 @@ export class SearchService {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.updateSearch = new Subject<Search>();
|
this.updateSearch = new Subject<Search>();
|
||||||
this.searchUpdated = new Subject<Search>();
|
this.searchUpdated = new ReplaySubject<Search>(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './user.model';
|
|
@ -0,0 +1,20 @@
|
||||||
|
export class User {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
role: string;
|
||||||
|
createdDate: Date;
|
||||||
|
|
||||||
|
constructor(hash: { id: string, username: string, role: string, createdDate?: Date }) {
|
||||||
|
this.id = hash.id;
|
||||||
|
this.username = hash.username;
|
||||||
|
this.role = hash.role;
|
||||||
|
|
||||||
|
if (hash.createdDate) {
|
||||||
|
this.createdDate = hash.createdDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isAdmin() {
|
||||||
|
return this.role === 'admin';
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
export * from './loader';
|
export * from './loader';
|
||||||
export * from './pagination.model';
|
|
||||||
export * from './sort-field.type';
|
export * from './sort-field.type';
|
||||||
export * from './video.model';
|
export * from './video.model';
|
||||||
export * from './video.service';
|
export * from './video.service';
|
||||||
|
|
|
@ -2,8 +2,8 @@ import { Component, Input } from '@angular/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-loader',
|
selector: 'my-loader',
|
||||||
styles: [ require('./loader.component.scss') ],
|
styleUrls: [ './loader.component.scss' ],
|
||||||
template: require('./loader.component.html')
|
templateUrl: './loader.component.html'
|
||||||
})
|
})
|
||||||
|
|
||||||
export class LoaderComponent {
|
export class LoaderComponent {
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Http, Response, URLSearchParams } from '@angular/http';
|
import { Http } from '@angular/http';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
|
||||||
import { Pagination } from './pagination.model';
|
|
||||||
import { Search } from '../../shared';
|
import { Search } from '../../shared';
|
||||||
import { SortField } from './sort-field.type';
|
import { SortField } from './sort-field.type';
|
||||||
import { AuthHttp, AuthService } from '../../shared';
|
import { AuthHttp, AuthService, RestExtractor, RestPagination, RestService, ResultList } from '../../shared';
|
||||||
import { Video } from './video.model';
|
import { Video } from './video.model';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -15,68 +14,51 @@ export class VideoService {
|
||||||
constructor(
|
constructor(
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private authHttp: AuthHttp,
|
private authHttp: AuthHttp,
|
||||||
private http: Http
|
private http: Http,
|
||||||
|
private restExtractor: RestExtractor,
|
||||||
|
private restService: RestService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
getVideo(id: string) {
|
getVideo(id: string): Observable<Video> {
|
||||||
return this.http.get(VideoService.BASE_VIDEO_URL + id)
|
return this.http.get(VideoService.BASE_VIDEO_URL + id)
|
||||||
.map(res => <Video> res.json())
|
.map(this.restExtractor.extractDataGet)
|
||||||
.catch(this.handleError);
|
.catch((res) => this.restExtractor.handleError(res));
|
||||||
}
|
}
|
||||||
|
|
||||||
getVideos(pagination: Pagination, sort: SortField) {
|
getVideos(pagination: RestPagination, sort: SortField) {
|
||||||
const params = this.createPaginationParams(pagination);
|
const params = this.restService.buildRestGetParams(pagination, sort);
|
||||||
|
|
||||||
if (sort) params.set('sort', sort);
|
|
||||||
|
|
||||||
return this.http.get(VideoService.BASE_VIDEO_URL, { search: params })
|
return this.http.get(VideoService.BASE_VIDEO_URL, { search: params })
|
||||||
.map(res => res.json())
|
.map(res => res.json())
|
||||||
.map(this.extractVideos)
|
.map(this.extractVideos)
|
||||||
.catch(this.handleError);
|
.catch((res) => this.restExtractor.handleError(res));
|
||||||
}
|
}
|
||||||
|
|
||||||
removeVideo(id: string) {
|
removeVideo(id: string) {
|
||||||
return this.authHttp.delete(VideoService.BASE_VIDEO_URL + id)
|
return this.authHttp.delete(VideoService.BASE_VIDEO_URL + id)
|
||||||
.map(res => <number> res.status)
|
.map(this.restExtractor.extractDataBool)
|
||||||
.catch(this.handleError);
|
.catch((res) => this.restExtractor.handleError(res));
|
||||||
}
|
}
|
||||||
|
|
||||||
searchVideos(search: Search, pagination: Pagination, sort: SortField) {
|
searchVideos(search: Search, pagination: RestPagination, sort: SortField) {
|
||||||
const params = this.createPaginationParams(pagination);
|
const params = this.restService.buildRestGetParams(pagination, sort);
|
||||||
|
|
||||||
if (search.field) params.set('field', search.field);
|
if (search.field) params.set('field', search.field);
|
||||||
if (sort) params.set('sort', sort);
|
|
||||||
|
|
||||||
return this.http.get(VideoService.BASE_VIDEO_URL + 'search/' + encodeURIComponent(search.value), { search: params })
|
return this.http.get(VideoService.BASE_VIDEO_URL + 'search/' + encodeURIComponent(search.value), { search: params })
|
||||||
.map(res => res.json())
|
.map(this.restExtractor.extractDataList)
|
||||||
.map(this.extractVideos)
|
.map(this.extractVideos)
|
||||||
.catch(this.handleError);
|
.catch((res) => this.restExtractor.handleError(res));
|
||||||
}
|
}
|
||||||
|
|
||||||
private createPaginationParams(pagination: Pagination) {
|
private extractVideos(result: ResultList) {
|
||||||
const params = new URLSearchParams();
|
const videosJson = result.data;
|
||||||
const start: number = (pagination.currentPage - 1) * pagination.itemsPerPage;
|
const totalVideos = result.total;
|
||||||
const count: number = pagination.itemsPerPage;
|
|
||||||
|
|
||||||
params.set('start', start.toString());
|
|
||||||
params.set('count', count.toString());
|
|
||||||
|
|
||||||
return params;
|
|
||||||
}
|
|
||||||
|
|
||||||
private extractVideos(body: any) {
|
|
||||||
const videos_json = body.data;
|
|
||||||
const totalVideos = body.total;
|
|
||||||
const videos = [];
|
const videos = [];
|
||||||
for (const video_json of videos_json) {
|
for (const videoJson of videosJson) {
|
||||||
videos.push(new Video(video_json));
|
videos.push(new Video(videoJson));
|
||||||
}
|
}
|
||||||
|
|
||||||
return { videos, totalVideos };
|
return { videos, totalVideos };
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleError(error: Response) {
|
|
||||||
console.error(error);
|
|
||||||
return Observable.throw(error.json().error || 'Server error');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,31 +2,31 @@
|
||||||
|
|
||||||
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
|
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
|
||||||
|
|
||||||
<form novalidate (ngSubmit)="upload()" [ngFormModel]="videoForm">
|
<form novalidate (ngSubmit)="upload()" [formGroup]="form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="name">Name</label>
|
<label for="name">Name</label>
|
||||||
<input
|
<input
|
||||||
type="text" class="form-control" name="name" id="name"
|
type="text" class="form-control" id="name"
|
||||||
ngControl="name" #name="ngForm" [(ngModel)]="video.name"
|
formControlName="name"
|
||||||
>
|
>
|
||||||
<div [hidden]="name.valid || name.pristine" class="alert alert-warning">
|
<div *ngIf="formErrors.name" class="alert alert-danger">
|
||||||
A name is required and should be between 3 and 50 characters long
|
{{ formErrors.name }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="tags">Tags</label>
|
<label for="tags">Tags</label>
|
||||||
<input
|
<input
|
||||||
type="text" class="form-control" name="tags" id="tags"
|
type="text" class="form-control" id="currentTag"
|
||||||
ngControl="tags" #tags="ngForm" [disabled]="isTagsInputDisabled" (keyup)="onTagKeyPress($event)" [(ngModel)]="currentTag"
|
formControlName="currentTag" (keyup)="onTagKeyPress($event)"
|
||||||
>
|
>
|
||||||
<div [hidden]="tags.valid || tags.pristine" class="alert alert-warning">
|
<div *ngIf="formErrors.currentTag" class="alert alert-danger">
|
||||||
A tag should be between 2 and 10 characters (alphanumeric) long
|
{{ formErrors.currentTag }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tags">
|
<div class="tags">
|
||||||
<div class="label label-primary tag" *ngFor="let tag of video.tags">
|
<div class="label label-primary tag" *ngFor="let tag of tags">
|
||||||
{{ tag }}
|
{{ tag }}
|
||||||
<span class="remove" (click)="removeTag(tag)">x</span>
|
<span class="remove" (click)="removeTag(tag)">x</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -53,12 +53,12 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="description">Description</label>
|
<label for="description">Description</label>
|
||||||
<textarea
|
<textarea
|
||||||
name="description" id="description" class="form-control" placeholder="Description..."
|
id="description" class="form-control" placeholder="Description..."
|
||||||
ngControl="description" #description="ngForm" [(ngModel)]="video.description"
|
formControlName="description"
|
||||||
>
|
>
|
||||||
</textarea>
|
</textarea>
|
||||||
<div [hidden]="description.valid || description.pristine" class="alert alert-warning">
|
<div *ngIf="formErrors.description" class="alert alert-danger">
|
||||||
A description is required and should be between 3 and 250 characters long
|
{{ formErrors.description }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input
|
<input
|
||||||
type="submit" value="Upload" class="btn btn-default form-control" [title]="getInvalidFieldsTitle()"
|
type="submit" value="Upload" class="btn btn-default form-control" [title]="getInvalidFieldsTitle()"
|
||||||
[disabled]="!videoForm.valid || video.tags.length === 0 || filename === null"
|
[disabled]="!form.valid || tags.length === 0 || filename === null"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -1,37 +1,42 @@
|
||||||
import { Control, ControlGroup, Validators } from '@angular/common';
|
|
||||||
import { Component, ElementRef, OnInit } from '@angular/core';
|
import { Component, ElementRef, OnInit } from '@angular/core';
|
||||||
|
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe';
|
import { FileUploader } from 'ng2-file-upload/ng2-file-upload';
|
||||||
import { PROGRESSBAR_DIRECTIVES } from 'ng2-bootstrap/components/progressbar';
|
|
||||||
import { FileSelectDirective, FileUploader } from 'ng2-file-upload/ng2-file-upload';
|
|
||||||
|
|
||||||
import { AuthService } from '../../shared';
|
import { AuthService, FormReactive, VIDEO_NAME, VIDEO_DESCRIPTION, VIDEO_TAGS } from '../../shared';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-videos-add',
|
selector: 'my-videos-add',
|
||||||
styles: [ require('./video-add.component.scss') ],
|
styleUrls: [ './video-add.component.scss' ],
|
||||||
template: require('./video-add.component.html'),
|
templateUrl: './video-add.component.html'
|
||||||
directives: [ FileSelectDirective, PROGRESSBAR_DIRECTIVES ],
|
|
||||||
pipes: [ BytesPipe ]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export class VideoAddComponent implements OnInit {
|
export class VideoAddComponent extends FormReactive implements OnInit {
|
||||||
currentTag: string; // Tag the user is writing in the input
|
tags: string[] = [];
|
||||||
error: string = null;
|
|
||||||
videoForm: ControlGroup;
|
|
||||||
uploader: FileUploader;
|
uploader: FileUploader;
|
||||||
video = {
|
|
||||||
|
error: string = null;
|
||||||
|
form: FormGroup;
|
||||||
|
formErrors = {
|
||||||
name: '',
|
name: '',
|
||||||
tags: [],
|
description: '',
|
||||||
description: ''
|
currentTag: ''
|
||||||
|
};
|
||||||
|
validationMessages = {
|
||||||
|
name: VIDEO_NAME.MESSAGES,
|
||||||
|
description: VIDEO_DESCRIPTION.MESSAGES,
|
||||||
|
currentTag: VIDEO_TAGS.MESSAGES
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private elementRef: ElementRef,
|
private elementRef: ElementRef,
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
private router: Router
|
private router: Router
|
||||||
) {}
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
get filename() {
|
get filename() {
|
||||||
if (this.uploader.queue.length === 0) {
|
if (this.uploader.queue.length === 0) {
|
||||||
|
@ -41,20 +46,26 @@ export class VideoAddComponent implements OnInit {
|
||||||
return this.uploader.queue[0].file.name;
|
return this.uploader.queue[0].file.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isTagsInputDisabled () {
|
buildForm() {
|
||||||
return this.video.tags.length >= 3;
|
this.form = this.formBuilder.group({
|
||||||
|
name: [ '', VIDEO_NAME.VALIDATORS ],
|
||||||
|
description: [ '', VIDEO_DESCRIPTION.VALIDATORS ],
|
||||||
|
currentTag: [ '', VIDEO_TAGS.VALIDATORS ]
|
||||||
|
});
|
||||||
|
|
||||||
|
this.form.valueChanges.subscribe(data => this.onValueChanged(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
getInvalidFieldsTitle() {
|
getInvalidFieldsTitle() {
|
||||||
let title = '';
|
let title = '';
|
||||||
const nameControl = this.videoForm.controls['name'];
|
const nameControl = this.form.controls['name'];
|
||||||
const descriptionControl = this.videoForm.controls['description'];
|
const descriptionControl = this.form.controls['description'];
|
||||||
|
|
||||||
if (!nameControl.valid) {
|
if (!nameControl.valid) {
|
||||||
title += 'A name is required\n';
|
title += 'A name is required\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.video.tags.length === 0) {
|
if (this.tags.length === 0) {
|
||||||
title += 'At least one tag is required\n';
|
title += 'At least one tag is required\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,13 +81,6 @@ export class VideoAddComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.videoForm = new ControlGroup({
|
|
||||||
name: new Control('', Validators.compose([ Validators.required, Validators.minLength(3), Validators.maxLength(50) ])),
|
|
||||||
description: new Control('', Validators.compose([ Validators.required, Validators.minLength(3), Validators.maxLength(250) ])),
|
|
||||||
tags: new Control('', Validators.pattern('^[a-zA-Z0-9]{2,10}$'))
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
this.uploader = new FileUploader({
|
this.uploader = new FileUploader({
|
||||||
authToken: this.authService.getRequestHeaderValue(),
|
authToken: this.authService.getRequestHeaderValue(),
|
||||||
queueLimit: 1,
|
queueLimit: 1,
|
||||||
|
@ -85,26 +89,37 @@ export class VideoAddComponent implements OnInit {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.uploader.onBuildItemForm = (item, form) => {
|
this.uploader.onBuildItemForm = (item, form) => {
|
||||||
form.append('name', this.video.name);
|
const name = this.form.value['name'];
|
||||||
form.append('description', this.video.description);
|
const description = this.form.value['description'];
|
||||||
|
|
||||||
for (let i = 0; i < this.video.tags.length; i++) {
|
form.append('name', name);
|
||||||
form.append(`tags[${i}]`, this.video.tags[i]);
|
form.append('description', description);
|
||||||
|
|
||||||
|
for (let i = 0; i < this.tags.length; i++) {
|
||||||
|
form.append(`tags[${i}]`, this.tags[i]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.buildForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
onTagKeyPress(event: KeyboardEvent) {
|
onTagKeyPress(event: KeyboardEvent) {
|
||||||
|
const currentTag = this.form.value['currentTag'];
|
||||||
|
|
||||||
// Enter press
|
// Enter press
|
||||||
if (event.keyCode === 13) {
|
if (event.keyCode === 13) {
|
||||||
// Check if the tag is valid and does not already exist
|
// Check if the tag is valid and does not already exist
|
||||||
if (
|
if (
|
||||||
this.currentTag !== '' &&
|
currentTag !== '' &&
|
||||||
this.videoForm.controls['tags'].valid &&
|
this.form.controls['currentTag'].valid &&
|
||||||
this.video.tags.indexOf(this.currentTag) === -1
|
this.tags.indexOf(currentTag) === -1
|
||||||
) {
|
) {
|
||||||
this.video.tags.push(this.currentTag);
|
this.tags.push(currentTag);
|
||||||
this.currentTag = '';
|
this.form.patchValue({ currentTag: '' });
|
||||||
|
|
||||||
|
if (this.tags.length >= 3) {
|
||||||
|
this.form.get('currentTag').disable();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -114,7 +129,7 @@ export class VideoAddComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
removeTag(tag: string) {
|
removeTag(tag: string) {
|
||||||
this.video.tags.splice(this.video.tags.indexOf(tag), 1);
|
this.tags.splice(this.tags.indexOf(tag), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
upload() {
|
upload() {
|
||||||
|
|
|
@ -1,39 +1,30 @@
|
||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { AsyncPipe } from '@angular/common';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { ActivatedRoute, Router, ROUTER_DIRECTIVES } from '@angular/router';
|
|
||||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||||
|
|
||||||
import { PAGINATION_DIRECTIVES } from 'ng2-bootstrap/components/pagination';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
LoaderComponent,
|
|
||||||
Pagination,
|
|
||||||
SortField,
|
SortField,
|
||||||
Video,
|
Video,
|
||||||
VideoService
|
VideoService
|
||||||
} from '../shared';
|
} from '../shared';
|
||||||
import { AuthService, Search, SearchField, User } from '../../shared';
|
import { AuthService, AuthUser, RestPagination, Search, SearchField } from '../../shared';
|
||||||
import { VideoMiniatureComponent } from './video-miniature.component';
|
|
||||||
import { VideoSortComponent } from './video-sort.component';
|
|
||||||
import { SearchService } from '../../shared';
|
import { SearchService } from '../../shared';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-videos-list',
|
selector: 'my-videos-list',
|
||||||
styles: [ require('./video-list.component.scss') ],
|
styleUrls: [ './video-list.component.scss' ],
|
||||||
pipes: [ AsyncPipe ],
|
templateUrl: './video-list.component.html'
|
||||||
template: require('./video-list.component.html'),
|
|
||||||
directives: [ LoaderComponent, PAGINATION_DIRECTIVES, ROUTER_DIRECTIVES, VideoMiniatureComponent, VideoSortComponent ]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export class VideoListComponent implements OnInit, OnDestroy {
|
export class VideoListComponent implements OnInit, OnDestroy {
|
||||||
loading: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
loading: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||||
pagination: Pagination = {
|
pagination: RestPagination = {
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
itemsPerPage: 9,
|
itemsPerPage: 9,
|
||||||
totalItems: null
|
totalItems: null
|
||||||
};
|
};
|
||||||
sort: SortField;
|
sort: SortField;
|
||||||
user: User = null;
|
user: AuthUser = null;
|
||||||
videos: Video[] = [];
|
videos: Video[] = [];
|
||||||
|
|
||||||
private search: Search;
|
private search: Search;
|
||||||
|
@ -51,7 +42,7 @@ export class VideoListComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
if (this.authService.isLoggedIn()) {
|
if (this.authService.isLoggedIn()) {
|
||||||
this.user = User.load();
|
this.user = AuthUser.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscribe to route changes
|
// Subscribe to route changes
|
||||||
|
@ -66,6 +57,8 @@ export class VideoListComponent implements OnInit, OnDestroy {
|
||||||
// Subscribe to search changes
|
// Subscribe to search changes
|
||||||
this.subSearch = this.searchService.searchUpdated.subscribe(search => {
|
this.subSearch = this.searchService.searchUpdated.subscribe(search => {
|
||||||
this.search = search;
|
this.search = search;
|
||||||
|
// Reset pagination
|
||||||
|
this.pagination.currentPage = 1;
|
||||||
|
|
||||||
this.navigateToNewParams();
|
this.navigateToNewParams();
|
||||||
});
|
});
|
||||||
|
@ -76,7 +69,7 @@ export class VideoListComponent implements OnInit, OnDestroy {
|
||||||
this.subSearch.unsubscribe();
|
this.subSearch.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
getVideos(detectChanges = true) {
|
getVideos() {
|
||||||
this.loading.next(true);
|
this.loading.next(true);
|
||||||
this.videos = [];
|
this.videos = [];
|
||||||
|
|
||||||
|
@ -97,7 +90,7 @@ export class VideoListComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
this.loading.next(false);
|
this.loading.next(false);
|
||||||
},
|
},
|
||||||
error => alert(error)
|
error => alert(error.text)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,7 +146,11 @@ export class VideoListComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
this.sort = <SortField>routeParams['sort'] || '-createdDate';
|
this.sort = <SortField>routeParams['sort'] || '-createdDate';
|
||||||
|
|
||||||
this.pagination.currentPage = parseInt(routeParams['page']) || 1;
|
if (routeParams['page'] !== undefined) {
|
||||||
|
this.pagination.currentPage = parseInt(routeParams['page']);
|
||||||
|
} else {
|
||||||
|
this.pagination.currentPage = 1;
|
||||||
|
}
|
||||||
|
|
||||||
this.changeDetector.detectChanges();
|
this.changeDetector.detectChanges();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,12 @@
|
||||||
import { DatePipe } from '@angular/common';
|
|
||||||
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||||
import { ROUTER_DIRECTIVES } from '@angular/router';
|
|
||||||
|
|
||||||
import { SortField, Video, VideoService } from '../shared';
|
import { SortField, Video, VideoService } from '../shared';
|
||||||
import { User } from '../../shared';
|
import { User } from '../../shared';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-video-miniature',
|
selector: 'my-video-miniature',
|
||||||
styles: [ require('./video-miniature.component.scss') ],
|
styleUrls: [ './video-miniature.component.scss' ],
|
||||||
template: require('./video-miniature.component.html'),
|
templateUrl: './video-miniature.component.html'
|
||||||
directives: [ ROUTER_DIRECTIVES ],
|
|
||||||
pipes: [ DatePipe ]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export class VideoMiniatureComponent {
|
export class VideoMiniatureComponent {
|
||||||
|
@ -40,7 +36,7 @@ export class VideoMiniatureComponent {
|
||||||
if (confirm('Do you really want to remove this video?')) {
|
if (confirm('Do you really want to remove this video?')) {
|
||||||
this.videoService.removeVideo(id).subscribe(
|
this.videoService.removeVideo(id).subscribe(
|
||||||
status => this.removed.emit(true),
|
status => this.removed.emit(true),
|
||||||
error => alert(error)
|
error => alert(error.text)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { SortField } from '../shared';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-video-sort',
|
selector: 'my-video-sort',
|
||||||
template: require('./video-sort.component.html')
|
templateUrl: './video-sort.component.html'
|
||||||
})
|
})
|
||||||
|
|
||||||
export class VideoSortComponent {
|
export class VideoSortComponent {
|
||||||
|
|
|
@ -1,18 +1,13 @@
|
||||||
import { Component, ElementRef, OnDestroy, OnInit } from '@angular/core';
|
import { Component, ElementRef, NgZone, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe';
|
import { Video, VideoService } from '../shared';
|
||||||
|
|
||||||
import { LoaderComponent, Video, VideoService } from '../shared';
|
|
||||||
import { WebTorrentService } from './webtorrent.service';
|
import { WebTorrentService } from './webtorrent.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-video-watch',
|
selector: 'my-video-watch',
|
||||||
template: require('./video-watch.component.html'),
|
templateUrl: './video-watch.component.html',
|
||||||
styles: [ require('./video-watch.component.scss') ],
|
styleUrls: [ './video-watch.component.scss' ]
|
||||||
providers: [ WebTorrentService ],
|
|
||||||
directives: [ LoaderComponent ],
|
|
||||||
pipes: [ BytesPipe ]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export class VideoWatchComponent implements OnInit, OnDestroy {
|
export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||||
|
@ -31,6 +26,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private elementRef: ElementRef,
|
private elementRef: ElementRef,
|
||||||
|
private ngZone: NgZone,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private videoService: VideoService,
|
private videoService: VideoService,
|
||||||
private webTorrentService: WebTorrentService
|
private webTorrentService: WebTorrentService
|
||||||
|
@ -65,12 +61,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Refresh each second
|
this.runInProgress(torrent);
|
||||||
this.torrentInfosInterval = setInterval(() => {
|
|
||||||
this.downloadSpeed = torrent.downloadSpeed;
|
|
||||||
this.numPeers = torrent.numPeers;
|
|
||||||
this.uploadSpeed = torrent.uploadSpeed;
|
|
||||||
}, 1000);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,7 +82,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||||
this.video = video;
|
this.video = video;
|
||||||
this.loadVideo();
|
this.loadVideo();
|
||||||
},
|
},
|
||||||
error => alert(error)
|
error => alert(error.text)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -100,4 +91,15 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||||
this.error = true;
|
this.error = true;
|
||||||
console.error('The video load seems to be abnormally long.');
|
console.error('The video load seems to be abnormally long.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private runInProgress(torrent: any) {
|
||||||
|
// Refresh each second
|
||||||
|
this.torrentInfosInterval = setInterval(() => {
|
||||||
|
this.ngZone.run(() => {
|
||||||
|
this.downloadSpeed = torrent.downloadSpeed;
|
||||||
|
this.numPeers = torrent.numPeers;
|
||||||
|
this.uploadSpeed = torrent.uploadSpeed;
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { ROUTER_DIRECTIVES } from '@angular/router';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: '<router-outlet></router-outlet>',
|
template: '<router-outlet></router-outlet>'
|
||||||
directives: [ ROUTER_DIRECTIVES ]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export class VideosComponent {
|
export class VideosComponent {
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { RouterConfig } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
|
|
||||||
import { VideoAddComponent } from './video-add';
|
import { VideoAddComponent } from './video-add';
|
||||||
import { VideoListComponent } from './video-list';
|
import { VideoListComponent } from './video-list';
|
||||||
import { VideosComponent } from './videos.component';
|
import { VideosComponent } from './videos.component';
|
||||||
import { VideoWatchComponent } from './video-watch';
|
import { VideoWatchComponent } from './video-watch';
|
||||||
|
|
||||||
export const VideosRoutes: RouterConfig = [
|
export const VideosRoutes: Routes = [
|
||||||
{
|
{
|
||||||
path: 'videos',
|
path: 'videos',
|
||||||
component: VideosComponent,
|
component: VideosComponent,
|
||||||
|
|
|
@ -1,15 +1,27 @@
|
||||||
/*
|
/*
|
||||||
* Custom Type Definitions
|
* Custom Type Definitions
|
||||||
* When including 3rd party modules you also need to include the type definition for the module
|
* When including 3rd party modules you also need to include the type definition for the module
|
||||||
* if they don't provide one within the module. You can try to install it with typings
|
* if they don't provide one within the module. You can try to install it with @types
|
||||||
|
|
||||||
typings install node --save
|
npm install @types/node
|
||||||
|
npm install @types/lodash
|
||||||
|
|
||||||
* If you can't find the type definition in the registry we can make an ambient definition in
|
* If you can't find the type definition in the registry we can make an ambient/global definition in
|
||||||
* this file for now. For example
|
* this file for now. For example
|
||||||
|
|
||||||
declare module "my-module" {
|
declare module 'my-module' {
|
||||||
export function doesSomething(value: string): string;
|
export function doesSomething(value: string): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
* If you are using a CommonJS module that is using module.exports then you will have to write your
|
||||||
|
* types using export = yourObjectOrFunction with a namespace above it
|
||||||
|
* notice how we have to create a namespace that is equal to the function we're
|
||||||
|
* assigning the export to
|
||||||
|
|
||||||
|
declare module 'jwt-decode' {
|
||||||
|
function jwtDecode(token: string): any;
|
||||||
|
namespace jwtDecode {}
|
||||||
|
export = jwtDecode;
|
||||||
}
|
}
|
||||||
|
|
||||||
*
|
*
|
||||||
|
@ -17,33 +29,65 @@ declare module "my-module" {
|
||||||
*
|
*
|
||||||
|
|
||||||
declare var assert: any;
|
declare var assert: any;
|
||||||
|
declare var _: any;
|
||||||
|
declare var $: any;
|
||||||
|
|
||||||
*
|
*
|
||||||
* If you're importing a module that uses Node.js modules which are CommonJS you need to import as
|
* If you're importing a module that uses Node.js modules which are CommonJS you need to import as
|
||||||
|
* in the files such as main.browser.ts or any file within app/
|
||||||
*
|
*
|
||||||
|
|
||||||
import * as _ from 'lodash'
|
import * as _ from 'lodash'
|
||||||
|
|
||||||
* You can include your type definitions in this file until you create one for the typings registry
|
* You can include your type definitions in this file until you create one for the @types
|
||||||
* see https://github.com/typings/registry
|
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// support NodeJS modules without type definitions
|
||||||
|
declare module '*';
|
||||||
|
|
||||||
// Extra variables that live on Global that will be replaced by webpack DefinePlugin
|
// Extra variables that live on Global that will be replaced by webpack DefinePlugin
|
||||||
declare var ENV: string;
|
declare var ENV: string;
|
||||||
declare var HMR: boolean;
|
declare var HMR: boolean;
|
||||||
|
declare var System: SystemJS;
|
||||||
|
|
||||||
|
interface SystemJS {
|
||||||
|
import: (path?: string) => Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
interface GlobalEnvironment {
|
interface GlobalEnvironment {
|
||||||
ENV;
|
ENV;
|
||||||
HMR;
|
HMR;
|
||||||
|
SystemJS: SystemJS;
|
||||||
|
System: SystemJS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Es6PromiseLoader {
|
||||||
|
(id: string): (exportName?: string) => Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type FactoryEs6PromiseLoader = () => Es6PromiseLoader;
|
||||||
|
type FactoryPromise = () => Promise<any>;
|
||||||
|
|
||||||
|
type AsyncRoutes = {
|
||||||
|
[component: string]: Es6PromiseLoader |
|
||||||
|
Function |
|
||||||
|
FactoryEs6PromiseLoader |
|
||||||
|
FactoryPromise
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
type IdleCallbacks = Es6PromiseLoader |
|
||||||
|
Function |
|
||||||
|
FactoryEs6PromiseLoader |
|
||||||
|
FactoryPromise ;
|
||||||
|
|
||||||
interface WebpackModule {
|
interface WebpackModule {
|
||||||
hot: {
|
hot: {
|
||||||
data?: any,
|
data?: any,
|
||||||
idle: any,
|
idle: any,
|
||||||
accept(dependencies?: string | string[], callback?: (updatedDependencies?: any) => void): void;
|
accept(dependencies?: string | string[], callback?: (updatedDependencies?: any) => void): void;
|
||||||
decline(dependencies?: string | string[]): void;
|
decline(deps?: any | string | string[]): void;
|
||||||
dispose(callback?: (data?: any) => void): void;
|
dispose(callback?: (data?: any) => void): void;
|
||||||
addDisposeHandler(callback?: (data?: any) => void): void;
|
addDisposeHandler(callback?: (data?: any) => void): void;
|
||||||
removeDisposeHandler(callback?: (data?: any) => void): void;
|
removeDisposeHandler(callback?: (data?: any) => void): void;
|
||||||
|
@ -54,66 +98,26 @@ interface WebpackModule {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
interface WebpackRequire {
|
interface WebpackRequire {
|
||||||
context(file: string, flag?: boolean, exp?: RegExp): any;
|
(id: string): any;
|
||||||
|
(paths: string[], callback: (...modules: any[]) => void): void;
|
||||||
|
ensure(ids: string[], callback: (req: WebpackRequire) => void, chunkName?: string): void;
|
||||||
|
context(directory: string, useSubDirectories?: boolean, regExp?: RegExp): WebpackContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface WebpackContext extends WebpackRequire {
|
||||||
|
keys(): string[];
|
||||||
|
}
|
||||||
|
|
||||||
interface ErrorStackTraceLimit {
|
interface ErrorStackTraceLimit {
|
||||||
stackTraceLimit: number;
|
stackTraceLimit: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Extend typings
|
// Extend typings
|
||||||
interface NodeRequire extends WebpackRequire {}
|
interface NodeRequire extends WebpackRequire {}
|
||||||
interface ErrorConstructor extends ErrorStackTraceLimit {}
|
interface ErrorConstructor extends ErrorStackTraceLimit {}
|
||||||
|
interface NodeRequireFunction extends Es6PromiseLoader {}
|
||||||
interface NodeModule extends WebpackModule {}
|
interface NodeModule extends WebpackModule {}
|
||||||
interface Global extends GlobalEnvironment {}
|
interface Global extends GlobalEnvironment {}
|
||||||
|
|
||||||
|
|
||||||
declare namespace Reflect {
|
|
||||||
function decorate(decorators: ClassDecorator[], target: Function): Function;
|
|
||||||
function decorate(
|
|
||||||
decorators: (PropertyDecorator | MethodDecorator)[],
|
|
||||||
target: Object,
|
|
||||||
targetKey: string | symbol,
|
|
||||||
descriptor?: PropertyDescriptor): PropertyDescriptor;
|
|
||||||
|
|
||||||
function metadata(metadataKey: any, metadataValue: any): {
|
|
||||||
(target: Function): void;
|
|
||||||
(target: Object, propertyKey: string | symbol): void;
|
|
||||||
};
|
|
||||||
function defineMetadata(metadataKey: any, metadataValue: any, target: Object): void;
|
|
||||||
function defineMetadata(
|
|
||||||
metadataKey: any,
|
|
||||||
metadataValue: any,
|
|
||||||
target: Object,
|
|
||||||
targetKey: string | symbol): void;
|
|
||||||
function hasMetadata(metadataKey: any, target: Object): boolean;
|
|
||||||
function hasMetadata(metadataKey: any, target: Object, targetKey: string | symbol): boolean;
|
|
||||||
function hasOwnMetadata(metadataKey: any, target: Object): boolean;
|
|
||||||
function hasOwnMetadata(metadataKey: any, target: Object, targetKey: string | symbol): boolean;
|
|
||||||
function getMetadata(metadataKey: any, target: Object): any;
|
|
||||||
function getMetadata(metadataKey: any, target: Object, targetKey: string | symbol): any;
|
|
||||||
function getOwnMetadata(metadataKey: any, target: Object): any;
|
|
||||||
function getOwnMetadata(metadataKey: any, target: Object, targetKey: string | symbol): any;
|
|
||||||
function getMetadataKeys(target: Object): any[];
|
|
||||||
function getMetadataKeys(target: Object, targetKey: string | symbol): any[];
|
|
||||||
function getOwnMetadataKeys(target: Object): any[];
|
|
||||||
function getOwnMetadataKeys(target: Object, targetKey: string | symbol): any[];
|
|
||||||
function deleteMetadata(metadataKey: any, target: Object): boolean;
|
|
||||||
function deleteMetadata(metadataKey: any, target: Object, targetKey: string | symbol): boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// We need this here since there is a problem with Zone.js typings
|
|
||||||
interface Thenable<T> {
|
|
||||||
then<U>(
|
|
||||||
onFulfilled?: (value: T) => U | Thenable<U>,
|
|
||||||
onRejected?: (error: any) => U | Thenable<U>): Thenable<U>;
|
|
||||||
then<U>(
|
|
||||||
onFulfilled?: (value: T) => U | Thenable<U>,
|
|
||||||
onRejected?: (error: any) => void): Thenable<U>;
|
|
||||||
catch<U>(onRejected?: (error: any) => U | Thenable<U>): Thenable<U>;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<base href="/">
|
<base href="/">
|
||||||
|
|
|
@ -1,28 +1,20 @@
|
||||||
import { enableProdMode, provide } from '@angular/core';
|
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||||
import {
|
import { decorateModuleRef } from './app/environment';
|
||||||
HTTP_PROVIDERS,
|
import { bootloader } from '@angularclass/hmr';
|
||||||
RequestOptions,
|
/*
|
||||||
XHRBackend
|
* App Module
|
||||||
} from '@angular/http';
|
* our top level module that holds all of our components
|
||||||
import { bootstrap } from '@angular/platform-browser-dynamic';
|
*/
|
||||||
import { provideRouter } from '@angular/router';
|
import { AppModule } from './app';
|
||||||
|
|
||||||
import { AppComponent } from './app/app.component';
|
/*
|
||||||
import { routes } from './app/app.routes';
|
* Bootstrap our Angular app with a top level NgModule
|
||||||
import { AuthHttp, AuthService } from './app/shared';
|
*/
|
||||||
|
export function main(): Promise<any> {
|
||||||
if (process.env.ENV === 'production') {
|
return platformBrowserDynamic()
|
||||||
enableProdMode();
|
.bootstrapModule(AppModule)
|
||||||
|
.then(decorateModuleRef)
|
||||||
|
.catch(err => console.error(err));
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrap(AppComponent, [
|
bootloader(main);
|
||||||
HTTP_PROVIDERS,
|
|
||||||
provide(AuthHttp, {
|
|
||||||
useFactory: (backend: XHRBackend, defaultOptions: RequestOptions, authService: AuthService) => {
|
|
||||||
return new AuthHttp(backend, defaultOptions, authService);
|
|
||||||
},
|
|
||||||
deps: [ XHRBackend, RequestOptions, AuthService ]
|
|
||||||
}),
|
|
||||||
AuthService,
|
|
||||||
provideRouter(routes)
|
|
||||||
]);
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue