Begin user quota
This commit is contained in:
parent
e7dbeae8d9
commit
b0f9f39ed7
29 changed files with 274 additions and 55 deletions
|
@ -80,9 +80,9 @@
|
||||||
"string-replace-loader": "^1.0.3",
|
"string-replace-loader": "^1.0.3",
|
||||||
"style-loader": "^0.18.2",
|
"style-loader": "^0.18.2",
|
||||||
"tslib": "^1.5.0",
|
"tslib": "^1.5.0",
|
||||||
"tslint": "^5.4.3",
|
"tslint": "^5.7.0",
|
||||||
"tslint-loader": "^3.3.0",
|
"tslint-loader": "^3.3.0",
|
||||||
"typescript": "~2.4.0",
|
"typescript": "^2.5.2",
|
||||||
"url-loader": "^0.5.7",
|
"url-loader": "^0.5.7",
|
||||||
"video.js": "^6.2.0",
|
"video.js": "^6.2.0",
|
||||||
"videojs-dock": "^2.0.2",
|
"videojs-dock": "^2.0.2",
|
||||||
|
|
|
@ -2,12 +2,15 @@ import { Injectable } from '@angular/core'
|
||||||
import 'rxjs/add/operator/catch'
|
import 'rxjs/add/operator/catch'
|
||||||
import 'rxjs/add/operator/map'
|
import 'rxjs/add/operator/map'
|
||||||
|
|
||||||
|
import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe'
|
||||||
|
|
||||||
import { AuthHttp, RestExtractor, RestDataSource, User } from '../../../shared'
|
import { AuthHttp, RestExtractor, RestDataSource, User } from '../../../shared'
|
||||||
import { UserCreate } from '../../../../../../shared'
|
import { UserCreate } from '../../../../../../shared'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserService {
|
export class UserService {
|
||||||
private static BASE_USERS_URL = API_URL + '/api/v1/users/'
|
private static BASE_USERS_URL = API_URL + '/api/v1/users/'
|
||||||
|
private bytesPipe = new BytesPipe()
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private authHttp: AuthHttp,
|
private authHttp: AuthHttp,
|
||||||
|
@ -21,10 +24,30 @@ export class UserService {
|
||||||
}
|
}
|
||||||
|
|
||||||
getDataSource () {
|
getDataSource () {
|
||||||
return new RestDataSource(this.authHttp, UserService.BASE_USERS_URL)
|
return new RestDataSource(this.authHttp, UserService.BASE_USERS_URL, this.formatDataSource.bind(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
removeUser (user: User) {
|
removeUser (user: User) {
|
||||||
return this.authHttp.delete(UserService.BASE_USERS_URL + user.id)
|
return this.authHttp.delete(UserService.BASE_USERS_URL + user.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private formatDataSource (users: User[]) {
|
||||||
|
const newUsers = []
|
||||||
|
|
||||||
|
users.forEach(user => {
|
||||||
|
let videoQuota
|
||||||
|
if (user.videoQuota === -1) {
|
||||||
|
videoQuota = 'Unlimited'
|
||||||
|
} else {
|
||||||
|
videoQuota = this.bytesPipe.transform(user.videoQuota)
|
||||||
|
}
|
||||||
|
|
||||||
|
const newUser = Object.assign(user, {
|
||||||
|
videoQuota
|
||||||
|
})
|
||||||
|
newUsers.push(newUser)
|
||||||
|
})
|
||||||
|
|
||||||
|
return newUsers
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<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" id="username" placeholder="Username"
|
type="text" class="form-control" id="username" placeholder="john"
|
||||||
formControlName="username"
|
formControlName="username"
|
||||||
>
|
>
|
||||||
<div *ngIf="formErrors.username" class="alert alert-danger">
|
<div *ngIf="formErrors.username" class="alert alert-danger">
|
||||||
|
@ -20,7 +20,7 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="email">Email</label>
|
<label for="email">Email</label>
|
||||||
<input
|
<input
|
||||||
type="text" class="form-control" id="email" placeholder="Email"
|
type="text" class="form-control" id="email" placeholder="mail@example.com"
|
||||||
formControlName="email"
|
formControlName="email"
|
||||||
>
|
>
|
||||||
<div *ngIf="formErrors.email" class="alert alert-danger">
|
<div *ngIf="formErrors.email" class="alert alert-danger">
|
||||||
|
@ -31,7 +31,7 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="password">Password</label>
|
<label for="password">Password</label>
|
||||||
<input
|
<input
|
||||||
type="password" class="form-control" id="password" placeholder="Password"
|
type="password" class="form-control" id="password"
|
||||||
formControlName="password"
|
formControlName="password"
|
||||||
>
|
>
|
||||||
<div *ngIf="formErrors.password" class="alert alert-danger">
|
<div *ngIf="formErrors.password" class="alert alert-danger">
|
||||||
|
@ -39,6 +39,19 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="videoQuota">Video quota</label>
|
||||||
|
<select class="form-control" id="videoQuota" formControlName="videoQuota">
|
||||||
|
<option value="-1">Unlimited</option>
|
||||||
|
<option value="100000000">100MB</option>
|
||||||
|
<option value="500000000">500MB</option>
|
||||||
|
<option value="1000000000">1GB</option>
|
||||||
|
<option value="5000000000">5GB</option>
|
||||||
|
<option value="20000000000">20GB</option>
|
||||||
|
<option value="50000000000">50GB</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<input type="submit" value="Add user" class="btn btn-default" [disabled]="!form.valid">
|
<input type="submit" value="Add user" class="btn btn-default" [disabled]="!form.valid">
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,7 +9,8 @@ import {
|
||||||
FormReactive,
|
FormReactive,
|
||||||
USER_USERNAME,
|
USER_USERNAME,
|
||||||
USER_EMAIL,
|
USER_EMAIL,
|
||||||
USER_PASSWORD
|
USER_PASSWORD,
|
||||||
|
USER_VIDEO_QUOTA
|
||||||
} from '../../../shared'
|
} from '../../../shared'
|
||||||
import { UserCreate } from '../../../../../../shared'
|
import { UserCreate } from '../../../../../../shared'
|
||||||
|
|
||||||
|
@ -24,12 +25,14 @@ export class UserAddComponent extends FormReactive implements OnInit {
|
||||||
formErrors = {
|
formErrors = {
|
||||||
'username': '',
|
'username': '',
|
||||||
'email': '',
|
'email': '',
|
||||||
'password': ''
|
'password': '',
|
||||||
|
'videoQuota': ''
|
||||||
}
|
}
|
||||||
validationMessages = {
|
validationMessages = {
|
||||||
'username': USER_USERNAME.MESSAGES,
|
'username': USER_USERNAME.MESSAGES,
|
||||||
'email': USER_EMAIL.MESSAGES,
|
'email': USER_EMAIL.MESSAGES,
|
||||||
'password': USER_PASSWORD.MESSAGES
|
'password': USER_PASSWORD.MESSAGES,
|
||||||
|
'videoQuota': USER_VIDEO_QUOTA.MESSAGES
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
|
@ -45,7 +48,8 @@ export class UserAddComponent extends FormReactive implements OnInit {
|
||||||
this.form = this.formBuilder.group({
|
this.form = this.formBuilder.group({
|
||||||
username: [ '', USER_USERNAME.VALIDATORS ],
|
username: [ '', USER_USERNAME.VALIDATORS ],
|
||||||
email: [ '', USER_EMAIL.VALIDATORS ],
|
email: [ '', USER_EMAIL.VALIDATORS ],
|
||||||
password: [ '', USER_PASSWORD.VALIDATORS ]
|
password: [ '', USER_PASSWORD.VALIDATORS ],
|
||||||
|
videoQuota: [ '-1', USER_VIDEO_QUOTA.VALIDATORS ]
|
||||||
})
|
})
|
||||||
|
|
||||||
this.form.valueChanges.subscribe(data => this.onValueChanged(data))
|
this.form.valueChanges.subscribe(data => this.onValueChanged(data))
|
||||||
|
@ -60,6 +64,9 @@ export class UserAddComponent extends FormReactive implements OnInit {
|
||||||
|
|
||||||
const userCreate: UserCreate = this.form.value
|
const userCreate: UserCreate = this.form.value
|
||||||
|
|
||||||
|
// A select in HTML is always mapped as a string, we convert it to number
|
||||||
|
userCreate.videoQuota = parseInt(this.form.value['videoQuota'], 10)
|
||||||
|
|
||||||
this.userService.addUser(userCreate).subscribe(
|
this.userService.addUser(userCreate).subscribe(
|
||||||
() => {
|
() => {
|
||||||
this.notificationsService.success('Success', `User ${userCreate.username} created.`)
|
this.notificationsService.success('Success', `User ${userCreate.username} created.`)
|
||||||
|
|
|
@ -30,7 +30,7 @@ export class UserListComponent {
|
||||||
},
|
},
|
||||||
pager: {
|
pager: {
|
||||||
display: true,
|
display: true,
|
||||||
perPage: 10
|
perPage: 1
|
||||||
},
|
},
|
||||||
columns: {
|
columns: {
|
||||||
id: {
|
id: {
|
||||||
|
@ -43,6 +43,9 @@ export class UserListComponent {
|
||||||
email: {
|
email: {
|
||||||
title: 'Email'
|
title: 'Email'
|
||||||
},
|
},
|
||||||
|
videoQuota: {
|
||||||
|
title: 'Video quota'
|
||||||
|
},
|
||||||
role: {
|
role: {
|
||||||
title: 'Role',
|
title: 'Role',
|
||||||
sort: false
|
sort: false
|
||||||
|
|
|
@ -22,3 +22,10 @@ export const USER_PASSWORD = {
|
||||||
'minlength': 'Password must be at least 6 characters long.'
|
'minlength': 'Password must be at least 6 characters long.'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export const USER_VIDEO_QUOTA = {
|
||||||
|
VALIDATORS: [ Validators.required, Validators.min(-1) ],
|
||||||
|
MESSAGES: {
|
||||||
|
'required': 'Video quota is required.',
|
||||||
|
'min': 'Quota must be greater than -1.'
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,14 +3,31 @@ import { Http, RequestOptionsArgs, URLSearchParams, Response } from '@angular/ht
|
||||||
import { ServerDataSource } from 'ng2-smart-table'
|
import { ServerDataSource } from 'ng2-smart-table'
|
||||||
|
|
||||||
export class RestDataSource extends ServerDataSource {
|
export class RestDataSource extends ServerDataSource {
|
||||||
constructor (http: Http, endpoint: string) {
|
private updateResponse: (input: any[]) => any[]
|
||||||
|
|
||||||
|
constructor (http: Http, endpoint: string, updateResponse?: (input: any[]) => any[]) {
|
||||||
const options = {
|
const options = {
|
||||||
endPoint: endpoint,
|
endPoint: endpoint,
|
||||||
sortFieldKey: 'sort',
|
sortFieldKey: 'sort',
|
||||||
dataKey: 'data'
|
dataKey: 'data'
|
||||||
}
|
}
|
||||||
|
|
||||||
super(http, options)
|
super(http, options)
|
||||||
|
|
||||||
|
if (updateResponse) {
|
||||||
|
this.updateResponse = updateResponse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected extractDataFromResponse (res: Response) {
|
||||||
|
const json = res.json()
|
||||||
|
if (!json) return []
|
||||||
|
let data = json.data
|
||||||
|
|
||||||
|
if (this.updateResponse !== undefined) {
|
||||||
|
data = this.updateResponse(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
protected extractTotalFromResponse (res: Response) {
|
protected extractTotalFromResponse (res: Response) {
|
||||||
|
|
|
@ -6,6 +6,7 @@ export class User implements UserServerModel {
|
||||||
email: string
|
email: string
|
||||||
role: UserRole
|
role: UserRole
|
||||||
displayNSFW: boolean
|
displayNSFW: boolean
|
||||||
|
videoQuota: number
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
|
|
||||||
constructor (hash: {
|
constructor (hash: {
|
||||||
|
@ -13,6 +14,7 @@ export class User implements UserServerModel {
|
||||||
username: string,
|
username: string,
|
||||||
email: string,
|
email: string,
|
||||||
role: UserRole,
|
role: UserRole,
|
||||||
|
videoQuota?: number,
|
||||||
displayNSFW?: boolean,
|
displayNSFW?: boolean,
|
||||||
createdAt?: Date
|
createdAt?: Date
|
||||||
}) {
|
}) {
|
||||||
|
@ -20,9 +22,16 @@ export class User implements UserServerModel {
|
||||||
this.username = hash.username
|
this.username = hash.username
|
||||||
this.email = hash.email
|
this.email = hash.email
|
||||||
this.role = hash.role
|
this.role = hash.role
|
||||||
this.displayNSFW = hash.displayNSFW
|
|
||||||
|
|
||||||
if (hash.createdAt) {
|
if (hash.videoQuota !== undefined) {
|
||||||
|
this.videoQuota = hash.videoQuota
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hash.displayNSFW !== undefined) {
|
||||||
|
this.displayNSFW = hash.displayNSFW
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hash.createdAt !== undefined) {
|
||||||
this.createdAt = hash.createdAt
|
this.createdAt = hash.createdAt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-inferrable-types": true,
|
"no-inferrable-types": true,
|
||||||
"eofline": true,
|
"eofline": true,
|
||||||
"indent": ["spaces"],
|
|
||||||
"max-line-length": [true, 140],
|
"max-line-length": [true, 140],
|
||||||
"no-floating-promises": false,
|
"no-floating-promises": false,
|
||||||
"no-unused-variable": false, // Bug, wait TypeScript 2.4
|
"no-unused-variable": false, // Bug, wait TypeScript 2.4
|
||||||
|
|
|
@ -6740,9 +6740,9 @@ tslint-loader@^3.3.0:
|
||||||
rimraf "^2.4.4"
|
rimraf "^2.4.4"
|
||||||
semver "^5.3.0"
|
semver "^5.3.0"
|
||||||
|
|
||||||
tslint@^5.4.3:
|
tslint@^5.7.0:
|
||||||
version "5.6.0"
|
version "5.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.6.0.tgz#088aa6c6026623338650b2900828ab3edf59f6cf"
|
resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.7.0.tgz#c25e0d0c92fa1201c2bc30e844e08e682b4f3552"
|
||||||
dependencies:
|
dependencies:
|
||||||
babel-code-frame "^6.22.0"
|
babel-code-frame "^6.22.0"
|
||||||
colors "^1.1.2"
|
colors "^1.1.2"
|
||||||
|
@ -6753,7 +6753,7 @@ tslint@^5.4.3:
|
||||||
resolve "^1.3.2"
|
resolve "^1.3.2"
|
||||||
semver "^5.3.0"
|
semver "^5.3.0"
|
||||||
tslib "^1.7.1"
|
tslib "^1.7.1"
|
||||||
tsutils "^2.7.1"
|
tsutils "^2.8.1"
|
||||||
|
|
||||||
tsml@1.0.1:
|
tsml@1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
|
@ -6763,9 +6763,9 @@ tsutils@^1.4.0:
|
||||||
version "1.9.1"
|
version "1.9.1"
|
||||||
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-1.9.1.tgz#b9f9ab44e55af9681831d5f28d0aeeaf5c750cb0"
|
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-1.9.1.tgz#b9f9ab44e55af9681831d5f28d0aeeaf5c750cb0"
|
||||||
|
|
||||||
tsutils@^2.7.1:
|
tsutils@^2.8.1:
|
||||||
version "2.8.1"
|
version "2.8.2"
|
||||||
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.8.1.tgz#3771404e7ca9f0bedf5d919a47a4b1890a68efff"
|
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.8.2.tgz#2c1486ba431260845b0ac6f902afd9d708a8ea6a"
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib "^1.7.1"
|
tslib "^1.7.1"
|
||||||
|
|
||||||
|
@ -6806,9 +6806,9 @@ typedarray@^0.0.6:
|
||||||
version "0.0.6"
|
version "0.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
||||||
|
|
||||||
typescript@~2.4.0:
|
typescript@^2.5.2:
|
||||||
version "2.4.2"
|
version "2.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.4.2.tgz#f8395f85d459276067c988aa41837a8f82870844"
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.5.2.tgz#038a95f7d9bbb420b1bf35ba31d4c5c1dd3ffe34"
|
||||||
|
|
||||||
uglify-js@3.0.x, uglify-js@^3.0.6:
|
uglify-js@3.0.x, uglify-js@^3.0.6:
|
||||||
version "3.0.28"
|
version "3.0.28"
|
||||||
|
|
|
@ -35,6 +35,11 @@ signup:
|
||||||
enabled: false
|
enabled: false
|
||||||
limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited
|
limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited
|
||||||
|
|
||||||
|
user:
|
||||||
|
# Default value of maximum video BYTES the user can upload (does not take into account transcoded files).
|
||||||
|
# -1 == unlimited
|
||||||
|
video_quota: -1
|
||||||
|
|
||||||
# If enabled, the video will be transcoded to mp4 (x264) with "faststart" flag
|
# If enabled, the video will be transcoded to mp4 (x264) with "faststart" flag
|
||||||
# Uses a lot of CPU!
|
# Uses a lot of CPU!
|
||||||
transcoding:
|
transcoding:
|
||||||
|
|
|
@ -36,6 +36,11 @@ signup:
|
||||||
enabled: false
|
enabled: false
|
||||||
limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited
|
limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited
|
||||||
|
|
||||||
|
user:
|
||||||
|
# Default value of maximum video BYTES the user can upload (does not take into account transcoded files).
|
||||||
|
# -1 == unlimited
|
||||||
|
video_quota: -1
|
||||||
|
|
||||||
# If enabled, the video will be transcoded to mp4 (x264) with "faststart" flag
|
# If enabled, the video will be transcoded to mp4 (x264) with "faststart" flag
|
||||||
# Uses a lot of CPU!
|
# Uses a lot of CPU!
|
||||||
transcoding:
|
transcoding:
|
||||||
|
|
|
@ -79,7 +79,7 @@
|
||||||
"scripty": "^1.5.0",
|
"scripty": "^1.5.0",
|
||||||
"sequelize": "^4.7.5",
|
"sequelize": "^4.7.5",
|
||||||
"ts-node": "^3.0.6",
|
"ts-node": "^3.0.6",
|
||||||
"typescript": "^2.4.1",
|
"typescript": "^2.5.2",
|
||||||
"validator": "^8.1.0",
|
"validator": "^8.1.0",
|
||||||
"winston": "^2.1.1",
|
"winston": "^2.1.1",
|
||||||
"ws": "^3.1.0"
|
"ws": "^3.1.0"
|
||||||
|
@ -109,7 +109,7 @@
|
||||||
"source-map-support": "^0.4.15",
|
"source-map-support": "^0.4.15",
|
||||||
"standard": "^10.0.0",
|
"standard": "^10.0.0",
|
||||||
"supertest": "^3.0.0",
|
"supertest": "^3.0.0",
|
||||||
"tslint": "^5.2.0",
|
"tslint": "^5.7.0",
|
||||||
"tslint-config-standard": "^6.0.0",
|
"tslint-config-standard": "^6.0.0",
|
||||||
"webtorrent": "^0.98.0"
|
"webtorrent": "^0.98.0"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import * as express from 'express'
|
import * as express from 'express'
|
||||||
|
|
||||||
import { database as db } from '../../initializers/database'
|
import { database as db } from '../../initializers/database'
|
||||||
import { USER_ROLES } from '../../initializers'
|
import { USER_ROLES, CONFIG } from '../../initializers'
|
||||||
import { logger, getFormattedObjects } from '../../helpers'
|
import { logger, getFormattedObjects } from '../../helpers'
|
||||||
import {
|
import {
|
||||||
authenticate,
|
authenticate,
|
||||||
|
@ -80,12 +80,18 @@ export {
|
||||||
function createUser (req: express.Request, res: express.Response, next: express.NextFunction) {
|
function createUser (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||||
const body: UserCreate = req.body
|
const body: UserCreate = req.body
|
||||||
|
|
||||||
|
// On registration, we set the user video quota
|
||||||
|
if (body.videoQuota === undefined) {
|
||||||
|
body.videoQuota = CONFIG.USER.VIDEO_QUOTA
|
||||||
|
}
|
||||||
|
|
||||||
const user = db.User.build({
|
const user = db.User.build({
|
||||||
username: body.username,
|
username: body.username,
|
||||||
password: body.password,
|
password: body.password,
|
||||||
email: body.email,
|
email: body.email,
|
||||||
displayNSFW: false,
|
displayNSFW: false,
|
||||||
role: USER_ROLES.USER
|
role: USER_ROLES.USER,
|
||||||
|
videoQuota: body.videoQuota
|
||||||
})
|
})
|
||||||
|
|
||||||
user.save()
|
user.save()
|
||||||
|
@ -140,6 +146,7 @@ function updateUser (req: express.Request, res: express.Response, next: express.
|
||||||
.then(user => {
|
.then(user => {
|
||||||
if (body.password) user.password = body.password
|
if (body.password) user.password = body.password
|
||||||
if (body.displayNSFW !== undefined) user.displayNSFW = body.displayNSFW
|
if (body.displayNSFW !== undefined) user.displayNSFW = body.displayNSFW
|
||||||
|
if (body.videoQuota !== undefined) user.videoQuota = body.videoQuota
|
||||||
|
|
||||||
return user.save()
|
return user.save()
|
||||||
})
|
})
|
||||||
|
|
|
@ -15,6 +15,10 @@ function isUserRoleValid (value: string) {
|
||||||
return values(USER_ROLES).indexOf(value as UserRole) !== -1
|
return values(USER_ROLES).indexOf(value as UserRole) !== -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isUserVideoQuotaValid (value: string) {
|
||||||
|
return exists(value) && validator.isInt(value + '', USERS_CONSTRAINTS_FIELDS.VIDEO_QUOTA)
|
||||||
|
}
|
||||||
|
|
||||||
function isUserUsernameValid (value: string) {
|
function isUserUsernameValid (value: string) {
|
||||||
const max = USERS_CONSTRAINTS_FIELDS.USERNAME.max
|
const max = USERS_CONSTRAINTS_FIELDS.USERNAME.max
|
||||||
const min = USERS_CONSTRAINTS_FIELDS.USERNAME.min
|
const min = USERS_CONSTRAINTS_FIELDS.USERNAME.min
|
||||||
|
@ -30,6 +34,7 @@ function isUserDisplayNSFWValid (value: any) {
|
||||||
export {
|
export {
|
||||||
isUserPasswordValid,
|
isUserPasswordValid,
|
||||||
isUserRoleValid,
|
isUserRoleValid,
|
||||||
|
isUserVideoQuotaValid,
|
||||||
isUserUsernameValid,
|
isUserUsernameValid,
|
||||||
isUserDisplayNSFWValid
|
isUserDisplayNSFWValid
|
||||||
}
|
}
|
||||||
|
@ -39,6 +44,7 @@ declare module 'express-validator' {
|
||||||
isUserPasswordValid,
|
isUserPasswordValid,
|
||||||
isUserRoleValid,
|
isUserRoleValid,
|
||||||
isUserUsernameValid,
|
isUserUsernameValid,
|
||||||
isUserDisplayNSFWValid
|
isUserDisplayNSFWValid,
|
||||||
|
isUserVideoQuotaValid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ import {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const LAST_MIGRATION_VERSION = 65
|
const LAST_MIGRATION_VERSION = 70
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@ -77,7 +77,10 @@ const CONFIG = {
|
||||||
},
|
},
|
||||||
SIGNUP: {
|
SIGNUP: {
|
||||||
ENABLED: config.get<boolean>('signup.enabled'),
|
ENABLED: config.get<boolean>('signup.enabled'),
|
||||||
LIMIT: config.get<number>('signup.limit')
|
LIMIT: config.get<number>('signup.limit'),
|
||||||
|
},
|
||||||
|
USER: {
|
||||||
|
VIDEO_QUOTA: config.get<number>('user.video_quota')
|
||||||
},
|
},
|
||||||
TRANSCODING: {
|
TRANSCODING: {
|
||||||
ENABLED: config.get<boolean>('transcoding.enabled'),
|
ENABLED: config.get<boolean>('transcoding.enabled'),
|
||||||
|
@ -97,7 +100,8 @@ CONFIG.WEBSERVER.HOST = CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
|
||||||
const CONSTRAINTS_FIELDS = {
|
const CONSTRAINTS_FIELDS = {
|
||||||
USERS: {
|
USERS: {
|
||||||
USERNAME: { min: 3, max: 20 }, // Length
|
USERNAME: { min: 3, max: 20 }, // Length
|
||||||
PASSWORD: { min: 6, max: 255 } // Length
|
PASSWORD: { min: 6, max: 255 }, // Length
|
||||||
|
VIDEO_QUOTA: { min: -1 }
|
||||||
},
|
},
|
||||||
VIDEO_ABUSES: {
|
VIDEO_ABUSES: {
|
||||||
REASON: { min: 2, max: 300 } // Length
|
REASON: { min: 2, max: 300 } // Length
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { flattenDepth } from 'lodash'
|
import { flattenDepth } from 'lodash'
|
||||||
|
require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
|
||||||
import * as Sequelize from 'sequelize'
|
import * as Sequelize from 'sequelize'
|
||||||
import * as Promise from 'bluebird'
|
import * as Promise from 'bluebird'
|
||||||
|
|
||||||
|
|
|
@ -38,12 +38,12 @@ function removeCacheDirectories () {
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDirectoriesIfNotExist () {
|
function createDirectoriesIfNotExist () {
|
||||||
const storages = CONFIG.STORAGE
|
const storage = CONFIG.STORAGE
|
||||||
const cacheDirectories = CACHE.DIRECTORIES
|
const cacheDirectories = CACHE.DIRECTORIES
|
||||||
|
|
||||||
const tasks = []
|
const tasks = []
|
||||||
Object.keys(storages).forEach(key => {
|
Object.keys(storage).forEach(key => {
|
||||||
const dir = storages[key]
|
const dir = storage[key]
|
||||||
tasks.push(mkdirpPromise(dir))
|
tasks.push(mkdirpPromise(dir))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -112,7 +112,8 @@ function createOAuthAdminIfNotExist () {
|
||||||
username,
|
username,
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
role
|
role,
|
||||||
|
videoQuota: -1
|
||||||
}
|
}
|
||||||
|
|
||||||
return db.User.create(userData, createOptions).then(createdUser => {
|
return db.User.create(userData, createOptions).then(createdUser => {
|
||||||
|
|
32
server/initializers/migrations/0070-user-video-quota.ts
Normal file
32
server/initializers/migrations/0070-user-video-quota.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import * as Sequelize from 'sequelize'
|
||||||
|
import * as Promise from 'bluebird'
|
||||||
|
|
||||||
|
function up (utils: {
|
||||||
|
transaction: Sequelize.Transaction,
|
||||||
|
queryInterface: Sequelize.QueryInterface,
|
||||||
|
sequelize: Sequelize.Sequelize,
|
||||||
|
db: any
|
||||||
|
}): Promise<void> {
|
||||||
|
const q = utils.queryInterface
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
type: Sequelize.BIGINT,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: -1
|
||||||
|
}
|
||||||
|
|
||||||
|
return q.addColumn('Users', 'videoQuota', data)
|
||||||
|
.then(() => {
|
||||||
|
data.defaultValue = null
|
||||||
|
return q.changeColumn('Users', 'videoQuota', data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function down (options) {
|
||||||
|
throw new Error('Not implemented.')
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
up,
|
||||||
|
down
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ function usersAddValidator (req: express.Request, res: express.Response, next: e
|
||||||
req.checkBody('username', 'Should have a valid username').isUserUsernameValid()
|
req.checkBody('username', 'Should have a valid username').isUserUsernameValid()
|
||||||
req.checkBody('password', 'Should have a valid password').isUserPasswordValid()
|
req.checkBody('password', 'Should have a valid password').isUserPasswordValid()
|
||||||
req.checkBody('email', 'Should have a valid email').isEmail()
|
req.checkBody('email', 'Should have a valid email').isEmail()
|
||||||
|
req.checkBody('videoQuota', 'Should have a valid user quota').isUserVideoQuotaValid()
|
||||||
|
|
||||||
logger.debug('Checking usersAdd parameters', { parameters: req.body })
|
logger.debug('Checking usersAdd parameters', { parameters: req.body })
|
||||||
|
|
||||||
|
@ -55,6 +56,7 @@ function usersUpdateValidator (req: express.Request, res: express.Response, next
|
||||||
// Add old password verification
|
// Add old password verification
|
||||||
req.checkBody('password', 'Should have a valid password').optional().isUserPasswordValid()
|
req.checkBody('password', 'Should have a valid password').optional().isUserPasswordValid()
|
||||||
req.checkBody('displayNSFW', 'Should have a valid display Not Safe For Work attribute').optional().isUserDisplayNSFWValid()
|
req.checkBody('displayNSFW', 'Should have a valid display Not Safe For Work attribute').optional().isUserDisplayNSFWValid()
|
||||||
|
req.checkBody('videoQuota', 'Should have a valid user quota').optional().isUserVideoQuotaValid()
|
||||||
|
|
||||||
logger.debug('Checking usersUpdate parameters', { parameters: req.body })
|
logger.debug('Checking usersUpdate parameters', { parameters: req.body })
|
||||||
|
|
||||||
|
|
|
@ -24,10 +24,23 @@ function videosAddValidator (req: express.Request, res: express.Response, next:
|
||||||
logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
|
logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
|
||||||
|
|
||||||
checkErrors(req, res, () => {
|
checkErrors(req, res, () => {
|
||||||
const videoFile = req.files['videofile'][0]
|
const videoFile: Express.Multer.File = req.files['videofile'][0]
|
||||||
|
const user = res.locals.oauth.token.User
|
||||||
|
|
||||||
db.Video.getDurationFromFile(videoFile.path)
|
user.isAbleToUploadVideo(videoFile)
|
||||||
|
.then(isAble => {
|
||||||
|
if (isAble === false) {
|
||||||
|
res.status(403).send('The user video quota is exceeded with this video.')
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.Video.getDurationFromFile(videoFile.path)
|
||||||
|
})
|
||||||
.then(duration => {
|
.then(duration => {
|
||||||
|
// Previous test failed, abort
|
||||||
|
if (duration === undefined) return
|
||||||
|
|
||||||
if (!isVideoDurationValid('' + duration)) {
|
if (!isVideoDurationValid('' + duration)) {
|
||||||
return res.status(400).send('Duration of the video file is too big (max: ' + CONSTRAINTS_FIELDS.VIDEOS.DURATION.max + 's).')
|
return res.status(400).send('Duration of the video file is too big (max: ' + CONSTRAINTS_FIELDS.VIDEOS.DURATION.max + 's).')
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ export namespace UserMethods {
|
||||||
|
|
||||||
export type ToFormattedJSON = (this: UserInstance) => FormattedUser
|
export type ToFormattedJSON = (this: UserInstance) => FormattedUser
|
||||||
export type IsAdmin = (this: UserInstance) => boolean
|
export type IsAdmin = (this: UserInstance) => boolean
|
||||||
|
export type IsAbleToUploadVideo = (this: UserInstance, videoFile: Express.Multer.File) => Promise<boolean>
|
||||||
|
|
||||||
export type CountTotal = () => Promise<number>
|
export type CountTotal = () => Promise<number>
|
||||||
|
|
||||||
|
@ -31,6 +32,7 @@ export interface UserClass {
|
||||||
isPasswordMatch: UserMethods.IsPasswordMatch,
|
isPasswordMatch: UserMethods.IsPasswordMatch,
|
||||||
toFormattedJSON: UserMethods.ToFormattedJSON,
|
toFormattedJSON: UserMethods.ToFormattedJSON,
|
||||||
isAdmin: UserMethods.IsAdmin,
|
isAdmin: UserMethods.IsAdmin,
|
||||||
|
isAbleToUploadVideo: UserMethods.IsAbleToUploadVideo,
|
||||||
|
|
||||||
countTotal: UserMethods.CountTotal,
|
countTotal: UserMethods.CountTotal,
|
||||||
getByUsername: UserMethods.GetByUsername,
|
getByUsername: UserMethods.GetByUsername,
|
||||||
|
@ -42,11 +44,13 @@ export interface UserClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserAttributes {
|
export interface UserAttributes {
|
||||||
|
id?: number
|
||||||
password: string
|
password: string
|
||||||
username: string
|
username: string
|
||||||
email: string
|
email: string
|
||||||
displayNSFW?: boolean
|
displayNSFW?: boolean
|
||||||
role: UserRole
|
role: UserRole
|
||||||
|
videoQuota: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserInstance extends UserClass, UserAttributes, Sequelize.Instance<UserAttributes> {
|
export interface UserInstance extends UserClass, UserAttributes, Sequelize.Instance<UserAttributes> {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { values } from 'lodash'
|
import { values } from 'lodash'
|
||||||
import * as Sequelize from 'sequelize'
|
import * as Sequelize from 'sequelize'
|
||||||
|
import * as Promise from 'bluebird'
|
||||||
|
|
||||||
import { getSort } from '../utils'
|
import { getSort } from '../utils'
|
||||||
import { USER_ROLES } from '../../initializers'
|
import { USER_ROLES } from '../../initializers'
|
||||||
|
@ -8,7 +9,8 @@ import {
|
||||||
comparePassword,
|
comparePassword,
|
||||||
isUserPasswordValid,
|
isUserPasswordValid,
|
||||||
isUserUsernameValid,
|
isUserUsernameValid,
|
||||||
isUserDisplayNSFWValid
|
isUserDisplayNSFWValid,
|
||||||
|
isUserVideoQuotaValid
|
||||||
} from '../../helpers'
|
} from '../../helpers'
|
||||||
|
|
||||||
import { addMethodsToModel } from '../utils'
|
import { addMethodsToModel } from '../utils'
|
||||||
|
@ -30,6 +32,7 @@ let listForApi: UserMethods.ListForApi
|
||||||
let loadById: UserMethods.LoadById
|
let loadById: UserMethods.LoadById
|
||||||
let loadByUsername: UserMethods.LoadByUsername
|
let loadByUsername: UserMethods.LoadByUsername
|
||||||
let loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail
|
let loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail
|
||||||
|
let isAbleToUploadVideo: UserMethods.IsAbleToUploadVideo
|
||||||
|
|
||||||
export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
|
export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
|
||||||
User = sequelize.define<UserInstance, UserAttributes>('User',
|
User = sequelize.define<UserInstance, UserAttributes>('User',
|
||||||
|
@ -75,6 +78,16 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
|
||||||
role: {
|
role: {
|
||||||
type: DataTypes.ENUM(values(USER_ROLES)),
|
type: DataTypes.ENUM(values(USER_ROLES)),
|
||||||
allowNull: false
|
allowNull: false
|
||||||
|
},
|
||||||
|
videoQuota: {
|
||||||
|
type: DataTypes.BIGINT,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
videoQuotaValid: value => {
|
||||||
|
const res = isUserVideoQuotaValid(value)
|
||||||
|
if (res === false) throw new Error('Video quota is not valid.')
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -109,7 +122,8 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
|
||||||
const instanceMethods = [
|
const instanceMethods = [
|
||||||
isPasswordMatch,
|
isPasswordMatch,
|
||||||
toFormattedJSON,
|
toFormattedJSON,
|
||||||
isAdmin
|
isAdmin,
|
||||||
|
isAbleToUploadVideo
|
||||||
]
|
]
|
||||||
addMethodsToModel(User, classMethods, instanceMethods)
|
addMethodsToModel(User, classMethods, instanceMethods)
|
||||||
|
|
||||||
|
@ -136,6 +150,7 @@ toFormattedJSON = function (this: UserInstance) {
|
||||||
email: this.email,
|
email: this.email,
|
||||||
displayNSFW: this.displayNSFW,
|
displayNSFW: this.displayNSFW,
|
||||||
role: this.role,
|
role: this.role,
|
||||||
|
videoQuota: this.videoQuota,
|
||||||
createdAt: this.createdAt
|
createdAt: this.createdAt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -144,6 +159,14 @@ isAdmin = function (this: UserInstance) {
|
||||||
return this.role === USER_ROLES.ADMIN
|
return this.role === USER_ROLES.ADMIN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isAbleToUploadVideo = function (this: UserInstance, videoFile: Express.Multer.File) {
|
||||||
|
if (this.videoQuota === -1) return Promise.resolve(true)
|
||||||
|
|
||||||
|
return getOriginalVideoFileTotalFromUser(this).then(totalBytes => {
|
||||||
|
return (videoFile.size + totalBytes) < this.videoQuota
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------ STATICS ------------------------------
|
// ------------------------------ STATICS ------------------------------
|
||||||
|
|
||||||
function associate (models) {
|
function associate (models) {
|
||||||
|
@ -215,3 +238,36 @@ loadByUsernameOrEmail = function (username: string, email: string) {
|
||||||
// FIXME: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18387
|
// FIXME: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18387
|
||||||
return (User as any).findOne(query)
|
return (User as any).findOne(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function getOriginalVideoFileTotalFromUser (user: UserInstance) {
|
||||||
|
const query = {
|
||||||
|
attributes: [
|
||||||
|
Sequelize.fn('COUNT', Sequelize.col('VideoFile.size'), 'totalVideoBytes')
|
||||||
|
],
|
||||||
|
where: {
|
||||||
|
id: user.id
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: User['sequelize'].models.Author,
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: User['sequelize'].models.Video,
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: User['sequelize'].models.VideoFile
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: cast to any because of bad typing...
|
||||||
|
return User.findAll(query).then((res: any) => {
|
||||||
|
return res.totalVideoBytes
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import * as Sequelize from 'sequelize'
|
||||||
import * as Promise from 'bluebird'
|
import * as Promise from 'bluebird'
|
||||||
|
|
||||||
import { TagInstance } from './tag-interface'
|
import { TagInstance } from './tag-interface'
|
||||||
|
import { UserInstance } from '../user/user-interface'
|
||||||
import {
|
import {
|
||||||
logger,
|
logger,
|
||||||
isVideoNameValid,
|
isVideoNameValid,
|
||||||
|
@ -582,7 +583,7 @@ transcodeVideofile = function (this: VideoInstance, inputVideoFile: VideoFileIns
|
||||||
return res()
|
return res()
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
// Autodestruction...
|
// Auto destruction...
|
||||||
this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
|
this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
|
||||||
|
|
||||||
return rej(err)
|
return rej(err)
|
||||||
|
@ -608,8 +609,8 @@ removeFile = function (this: VideoInstance, videoFile: VideoFileInstance) {
|
||||||
}
|
}
|
||||||
|
|
||||||
removeTorrent = function (this: VideoInstance, videoFile: VideoFileInstance) {
|
removeTorrent = function (this: VideoInstance, videoFile: VideoFileInstance) {
|
||||||
const torrenPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
|
const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
|
||||||
return unlinkPromise(torrenPath)
|
return unlinkPromise(torrentPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------ STATICS ------------------------------
|
// ------------------------------ STATICS ------------------------------
|
||||||
|
|
|
@ -2,4 +2,5 @@ export interface UserCreate {
|
||||||
username: string
|
username: string
|
||||||
password: string
|
password: string
|
||||||
email: string
|
email: string
|
||||||
|
videoQuota: number
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export interface UserUpdate {
|
export interface UserUpdate {
|
||||||
displayNSFW?: boolean
|
displayNSFW?: boolean
|
||||||
password?: string
|
password?: string
|
||||||
|
videoQuota?: number
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,5 +6,6 @@ export interface User {
|
||||||
email: string
|
email: string
|
||||||
displayNSFW: boolean
|
displayNSFW: boolean
|
||||||
role: UserRole
|
role: UserRole
|
||||||
|
videoQuota: number
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
"no-inferrable-types": true,
|
"no-inferrable-types": true,
|
||||||
"eofline": true,
|
"eofline": true,
|
||||||
"indent": ["spaces"],
|
"indent": ["spaces"],
|
||||||
|
"ter-indent": [true, 2],
|
||||||
"max-line-length": [true, 140],
|
"max-line-length": [true, 140],
|
||||||
"no-floating-promises": false
|
"no-floating-promises": false
|
||||||
}
|
}
|
||||||
|
|
20
yarn.lock
20
yarn.lock
|
@ -3755,9 +3755,9 @@ tslint-eslint-rules@^4.0.0:
|
||||||
tslib "^1.0.0"
|
tslib "^1.0.0"
|
||||||
tsutils "^1.4.0"
|
tsutils "^1.4.0"
|
||||||
|
|
||||||
tslint@^5.2.0:
|
tslint@^5.7.0:
|
||||||
version "5.6.0"
|
version "5.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.6.0.tgz#088aa6c6026623338650b2900828ab3edf59f6cf"
|
resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.7.0.tgz#c25e0d0c92fa1201c2bc30e844e08e682b4f3552"
|
||||||
dependencies:
|
dependencies:
|
||||||
babel-code-frame "^6.22.0"
|
babel-code-frame "^6.22.0"
|
||||||
colors "^1.1.2"
|
colors "^1.1.2"
|
||||||
|
@ -3768,15 +3768,15 @@ tslint@^5.2.0:
|
||||||
resolve "^1.3.2"
|
resolve "^1.3.2"
|
||||||
semver "^5.3.0"
|
semver "^5.3.0"
|
||||||
tslib "^1.7.1"
|
tslib "^1.7.1"
|
||||||
tsutils "^2.7.1"
|
tsutils "^2.8.1"
|
||||||
|
|
||||||
tsutils@^1.4.0:
|
tsutils@^1.4.0:
|
||||||
version "1.9.1"
|
version "1.9.1"
|
||||||
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-1.9.1.tgz#b9f9ab44e55af9681831d5f28d0aeeaf5c750cb0"
|
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-1.9.1.tgz#b9f9ab44e55af9681831d5f28d0aeeaf5c750cb0"
|
||||||
|
|
||||||
tsutils@^2.7.1:
|
tsutils@^2.8.1:
|
||||||
version "2.8.1"
|
version "2.8.2"
|
||||||
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.8.1.tgz#3771404e7ca9f0bedf5d919a47a4b1890a68efff"
|
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.8.2.tgz#2c1486ba431260845b0ac6f902afd9d708a8ea6a"
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib "^1.7.1"
|
tslib "^1.7.1"
|
||||||
|
|
||||||
|
@ -3821,9 +3821,9 @@ typedarray@^0.0.6:
|
||||||
version "0.0.6"
|
version "0.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
||||||
|
|
||||||
typescript@^2.4.1:
|
typescript@^2.5.2:
|
||||||
version "2.5.1"
|
version "2.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.5.1.tgz#ce7cc93ada3de19475cc9d17e3adea7aee1832aa"
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.5.2.tgz#038a95f7d9bbb420b1bf35ba31d4c5c1dd3ffe34"
|
||||||
|
|
||||||
uid-number@^0.0.6:
|
uid-number@^0.0.6:
|
||||||
version "0.0.6"
|
version "0.0.6"
|
||||||
|
|
Loading…
Reference in a new issue