aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/feeds.ts15
-rw-r--r--server/helpers/middlewares/accounts.ts4
-rw-r--r--server/initializers/constants.ts2
-rw-r--r--server/initializers/migrations/0560-user-feed-token.ts (renamed from server/initializers/migrations/0530-user-feed-token.ts)23
-rw-r--r--server/middlewares/validators/feeds.ts66
-rw-r--r--server/tests/api/check-params/users.ts32
-rw-r--r--server/tests/feeds/feeds.ts53
7 files changed, 133 insertions, 62 deletions
diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts
index 5c95069fc..2182b42f5 100644
--- a/server/controllers/feeds.ts
+++ b/server/controllers/feeds.ts
@@ -12,7 +12,7 @@ import {
12 videoCommentsFeedsValidator, 12 videoCommentsFeedsValidator,
13 videoFeedsValidator, 13 videoFeedsValidator,
14 videosSortValidator, 14 videosSortValidator,
15 videoSubscriptonFeedsValidator 15 videoSubscriptionFeedsValidator
16} from '../middlewares' 16} from '../middlewares'
17import { cacheRoute } from '../middlewares/cache' 17import { cacheRoute } from '../middlewares/cache'
18import { VideoModel } from '../models/video/video' 18import { VideoModel } from '../models/video/video'
@@ -60,7 +60,7 @@ feedsRouter.get('/feeds/subscriptions.:format',
60 ] 60 ]
61 })(ROUTE_CACHE_LIFETIME.FEEDS)), 61 })(ROUTE_CACHE_LIFETIME.FEEDS)),
62 commonVideosFiltersValidator, 62 commonVideosFiltersValidator,
63 asyncMiddleware(videoSubscriptonFeedsValidator), 63 asyncMiddleware(videoSubscriptionFeedsValidator),
64 asyncMiddleware(generateVideoFeedForSubscriptions) 64 asyncMiddleware(generateVideoFeedForSubscriptions)
65) 65)
66 66
@@ -198,20 +198,17 @@ async function generateVideoFeedForSubscriptions (req: express.Request, res: exp
198 queryString: new URL(WEBSERVER.URL + req.url).search 198 queryString: new URL(WEBSERVER.URL + req.url).search
199 }) 199 })
200 200
201 const options = {
202 followerActorId: res.locals.user.Account.Actor.id,
203 user: res.locals.user
204 }
205
206 const resultList = await VideoModel.listForApi({ 201 const resultList = await VideoModel.listForApi({
207 start, 202 start,
208 count: FEEDS.COUNT, 203 count: FEEDS.COUNT,
209 sort: req.query.sort, 204 sort: req.query.sort,
210 includeLocalVideos: true, 205 includeLocalVideos: false,
211 nsfw, 206 nsfw,
212 filter: req.query.filter as VideoFilter, 207 filter: req.query.filter as VideoFilter,
213 withFiles: true, 208 withFiles: true,
214 ...options 209
210 followerActorId: res.locals.user.Account.Actor.id,
211 user: res.locals.user
215 }) 212 })
216 213
217 addVideosToFeed(feed, resultList.data) 214 addVideosToFeed(feed, resultList.data)
diff --git a/server/helpers/middlewares/accounts.ts b/server/helpers/middlewares/accounts.ts
index fa4a51e6c..e9b981262 100644
--- a/server/helpers/middlewares/accounts.ts
+++ b/server/helpers/middlewares/accounts.ts
@@ -39,11 +39,11 @@ async function doesAccountExist (p: Bluebird<MAccountDefault>, res: Response, se
39 return true 39 return true
40} 40}
41 41
42async function doesUserFeedTokenCorrespond (id: number | string, token: string, res: Response) { 42async function doesUserFeedTokenCorrespond (id: number, token: string, res: Response) {
43 const user = await UserModel.loadByIdWithChannels(parseInt(id + '', 10)) 43 const user = await UserModel.loadByIdWithChannels(parseInt(id + '', 10))
44 44
45 if (token !== user.feedToken) { 45 if (token !== user.feedToken) {
46 res.status(401) 46 res.status(403)
47 .json({ error: 'User and token mismatch' }) 47 .json({ error: 'User and token mismatch' })
48 48
49 return false 49 return false
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 945185f62..6c44d703e 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
24 24
25// --------------------------------------------------------------------------- 25// ---------------------------------------------------------------------------
26 26
27const LAST_MIGRATION_VERSION = 555 27const LAST_MIGRATION_VERSION = 560
28 28
29// --------------------------------------------------------------------------- 29// ---------------------------------------------------------------------------
30 30
diff --git a/server/initializers/migrations/0530-user-feed-token.ts b/server/initializers/migrations/0560-user-feed-token.ts
index 421016b11..7c61def17 100644
--- a/server/initializers/migrations/0530-user-feed-token.ts
+++ b/server/initializers/migrations/0560-user-feed-token.ts
@@ -9,13 +9,15 @@ async function up (utils: {
9}): Promise<void> { 9}): Promise<void> {
10 const q = utils.queryInterface 10 const q = utils.queryInterface
11 11
12 // Create uuid column for users 12 {
13 const userFeedTokenUUID = { 13 // Create uuid column for users
14 type: Sequelize.UUID, 14 const userFeedTokenUUID = {
15 defaultValue: Sequelize.UUIDV4, 15 type: Sequelize.UUID,
16 allowNull: true 16 defaultValue: Sequelize.UUIDV4,
17 allowNull: true
18 }
19 await q.addColumn('user', 'feedToken', userFeedTokenUUID)
17 } 20 }
18 await q.addColumn('user', 'feedToken', userFeedTokenUUID)
19 21
20 // Set UUID to previous users 22 // Set UUID to previous users
21 { 23 {
@@ -28,6 +30,15 @@ async function up (utils: {
28 await utils.sequelize.query(queryUpdate) 30 await utils.sequelize.query(queryUpdate)
29 } 31 }
30 } 32 }
33
34 {
35 const userFeedTokenUUID = {
36 type: Sequelize.UUID,
37 defaultValue: Sequelize.UUIDV4,
38 allowNull: false
39 }
40 await q.changeColumn('user', 'feedToken', userFeedTokenUUID)
41 }
31} 42}
32 43
33function down (options) { 44function down (options) {
diff --git a/server/middlewares/validators/feeds.ts b/server/middlewares/validators/feeds.ts
index 35080ffca..18469bad3 100644
--- a/server/middlewares/validators/feeds.ts
+++ b/server/middlewares/validators/feeds.ts
@@ -1,17 +1,17 @@
1import * as express from 'express' 1import * as express from 'express'
2import { param, query } from 'express-validator' 2import { param, query } from 'express-validator'
3import { isIdOrUUIDValid, isIdValid } from '../../helpers/custom-validators/misc'
4import { logger } from '../../helpers/logger'
5import { areValidationErrors } from './utils'
6import { isValidRSSFeed } from '../../helpers/custom-validators/feeds' 3import { isValidRSSFeed } from '../../helpers/custom-validators/feeds'
7import { doesVideoExist } from '../../helpers/middlewares/videos' 4import { exists, isIdOrUUIDValid, isIdValid } from '../../helpers/custom-validators/misc'
5import { logger } from '../../helpers/logger'
8import { 6import {
9 doesAccountIdExist, 7 doesAccountIdExist,
10 doesAccountNameWithHostExist, 8 doesAccountNameWithHostExist,
9 doesUserFeedTokenCorrespond,
11 doesVideoChannelIdExist, 10 doesVideoChannelIdExist,
12 doesVideoChannelNameWithHostExist, 11 doesVideoChannelNameWithHostExist
13 doesUserFeedTokenCorrespond
14} from '../../helpers/middlewares' 12} from '../../helpers/middlewares'
13import { doesVideoExist } from '../../helpers/middlewares/videos'
14import { areValidationErrors } from './utils'
15 15
16const feedsFormatValidator = [ 16const feedsFormatValidator = [
17 param('format').optional().custom(isValidRSSFeed).withMessage('Should have a valid format (rss, atom, json)'), 17 param('format').optional().custom(isValidRSSFeed).withMessage('Should have a valid format (rss, atom, json)'),
@@ -35,19 +35,31 @@ function setFeedFormatContentType (req: express.Request, res: express.Response,
35 if (req.accepts(acceptableContentTypes)) { 35 if (req.accepts(acceptableContentTypes)) {
36 res.set('Content-Type', req.accepts(acceptableContentTypes) as string) 36 res.set('Content-Type', req.accepts(acceptableContentTypes) as string)
37 } else { 37 } else {
38 return res.status(406).send({ 38 return res.status(406)
39 message: `You should accept at least one of the following content-types: ${acceptableContentTypes.join(', ')}` 39 .json({
40 }).end() 40 message: `You should accept at least one of the following content-types: ${acceptableContentTypes.join(', ')}`
41 })
41 } 42 }
42 43
43 return next() 44 return next()
44} 45}
45 46
46const videoFeedsValidator = [ 47const videoFeedsValidator = [
47 query('accountId').optional().custom(isIdValid), 48 query('accountId')
48 query('accountName').optional(), 49 .optional()
49 query('videoChannelId').optional().custom(isIdValid), 50 .custom(isIdValid)
50 query('videoChannelName').optional(), 51 .withMessage('Should have a valid account id'),
52
53 query('accountName')
54 .optional(),
55
56 query('videoChannelId')
57 .optional()
58 .custom(isIdValid)
59 .withMessage('Should have a valid channel id'),
60
61 query('videoChannelName')
62 .optional(),
51 63
52 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 64 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
53 logger.debug('Checking feeds parameters', { parameters: req.query }) 65 logger.debug('Checking feeds parameters', { parameters: req.query })
@@ -63,19 +75,22 @@ const videoFeedsValidator = [
63 } 75 }
64] 76]
65 77
66const videoSubscriptonFeedsValidator = [ 78const videoSubscriptionFeedsValidator = [
67 query('accountId').custom(isIdValid), 79 query('accountId')
68 query('token'), 80 .custom(isIdValid)
81 .withMessage('Should have a valid account id'),
82
83 query('token')
84 .custom(exists)
85 .withMessage('Should have a token'),
69 86
70 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 87 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
71 logger.debug('Checking feeds parameters', { parameters: req.query }) 88 logger.debug('Checking subscription feeds parameters', { parameters: req.query })
72 89
73 if (areValidationErrors(req, res)) return 90 if (areValidationErrors(req, res)) return
74 91
75 // a token alone is erroneous 92 if (!await doesAccountIdExist(req.query.accountId, res)) return
76 if (req.query.token && !req.query.accountId) return 93 if (!await doesUserFeedTokenCorrespond(res.locals.account.userId, req.query.token, res)) return
77 if (req.query.accountId && !await doesAccountIdExist(req.query.accountId, res)) return
78 if (req.query.token && !await doesUserFeedTokenCorrespond(res.locals.account.userId, req.query.token, res)) return
79 94
80 return next() 95 return next()
81 } 96 }
@@ -90,9 +105,10 @@ const videoCommentsFeedsValidator = [
90 if (areValidationErrors(req, res)) return 105 if (areValidationErrors(req, res)) return
91 106
92 if (req.query.videoId && (req.query.videoChannelId || req.query.videoChannelName)) { 107 if (req.query.videoId && (req.query.videoChannelId || req.query.videoChannelName)) {
93 return res.status(400).send({ 108 return res.status(400)
94 message: 'videoId cannot be mixed with a channel filter' 109 .json({
95 }).end() 110 message: 'videoId cannot be mixed with a channel filter'
111 })
96 } 112 }
97 113
98 if (req.query.videoId && !await doesVideoExist(req.query.videoId, res)) return 114 if (req.query.videoId && !await doesVideoExist(req.query.videoId, res)) return
@@ -107,6 +123,6 @@ export {
107 feedsFormatValidator, 123 feedsFormatValidator,
108 setFeedFormatContentType, 124 setFeedFormatContentType,
109 videoFeedsValidator, 125 videoFeedsValidator,
110 videoSubscriptonFeedsValidator, 126 videoSubscriptionFeedsValidator,
111 videoCommentsFeedsValidator 127 videoCommentsFeedsValidator
112} 128}
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts
index 2a220be83..da7dc9704 100644
--- a/server/tests/api/check-params/users.ts
+++ b/server/tests/api/check-params/users.ts
@@ -14,6 +14,7 @@ import {
14 flushAndRunServer, 14 flushAndRunServer,
15 getMyUserInformation, 15 getMyUserInformation,
16 getMyUserVideoRating, 16 getMyUserVideoRating,
17 getUserScopedTokens,
17 getUsersList, 18 getUsersList,
18 immutableAssign, 19 immutableAssign,
19 killallServers, 20 killallServers,
@@ -23,6 +24,7 @@ import {
23 makeUploadRequest, 24 makeUploadRequest,
24 registerUser, 25 registerUser,
25 removeUser, 26 removeUser,
27 renewUserScopedTokens,
26 reRunServer, 28 reRunServer,
27 ServerInfo, 29 ServerInfo,
28 setAccessTokensToServers, 30 setAccessTokensToServers,
@@ -38,7 +40,7 @@ import {
38 checkBadStartPagination 40 checkBadStartPagination
39} from '../../../../shared/extra-utils/requests/check-api-params' 41} from '../../../../shared/extra-utils/requests/check-api-params'
40import { waitJobs } from '../../../../shared/extra-utils/server/jobs' 42import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
41import { getMagnetURI, getMyVideoImports, getGoodVideoUrl, importVideo } from '../../../../shared/extra-utils/videos/video-imports' 43import { getGoodVideoUrl, getMagnetURI, getMyVideoImports, importVideo } from '../../../../shared/extra-utils/videos/video-imports'
42import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' 44import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
43import { VideoPrivacy } from '../../../../shared/models/videos' 45import { VideoPrivacy } from '../../../../shared/models/videos'
44 46
@@ -609,6 +611,34 @@ describe('Test users API validators', function () {
609 }) 611 })
610 }) 612 })
611 613
614 describe('When managing my scoped tokens', function () {
615
616 it('Should fail to get my scoped tokens with an non authenticated user', async function () {
617 await getUserScopedTokens(server.url, null, 401)
618 })
619
620 it('Should fail to get my scoped tokens with a bad token', async function () {
621 await getUserScopedTokens(server.url, 'bad', 401)
622
623 })
624
625 it('Should succeed to get my scoped tokens', async function () {
626 await getUserScopedTokens(server.url, server.accessToken)
627 })
628
629 it('Should fail to renew my scoped tokens with an non authenticated user', async function () {
630 await renewUserScopedTokens(server.url, null, 401)
631 })
632
633 it('Should fail to renew my scoped tokens with a bad token', async function () {
634 await renewUserScopedTokens(server.url, 'bad', 401)
635 })
636
637 it('Should succeed to renew my scoped tokens', async function () {
638 await renewUserScopedTokens(server.url, server.accessToken)
639 })
640 })
641
612 describe('When getting a user', function () { 642 describe('When getting a user', function () {
613 643
614 it('Should fail with an non authenticated user', async function () { 644 it('Should fail with an non authenticated user', async function () {
diff --git a/server/tests/feeds/feeds.ts b/server/tests/feeds/feeds.ts
index 175ea9102..92a468192 100644
--- a/server/tests/feeds/feeds.ts
+++ b/server/tests/feeds/feeds.ts
@@ -8,28 +8,29 @@ import {
8 addAccountToServerBlocklist, 8 addAccountToServerBlocklist,
9 removeAccountFromServerBlocklist 9 removeAccountFromServerBlocklist
10} from '@shared/extra-utils/users/blocklist' 10} from '@shared/extra-utils/users/blocklist'
11import { addUserSubscription, listUserSubscriptionVideos } from '@shared/extra-utils/users/user-subscriptions'
11import { VideoPrivacy } from '@shared/models' 12import { VideoPrivacy } from '@shared/models'
13import { ScopedToken } from '@shared/models/users/user-scoped-token'
12import { 14import {
13 cleanupTests, 15 cleanupTests,
14 createUser, 16 createUser,
15 doubleFollow, 17 doubleFollow,
16 flushAndRunMultipleServers, 18 flushAndRunMultipleServers,
19 flushAndRunServer,
17 getJSONfeed, 20 getJSONfeed,
18 getMyUserInformation, 21 getMyUserInformation,
22 getUserScopedTokens,
19 getXMLfeed, 23 getXMLfeed,
24 renewUserScopedTokens,
20 ServerInfo, 25 ServerInfo,
21 setAccessTokensToServers, 26 setAccessTokensToServers,
22 uploadVideo, 27 uploadVideo,
23 uploadVideoAndGetId, 28 uploadVideoAndGetId,
24 userLogin, 29 userLogin
25 flushAndRunServer,
26 getUserScopedTokens
27} from '../../../shared/extra-utils' 30} from '../../../shared/extra-utils'
28import { waitJobs } from '../../../shared/extra-utils/server/jobs' 31import { waitJobs } from '../../../shared/extra-utils/server/jobs'
29import { addVideoCommentThread } from '../../../shared/extra-utils/videos/video-comments' 32import { addVideoCommentThread } from '../../../shared/extra-utils/videos/video-comments'
30import { User } from '../../../shared/models/users' 33import { User } from '../../../shared/models/users'
31import { ScopedToken } from '@shared/models/users/user-scoped-token'
32import { listUserSubscriptionVideos, addUserSubscription } from '@shared/extra-utils/users/user-subscriptions'
33 34
34chai.use(require('chai-xml')) 35chai.use(require('chai-xml'))
35chai.use(require('chai-json-schema')) 36chai.use(require('chai-json-schema'))
@@ -298,14 +299,10 @@ describe('Test syndication feeds', () => {
298 }) 299 })
299 300
300 describe('Video feed from my subscriptions', function () { 301 describe('Video feed from my subscriptions', function () {
301 /** 302 let feeduserAccountId: number
302 * use the 'version' query parameter to bust cache between tests 303 let feeduserFeedToken: string
303 */
304 304
305 it('Should list no videos for a user with no videos and no subscriptions', async function () { 305 it('Should list no videos for a user with no videos and no subscriptions', async function () {
306 let feeduserAccountId: number
307 let feeduserFeedToken: string
308
309 const attr = { username: 'feeduser', password: 'password' } 306 const attr = { username: 'feeduser', password: 'password' }
310 await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: attr.username, password: attr.password }) 307 await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: attr.username, password: attr.password })
311 const feeduserAccessToken = await userLogin(servers[0], attr) 308 const feeduserAccessToken = await userLogin(servers[0], attr)
@@ -332,15 +329,21 @@ describe('Test syndication feeds', () => {
332 } 329 }
333 }) 330 })
334 331
332 it('Should fail with an invalid token', async function () {
333 await getJSONfeed(servers[0].url, 'subscriptions', { accountId: feeduserAccountId, token: 'toto' }, 403)
334 })
335
336 it('Should fail with a token of another user', async function () {
337 await getJSONfeed(servers[0].url, 'subscriptions', { accountId: feeduserAccountId, token: userFeedToken }, 403)
338 })
339
335 it('Should list no videos for a user with videos but no subscriptions', async function () { 340 it('Should list no videos for a user with videos but no subscriptions', async function () {
336 { 341 const res = await listUserSubscriptionVideos(servers[0].url, userAccessToken)
337 const res = await listUserSubscriptionVideos(servers[0].url, userAccessToken) 342 expect(res.body.total).to.equal(0)
338 expect(res.body.total).to.equal(0)
339 343
340 const json = await getJSONfeed(servers[0].url, 'subscriptions', { accountId: userAccountId, token: userFeedToken }) 344 const json = await getJSONfeed(servers[0].url, 'subscriptions', { accountId: userAccountId, token: userFeedToken })
341 const jsonObj = JSON.parse(json.text) 345 const jsonObj = JSON.parse(json.text)
342 expect(jsonObj.items.length).to.be.equal(0) // no subscription, it should not list the instance's videos but list 0 videos 346 expect(jsonObj.items.length).to.be.equal(0) // no subscription, it should not list the instance's videos but list 0 videos
343 }
344 }) 347 })
345 348
346 it('Should list self videos for a user with a subscription to themselves', async function () { 349 it('Should list self videos for a user with a subscription to themselves', async function () {
@@ -376,6 +379,20 @@ describe('Test syndication feeds', () => {
376 } 379 }
377 }) 380 })
378 381
382 it('Should renew the token, and so have an invalid old token', async function () {
383 await renewUserScopedTokens(servers[0].url, userAccessToken)
384
385 await getJSONfeed(servers[0].url, 'subscriptions', { accountId: userAccountId, token: userFeedToken, version: 3 }, 403)
386 })
387
388 it('Should succeed with the new token', async function () {
389 const res2 = await getUserScopedTokens(servers[0].url, userAccessToken)
390 const token: ScopedToken = res2.body
391 userFeedToken = token.feedToken
392
393 await getJSONfeed(servers[0].url, 'subscriptions', { accountId: userAccountId, token: userFeedToken, version: 4 })
394 })
395
379 }) 396 })
380 397
381 after(async function () { 398 after(async function () {