aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/middlewares/validators/users.ts9
-rw-r--r--server/models/account/user.ts149
-rw-r--r--server/tests/api/users/users.ts122
3 files changed, 241 insertions, 39 deletions
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts
index adc67a046..840b9fc74 100644
--- a/server/middlewares/validators/users.ts
+++ b/server/middlewares/validators/users.ts
@@ -1,6 +1,6 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import * as express from 'express' 2import * as express from 'express'
3import { body, param } from 'express-validator' 3import { body, param, query } from 'express-validator'
4import { omit } from 'lodash' 4import { omit } from 'lodash'
5import { isIdOrUUIDValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' 5import { isIdOrUUIDValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
6import { 6import {
@@ -256,12 +256,13 @@ const usersUpdateMeValidator = [
256 256
257const usersGetValidator = [ 257const usersGetValidator = [
258 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'), 258 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
259 query('withStats').optional().isBoolean().withMessage('Should have a valid stats flag'),
259 260
260 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 261 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
261 logger.debug('Checking usersGet parameters', { parameters: req.params }) 262 logger.debug('Checking usersGet parameters', { parameters: req.params })
262 263
263 if (areValidationErrors(req, res)) return 264 if (areValidationErrors(req, res)) return
264 if (!await checkUserIdExist(req.params.id, res)) return 265 if (!await checkUserIdExist(req.params.id, res, req.query.withStats)) return
265 266
266 return next() 267 return next()
267 } 268 }
@@ -460,9 +461,9 @@ export {
460 461
461// --------------------------------------------------------------------------- 462// ---------------------------------------------------------------------------
462 463
463function checkUserIdExist (idArg: number | string, res: express.Response) { 464function checkUserIdExist (idArg: number | string, res: express.Response, withStats = false) {
464 const id = parseInt(idArg + '', 10) 465 const id = parseInt(idArg + '', 10)
465 return checkUserExist(() => UserModel.loadById(id), res) 466 return checkUserExist(() => UserModel.loadById(id, withStats), res)
466} 467}
467 468
468function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) { 469function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) {
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index 777f09666..026bf1318 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -19,7 +19,7 @@ import {
19 Table, 19 Table,
20 UpdatedAt 20 UpdatedAt
21} from 'sequelize-typescript' 21} from 'sequelize-typescript'
22import { hasUserRight, MyUser, USER_ROLE_LABELS, UserRight, VideoPlaylistType, VideoPrivacy } from '../../../shared' 22import { hasUserRight, MyUser, USER_ROLE_LABELS, UserRight, VideoPlaylistType, VideoPrivacy, VideoAbuseState } from '../../../shared'
23import { User, UserRole } from '../../../shared/models/users' 23import { User, UserRole } from '../../../shared/models/users'
24import { 24import {
25 isNoInstanceConfigWarningModal, 25 isNoInstanceConfigWarningModal,
@@ -70,8 +70,26 @@ import {
70 MVideoFullLight 70 MVideoFullLight
71} from '@server/typings/models' 71} from '@server/typings/models'
72 72
73const literalVideoQuotaUsed: any = [
74 literal(
75 '(' +
76 'SELECT COALESCE(SUM("size"), 0) ' +
77 'FROM (' +
78 'SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' +
79 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' +
80 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
81 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
82 'WHERE "account"."userId" = "UserModel"."id" GROUP BY "video"."id"' +
83 ') t' +
84 ')'
85 ),
86 'videoQuotaUsed'
87]
88
73enum ScopeNames { 89enum ScopeNames {
74 FOR_ME_API = 'FOR_ME_API' 90 FOR_ME_API = 'FOR_ME_API',
91 WITH_VIDEOCHANNELS = 'WITH_VIDEOCHANNELS',
92 WITH_STATS = 'WITH_STATS'
75} 93}
76 94
77@DefaultScope(() => ({ 95@DefaultScope(() => ({
@@ -112,6 +130,86 @@ enum ScopeNames {
112 required: true 130 required: true
113 } 131 }
114 ] 132 ]
133 },
134 [ScopeNames.WITH_VIDEOCHANNELS]: {
135 include: [
136 {
137 model: AccountModel,
138 include: [
139 {
140 model: VideoChannelModel
141 },
142 {
143 attributes: [ 'id', 'name', 'type' ],
144 model: VideoPlaylistModel.unscoped(),
145 required: true,
146 where: {
147 type: {
148 [Op.ne]: VideoPlaylistType.REGULAR
149 }
150 }
151 }
152 ]
153 }
154 ]
155 },
156 [ScopeNames.WITH_STATS]: {
157 attributes: {
158 include: [
159 literalVideoQuotaUsed,
160 [
161 literal(
162 '(' +
163 'SELECT COUNT("video"."id") ' +
164 'FROM "video" ' +
165 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
166 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' +
167 'WHERE "account"."userId" = "UserModel"."id"' +
168 ')'
169 ),
170 'videosCount'
171 ],
172 [
173 literal(
174 '(' +
175 `SELECT concat_ws(':', "abuses", "acceptedAbuses") ` +
176 'FROM (' +
177 'SELECT COUNT("videoAbuse"."id") AS "abuses", ' +
178 `COUNT("videoAbuse"."id") FILTER (WHERE "videoAbuse"."state" = ${VideoAbuseState.ACCEPTED}) AS "acceptedAbuses" ` +
179 'FROM "videoAbuse" ' +
180 'INNER JOIN "video" ON "videoAbuse"."videoId" = "video"."id" ' +
181 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
182 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' +
183 'WHERE "account"."userId" = "UserModel"."id"' +
184 ') t' +
185 ')'
186 ),
187 'videoAbusesCount'
188 ],
189 [
190 literal(
191 '(' +
192 'SELECT COUNT("videoAbuse"."id") ' +
193 'FROM "videoAbuse" ' +
194 'INNER JOIN "account" ON "account"."id" = "videoAbuse"."reporterAccountId" ' +
195 'WHERE "account"."userId" = "UserModel"."id"' +
196 ')'
197 ),
198 'videoAbusesCreatedCount'
199 ],
200 [
201 literal(
202 '(' +
203 'SELECT COUNT("videoComment"."id") ' +
204 'FROM "videoComment" ' +
205 'INNER JOIN "account" ON "account"."id" = "videoComment"."accountId" ' +
206 'WHERE "account"."userId" = "UserModel"."id"' +
207 ')'
208 ),
209 'videoCommentsCount'
210 ]
211 ]
212 }
115 } 213 }
116})) 214}))
117@Table({ 215@Table({
@@ -332,23 +430,7 @@ export class UserModel extends Model<UserModel> {
332 430
333 const query: FindOptions = { 431 const query: FindOptions = {
334 attributes: { 432 attributes: {
335 include: [ 433 include: [ literalVideoQuotaUsed ]
336 [
337 literal(
338 '(' +
339 'SELECT COALESCE(SUM("size"), 0) ' +
340 'FROM (' +
341 'SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' +
342 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' +
343 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
344 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
345 'WHERE "account"."userId" = "UserModel"."id" GROUP BY "video"."id"' +
346 ') t' +
347 ')'
348 ),
349 'videoQuotaUsed'
350 ]
351 ]
352 }, 434 },
353 offset: start, 435 offset: start,
354 limit: count, 436 limit: count,
@@ -430,8 +512,14 @@ export class UserModel extends Model<UserModel> {
430 return UserModel.findAll(query) 512 return UserModel.findAll(query)
431 } 513 }
432 514
433 static loadById (id: number): Bluebird<MUserDefault> { 515 static loadById (id: number, withStats = false): Bluebird<MUserDefault> {
434 return UserModel.findByPk(id) 516 const scopes = [
517 ScopeNames.WITH_VIDEOCHANNELS
518 ]
519
520 if (withStats) scopes.push(ScopeNames.WITH_STATS)
521
522 return UserModel.scope(scopes).findByPk(id)
435 } 523 }
436 524
437 static loadByUsername (username: string): Bluebird<MUserDefault> { 525 static loadByUsername (username: string): Bluebird<MUserDefault> {
@@ -637,6 +725,10 @@ export class UserModel extends Model<UserModel> {
637 toFormattedJSON (this: MUserFormattable, parameters: { withAdminFlags?: boolean } = {}): User { 725 toFormattedJSON (this: MUserFormattable, parameters: { withAdminFlags?: boolean } = {}): User {
638 const videoQuotaUsed = this.get('videoQuotaUsed') 726 const videoQuotaUsed = this.get('videoQuotaUsed')
639 const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily') 727 const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily')
728 const videosCount = this.get('videosCount')
729 const [ videoAbusesCount, videoAbusesAcceptedCount ] = (this.get('videoAbusesCount') as string || ':').split(':')
730 const videoAbusesCreatedCount = this.get('videoAbusesCreatedCount')
731 const videoCommentsCount = this.get('videoCommentsCount')
640 732
641 const json: User = { 733 const json: User = {
642 id: this.id, 734 id: this.id,
@@ -666,6 +758,21 @@ export class UserModel extends Model<UserModel> {
666 videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined 758 videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined
667 ? parseInt(videoQuotaUsedDaily + '', 10) 759 ? parseInt(videoQuotaUsedDaily + '', 10)
668 : undefined, 760 : undefined,
761 videosCount: videosCount !== undefined
762 ? parseInt(videosCount + '', 10)
763 : undefined,
764 videoAbusesCount: videoAbusesCount
765 ? parseInt(videoAbusesCount, 10)
766 : undefined,
767 videoAbusesAcceptedCount: videoAbusesAcceptedCount
768 ? parseInt(videoAbusesAcceptedCount, 10)
769 : undefined,
770 videoAbusesCreatedCount: videoAbusesCreatedCount !== undefined
771 ? parseInt(videoAbusesCreatedCount + '', 10)
772 : undefined,
773 videoCommentsCount: videoCommentsCount !== undefined
774 ? parseInt(videoCommentsCount + '', 10)
775 : undefined,
669 776
670 noInstanceConfigWarningModal: this.noInstanceConfigWarningModal, 777 noInstanceConfigWarningModal: this.noInstanceConfigWarningModal,
671 noWelcomeModal: this.noWelcomeModal, 778 noWelcomeModal: this.noWelcomeModal,
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts
index 502eac0bb..3e1a0c19b 100644
--- a/server/tests/api/users/users.ts
+++ b/server/tests/api/users/users.ts
@@ -2,7 +2,7 @@
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { MyUser, User, UserRole, Video, VideoPlaylistType } from '../../../../shared/index' 5import { MyUser, User, UserRole, Video, VideoPlaylistType, VideoAbuseState, VideoAbuseUpdate } from '../../../../shared/index'
6import { 6import {
7 blockUser, 7 blockUser,
8 cleanupTests, 8 cleanupTests,
@@ -33,7 +33,11 @@ import {
33 updateMyUser, 33 updateMyUser,
34 updateUser, 34 updateUser,
35 uploadVideo, 35 uploadVideo,
36 userLogin 36 userLogin,
37 reportVideoAbuse,
38 addVideoCommentThread,
39 updateVideoAbuse,
40 getVideoAbusesList
37} from '../../../../shared/extra-utils' 41} from '../../../../shared/extra-utils'
38import { follow } from '../../../../shared/extra-utils/server/follows' 42import { follow } from '../../../../shared/extra-utils/server/follows'
39import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' 43import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
@@ -254,7 +258,7 @@ describe('Test users', function () {
254 const res1 = await getMyUserInformation(server.url, accessTokenUser) 258 const res1 = await getMyUserInformation(server.url, accessTokenUser)
255 const userMe: MyUser = res1.body 259 const userMe: MyUser = res1.body
256 260
257 const res2 = await getUserInformation(server.url, server.accessToken, userMe.id) 261 const res2 = await getUserInformation(server.url, server.accessToken, userMe.id, true)
258 const userGet: User = res2.body 262 const userGet: User = res2.body
259 263
260 for (const user of [ userMe, userGet ]) { 264 for (const user of [ userMe, userGet ]) {
@@ -273,6 +277,16 @@ describe('Test users', function () {
273 277
274 expect(userMe.specialPlaylists).to.have.lengthOf(1) 278 expect(userMe.specialPlaylists).to.have.lengthOf(1)
275 expect(userMe.specialPlaylists[0].type).to.equal(VideoPlaylistType.WATCH_LATER) 279 expect(userMe.specialPlaylists[0].type).to.equal(VideoPlaylistType.WATCH_LATER)
280
281 // Check stats are included with withStats
282 expect(userGet.videosCount).to.be.a('number')
283 expect(userGet.videosCount).to.equal(0)
284 expect(userGet.videoCommentsCount).to.be.a('number')
285 expect(userGet.videoCommentsCount).to.equal(0)
286 expect(userGet.videoAbusesCount).to.be.a('number')
287 expect(userGet.videoAbusesCount).to.equal(0)
288 expect(userGet.videoAbusesAcceptedCount).to.be.a('number')
289 expect(userGet.videoAbusesAcceptedCount).to.equal(0)
276 }) 290 })
277 }) 291 })
278 292
@@ -623,7 +637,6 @@ describe('Test users', function () {
623 }) 637 })
624 638
625 describe('Updating another user', function () { 639 describe('Updating another user', function () {
626
627 it('Should be able to update another user', async function () { 640 it('Should be able to update another user', async function () {
628 await updateUser({ 641 await updateUser({
629 url: server.url, 642 url: server.url,
@@ -698,6 +711,8 @@ describe('Test users', function () {
698 }) 711 })
699 712
700 describe('Registering a new user', function () { 713 describe('Registering a new user', function () {
714 let user15AccessToken
715
701 it('Should register a new user', async function () { 716 it('Should register a new user', async function () {
702 const user = { displayName: 'super user 15', username: 'user_15', password: 'my super password' } 717 const user = { displayName: 'super user 15', username: 'user_15', password: 'my super password' }
703 const channel = { name: 'my_user_15_channel', displayName: 'my channel rocks' } 718 const channel = { name: 'my_user_15_channel', displayName: 'my channel rocks' }
@@ -711,18 +726,18 @@ describe('Test users', function () {
711 password: 'my super password' 726 password: 'my super password'
712 } 727 }
713 728
714 accessToken = await userLogin(server, user15) 729 user15AccessToken = await userLogin(server, user15)
715 }) 730 })
716 731
717 it('Should have the correct display name', async function () { 732 it('Should have the correct display name', async function () {
718 const res = await getMyUserInformation(server.url, accessToken) 733 const res = await getMyUserInformation(server.url, user15AccessToken)
719 const user: User = res.body 734 const user: User = res.body
720 735
721 expect(user.account.displayName).to.equal('super user 15') 736 expect(user.account.displayName).to.equal('super user 15')
722 }) 737 })
723 738
724 it('Should have the correct video quota', async function () { 739 it('Should have the correct video quota', async function () {
725 const res = await getMyUserInformation(server.url, accessToken) 740 const res = await getMyUserInformation(server.url, user15AccessToken)
726 const user = res.body 741 const user = res.body
727 742
728 expect(user.videoQuota).to.equal(5 * 1024 * 1024) 743 expect(user.videoQuota).to.equal(5 * 1024 * 1024)
@@ -740,7 +755,7 @@ describe('Test users', function () {
740 expect(res.body.data.find(u => u.username === 'user_15')).to.not.be.undefined 755 expect(res.body.data.find(u => u.username === 'user_15')).to.not.be.undefined
741 } 756 }
742 757
743 await deleteMe(server.url, accessToken) 758 await deleteMe(server.url, user15AccessToken)
744 759
745 { 760 {
746 const res = await getUsersList(server.url, server.accessToken) 761 const res = await getUsersList(server.url, server.accessToken)
@@ -750,6 +765,9 @@ describe('Test users', function () {
750 }) 765 })
751 766
752 describe('User blocking', function () { 767 describe('User blocking', function () {
768 let user16Id
769 let user16AccessToken
770
753 it('Should block and unblock a user', async function () { 771 it('Should block and unblock a user', async function () {
754 const user16 = { 772 const user16 = {
755 username: 'user_16', 773 username: 'user_16',
@@ -761,19 +779,95 @@ describe('Test users', function () {
761 username: user16.username, 779 username: user16.username,
762 password: user16.password 780 password: user16.password
763 }) 781 })
764 const user16Id = resUser.body.user.id 782 user16Id = resUser.body.user.id
765 783
766 accessToken = await userLogin(server, user16) 784 user16AccessToken = await userLogin(server, user16)
767 785
768 await getMyUserInformation(server.url, accessToken, 200) 786 await getMyUserInformation(server.url, user16AccessToken, 200)
769 await blockUser(server.url, user16Id, server.accessToken) 787 await blockUser(server.url, user16Id, server.accessToken)
770 788
771 await getMyUserInformation(server.url, accessToken, 401) 789 await getMyUserInformation(server.url, user16AccessToken, 401)
772 await userLogin(server, user16, 400) 790 await userLogin(server, user16, 400)
773 791
774 await unblockUser(server.url, user16Id, server.accessToken) 792 await unblockUser(server.url, user16Id, server.accessToken)
775 accessToken = await userLogin(server, user16) 793 user16AccessToken = await userLogin(server, user16)
776 await getMyUserInformation(server.url, accessToken, 200) 794 await getMyUserInformation(server.url, user16AccessToken, 200)
795 })
796 })
797
798 describe('User stats', function () {
799 let user17Id
800 let user17AccessToken
801
802 it('Should report correct initial statistics about a user', async function () {
803 const user17 = {
804 username: 'user_17',
805 password: 'my super password'
806 }
807 const resUser = await createUser({
808 url: server.url,
809 accessToken: server.accessToken,
810 username: user17.username,
811 password: user17.password
812 })
813
814 user17Id = resUser.body.user.id
815 user17AccessToken = await userLogin(server, user17)
816
817 const res = await getUserInformation(server.url, server.accessToken, user17Id, true)
818 const user: User = res.body
819
820 expect(user.videosCount).to.equal(0)
821 expect(user.videoCommentsCount).to.equal(0)
822 expect(user.videoAbusesCount).to.equal(0)
823 expect(user.videoAbusesCreatedCount).to.equal(0)
824 expect(user.videoAbusesAcceptedCount).to.equal(0)
825 })
826
827 it('Should report correct videos count', async function () {
828 const videoAttributes = {
829 name: 'video to test user stats'
830 }
831 await uploadVideo(server.url, user17AccessToken, videoAttributes)
832 const res1 = await getVideosList(server.url)
833 videoId = res1.body.data.find(video => video.name === videoAttributes.name).id
834
835 const res2 = await getUserInformation(server.url, server.accessToken, user17Id, true)
836 const user: User = res2.body
837
838 expect(user.videosCount).to.equal(1)
839 })
840
841 it('Should report correct video comments for user', async function () {
842 const text = 'super comment'
843 await addVideoCommentThread(server.url, user17AccessToken, videoId, text)
844
845 const res = await getUserInformation(server.url, server.accessToken, user17Id, true)
846 const user: User = res.body
847
848 expect(user.videoCommentsCount).to.equal(1)
849 })
850
851 it('Should report correct video abuses counts', async function () {
852 const reason = 'my super bad reason'
853 await reportVideoAbuse(server.url, user17AccessToken, videoId, reason)
854
855 const res1 = await getVideoAbusesList(server.url, server.accessToken)
856 const abuseId = res1.body.data[0].id
857
858 const res2 = await getUserInformation(server.url, server.accessToken, user17Id, true)
859 const user2: User = res2.body
860
861 expect(user2.videoAbusesCount).to.equal(1) // number of incriminations
862 expect(user2.videoAbusesCreatedCount).to.equal(1) // number of reports created
863
864 const body: VideoAbuseUpdate = { state: VideoAbuseState.ACCEPTED }
865 await updateVideoAbuse(server.url, server.accessToken, videoId, abuseId, body)
866
867 const res3 = await getUserInformation(server.url, server.accessToken, user17Id, true)
868 const user3: User = res3.body
869
870 expect(user3.videoAbusesAcceptedCount).to.equal(1) // number of reports created accepted
777 }) 871 })
778 }) 872 })
779 873