aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.component.html2
-rw-r--r--client/src/app/account/account-settings/account-settings.component.html6
-rw-r--r--client/src/app/account/account-settings/account-settings.component.scss9
-rw-r--r--client/src/app/account/account-settings/account-settings.component.ts4
-rw-r--r--client/src/app/core/auth/auth.service.ts21
-rw-r--r--client/src/app/shared/users/user.model.ts6
-rw-r--r--client/src/app/shared/users/user.service.ts5
-rw-r--r--client/src/app/videos/+video-edit/video-add.component.html2
-rw-r--r--client/src/app/videos/+video-edit/video-add.component.ts25
-rw-r--r--client/src/app/videos/+video-edit/video-update.component.ts5
-rw-r--r--server/controllers/api/users.ts17
-rw-r--r--server/models/account/user.ts2
-rw-r--r--server/tests/api/users/users.ts13
-rw-r--r--server/tests/utils/users/users.ts12
14 files changed, 93 insertions, 36 deletions
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.html b/client/src/app/+admin/users/user-edit/user-edit.component.html
index 77aa613a1..a69ffee77 100644
--- a/client/src/app/+admin/users/user-edit/user-edit.component.html
+++ b/client/src/app/+admin/users/user-edit/user-edit.component.html
@@ -64,7 +64,7 @@
64 64
65 <div class="transcoding-information" *ngIf="isTranscodingInformationDisplayed()"> 65 <div class="transcoding-information" *ngIf="isTranscodingInformationDisplayed()">
66 Transcoding is enabled on server. The video quota only take in account <strong>original</strong> video. <br /> 66 Transcoding is enabled on server. The video quota only take in account <strong>original</strong> video. <br />
67 In maximum, this user could use ~ {{ computeQuotaWithTranscoding() | bytes }}. 67 In maximum, this user could use ~ {{ computeQuotaWithTranscoding() | bytes: 0 }}.
68 </div> 68 </div>
69 </div> 69 </div>
70 70
diff --git a/client/src/app/account/account-settings/account-settings.component.html b/client/src/app/account/account-settings/account-settings.component.html
index 0d1637c40..2f41b5ecf 100644
--- a/client/src/app/account/account-settings/account-settings.component.html
+++ b/client/src/app/account/account-settings/account-settings.component.html
@@ -13,8 +13,12 @@
13</div> 13</div>
14<div class="file-max-size">(extensions: {{ avatarExtensions }}, max size: {{ maxAvatarSize | bytes }})</div> 14<div class="file-max-size">(extensions: {{ avatarExtensions }}, max size: {{ maxAvatarSize | bytes }})</div>
15 15
16<div class="user-quota">
17 <span class="user-quota-label">Video quota:</span> {{ userVideoQuotaUsed | bytes: 0 }} / {{ user.videoQuota | bytes: 0 }}
18</div>
19
16<div class="account-title">Account settings</div> 20<div class="account-title">Account settings</div>
17<my-account-change-password></my-account-change-password> 21<my-account-change-password></my-account-change-password>
18 22
19<div class="account-title">Videos</div> 23<div class="account-title">Video settings</div>
20<my-account-details [user]="user"></my-account-details> 24<my-account-details [user]="user"></my-account-details>
diff --git a/client/src/app/account/account-settings/account-settings.component.scss b/client/src/app/account/account-settings/account-settings.component.scss
index fbd1cb9f0..aaf9d79f0 100644
--- a/client/src/app/account/account-settings/account-settings.component.scss
+++ b/client/src/app/account/account-settings/account-settings.component.scss
@@ -36,6 +36,15 @@
36 top: -10px; 36 top: -10px;
37} 37}
38 38
39.user-quota {
40 font-size: 15px;
41 margin-top: 20px;
42
43 .user-quota-label {
44 font-weight: $font-semibold;
45 }
46}
47
39.account-title { 48.account-title {
40 text-transform: uppercase; 49 text-transform: uppercase;
41 color: $orange-color; 50 color: $orange-color;
diff --git a/client/src/app/account/account-settings/account-settings.component.ts b/client/src/app/account/account-settings/account-settings.component.ts
index d5f5ff30f..a375072a0 100644
--- a/client/src/app/account/account-settings/account-settings.component.ts
+++ b/client/src/app/account/account-settings/account-settings.component.ts
@@ -14,6 +14,7 @@ export class AccountSettingsComponent implements OnInit {
14 @ViewChild('avatarfileInput') avatarfileInput 14 @ViewChild('avatarfileInput') avatarfileInput
15 15
16 user: User = null 16 user: User = null
17 userVideoQuotaUsed = 0
17 18
18 constructor ( 19 constructor (
19 private userService: UserService, 20 private userService: UserService,
@@ -24,6 +25,9 @@ export class AccountSettingsComponent implements OnInit {
24 25
25 ngOnInit () { 26 ngOnInit () {
26 this.user = this.authService.getUser() 27 this.user = this.authService.getUser()
28
29 this.userService.getMyVideoQuotaUsed()
30 .subscribe(data => this.userVideoQuotaUsed = data.videoQuotaUsed)
27 } 31 }
28 32
29 getAvatarUrl () { 33 getAvatarUrl () {
diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts
index 8a2ba77d6..8700e8c74 100644
--- a/client/src/app/core/auth/auth.service.ts
+++ b/client/src/app/core/auth/auth.service.ts
@@ -14,7 +14,6 @@ import { User } from '../../../../../shared/models/users'
14import { UserLogin } from '../../../../../shared/models/users/user-login.model' 14import { UserLogin } from '../../../../../shared/models/users/user-login.model'
15import { environment } from '../../../environments/environment' 15import { environment } from '../../../environments/environment'
16import { RestExtractor } from '../../shared/rest' 16import { RestExtractor } from '../../shared/rest'
17import { UserConstructorHash } from '../../shared/users/user.model'
18import { AuthStatus } from './auth-status.model' 17import { AuthStatus } from './auth-status.model'
19import { AuthUser } from './auth-user.model' 18import { AuthUser } from './auth-user.model'
20 19
@@ -178,12 +177,7 @@ export class AuthService {
178 this.mergeUserInformation(obj) 177 this.mergeUserInformation(obj)
179 .subscribe( 178 .subscribe(
180 res => { 179 res => {
181 this.user.displayNSFW = res.displayNSFW 180 this.user.patch(res)
182 this.user.autoPlayVideo = res.autoPlayVideo
183 this.user.role = res.role
184 this.user.videoChannels = res.videoChannels
185 this.user.account = res.account
186
187 this.user.save() 181 this.user.save()
188 182
189 this.userInformationLoaded.next(true) 183 this.userInformationLoaded.next(true)
@@ -200,24 +194,13 @@ export class AuthService {
200 } 194 }
201 195
202 private handleLogin (obj: UserLoginWithUserInformation) { 196 private handleLogin (obj: UserLoginWithUserInformation) {
203 const hashUser: UserConstructorHash = {
204 id: obj.id,
205 username: obj.username,
206 role: obj.role,
207 email: obj.email,
208 displayNSFW: obj.displayNSFW,
209 autoPlayVideo: obj.autoPlayVideo,
210 videoQuota: obj.videoQuota,
211 videoChannels: obj.videoChannels,
212 account: obj.account
213 }
214 const hashTokens = { 197 const hashTokens = {
215 accessToken: obj.access_token, 198 accessToken: obj.access_token,
216 tokenType: obj.token_type, 199 tokenType: obj.token_type,
217 refreshToken: obj.refresh_token 200 refreshToken: obj.refresh_token
218 } 201 }
219 202
220 this.user = new AuthUser(hashUser, hashTokens) 203 this.user = new AuthUser(obj, hashTokens)
221 this.user.save() 204 this.user.save()
222 205
223 this.setStatus(AuthStatus.LoggedIn) 206 this.setStatus(AuthStatus.LoggedIn)
diff --git a/client/src/app/shared/users/user.model.ts b/client/src/app/shared/users/user.model.ts
index 83aae4463..4a94b032d 100644
--- a/client/src/app/shared/users/user.model.ts
+++ b/client/src/app/shared/users/user.model.ts
@@ -60,4 +60,10 @@ export class User implements UserServerModel {
60 getAvatarUrl () { 60 getAvatarUrl () {
61 return Account.GET_ACCOUNT_AVATAR_URL(this.account) 61 return Account.GET_ACCOUNT_AVATAR_URL(this.account)
62 } 62 }
63
64 patch (obj: UserServerModel) {
65 for (const key of Object.keys(obj)) {
66 this[key] = obj[key]
67 }
68 }
63} 69}
diff --git a/client/src/app/shared/users/user.service.ts b/client/src/app/shared/users/user.service.ts
index 58ddaa5ee..742fb0728 100644
--- a/client/src/app/shared/users/user.service.ts
+++ b/client/src/app/shared/users/user.service.ts
@@ -48,11 +48,10 @@ export class UserService {
48 .catch(res => this.restExtractor.handleError(res)) 48 .catch(res => this.restExtractor.handleError(res))
49 } 49 }
50 50
51 getMyInformation () { 51 getMyVideoQuotaUsed () {
52 const url = UserService.BASE_USERS_URL + 'me' 52 const url = UserService.BASE_USERS_URL + '/me/video-quota-used'
53 53
54 return this.authHttp.get(url) 54 return this.authHttp.get(url)
55 .map((userHash: any) => new User(userHash))
56 .catch(res => this.restExtractor.handleError(res)) 55 .catch(res => this.restExtractor.handleError(res))
57 } 56 }
58} 57}
diff --git a/client/src/app/videos/+video-edit/video-add.component.html b/client/src/app/videos/+video-edit/video-add.component.html
index 193cc55ee..2040ff9d4 100644
--- a/client/src/app/videos/+video-edit/video-add.component.html
+++ b/client/src/app/videos/+video-edit/video-add.component.html
@@ -3,8 +3,6 @@
3 Upload your video 3 Upload your video
4 </div> 4 </div>
5 5
6 <div *ngIf="error" class="alert alert-danger">{{ error }}</div>
7
8 <div *ngIf="!isUploadingVideo" class="upload-video-container"> 6 <div *ngIf="!isUploadingVideo" class="upload-video-container">
9 <div class="upload-video"> 7 <div class="upload-video">
10 <div class="icon icon-upload"></div> 8 <div class="icon icon-upload"></div>
diff --git a/client/src/app/videos/+video-edit/video-add.component.ts b/client/src/app/videos/+video-edit/video-add.component.ts
index 066f945fc..a86d9d3c2 100644
--- a/client/src/app/videos/+video-edit/video-add.component.ts
+++ b/client/src/app/videos/+video-edit/video-add.component.ts
@@ -2,7 +2,9 @@ import { HttpEventType, HttpResponse } from '@angular/common/http'
2import { Component, OnInit, ViewChild } from '@angular/core' 2import { Component, OnInit, ViewChild } from '@angular/core'
3import { FormBuilder, FormGroup } from '@angular/forms' 3import { FormBuilder, FormGroup } from '@angular/forms'
4import { Router } from '@angular/router' 4import { Router } from '@angular/router'
5import { UserService } from '@app/shared'
5import { NotificationsService } from 'angular2-notifications' 6import { NotificationsService } from 'angular2-notifications'
7import { BytesPipe } from 'ngx-pipes'
6import { VideoPrivacy } from '../../../../../shared/models/videos' 8import { VideoPrivacy } from '../../../../../shared/models/videos'
7import { AuthService, ServerService } from '../../core' 9import { AuthService, ServerService } from '../../core'
8import { FormReactive } from '../../shared' 10import { FormReactive } from '../../shared'
@@ -31,12 +33,12 @@ export class VideoAddComponent extends FormReactive implements OnInit {
31 uuid: '' 33 uuid: ''
32 } 34 }
33 35
34 error: string = null
35 form: FormGroup 36 form: FormGroup
36 formErrors: { [ id: string ]: string } = {} 37 formErrors: { [ id: string ]: string } = {}
37 validationMessages: ValidatorMessage = {} 38 validationMessages: ValidatorMessage = {}
38 39
39 userVideoChannels = [] 40 userVideoChannels = []
41 userVideoQuotaUsed = 0
40 videoPrivacies = [] 42 videoPrivacies = []
41 firstStepPrivacyId = 0 43 firstStepPrivacyId = 0
42 firstStepChannelId = 0 44 firstStepChannelId = 0
@@ -46,6 +48,7 @@ export class VideoAddComponent extends FormReactive implements OnInit {
46 private router: Router, 48 private router: Router,
47 private notificationsService: NotificationsService, 49 private notificationsService: NotificationsService,
48 private authService: AuthService, 50 private authService: AuthService,
51 private userService: UserService,
49 private serverService: ServerService, 52 private serverService: ServerService,
50 private videoService: VideoService 53 private videoService: VideoService
51 ) { 54 ) {
@@ -67,6 +70,9 @@ export class VideoAddComponent extends FormReactive implements OnInit {
67 populateAsyncUserVideoChannels(this.authService, this.userVideoChannels) 70 populateAsyncUserVideoChannels(this.authService, this.userVideoChannels)
68 .then(() => this.firstStepChannelId = this.userVideoChannels[0].id) 71 .then(() => this.firstStepChannelId = this.userVideoChannels[0].id)
69 72
73 this.userService.getMyVideoQuotaUsed()
74 .subscribe(data => this.userVideoQuotaUsed = data.videoQuotaUsed)
75
70 this.serverService.videoPrivaciesLoaded 76 this.serverService.videoPrivaciesLoaded
71 .subscribe( 77 .subscribe(
72 () => { 78 () => {
@@ -89,6 +95,18 @@ export class VideoAddComponent extends FormReactive implements OnInit {
89 95
90 uploadFirstStep () { 96 uploadFirstStep () {
91 const videofile = this.videofileInput.nativeElement.files[0] 97 const videofile = this.videofileInput.nativeElement.files[0]
98 const videoQuota = this.authService.getUser().videoQuota
99 if ((this.userVideoQuotaUsed + videofile.size) > videoQuota) {
100 const bytePipes = new BytesPipe()
101
102 const msg = 'Your video quota is exceeded with this video ' +
103 `(video size: ${bytePipes.transform(videofile.size, 0)}, ` +
104 `used: ${bytePipes.transform(this.userVideoQuotaUsed, 0)}, ` +
105 `quota: ${bytePipes.transform(videoQuota, 0)})`
106 this.notificationsService.error('Error', msg)
107 return
108 }
109
92 const name = videofile.name.replace(/\.[^/.]+$/, '') 110 const name = videofile.name.replace(/\.[^/.]+$/, '')
93 const privacy = this.firstStepPrivacyId.toString() 111 const privacy = this.firstStepPrivacyId.toString()
94 const nsfw = false 112 const nsfw = false
@@ -127,8 +145,9 @@ export class VideoAddComponent extends FormReactive implements OnInit {
127 145
128 err => { 146 err => {
129 // Reset progress 147 // Reset progress
148 this.isUploadingVideo = false
130 this.videoUploadPercents = 0 149 this.videoUploadPercents = 0
131 this.error = err.message 150 this.notificationsService.error('Error', err.message)
132 } 151 }
133 ) 152 )
134 } 153 }
@@ -152,7 +171,7 @@ export class VideoAddComponent extends FormReactive implements OnInit {
152 }, 171 },
153 172
154 err => { 173 err => {
155 this.error = 'Cannot update the video.' 174 this.notificationsService.error('Error', err.message)
156 console.error(err) 175 console.error(err)
157 } 176 }
158 ) 177 )
diff --git a/client/src/app/videos/+video-edit/video-update.component.ts b/client/src/app/videos/+video-edit/video-update.component.ts
index 941ef2478..7f41b56d8 100644
--- a/client/src/app/videos/+video-edit/video-update.component.ts
+++ b/client/src/app/videos/+video-edit/video-update.component.ts
@@ -21,7 +21,6 @@ import { VideoService } from '../../shared/video/video.service'
21export class VideoUpdateComponent extends FormReactive implements OnInit { 21export class VideoUpdateComponent extends FormReactive implements OnInit {
22 video: VideoEdit 22 video: VideoEdit
23 23
24 error: string = null
25 form: FormGroup 24 form: FormGroup
26 formErrors: { [ id: string ]: string } = {} 25 formErrors: { [ id: string ]: string } = {}
27 validationMessages: ValidatorMessage = {} 26 validationMessages: ValidatorMessage = {}
@@ -82,7 +81,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
82 81
83 err => { 82 err => {
84 console.error(err) 83 console.error(err)
85 this.error = 'Cannot fetch video.' 84 this.notificationsService.error('Error', err.message)
86 } 85 }
87 ) 86 )
88 } 87 }
@@ -108,7 +107,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
108 }, 107 },
109 108
110 err => { 109 err => {
111 this.error = 'Cannot update the video.' 110 this.notificationsService.error('Error', err.message)
112 console.error(err) 111 console.error(err)
113 } 112 }
114 ) 113 )
diff --git a/server/controllers/api/users.ts b/server/controllers/api/users.ts
index 2d77a5249..5374c4b6a 100644
--- a/server/controllers/api/users.ts
+++ b/server/controllers/api/users.ts
@@ -30,6 +30,11 @@ usersRouter.get('/me',
30 asyncMiddleware(getUserInformation) 30 asyncMiddleware(getUserInformation)
31) 31)
32 32
33usersRouter.get('/me/video-quota-used',
34 authenticate,
35 asyncMiddleware(getUserVideoQuotaUsed)
36)
37
33usersRouter.get('/me/videos', 38usersRouter.get('/me/videos',
34 authenticate, 39 authenticate,
35 paginationValidator, 40 paginationValidator,
@@ -183,8 +188,18 @@ async function getUserInformation (req: express.Request, res: express.Response,
183 return res.json(user.toFormattedJSON()) 188 return res.json(user.toFormattedJSON())
184} 189}
185 190
191async function getUserVideoQuotaUsed (req: express.Request, res: express.Response, next: express.NextFunction) {
192 // We did not load channels in res.locals.user
193 const user = await UserModel.loadByUsernameAndPopulateChannels(res.locals.oauth.token.user.username)
194 const videoQuotaUsed = await UserModel.getOriginalVideoFileTotalFromUser(user)
195
196 return res.json({
197 videoQuotaUsed
198 })
199}
200
186function getUser (req: express.Request, res: express.Response, next: express.NextFunction) { 201function getUser (req: express.Request, res: express.Response, next: express.NextFunction) {
187 return res.json(res.locals.user.toFormattedJSON()) 202 return res.json((res.locals.user as UserModel).toFormattedJSON())
188} 203}
189 204
190async function getUserVideoRating (req: express.Request, res: express.Response, next: express.NextFunction) { 205async function getUserVideoRating (req: express.Request, res: express.Response, next: express.NextFunction) {
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index 4226bcb35..e37fd4d3b 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -181,7 +181,7 @@ export class UserModel extends Model<UserModel> {
181 return UserModel.findOne(query) 181 return UserModel.findOne(query)
182 } 182 }
183 183
184 private static getOriginalVideoFileTotalFromUser (user: UserModel) { 184 static getOriginalVideoFileTotalFromUser (user: UserModel) {
185 // Don't use sequelize because we need to use a sub query 185 // Don't use sequelize because we need to use a sub query
186 const query = 'SELECT SUM("size") AS "total" FROM ' + 186 const query = 'SELECT SUM("size") AS "total" FROM ' +
187 '(SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' + 187 '(SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' +
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts
index f7e5972d3..b788637e7 100644
--- a/server/tests/api/users/users.ts
+++ b/server/tests/api/users/users.ts
@@ -4,7 +4,8 @@ import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { UserRole } from '../../../../shared/index' 5import { UserRole } from '../../../../shared/index'
6import { 6import {
7 createUser, flushTests, getBlacklistedVideosList, getMyUserInformation, getMyUserVideoRating, getUserInformation, getUsersList, 7 createUser, flushTests, getBlacklistedVideosList, getMyUserInformation, getMyUserVideoQuotaUsed, getMyUserVideoRating, getUserInformation,
8 getUsersList,
8 getUsersListPaginationAndSort, getVideosList, killallServers, login, makePutBodyRequest, rateVideo, registerUser, removeUser, removeVideo, 9 getUsersListPaginationAndSort, getVideosList, killallServers, login, makePutBodyRequest, rateVideo, registerUser, removeUser, removeVideo,
9 runServer, ServerInfo, serverLogin, testVideoImage, updateMyAvatar, updateMyUser, updateUser, uploadVideo 10 runServer, ServerInfo, serverLogin, testVideoImage, updateMyAvatar, updateMyUser, updateUser, uploadVideo
10} from '../../utils/index' 11} from '../../utils/index'
@@ -179,11 +180,19 @@ describe('Test users', function () {
179 this.timeout(5000) 180 this.timeout(5000)
180 181
181 const videoAttributes = { 182 const videoAttributes = {
182 name: 'super user video' 183 name: 'super user video',
184 fixture: 'video_short.webm'
183 } 185 }
184 await uploadVideo(server.url, accessTokenUser, videoAttributes) 186 await uploadVideo(server.url, accessTokenUser, videoAttributes)
185 }) 187 })
186 188
189 it('Should have video quota updated', async function () {
190 const res = await getMyUserVideoQuotaUsed(server.url, accessTokenUser)
191 const data = res.body
192
193 expect(data.videoQuotaUsed).to.equal(218910)
194 })
195
187 it('Should be able to list my videos', async function () { 196 it('Should be able to list my videos', async function () {
188 const res = await getMyVideos(server.url, accessTokenUser, 0, 5) 197 const res = await getMyVideos(server.url, accessTokenUser, 0, 5)
189 expect(res.body.total).to.equal(1) 198 expect(res.body.total).to.equal(1)
diff --git a/server/tests/utils/users/users.ts b/server/tests/utils/users/users.ts
index 90b1ca0a6..12945a805 100644
--- a/server/tests/utils/users/users.ts
+++ b/server/tests/utils/users/users.ts
@@ -56,6 +56,17 @@ function getMyUserInformation (url: string, accessToken: string, specialStatus =
56 .expect('Content-Type', /json/) 56 .expect('Content-Type', /json/)
57} 57}
58 58
59function getMyUserVideoQuotaUsed (url: string, accessToken: string, specialStatus = 200) {
60 const path = '/api/v1/users/me/video-quota-used'
61
62 return request(url)
63 .get(path)
64 .set('Accept', 'application/json')
65 .set('Authorization', 'Bearer ' + accessToken)
66 .expect(specialStatus)
67 .expect('Content-Type', /json/)
68}
69
59function getUserInformation (url: string, accessToken: string, userId: number) { 70function getUserInformation (url: string, accessToken: string, userId: number) {
60 const path = '/api/v1/users/' + userId 71 const path = '/api/v1/users/' + userId
61 72
@@ -192,6 +203,7 @@ export {
192 registerUser, 203 registerUser,
193 getMyUserInformation, 204 getMyUserInformation,
194 getMyUserVideoRating, 205 getMyUserVideoRating,
206 getMyUserVideoQuotaUsed,
195 getUsersList, 207 getUsersList,
196 getUsersListPaginationAndSort, 208 getUsersListPaginationAndSort,
197 removeUser, 209 removeUser,