aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <florian.bigard@gmail.com>2017-09-04 20:07:54 +0200
committerChocobozzz <florian.bigard@gmail.com>2017-09-04 20:07:54 +0200
commitb0f9f39ed70299a208d1b388c72de8b7f3510cb7 (patch)
tree4b7d388125265533ac2f6d4bf457d018617e1db6
parente7dbeae8d915cdf4470ceb51c2724b04148b30b5 (diff)
downloadPeerTube-b0f9f39ed70299a208d1b388c72de8b7f3510cb7.tar.gz
PeerTube-b0f9f39ed70299a208d1b388c72de8b7f3510cb7.tar.zst
PeerTube-b0f9f39ed70299a208d1b388c72de8b7f3510cb7.zip
Begin user quota
-rw-r--r--client/package.json4
-rw-r--r--client/src/app/+admin/users/shared/user.service.ts25
-rw-r--r--client/src/app/+admin/users/user-add/user-add.component.html19
-rw-r--r--client/src/app/+admin/users/user-add/user-add.component.ts15
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.ts5
-rw-r--r--client/src/app/shared/forms/form-validators/user.ts7
-rw-r--r--client/src/app/shared/rest/rest-data-source.ts21
-rw-r--r--client/src/app/shared/users/user.model.ts13
-rw-r--r--client/tslint.json1
-rw-r--r--client/yarn.lock20
-rw-r--r--config/default.yaml5
-rw-r--r--config/production.yaml.example5
-rw-r--r--package.json4
-rw-r--r--server/controllers/api/users.ts11
-rw-r--r--server/helpers/custom-validators/users.ts8
-rw-r--r--server/initializers/constants.ts10
-rw-r--r--server/initializers/database.ts1
-rw-r--r--server/initializers/installer.ts9
-rw-r--r--server/initializers/migrations/0070-user-video-quota.ts32
-rw-r--r--server/middlewares/validators/users.ts2
-rw-r--r--server/middlewares/validators/videos.ts17
-rw-r--r--server/models/user/user-interface.ts4
-rw-r--r--server/models/user/user.ts60
-rw-r--r--server/models/video/video.ts7
-rw-r--r--shared/models/users/user-create.model.ts1
-rw-r--r--shared/models/users/user-update.model.ts1
-rw-r--r--shared/models/users/user.model.ts1
-rw-r--r--tslint.json1
-rw-r--r--yarn.lock20
29 files changed, 274 insertions, 55 deletions
diff --git a/client/package.json b/client/package.json
index 27246027b..f1c7e8799 100644
--- a/client/package.json
+++ b/client/package.json
@@ -80,9 +80,9 @@
80 "string-replace-loader": "^1.0.3", 80 "string-replace-loader": "^1.0.3",
81 "style-loader": "^0.18.2", 81 "style-loader": "^0.18.2",
82 "tslib": "^1.5.0", 82 "tslib": "^1.5.0",
83 "tslint": "^5.4.3", 83 "tslint": "^5.7.0",
84 "tslint-loader": "^3.3.0", 84 "tslint-loader": "^3.3.0",
85 "typescript": "~2.4.0", 85 "typescript": "^2.5.2",
86 "url-loader": "^0.5.7", 86 "url-loader": "^0.5.7",
87 "video.js": "^6.2.0", 87 "video.js": "^6.2.0",
88 "videojs-dock": "^2.0.2", 88 "videojs-dock": "^2.0.2",
diff --git a/client/src/app/+admin/users/shared/user.service.ts b/client/src/app/+admin/users/shared/user.service.ts
index 1c1cd575e..ffd7ba7da 100644
--- a/client/src/app/+admin/users/shared/user.service.ts
+++ b/client/src/app/+admin/users/shared/user.service.ts
@@ -2,12 +2,15 @@ import { Injectable } from '@angular/core'
2import 'rxjs/add/operator/catch' 2import 'rxjs/add/operator/catch'
3import 'rxjs/add/operator/map' 3import 'rxjs/add/operator/map'
4 4
5import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe'
6
5import { AuthHttp, RestExtractor, RestDataSource, User } from '../../../shared' 7import { AuthHttp, RestExtractor, RestDataSource, User } from '../../../shared'
6import { UserCreate } from '../../../../../../shared' 8import { UserCreate } from '../../../../../../shared'
7 9
8@Injectable() 10@Injectable()
9export class UserService { 11export class UserService {
10 private static BASE_USERS_URL = API_URL + '/api/v1/users/' 12 private static BASE_USERS_URL = API_URL + '/api/v1/users/'
13 private bytesPipe = new BytesPipe()
11 14
12 constructor ( 15 constructor (
13 private authHttp: AuthHttp, 16 private authHttp: AuthHttp,
@@ -21,10 +24,30 @@ export class UserService {
21 } 24 }
22 25
23 getDataSource () { 26 getDataSource () {
24 return new RestDataSource(this.authHttp, UserService.BASE_USERS_URL) 27 return new RestDataSource(this.authHttp, UserService.BASE_USERS_URL, this.formatDataSource.bind(this))
25 } 28 }
26 29
27 removeUser (user: User) { 30 removeUser (user: User) {
28 return this.authHttp.delete(UserService.BASE_USERS_URL + user.id) 31 return this.authHttp.delete(UserService.BASE_USERS_URL + user.id)
29 } 32 }
33
34 private formatDataSource (users: User[]) {
35 const newUsers = []
36
37 users.forEach(user => {
38 let videoQuota
39 if (user.videoQuota === -1) {
40 videoQuota = 'Unlimited'
41 } else {
42 videoQuota = this.bytesPipe.transform(user.videoQuota)
43 }
44
45 const newUser = Object.assign(user, {
46 videoQuota
47 })
48 newUsers.push(newUser)
49 })
50
51 return newUsers
52 }
30} 53}
diff --git a/client/src/app/+admin/users/user-add/user-add.component.html b/client/src/app/+admin/users/user-add/user-add.component.html
index 9b487aa75..f84d72c7c 100644
--- a/client/src/app/+admin/users/user-add/user-add.component.html
+++ b/client/src/app/+admin/users/user-add/user-add.component.html
@@ -9,7 +9,7 @@
9 <div class="form-group"> 9 <div class="form-group">
10 <label for="username">Username</label> 10 <label for="username">Username</label>
11 <input 11 <input
12 type="text" class="form-control" id="username" placeholder="Username" 12 type="text" class="form-control" id="username" placeholder="john"
13 formControlName="username" 13 formControlName="username"
14 > 14 >
15 <div *ngIf="formErrors.username" class="alert alert-danger"> 15 <div *ngIf="formErrors.username" class="alert alert-danger">
@@ -20,7 +20,7 @@
20 <div class="form-group"> 20 <div class="form-group">
21 <label for="email">Email</label> 21 <label for="email">Email</label>
22 <input 22 <input
23 type="text" class="form-control" id="email" placeholder="Email" 23 type="text" class="form-control" id="email" placeholder="mail@example.com"
24 formControlName="email" 24 formControlName="email"
25 > 25 >
26 <div *ngIf="formErrors.email" class="alert alert-danger"> 26 <div *ngIf="formErrors.email" class="alert alert-danger">
@@ -31,7 +31,7 @@
31 <div class="form-group"> 31 <div class="form-group">
32 <label for="password">Password</label> 32 <label for="password">Password</label>
33 <input 33 <input
34 type="password" class="form-control" id="password" placeholder="Password" 34 type="password" class="form-control" id="password"
35 formControlName="password" 35 formControlName="password"
36 > 36 >
37 <div *ngIf="formErrors.password" class="alert alert-danger"> 37 <div *ngIf="formErrors.password" class="alert alert-danger">
@@ -39,6 +39,19 @@
39 </div> 39 </div>
40 </div> 40 </div>
41 41
42 <div class="form-group">
43 <label for="videoQuota">Video quota</label>
44 <select class="form-control" id="videoQuota" formControlName="videoQuota">
45 <option value="-1">Unlimited</option>
46 <option value="100000000">100MB</option>
47 <option value="500000000">500MB</option>
48 <option value="1000000000">1GB</option>
49 <option value="5000000000">5GB</option>
50 <option value="20000000000">20GB</option>
51 <option value="50000000000">50GB</option>
52 </select>
53 </div>
54
42 <input type="submit" value="Add user" class="btn btn-default" [disabled]="!form.valid"> 55 <input type="submit" value="Add user" class="btn btn-default" [disabled]="!form.valid">
43 </form> 56 </form>
44 </div> 57 </div>
diff --git a/client/src/app/+admin/users/user-add/user-add.component.ts b/client/src/app/+admin/users/user-add/user-add.component.ts
index 0dd99eccd..91377a933 100644
--- a/client/src/app/+admin/users/user-add/user-add.component.ts
+++ b/client/src/app/+admin/users/user-add/user-add.component.ts
@@ -9,7 +9,8 @@ import {
9 FormReactive, 9 FormReactive,
10 USER_USERNAME, 10 USER_USERNAME,
11 USER_EMAIL, 11 USER_EMAIL,
12 USER_PASSWORD 12 USER_PASSWORD,
13 USER_VIDEO_QUOTA
13} from '../../../shared' 14} from '../../../shared'
14import { UserCreate } from '../../../../../../shared' 15import { UserCreate } from '../../../../../../shared'
15 16
@@ -24,12 +25,14 @@ export class UserAddComponent extends FormReactive implements OnInit {
24 formErrors = { 25 formErrors = {
25 'username': '', 26 'username': '',
26 'email': '', 27 'email': '',
27 'password': '' 28 'password': '',
29 'videoQuota': ''
28 } 30 }
29 validationMessages = { 31 validationMessages = {
30 'username': USER_USERNAME.MESSAGES, 32 'username': USER_USERNAME.MESSAGES,
31 'email': USER_EMAIL.MESSAGES, 33 'email': USER_EMAIL.MESSAGES,
32 'password': USER_PASSWORD.MESSAGES 34 'password': USER_PASSWORD.MESSAGES,
35 'videoQuota': USER_VIDEO_QUOTA.MESSAGES
33 } 36 }
34 37
35 constructor ( 38 constructor (
@@ -45,7 +48,8 @@ export class UserAddComponent extends FormReactive implements OnInit {
45 this.form = this.formBuilder.group({ 48 this.form = this.formBuilder.group({
46 username: [ '', USER_USERNAME.VALIDATORS ], 49 username: [ '', USER_USERNAME.VALIDATORS ],
47 email: [ '', USER_EMAIL.VALIDATORS ], 50 email: [ '', USER_EMAIL.VALIDATORS ],
48 password: [ '', USER_PASSWORD.VALIDATORS ] 51 password: [ '', USER_PASSWORD.VALIDATORS ],
52 videoQuota: [ '-1', USER_VIDEO_QUOTA.VALIDATORS ]
49 }) 53 })
50 54
51 this.form.valueChanges.subscribe(data => this.onValueChanged(data)) 55 this.form.valueChanges.subscribe(data => this.onValueChanged(data))
@@ -60,6 +64,9 @@ export class UserAddComponent extends FormReactive implements OnInit {
60 64
61 const userCreate: UserCreate = this.form.value 65 const userCreate: UserCreate = this.form.value
62 66
67 // A select in HTML is always mapped as a string, we convert it to number
68 userCreate.videoQuota = parseInt(this.form.value['videoQuota'], 10)
69
63 this.userService.addUser(userCreate).subscribe( 70 this.userService.addUser(userCreate).subscribe(
64 () => { 71 () => {
65 this.notificationsService.success('Success', `User ${userCreate.username} created.`) 72 this.notificationsService.success('Success', `User ${userCreate.username} created.`)
diff --git a/client/src/app/+admin/users/user-list/user-list.component.ts b/client/src/app/+admin/users/user-list/user-list.component.ts
index 12826741c..dbb85cedd 100644
--- a/client/src/app/+admin/users/user-list/user-list.component.ts
+++ b/client/src/app/+admin/users/user-list/user-list.component.ts
@@ -30,7 +30,7 @@ export class UserListComponent {
30 }, 30 },
31 pager: { 31 pager: {
32 display: true, 32 display: true,
33 perPage: 10 33 perPage: 1
34 }, 34 },
35 columns: { 35 columns: {
36 id: { 36 id: {
@@ -43,6 +43,9 @@ export class UserListComponent {
43 email: { 43 email: {
44 title: 'Email' 44 title: 'Email'
45 }, 45 },
46 videoQuota: {
47 title: 'Video quota'
48 },
46 role: { 49 role: {
47 title: 'Role', 50 title: 'Role',
48 sort: false 51 sort: false
diff --git a/client/src/app/shared/forms/form-validators/user.ts b/client/src/app/shared/forms/form-validators/user.ts
index fd316583e..087a99760 100644
--- a/client/src/app/shared/forms/form-validators/user.ts
+++ b/client/src/app/shared/forms/form-validators/user.ts
@@ -22,3 +22,10 @@ export const USER_PASSWORD = {
22 'minlength': 'Password must be at least 6 characters long.' 22 'minlength': 'Password must be at least 6 characters long.'
23 } 23 }
24} 24}
25export const USER_VIDEO_QUOTA = {
26 VALIDATORS: [ Validators.required, Validators.min(-1) ],
27 MESSAGES: {
28 'required': 'Video quota is required.',
29 'min': 'Quota must be greater than -1.'
30 }
31} \ No newline at end of file
diff --git a/client/src/app/shared/rest/rest-data-source.ts b/client/src/app/shared/rest/rest-data-source.ts
index 7956637e0..5c205d280 100644
--- a/client/src/app/shared/rest/rest-data-source.ts
+++ b/client/src/app/shared/rest/rest-data-source.ts
@@ -3,14 +3,31 @@ import { Http, RequestOptionsArgs, URLSearchParams, Response } from '@angular/ht
3import { ServerDataSource } from 'ng2-smart-table' 3import { ServerDataSource } from 'ng2-smart-table'
4 4
5export class RestDataSource extends ServerDataSource { 5export class RestDataSource extends ServerDataSource {
6 constructor (http: Http, endpoint: string) { 6 private updateResponse: (input: any[]) => any[]
7
8 constructor (http: Http, endpoint: string, updateResponse?: (input: any[]) => any[]) {
7 const options = { 9 const options = {
8 endPoint: endpoint, 10 endPoint: endpoint,
9 sortFieldKey: 'sort', 11 sortFieldKey: 'sort',
10 dataKey: 'data' 12 dataKey: 'data'
11 } 13 }
12
13 super(http, options) 14 super(http, options)
15
16 if (updateResponse) {
17 this.updateResponse = updateResponse
18 }
19 }
20
21 protected extractDataFromResponse (res: Response) {
22 const json = res.json()
23 if (!json) return []
24 let data = json.data
25
26 if (this.updateResponse !== undefined) {
27 data = this.updateResponse(data)
28 }
29
30 return data
14 } 31 }
15 32
16 protected extractTotalFromResponse (res: Response) { 33 protected extractTotalFromResponse (res: Response) {
diff --git a/client/src/app/shared/users/user.model.ts b/client/src/app/shared/users/user.model.ts
index 1c2b481e3..bf12876c7 100644
--- a/client/src/app/shared/users/user.model.ts
+++ b/client/src/app/shared/users/user.model.ts
@@ -6,6 +6,7 @@ export class User implements UserServerModel {
6 email: string 6 email: string
7 role: UserRole 7 role: UserRole
8 displayNSFW: boolean 8 displayNSFW: boolean
9 videoQuota: number
9 createdAt: Date 10 createdAt: Date
10 11
11 constructor (hash: { 12 constructor (hash: {
@@ -13,6 +14,7 @@ export class User implements UserServerModel {
13 username: string, 14 username: string,
14 email: string, 15 email: string,
15 role: UserRole, 16 role: UserRole,
17 videoQuota?: number,
16 displayNSFW?: boolean, 18 displayNSFW?: boolean,
17 createdAt?: Date 19 createdAt?: Date
18 }) { 20 }) {
@@ -20,9 +22,16 @@ export class User implements UserServerModel {
20 this.username = hash.username 22 this.username = hash.username
21 this.email = hash.email 23 this.email = hash.email
22 this.role = hash.role 24 this.role = hash.role
23 this.displayNSFW = hash.displayNSFW
24 25
25 if (hash.createdAt) { 26 if (hash.videoQuota !== undefined) {
27 this.videoQuota = hash.videoQuota
28 }
29
30 if (hash.displayNSFW !== undefined) {
31 this.displayNSFW = hash.displayNSFW
32 }
33
34 if (hash.createdAt !== undefined) {
26 this.createdAt = hash.createdAt 35 this.createdAt = hash.createdAt
27 } 36 }
28 } 37 }
diff --git a/client/tslint.json b/client/tslint.json
index cfad2a5d9..b1e211ee9 100644
--- a/client/tslint.json
+++ b/client/tslint.json
@@ -4,7 +4,6 @@
4 "rules": { 4 "rules": {
5 "no-inferrable-types": true, 5 "no-inferrable-types": true,
6 "eofline": true, 6 "eofline": true,
7 "indent": ["spaces"],
8 "max-line-length": [true, 140], 7 "max-line-length": [true, 140],
9 "no-floating-promises": false, 8 "no-floating-promises": false,
10 "no-unused-variable": false, // Bug, wait TypeScript 2.4 9 "no-unused-variable": false, // Bug, wait TypeScript 2.4
diff --git a/client/yarn.lock b/client/yarn.lock
index 0fc5ec418..9478e23b2 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -6740,9 +6740,9 @@ tslint-loader@^3.3.0:
6740 rimraf "^2.4.4" 6740 rimraf "^2.4.4"
6741 semver "^5.3.0" 6741 semver "^5.3.0"
6742 6742
6743tslint@^5.4.3: 6743tslint@^5.7.0:
6744 version "5.6.0" 6744 version "5.7.0"
6745 resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.6.0.tgz#088aa6c6026623338650b2900828ab3edf59f6cf" 6745 resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.7.0.tgz#c25e0d0c92fa1201c2bc30e844e08e682b4f3552"
6746 dependencies: 6746 dependencies:
6747 babel-code-frame "^6.22.0" 6747 babel-code-frame "^6.22.0"
6748 colors "^1.1.2" 6748 colors "^1.1.2"
@@ -6753,7 +6753,7 @@ tslint@^5.4.3:
6753 resolve "^1.3.2" 6753 resolve "^1.3.2"
6754 semver "^5.3.0" 6754 semver "^5.3.0"
6755 tslib "^1.7.1" 6755 tslib "^1.7.1"
6756 tsutils "^2.7.1" 6756 tsutils "^2.8.1"
6757 6757
6758tsml@1.0.1: 6758tsml@1.0.1:
6759 version "1.0.1" 6759 version "1.0.1"
@@ -6763,9 +6763,9 @@ tsutils@^1.4.0:
6763 version "1.9.1" 6763 version "1.9.1"
6764 resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-1.9.1.tgz#b9f9ab44e55af9681831d5f28d0aeeaf5c750cb0" 6764 resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-1.9.1.tgz#b9f9ab44e55af9681831d5f28d0aeeaf5c750cb0"
6765 6765
6766tsutils@^2.7.1: 6766tsutils@^2.8.1:
6767 version "2.8.1" 6767 version "2.8.2"
6768 resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.8.1.tgz#3771404e7ca9f0bedf5d919a47a4b1890a68efff" 6768 resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.8.2.tgz#2c1486ba431260845b0ac6f902afd9d708a8ea6a"
6769 dependencies: 6769 dependencies:
6770 tslib "^1.7.1" 6770 tslib "^1.7.1"
6771 6771
@@ -6806,9 +6806,9 @@ typedarray@^0.0.6:
6806 version "0.0.6" 6806 version "0.0.6"
6807 resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" 6807 resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
6808 6808
6809typescript@~2.4.0: 6809typescript@^2.5.2:
6810 version "2.4.2" 6810 version "2.5.2"
6811 resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.4.2.tgz#f8395f85d459276067c988aa41837a8f82870844" 6811 resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.5.2.tgz#038a95f7d9bbb420b1bf35ba31d4c5c1dd3ffe34"
6812 6812
6813uglify-js@3.0.x, uglify-js@^3.0.6: 6813uglify-js@3.0.x, uglify-js@^3.0.6:
6814 version "3.0.28" 6814 version "3.0.28"
diff --git a/config/default.yaml b/config/default.yaml
index a97d3ff78..4c19a5b2d 100644
--- a/config/default.yaml
+++ b/config/default.yaml
@@ -35,6 +35,11 @@ signup:
35 enabled: false 35 enabled: false
36 limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited 36 limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited
37 37
38user:
39 # Default value of maximum video BYTES the user can upload (does not take into account transcoded files).
40 # -1 == unlimited
41 video_quota: -1
42
38# If enabled, the video will be transcoded to mp4 (x264) with "faststart" flag 43# If enabled, the video will be transcoded to mp4 (x264) with "faststart" flag
39# Uses a lot of CPU! 44# Uses a lot of CPU!
40transcoding: 45transcoding:
diff --git a/config/production.yaml.example b/config/production.yaml.example
index 90e07f577..987da12cc 100644
--- a/config/production.yaml.example
+++ b/config/production.yaml.example
@@ -36,6 +36,11 @@ signup:
36 enabled: false 36 enabled: false
37 limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited 37 limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited
38 38
39user:
40 # Default value of maximum video BYTES the user can upload (does not take into account transcoded files).
41 # -1 == unlimited
42 video_quota: -1
43
39# If enabled, the video will be transcoded to mp4 (x264) with "faststart" flag 44# If enabled, the video will be transcoded to mp4 (x264) with "faststart" flag
40# Uses a lot of CPU! 45# Uses a lot of CPU!
41transcoding: 46transcoding:
diff --git a/package.json b/package.json
index 2a1b0bde3..900d04052 100644
--- a/package.json
+++ b/package.json
@@ -79,7 +79,7 @@
79 "scripty": "^1.5.0", 79 "scripty": "^1.5.0",
80 "sequelize": "^4.7.5", 80 "sequelize": "^4.7.5",
81 "ts-node": "^3.0.6", 81 "ts-node": "^3.0.6",
82 "typescript": "^2.4.1", 82 "typescript": "^2.5.2",
83 "validator": "^8.1.0", 83 "validator": "^8.1.0",
84 "winston": "^2.1.1", 84 "winston": "^2.1.1",
85 "ws": "^3.1.0" 85 "ws": "^3.1.0"
@@ -109,7 +109,7 @@
109 "source-map-support": "^0.4.15", 109 "source-map-support": "^0.4.15",
110 "standard": "^10.0.0", 110 "standard": "^10.0.0",
111 "supertest": "^3.0.0", 111 "supertest": "^3.0.0",
112 "tslint": "^5.2.0", 112 "tslint": "^5.7.0",
113 "tslint-config-standard": "^6.0.0", 113 "tslint-config-standard": "^6.0.0",
114 "webtorrent": "^0.98.0" 114 "webtorrent": "^0.98.0"
115 }, 115 },
diff --git a/server/controllers/api/users.ts b/server/controllers/api/users.ts
index 04d885185..1b5b7f903 100644
--- a/server/controllers/api/users.ts
+++ b/server/controllers/api/users.ts
@@ -1,7 +1,7 @@
1import * as express from 'express' 1import * as express from 'express'
2 2
3import { database as db } from '../../initializers/database' 3import { database as db } from '../../initializers/database'
4import { USER_ROLES } from '../../initializers' 4import { USER_ROLES, CONFIG } from '../../initializers'
5import { logger, getFormattedObjects } from '../../helpers' 5import { logger, getFormattedObjects } from '../../helpers'
6import { 6import {
7 authenticate, 7 authenticate,
@@ -80,12 +80,18 @@ export {
80function createUser (req: express.Request, res: express.Response, next: express.NextFunction) { 80function createUser (req: express.Request, res: express.Response, next: express.NextFunction) {
81 const body: UserCreate = req.body 81 const body: UserCreate = req.body
82 82
83 // On registration, we set the user video quota
84 if (body.videoQuota === undefined) {
85 body.videoQuota = CONFIG.USER.VIDEO_QUOTA
86 }
87
83 const user = db.User.build({ 88 const user = db.User.build({
84 username: body.username, 89 username: body.username,
85 password: body.password, 90 password: body.password,
86 email: body.email, 91 email: body.email,
87 displayNSFW: false, 92 displayNSFW: false,
88 role: USER_ROLES.USER 93 role: USER_ROLES.USER,
94 videoQuota: body.videoQuota
89 }) 95 })
90 96
91 user.save() 97 user.save()
@@ -140,6 +146,7 @@ function updateUser (req: express.Request, res: express.Response, next: express.
140 .then(user => { 146 .then(user => {
141 if (body.password) user.password = body.password 147 if (body.password) user.password = body.password
142 if (body.displayNSFW !== undefined) user.displayNSFW = body.displayNSFW 148 if (body.displayNSFW !== undefined) user.displayNSFW = body.displayNSFW
149 if (body.videoQuota !== undefined) user.videoQuota = body.videoQuota
143 150
144 return user.save() 151 return user.save()
145 }) 152 })
diff --git a/server/helpers/custom-validators/users.ts b/server/helpers/custom-validators/users.ts
index 2b37bdde8..00061f9df 100644
--- a/server/helpers/custom-validators/users.ts
+++ b/server/helpers/custom-validators/users.ts
@@ -15,6 +15,10 @@ function isUserRoleValid (value: string) {
15 return values(USER_ROLES).indexOf(value as UserRole) !== -1 15 return values(USER_ROLES).indexOf(value as UserRole) !== -1
16} 16}
17 17
18function isUserVideoQuotaValid (value: string) {
19 return exists(value) && validator.isInt(value + '', USERS_CONSTRAINTS_FIELDS.VIDEO_QUOTA)
20}
21
18function isUserUsernameValid (value: string) { 22function isUserUsernameValid (value: string) {
19 const max = USERS_CONSTRAINTS_FIELDS.USERNAME.max 23 const max = USERS_CONSTRAINTS_FIELDS.USERNAME.max
20 const min = USERS_CONSTRAINTS_FIELDS.USERNAME.min 24 const min = USERS_CONSTRAINTS_FIELDS.USERNAME.min
@@ -30,6 +34,7 @@ function isUserDisplayNSFWValid (value: any) {
30export { 34export {
31 isUserPasswordValid, 35 isUserPasswordValid,
32 isUserRoleValid, 36 isUserRoleValid,
37 isUserVideoQuotaValid,
33 isUserUsernameValid, 38 isUserUsernameValid,
34 isUserDisplayNSFWValid 39 isUserDisplayNSFWValid
35} 40}
@@ -39,6 +44,7 @@ declare module 'express-validator' {
39 isUserPasswordValid, 44 isUserPasswordValid,
40 isUserRoleValid, 45 isUserRoleValid,
41 isUserUsernameValid, 46 isUserUsernameValid,
42 isUserDisplayNSFWValid 47 isUserDisplayNSFWValid,
48 isUserVideoQuotaValid
43 } 49 }
44} 50}
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 50a939083..b93a85859 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -15,7 +15,7 @@ import {
15 15
16// --------------------------------------------------------------------------- 16// ---------------------------------------------------------------------------
17 17
18const LAST_MIGRATION_VERSION = 65 18const LAST_MIGRATION_VERSION = 70
19 19
20// --------------------------------------------------------------------------- 20// ---------------------------------------------------------------------------
21 21
@@ -77,7 +77,10 @@ const CONFIG = {
77 }, 77 },
78 SIGNUP: { 78 SIGNUP: {
79 ENABLED: config.get<boolean>('signup.enabled'), 79 ENABLED: config.get<boolean>('signup.enabled'),
80 LIMIT: config.get<number>('signup.limit') 80 LIMIT: config.get<number>('signup.limit'),
81 },
82 USER: {
83 VIDEO_QUOTA: config.get<number>('user.video_quota')
81 }, 84 },
82 TRANSCODING: { 85 TRANSCODING: {
83 ENABLED: config.get<boolean>('transcoding.enabled'), 86 ENABLED: config.get<boolean>('transcoding.enabled'),
@@ -97,7 +100,8 @@ CONFIG.WEBSERVER.HOST = CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
97const CONSTRAINTS_FIELDS = { 100const CONSTRAINTS_FIELDS = {
98 USERS: { 101 USERS: {
99 USERNAME: { min: 3, max: 20 }, // Length 102 USERNAME: { min: 3, max: 20 }, // Length
100 PASSWORD: { min: 6, max: 255 } // Length 103 PASSWORD: { min: 6, max: 255 }, // Length
104 VIDEO_QUOTA: { min: -1 }
101 }, 105 },
102 VIDEO_ABUSES: { 106 VIDEO_ABUSES: {
103 REASON: { min: 2, max: 300 } // Length 107 REASON: { min: 2, max: 300 } // Length
diff --git a/server/initializers/database.ts b/server/initializers/database.ts
index c0df2b63a..d04c8db1b 100644
--- a/server/initializers/database.ts
+++ b/server/initializers/database.ts
@@ -1,5 +1,6 @@
1import { join } from 'path' 1import { join } from 'path'
2import { flattenDepth } from 'lodash' 2import { flattenDepth } from 'lodash'
3require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
3import * as Sequelize from 'sequelize' 4import * as Sequelize from 'sequelize'
4import * as Promise from 'bluebird' 5import * as Promise from 'bluebird'
5 6
diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts
index 43b5adfed..10b74b85f 100644
--- a/server/initializers/installer.ts
+++ b/server/initializers/installer.ts
@@ -38,12 +38,12 @@ function removeCacheDirectories () {
38} 38}
39 39
40function createDirectoriesIfNotExist () { 40function createDirectoriesIfNotExist () {
41 const storages = CONFIG.STORAGE 41 const storage = CONFIG.STORAGE
42 const cacheDirectories = CACHE.DIRECTORIES 42 const cacheDirectories = CACHE.DIRECTORIES
43 43
44 const tasks = [] 44 const tasks = []
45 Object.keys(storages).forEach(key => { 45 Object.keys(storage).forEach(key => {
46 const dir = storages[key] 46 const dir = storage[key]
47 tasks.push(mkdirpPromise(dir)) 47 tasks.push(mkdirpPromise(dir))
48 }) 48 })
49 49
@@ -112,7 +112,8 @@ function createOAuthAdminIfNotExist () {
112 username, 112 username,
113 email, 113 email,
114 password, 114 password,
115 role 115 role,
116 videoQuota: -1
116 } 117 }
117 118
118 return db.User.create(userData, createOptions).then(createdUser => { 119 return db.User.create(userData, createOptions).then(createdUser => {
diff --git a/server/initializers/migrations/0070-user-video-quota.ts b/server/initializers/migrations/0070-user-video-quota.ts
new file mode 100644
index 000000000..dec4d46dd
--- /dev/null
+++ b/server/initializers/migrations/0070-user-video-quota.ts
@@ -0,0 +1,32 @@
1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird'
3
4function up (utils: {
5 transaction: Sequelize.Transaction,
6 queryInterface: Sequelize.QueryInterface,
7 sequelize: Sequelize.Sequelize,
8 db: any
9}): Promise<void> {
10 const q = utils.queryInterface
11
12 const data = {
13 type: Sequelize.BIGINT,
14 allowNull: false,
15 defaultValue: -1
16 }
17
18 return q.addColumn('Users', 'videoQuota', data)
19 .then(() => {
20 data.defaultValue = null
21 return q.changeColumn('Users', 'videoQuota', data)
22 })
23}
24
25function down (options) {
26 throw new Error('Not implemented.')
27}
28
29export {
30 up,
31 down
32}
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts
index 71e529872..eeb0e3557 100644
--- a/server/middlewares/validators/users.ts
+++ b/server/middlewares/validators/users.ts
@@ -12,6 +12,7 @@ function usersAddValidator (req: express.Request, res: express.Response, next: e
12 req.checkBody('username', 'Should have a valid username').isUserUsernameValid() 12 req.checkBody('username', 'Should have a valid username').isUserUsernameValid()
13 req.checkBody('password', 'Should have a valid password').isUserPasswordValid() 13 req.checkBody('password', 'Should have a valid password').isUserPasswordValid()
14 req.checkBody('email', 'Should have a valid email').isEmail() 14 req.checkBody('email', 'Should have a valid email').isEmail()
15 req.checkBody('videoQuota', 'Should have a valid user quota').isUserVideoQuotaValid()
15 16
16 logger.debug('Checking usersAdd parameters', { parameters: req.body }) 17 logger.debug('Checking usersAdd parameters', { parameters: req.body })
17 18
@@ -55,6 +56,7 @@ function usersUpdateValidator (req: express.Request, res: express.Response, next
55 // Add old password verification 56 // Add old password verification
56 req.checkBody('password', 'Should have a valid password').optional().isUserPasswordValid() 57 req.checkBody('password', 'Should have a valid password').optional().isUserPasswordValid()
57 req.checkBody('displayNSFW', 'Should have a valid display Not Safe For Work attribute').optional().isUserDisplayNSFWValid() 58 req.checkBody('displayNSFW', 'Should have a valid display Not Safe For Work attribute').optional().isUserDisplayNSFWValid()
59 req.checkBody('videoQuota', 'Should have a valid user quota').optional().isUserVideoQuotaValid()
58 60
59 logger.debug('Checking usersUpdate parameters', { parameters: req.body }) 61 logger.debug('Checking usersUpdate parameters', { parameters: req.body })
60 62
diff --git a/server/middlewares/validators/videos.ts b/server/middlewares/validators/videos.ts
index 29c1ee0ef..1d19ebfd9 100644
--- a/server/middlewares/validators/videos.ts
+++ b/server/middlewares/validators/videos.ts
@@ -24,10 +24,23 @@ function videosAddValidator (req: express.Request, res: express.Response, next:
24 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files }) 24 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
25 25
26 checkErrors(req, res, () => { 26 checkErrors(req, res, () => {
27 const videoFile = req.files['videofile'][0] 27 const videoFile: Express.Multer.File = req.files['videofile'][0]
28 const user = res.locals.oauth.token.User
28 29
29 db.Video.getDurationFromFile(videoFile.path) 30 user.isAbleToUploadVideo(videoFile)
31 .then(isAble => {
32 if (isAble === false) {
33 res.status(403).send('The user video quota is exceeded with this video.')
34
35 return undefined
36 }
37
38 return db.Video.getDurationFromFile(videoFile.path)
39 })
30 .then(duration => { 40 .then(duration => {
41 // Previous test failed, abort
42 if (duration === undefined) return
43
31 if (!isVideoDurationValid('' + duration)) { 44 if (!isVideoDurationValid('' + duration)) {
32 return res.status(400).send('Duration of the video file is too big (max: ' + CONSTRAINTS_FIELDS.VIDEOS.DURATION.max + 's).') 45 return res.status(400).send('Duration of the video file is too big (max: ' + CONSTRAINTS_FIELDS.VIDEOS.DURATION.max + 's).')
33 } 46 }
diff --git a/server/models/user/user-interface.ts b/server/models/user/user-interface.ts
index 0b97a8f6d..8974a9a97 100644
--- a/server/models/user/user-interface.ts
+++ b/server/models/user/user-interface.ts
@@ -11,6 +11,7 @@ export namespace UserMethods {
11 11
12 export type ToFormattedJSON = (this: UserInstance) => FormattedUser 12 export type ToFormattedJSON = (this: UserInstance) => FormattedUser
13 export type IsAdmin = (this: UserInstance) => boolean 13 export type IsAdmin = (this: UserInstance) => boolean
14 export type IsAbleToUploadVideo = (this: UserInstance, videoFile: Express.Multer.File) => Promise<boolean>
14 15
15 export type CountTotal = () => Promise<number> 16 export type CountTotal = () => Promise<number>
16 17
@@ -31,6 +32,7 @@ export interface UserClass {
31 isPasswordMatch: UserMethods.IsPasswordMatch, 32 isPasswordMatch: UserMethods.IsPasswordMatch,
32 toFormattedJSON: UserMethods.ToFormattedJSON, 33 toFormattedJSON: UserMethods.ToFormattedJSON,
33 isAdmin: UserMethods.IsAdmin, 34 isAdmin: UserMethods.IsAdmin,
35 isAbleToUploadVideo: UserMethods.IsAbleToUploadVideo,
34 36
35 countTotal: UserMethods.CountTotal, 37 countTotal: UserMethods.CountTotal,
36 getByUsername: UserMethods.GetByUsername, 38 getByUsername: UserMethods.GetByUsername,
@@ -42,11 +44,13 @@ export interface UserClass {
42} 44}
43 45
44export interface UserAttributes { 46export interface UserAttributes {
47 id?: number
45 password: string 48 password: string
46 username: string 49 username: string
47 email: string 50 email: string
48 displayNSFW?: boolean 51 displayNSFW?: boolean
49 role: UserRole 52 role: UserRole
53 videoQuota: number
50} 54}
51 55
52export interface UserInstance extends UserClass, UserAttributes, Sequelize.Instance<UserAttributes> { 56export interface UserInstance extends UserClass, UserAttributes, Sequelize.Instance<UserAttributes> {
diff --git a/server/models/user/user.ts b/server/models/user/user.ts
index d481fa13c..12a7547f5 100644
--- a/server/models/user/user.ts
+++ b/server/models/user/user.ts
@@ -1,5 +1,6 @@
1import { values } from 'lodash' 1import { values } from 'lodash'
2import * as Sequelize from 'sequelize' 2import * as Sequelize from 'sequelize'
3import * as Promise from 'bluebird'
3 4
4import { getSort } from '../utils' 5import { getSort } from '../utils'
5import { USER_ROLES } from '../../initializers' 6import { USER_ROLES } from '../../initializers'
@@ -8,7 +9,8 @@ import {
8 comparePassword, 9 comparePassword,
9 isUserPasswordValid, 10 isUserPasswordValid,
10 isUserUsernameValid, 11 isUserUsernameValid,
11 isUserDisplayNSFWValid 12 isUserDisplayNSFWValid,
13 isUserVideoQuotaValid
12} from '../../helpers' 14} from '../../helpers'
13 15
14import { addMethodsToModel } from '../utils' 16import { addMethodsToModel } from '../utils'
@@ -30,6 +32,7 @@ let listForApi: UserMethods.ListForApi
30let loadById: UserMethods.LoadById 32let loadById: UserMethods.LoadById
31let loadByUsername: UserMethods.LoadByUsername 33let loadByUsername: UserMethods.LoadByUsername
32let loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail 34let loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail
35let isAbleToUploadVideo: UserMethods.IsAbleToUploadVideo
33 36
34export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { 37export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
35 User = sequelize.define<UserInstance, UserAttributes>('User', 38 User = sequelize.define<UserInstance, UserAttributes>('User',
@@ -75,6 +78,16 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
75 role: { 78 role: {
76 type: DataTypes.ENUM(values(USER_ROLES)), 79 type: DataTypes.ENUM(values(USER_ROLES)),
77 allowNull: false 80 allowNull: false
81 },
82 videoQuota: {
83 type: DataTypes.BIGINT,
84 allowNull: false,
85 validate: {
86 videoQuotaValid: value => {
87 const res = isUserVideoQuotaValid(value)
88 if (res === false) throw new Error('Video quota is not valid.')
89 }
90 }
78 } 91 }
79 }, 92 },
80 { 93 {
@@ -109,7 +122,8 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
109 const instanceMethods = [ 122 const instanceMethods = [
110 isPasswordMatch, 123 isPasswordMatch,
111 toFormattedJSON, 124 toFormattedJSON,
112 isAdmin 125 isAdmin,
126 isAbleToUploadVideo
113 ] 127 ]
114 addMethodsToModel(User, classMethods, instanceMethods) 128 addMethodsToModel(User, classMethods, instanceMethods)
115 129
@@ -136,6 +150,7 @@ toFormattedJSON = function (this: UserInstance) {
136 email: this.email, 150 email: this.email,
137 displayNSFW: this.displayNSFW, 151 displayNSFW: this.displayNSFW,
138 role: this.role, 152 role: this.role,
153 videoQuota: this.videoQuota,
139 createdAt: this.createdAt 154 createdAt: this.createdAt
140 } 155 }
141} 156}
@@ -144,6 +159,14 @@ isAdmin = function (this: UserInstance) {
144 return this.role === USER_ROLES.ADMIN 159 return this.role === USER_ROLES.ADMIN
145} 160}
146 161
162isAbleToUploadVideo = function (this: UserInstance, videoFile: Express.Multer.File) {
163 if (this.videoQuota === -1) return Promise.resolve(true)
164
165 return getOriginalVideoFileTotalFromUser(this).then(totalBytes => {
166 return (videoFile.size + totalBytes) < this.videoQuota
167 })
168}
169
147// ------------------------------ STATICS ------------------------------ 170// ------------------------------ STATICS ------------------------------
148 171
149function associate (models) { 172function associate (models) {
@@ -215,3 +238,36 @@ loadByUsernameOrEmail = function (username: string, email: string) {
215 // FIXME: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18387 238 // FIXME: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18387
216 return (User as any).findOne(query) 239 return (User as any).findOne(query)
217} 240}
241
242// ---------------------------------------------------------------------------
243
244function getOriginalVideoFileTotalFromUser (user: UserInstance) {
245 const query = {
246 attributes: [
247 Sequelize.fn('COUNT', Sequelize.col('VideoFile.size'), 'totalVideoBytes')
248 ],
249 where: {
250 id: user.id
251 },
252 include: [
253 {
254 model: User['sequelize'].models.Author,
255 include: [
256 {
257 model: User['sequelize'].models.Video,
258 include: [
259 {
260 model: User['sequelize'].models.VideoFile
261 }
262 ]
263 }
264 ]
265 }
266 ]
267 }
268
269 // FIXME: cast to any because of bad typing...
270 return User.findAll(query).then((res: any) => {
271 return res.totalVideoBytes
272 })
273}
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 7dfea8ac9..4fb4485d8 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -9,6 +9,7 @@ import * as Sequelize from 'sequelize'
9import * as Promise from 'bluebird' 9import * as Promise from 'bluebird'
10 10
11import { TagInstance } from './tag-interface' 11import { TagInstance } from './tag-interface'
12import { UserInstance } from '../user/user-interface'
12import { 13import {
13 logger, 14 logger,
14 isVideoNameValid, 15 isVideoNameValid,
@@ -582,7 +583,7 @@ transcodeVideofile = function (this: VideoInstance, inputVideoFile: VideoFileIns
582 return res() 583 return res()
583 }) 584 })
584 .catch(err => { 585 .catch(err => {
585 // Autodestruction... 586 // Auto destruction...
586 this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err)) 587 this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
587 588
588 return rej(err) 589 return rej(err)
@@ -608,8 +609,8 @@ removeFile = function (this: VideoInstance, videoFile: VideoFileInstance) {
608} 609}
609 610
610removeTorrent = function (this: VideoInstance, videoFile: VideoFileInstance) { 611removeTorrent = function (this: VideoInstance, videoFile: VideoFileInstance) {
611 const torrenPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) 612 const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
612 return unlinkPromise(torrenPath) 613 return unlinkPromise(torrentPath)
613} 614}
614 615
615// ------------------------------ STATICS ------------------------------ 616// ------------------------------ STATICS ------------------------------
diff --git a/shared/models/users/user-create.model.ts b/shared/models/users/user-create.model.ts
index 2cddcdcb0..49fa2549d 100644
--- a/shared/models/users/user-create.model.ts
+++ b/shared/models/users/user-create.model.ts
@@ -2,4 +2,5 @@ export interface UserCreate {
2 username: string 2 username: string
3 password: string 3 password: string
4 email: string 4 email: string
5 videoQuota: number
5} 6}
diff --git a/shared/models/users/user-update.model.ts b/shared/models/users/user-update.model.ts
index 8b9abfb15..895ec0681 100644
--- a/shared/models/users/user-update.model.ts
+++ b/shared/models/users/user-update.model.ts
@@ -1,4 +1,5 @@
1export interface UserUpdate { 1export interface UserUpdate {
2 displayNSFW?: boolean 2 displayNSFW?: boolean
3 password?: string 3 password?: string
4 videoQuota?: number
4} 5}
diff --git a/shared/models/users/user.model.ts b/shared/models/users/user.model.ts
index 5c48a17b2..867a6dde5 100644
--- a/shared/models/users/user.model.ts
+++ b/shared/models/users/user.model.ts
@@ -6,5 +6,6 @@ export interface User {
6 email: string 6 email: string
7 displayNSFW: boolean 7 displayNSFW: boolean
8 role: UserRole 8 role: UserRole
9 videoQuota: number
9 createdAt: Date 10 createdAt: Date
10} 11}
diff --git a/tslint.json b/tslint.json
index 70e5d9bb4..6e982ca85 100644
--- a/tslint.json
+++ b/tslint.json
@@ -4,6 +4,7 @@
4 "no-inferrable-types": true, 4 "no-inferrable-types": true,
5 "eofline": true, 5 "eofline": true,
6 "indent": ["spaces"], 6 "indent": ["spaces"],
7 "ter-indent": [true, 2],
7 "max-line-length": [true, 140], 8 "max-line-length": [true, 140],
8 "no-floating-promises": false 9 "no-floating-promises": false
9 } 10 }
diff --git a/yarn.lock b/yarn.lock
index c0f35b21d..1a6af175a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3755,9 +3755,9 @@ tslint-eslint-rules@^4.0.0:
3755 tslib "^1.0.0" 3755 tslib "^1.0.0"
3756 tsutils "^1.4.0" 3756 tsutils "^1.4.0"
3757 3757
3758tslint@^5.2.0: 3758tslint@^5.7.0:
3759 version "5.6.0" 3759 version "5.7.0"
3760 resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.6.0.tgz#088aa6c6026623338650b2900828ab3edf59f6cf" 3760 resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.7.0.tgz#c25e0d0c92fa1201c2bc30e844e08e682b4f3552"
3761 dependencies: 3761 dependencies:
3762 babel-code-frame "^6.22.0" 3762 babel-code-frame "^6.22.0"
3763 colors "^1.1.2" 3763 colors "^1.1.2"
@@ -3768,15 +3768,15 @@ tslint@^5.2.0:
3768 resolve "^1.3.2" 3768 resolve "^1.3.2"
3769 semver "^5.3.0" 3769 semver "^5.3.0"
3770 tslib "^1.7.1" 3770 tslib "^1.7.1"
3771 tsutils "^2.7.1" 3771 tsutils "^2.8.1"
3772 3772
3773tsutils@^1.4.0: 3773tsutils@^1.4.0:
3774 version "1.9.1" 3774 version "1.9.1"
3775 resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-1.9.1.tgz#b9f9ab44e55af9681831d5f28d0aeeaf5c750cb0" 3775 resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-1.9.1.tgz#b9f9ab44e55af9681831d5f28d0aeeaf5c750cb0"
3776 3776
3777tsutils@^2.7.1: 3777tsutils@^2.8.1:
3778 version "2.8.1" 3778 version "2.8.2"
3779 resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.8.1.tgz#3771404e7ca9f0bedf5d919a47a4b1890a68efff" 3779 resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.8.2.tgz#2c1486ba431260845b0ac6f902afd9d708a8ea6a"
3780 dependencies: 3780 dependencies:
3781 tslib "^1.7.1" 3781 tslib "^1.7.1"
3782 3782
@@ -3821,9 +3821,9 @@ typedarray@^0.0.6:
3821 version "0.0.6" 3821 version "0.0.6"
3822 resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" 3822 resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
3823 3823
3824typescript@^2.4.1: 3824typescript@^2.5.2:
3825 version "2.5.1" 3825 version "2.5.2"
3826 resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.5.1.tgz#ce7cc93ada3de19475cc9d17e3adea7aee1832aa" 3826 resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.5.2.tgz#038a95f7d9bbb420b1bf35ba31d4c5c1dd3ffe34"
3827 3827
3828uid-number@^0.0.6: 3828uid-number@^0.0.6:
3829 version "0.0.6" 3829 version "0.0.6"