diff options
Diffstat (limited to 'server')
-rw-r--r-- | server/controllers/api/users/index.ts | 12 | ||||
-rw-r--r-- | server/controllers/api/videos/index.ts | 2 | ||||
-rw-r--r-- | server/controllers/api/videos/ownership.ts | 117 | ||||
-rw-r--r-- | server/helpers/custom-validators/video-ownership.ts | 42 | ||||
-rw-r--r-- | server/initializers/database.ts | 2 | ||||
-rw-r--r-- | server/middlewares/validators/users.ts | 7 | ||||
-rw-r--r-- | server/middlewares/validators/videos.ts | 82 | ||||
-rw-r--r-- | server/models/account/user.ts | 12 | ||||
-rw-r--r-- | server/models/video/video-change-ownership.ts | 127 | ||||
-rw-r--r-- | server/tests/api/videos/video-change-ownership.ts | 262 | ||||
-rw-r--r-- | server/tests/utils/index.ts | 1 | ||||
-rw-r--r-- | server/tests/utils/videos/video-change-ownership.ts | 54 |
12 files changed, 718 insertions, 2 deletions
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts index 01ee73a53..faba7e208 100644 --- a/server/controllers/api/users/index.ts +++ b/server/controllers/api/users/index.ts | |||
@@ -18,6 +18,7 @@ import { | |||
18 | setDefaultPagination, | 18 | setDefaultPagination, |
19 | setDefaultSort, | 19 | setDefaultSort, |
20 | token, | 20 | token, |
21 | userAutocompleteValidator, | ||
21 | usersAddValidator, | 22 | usersAddValidator, |
22 | usersGetValidator, | 23 | usersGetValidator, |
23 | usersRegisterValidator, | 24 | usersRegisterValidator, |
@@ -51,6 +52,11 @@ const askSendEmailLimiter = new RateLimit({ | |||
51 | const usersRouter = express.Router() | 52 | const usersRouter = express.Router() |
52 | usersRouter.use('/', meRouter) | 53 | usersRouter.use('/', meRouter) |
53 | 54 | ||
55 | usersRouter.get('/autocomplete', | ||
56 | userAutocompleteValidator, | ||
57 | asyncMiddleware(autocompleteUsers) | ||
58 | ) | ||
59 | |||
54 | usersRouter.get('/', | 60 | usersRouter.get('/', |
55 | authenticate, | 61 | authenticate, |
56 | ensureUserHasRight(UserRight.MANAGE_USERS), | 62 | ensureUserHasRight(UserRight.MANAGE_USERS), |
@@ -222,6 +228,12 @@ function getUser (req: express.Request, res: express.Response, next: express.Nex | |||
222 | return res.json((res.locals.user as UserModel).toFormattedJSON()) | 228 | return res.json((res.locals.user as UserModel).toFormattedJSON()) |
223 | } | 229 | } |
224 | 230 | ||
231 | async function autocompleteUsers (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
232 | const resultList = await UserModel.autocomplete(req.query.search as string) | ||
233 | |||
234 | return res.json(resultList) | ||
235 | } | ||
236 | |||
225 | async function listUsers (req: express.Request, res: express.Response, next: express.NextFunction) { | 237 | async function listUsers (req: express.Request, res: express.Response, next: express.NextFunction) { |
226 | const resultList = await UserModel.listForApi(req.query.start, req.query.count, req.query.sort) | 238 | const resultList = await UserModel.listForApi(req.query.start, req.query.count, req.query.sort) |
227 | 239 | ||
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index be803490b..0c9e6c2d1 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -49,6 +49,7 @@ import { abuseVideoRouter } from './abuse' | |||
49 | import { blacklistRouter } from './blacklist' | 49 | import { blacklistRouter } from './blacklist' |
50 | import { videoCommentRouter } from './comment' | 50 | import { videoCommentRouter } from './comment' |
51 | import { rateVideoRouter } from './rate' | 51 | import { rateVideoRouter } from './rate' |
52 | import { ownershipVideoRouter } from './ownership' | ||
52 | import { VideoFilter } from '../../../../shared/models/videos/video-query.type' | 53 | import { VideoFilter } from '../../../../shared/models/videos/video-query.type' |
53 | import { buildNSFWFilter, createReqFiles } from '../../../helpers/express-utils' | 54 | import { buildNSFWFilter, createReqFiles } from '../../../helpers/express-utils' |
54 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' | 55 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' |
@@ -84,6 +85,7 @@ videosRouter.use('/', rateVideoRouter) | |||
84 | videosRouter.use('/', videoCommentRouter) | 85 | videosRouter.use('/', videoCommentRouter) |
85 | videosRouter.use('/', videoCaptionsRouter) | 86 | videosRouter.use('/', videoCaptionsRouter) |
86 | videosRouter.use('/', videoImportsRouter) | 87 | videosRouter.use('/', videoImportsRouter) |
88 | videosRouter.use('/', ownershipVideoRouter) | ||
87 | 89 | ||
88 | videosRouter.get('/categories', listVideoCategories) | 90 | videosRouter.get('/categories', listVideoCategories) |
89 | videosRouter.get('/licences', listVideoLicences) | 91 | videosRouter.get('/licences', listVideoLicences) |
diff --git a/server/controllers/api/videos/ownership.ts b/server/controllers/api/videos/ownership.ts new file mode 100644 index 000000000..fc42f5fff --- /dev/null +++ b/server/controllers/api/videos/ownership.ts | |||
@@ -0,0 +1,117 @@ | |||
1 | import * as express from 'express' | ||
2 | import { logger } from '../../../helpers/logger' | ||
3 | import { sequelizeTypescript } from '../../../initializers' | ||
4 | import { | ||
5 | asyncMiddleware, | ||
6 | asyncRetryTransactionMiddleware, | ||
7 | authenticate, | ||
8 | paginationValidator, | ||
9 | setDefaultPagination, | ||
10 | videosAcceptChangeOwnershipValidator, | ||
11 | videosChangeOwnershipValidator, | ||
12 | videosTerminateChangeOwnershipValidator | ||
13 | } from '../../../middlewares' | ||
14 | import { AccountModel } from '../../../models/account/account' | ||
15 | import { VideoModel } from '../../../models/video/video' | ||
16 | import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership' | ||
17 | import { VideoChangeOwnershipStatus } from '../../../../shared/models/videos' | ||
18 | import { VideoChannelModel } from '../../../models/video/video-channel' | ||
19 | import { getFormattedObjects } from '../../../helpers/utils' | ||
20 | |||
21 | const ownershipVideoRouter = express.Router() | ||
22 | |||
23 | ownershipVideoRouter.post('/:videoId/give-ownership', | ||
24 | authenticate, | ||
25 | asyncMiddleware(videosChangeOwnershipValidator), | ||
26 | asyncRetryTransactionMiddleware(giveVideoOwnership) | ||
27 | ) | ||
28 | |||
29 | ownershipVideoRouter.get('/ownership', | ||
30 | authenticate, | ||
31 | paginationValidator, | ||
32 | setDefaultPagination, | ||
33 | asyncRetryTransactionMiddleware(listVideoOwnership) | ||
34 | ) | ||
35 | |||
36 | ownershipVideoRouter.post('/ownership/:id/accept', | ||
37 | authenticate, | ||
38 | asyncMiddleware(videosTerminateChangeOwnershipValidator), | ||
39 | asyncMiddleware(videosAcceptChangeOwnershipValidator), | ||
40 | asyncRetryTransactionMiddleware(acceptOwnership) | ||
41 | ) | ||
42 | |||
43 | ownershipVideoRouter.post('/ownership/:id/refuse', | ||
44 | authenticate, | ||
45 | asyncMiddleware(videosTerminateChangeOwnershipValidator), | ||
46 | asyncRetryTransactionMiddleware(refuseOwnership) | ||
47 | ) | ||
48 | |||
49 | // --------------------------------------------------------------------------- | ||
50 | |||
51 | export { | ||
52 | ownershipVideoRouter | ||
53 | } | ||
54 | |||
55 | // --------------------------------------------------------------------------- | ||
56 | |||
57 | async function giveVideoOwnership (req: express.Request, res: express.Response) { | ||
58 | const videoInstance = res.locals.video as VideoModel | ||
59 | const initiatorAccount = res.locals.oauth.token.User.Account as AccountModel | ||
60 | const nextOwner = res.locals.nextOwner as AccountModel | ||
61 | |||
62 | await sequelizeTypescript.transaction(async t => { | ||
63 | await VideoChangeOwnershipModel.findOrCreate({ | ||
64 | where: { | ||
65 | initiatorAccountId: initiatorAccount.id, | ||
66 | nextOwnerAccountId: nextOwner.id, | ||
67 | videoId: videoInstance.id, | ||
68 | status: VideoChangeOwnershipStatus.WAITING | ||
69 | }, | ||
70 | defaults: { | ||
71 | initiatorAccountId: initiatorAccount.id, | ||
72 | nextOwnerAccountId: nextOwner.id, | ||
73 | videoId: videoInstance.id, | ||
74 | status: VideoChangeOwnershipStatus.WAITING | ||
75 | } | ||
76 | }) | ||
77 | logger.info('Ownership change for video %s created.', videoInstance.name) | ||
78 | return res.type('json').status(204).end() | ||
79 | }) | ||
80 | } | ||
81 | |||
82 | async function listVideoOwnership (req: express.Request, res: express.Response) { | ||
83 | const currentAccount = res.locals.oauth.token.User.Account as AccountModel | ||
84 | const resultList = await VideoChangeOwnershipModel.listForApi( | ||
85 | currentAccount.id, | ||
86 | req.query.start || 0, | ||
87 | req.query.count || 10, | ||
88 | req.query.sort || 'createdAt' | ||
89 | ) | ||
90 | |||
91 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
92 | } | ||
93 | |||
94 | async function acceptOwnership (req: express.Request, res: express.Response) { | ||
95 | return sequelizeTypescript.transaction(async t => { | ||
96 | const videoChangeOwnership = res.locals.videoChangeOwnership as VideoChangeOwnershipModel | ||
97 | const targetVideo = videoChangeOwnership.Video | ||
98 | const channel = res.locals.videoChannel as VideoChannelModel | ||
99 | |||
100 | targetVideo.set('channelId', channel.id) | ||
101 | |||
102 | await targetVideo.save() | ||
103 | videoChangeOwnership.set('status', VideoChangeOwnershipStatus.ACCEPTED) | ||
104 | await videoChangeOwnership.save() | ||
105 | |||
106 | return res.sendStatus(204) | ||
107 | }) | ||
108 | } | ||
109 | |||
110 | async function refuseOwnership (req: express.Request, res: express.Response) { | ||
111 | return sequelizeTypescript.transaction(async t => { | ||
112 | const videoChangeOwnership = res.locals.videoChangeOwnership as VideoChangeOwnershipModel | ||
113 | videoChangeOwnership.set('status', VideoChangeOwnershipStatus.REFUSED) | ||
114 | await videoChangeOwnership.save() | ||
115 | return res.sendStatus(204) | ||
116 | }) | ||
117 | } | ||
diff --git a/server/helpers/custom-validators/video-ownership.ts b/server/helpers/custom-validators/video-ownership.ts new file mode 100644 index 000000000..aaa0c736b --- /dev/null +++ b/server/helpers/custom-validators/video-ownership.ts | |||
@@ -0,0 +1,42 @@ | |||
1 | import { Response } from 'express' | ||
2 | import * as validator from 'validator' | ||
3 | import { VideoChangeOwnershipModel } from '../../models/video/video-change-ownership' | ||
4 | import { UserModel } from '../../models/account/user' | ||
5 | |||
6 | export async function doesChangeVideoOwnershipExist (id: string, res: Response): Promise<boolean> { | ||
7 | const videoChangeOwnership = await loadVideoChangeOwnership(id) | ||
8 | |||
9 | if (!videoChangeOwnership) { | ||
10 | res.status(404) | ||
11 | .json({ error: 'Video change ownership not found' }) | ||
12 | .end() | ||
13 | |||
14 | return false | ||
15 | } | ||
16 | |||
17 | res.locals.videoChangeOwnership = videoChangeOwnership | ||
18 | return true | ||
19 | } | ||
20 | |||
21 | async function loadVideoChangeOwnership (id: string): Promise<VideoChangeOwnershipModel | undefined> { | ||
22 | if (validator.isInt(id)) { | ||
23 | return VideoChangeOwnershipModel.load(parseInt(id, 10)) | ||
24 | } | ||
25 | |||
26 | return undefined | ||
27 | } | ||
28 | |||
29 | export function checkUserCanTerminateOwnershipChange ( | ||
30 | user: UserModel, | ||
31 | videoChangeOwnership: VideoChangeOwnershipModel, | ||
32 | res: Response | ||
33 | ): boolean { | ||
34 | if (videoChangeOwnership.NextOwner.userId === user.Account.userId) { | ||
35 | return true | ||
36 | } | ||
37 | |||
38 | res.status(403) | ||
39 | .json({ error: 'Cannot terminate an ownership change of another user' }) | ||
40 | .end() | ||
41 | return false | ||
42 | } | ||
diff --git a/server/initializers/database.ts b/server/initializers/database.ts index 78bc8101c..b68e1a882 100644 --- a/server/initializers/database.ts +++ b/server/initializers/database.ts | |||
@@ -26,6 +26,7 @@ import { ScheduleVideoUpdateModel } from '../models/video/schedule-video-update' | |||
26 | import { VideoCaptionModel } from '../models/video/video-caption' | 26 | import { VideoCaptionModel } from '../models/video/video-caption' |
27 | import { VideoImportModel } from '../models/video/video-import' | 27 | import { VideoImportModel } from '../models/video/video-import' |
28 | import { VideoViewModel } from '../models/video/video-views' | 28 | import { VideoViewModel } from '../models/video/video-views' |
29 | import { VideoChangeOwnershipModel } from '../models/video/video-change-ownership' | ||
29 | 30 | ||
30 | require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string | 31 | require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string |
31 | 32 | ||
@@ -75,6 +76,7 @@ async function initDatabaseModels (silent: boolean) { | |||
75 | AccountVideoRateModel, | 76 | AccountVideoRateModel, |
76 | UserModel, | 77 | UserModel, |
77 | VideoAbuseModel, | 78 | VideoAbuseModel, |
79 | VideoChangeOwnershipModel, | ||
78 | VideoChannelModel, | 80 | VideoChannelModel, |
79 | VideoShareModel, | 81 | VideoShareModel, |
80 | VideoFileModel, | 82 | VideoFileModel, |
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index a595c39ec..d13c50c84 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts | |||
@@ -290,6 +290,10 @@ const usersVerifyEmailValidator = [ | |||
290 | } | 290 | } |
291 | ] | 291 | ] |
292 | 292 | ||
293 | const userAutocompleteValidator = [ | ||
294 | param('search').isString().not().isEmpty().withMessage('Should have a search parameter') | ||
295 | ] | ||
296 | |||
293 | // --------------------------------------------------------------------------- | 297 | // --------------------------------------------------------------------------- |
294 | 298 | ||
295 | export { | 299 | export { |
@@ -307,7 +311,8 @@ export { | |||
307 | usersAskResetPasswordValidator, | 311 | usersAskResetPasswordValidator, |
308 | usersResetPasswordValidator, | 312 | usersResetPasswordValidator, |
309 | usersAskSendVerifyEmailValidator, | 313 | usersAskSendVerifyEmailValidator, |
310 | usersVerifyEmailValidator | 314 | usersVerifyEmailValidator, |
315 | userAutocompleteValidator | ||
311 | } | 316 | } |
312 | 317 | ||
313 | // --------------------------------------------------------------------------- | 318 | // --------------------------------------------------------------------------- |
diff --git a/server/middlewares/validators/videos.ts b/server/middlewares/validators/videos.ts index a2c866152..9befbc9ee 100644 --- a/server/middlewares/validators/videos.ts +++ b/server/middlewares/validators/videos.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import 'express-validator' | 2 | import 'express-validator' |
3 | import { body, param, ValidationChain } from 'express-validator/check' | 3 | import { body, param, ValidationChain } from 'express-validator/check' |
4 | import { UserRight, VideoPrivacy } from '../../../shared' | 4 | import { UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../shared' |
5 | import { | 5 | import { |
6 | isBooleanValid, | 6 | isBooleanValid, |
7 | isDateValid, | 7 | isDateValid, |
@@ -37,6 +37,10 @@ import { areValidationErrors } from './utils' | |||
37 | import { cleanUpReqFiles } from '../../helpers/express-utils' | 37 | import { cleanUpReqFiles } from '../../helpers/express-utils' |
38 | import { VideoModel } from '../../models/video/video' | 38 | import { VideoModel } from '../../models/video/video' |
39 | import { UserModel } from '../../models/account/user' | 39 | import { UserModel } from '../../models/account/user' |
40 | import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../helpers/custom-validators/video-ownership' | ||
41 | import { VideoChangeOwnershipAccept } from '../../../shared/models/videos/video-change-ownership-accept.model' | ||
42 | import { VideoChangeOwnershipModel } from '../../models/video/video-change-ownership' | ||
43 | import { AccountModel } from '../../models/account/account' | ||
40 | 44 | ||
41 | const videosAddValidator = getCommonVideoAttributes().concat([ | 45 | const videosAddValidator = getCommonVideoAttributes().concat([ |
42 | body('videofile') | 46 | body('videofile') |
@@ -217,6 +221,78 @@ const videosShareValidator = [ | |||
217 | } | 221 | } |
218 | ] | 222 | ] |
219 | 223 | ||
224 | const videosChangeOwnershipValidator = [ | ||
225 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), | ||
226 | |||
227 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
228 | logger.debug('Checking changeOwnership parameters', { parameters: req.params }) | ||
229 | |||
230 | if (areValidationErrors(req, res)) return | ||
231 | if (!await isVideoExist(req.params.videoId, res)) return | ||
232 | |||
233 | // Check if the user who did the request is able to change the ownership of the video | ||
234 | if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return | ||
235 | |||
236 | const nextOwner = await AccountModel.loadLocalByName(req.body.username) | ||
237 | if (!nextOwner) { | ||
238 | res.status(400) | ||
239 | .type('json') | ||
240 | .end() | ||
241 | return | ||
242 | } | ||
243 | res.locals.nextOwner = nextOwner | ||
244 | |||
245 | return next() | ||
246 | } | ||
247 | ] | ||
248 | |||
249 | const videosTerminateChangeOwnershipValidator = [ | ||
250 | param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), | ||
251 | |||
252 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
253 | logger.debug('Checking changeOwnership parameters', { parameters: req.params }) | ||
254 | |||
255 | if (areValidationErrors(req, res)) return | ||
256 | if (!await doesChangeVideoOwnershipExist(req.params.id, res)) return | ||
257 | |||
258 | // Check if the user who did the request is able to change the ownership of the video | ||
259 | if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return | ||
260 | |||
261 | return next() | ||
262 | }, | ||
263 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
264 | const videoChangeOwnership = res.locals.videoChangeOwnership as VideoChangeOwnershipModel | ||
265 | |||
266 | if (videoChangeOwnership.status === VideoChangeOwnershipStatus.WAITING) { | ||
267 | return next() | ||
268 | } else { | ||
269 | res.status(403) | ||
270 | .json({ error: 'Ownership already accepted or refused' }) | ||
271 | .end() | ||
272 | return | ||
273 | } | ||
274 | } | ||
275 | ] | ||
276 | |||
277 | const videosAcceptChangeOwnershipValidator = [ | ||
278 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
279 | const body = req.body as VideoChangeOwnershipAccept | ||
280 | if (!await isVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return | ||
281 | |||
282 | const user = res.locals.oauth.token.User | ||
283 | const videoChangeOwnership = res.locals.videoChangeOwnership as VideoChangeOwnershipModel | ||
284 | const isAble = await user.isAbleToUploadVideo(videoChangeOwnership.Video.getOriginalFile()) | ||
285 | if (isAble === false) { | ||
286 | res.status(403) | ||
287 | .json({ error: 'The user video quota is exceeded with this video.' }) | ||
288 | .end() | ||
289 | return | ||
290 | } | ||
291 | |||
292 | return next() | ||
293 | } | ||
294 | ] | ||
295 | |||
220 | function getCommonVideoAttributes () { | 296 | function getCommonVideoAttributes () { |
221 | return [ | 297 | return [ |
222 | body('thumbnailfile') | 298 | body('thumbnailfile') |
@@ -295,6 +371,10 @@ export { | |||
295 | 371 | ||
296 | videoRateValidator, | 372 | videoRateValidator, |
297 | 373 | ||
374 | videosChangeOwnershipValidator, | ||
375 | videosTerminateChangeOwnershipValidator, | ||
376 | videosAcceptChangeOwnershipValidator, | ||
377 | |||
298 | getCommonVideoAttributes | 378 | getCommonVideoAttributes |
299 | } | 379 | } |
300 | 380 | ||
diff --git a/server/models/account/user.ts b/server/models/account/user.ts index 89265774b..4b13e47a0 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts | |||
@@ -39,6 +39,7 @@ import { AccountModel } from './account' | |||
39 | import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type' | 39 | import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type' |
40 | import { values } from 'lodash' | 40 | import { values } from 'lodash' |
41 | import { NSFW_POLICY_TYPES } from '../../initializers' | 41 | import { NSFW_POLICY_TYPES } from '../../initializers' |
42 | import { VideoFileModel } from '../video/video-file' | ||
42 | 43 | ||
43 | enum ScopeNames { | 44 | enum ScopeNames { |
44 | WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL' | 45 | WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL' |
@@ -393,4 +394,15 @@ export class UserModel extends Model<UserModel> { | |||
393 | return parseInt(total, 10) | 394 | return parseInt(total, 10) |
394 | }) | 395 | }) |
395 | } | 396 | } |
397 | |||
398 | static autocomplete (search: string) { | ||
399 | return UserModel.findAll({ | ||
400 | where: { | ||
401 | username: { | ||
402 | [Sequelize.Op.like]: `%${search}%` | ||
403 | } | ||
404 | } | ||
405 | }) | ||
406 | .then(u => u.map(u => u.username)) | ||
407 | } | ||
396 | } | 408 | } |
diff --git a/server/models/video/video-change-ownership.ts b/server/models/video/video-change-ownership.ts new file mode 100644 index 000000000..c9cff5054 --- /dev/null +++ b/server/models/video/video-change-ownership.ts | |||
@@ -0,0 +1,127 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | ||
2 | import { AccountModel } from '../account/account' | ||
3 | import { VideoModel } from './video' | ||
4 | import { VideoChangeOwnership, VideoChangeOwnershipStatus } from '../../../shared/models/videos' | ||
5 | import { getSort } from '../utils' | ||
6 | import { VideoFileModel } from './video-file' | ||
7 | |||
8 | enum ScopeNames { | ||
9 | FULL = 'FULL' | ||
10 | } | ||
11 | |||
12 | @Table({ | ||
13 | tableName: 'videoChangeOwnership', | ||
14 | indexes: [ | ||
15 | { | ||
16 | fields: ['videoId'] | ||
17 | }, | ||
18 | { | ||
19 | fields: ['initiatorAccountId'] | ||
20 | }, | ||
21 | { | ||
22 | fields: ['nextOwnerAccountId'] | ||
23 | } | ||
24 | ] | ||
25 | }) | ||
26 | @Scopes({ | ||
27 | [ScopeNames.FULL]: { | ||
28 | include: [ | ||
29 | { | ||
30 | model: () => AccountModel, | ||
31 | as: 'Initiator', | ||
32 | required: true | ||
33 | }, | ||
34 | { | ||
35 | model: () => AccountModel, | ||
36 | as: 'NextOwner', | ||
37 | required: true | ||
38 | }, | ||
39 | { | ||
40 | model: () => VideoModel, | ||
41 | required: true, | ||
42 | include: [{ model: () => VideoFileModel }] | ||
43 | } | ||
44 | ] | ||
45 | } | ||
46 | }) | ||
47 | export class VideoChangeOwnershipModel extends Model<VideoChangeOwnershipModel> { | ||
48 | @CreatedAt | ||
49 | createdAt: Date | ||
50 | |||
51 | @UpdatedAt | ||
52 | updatedAt: Date | ||
53 | |||
54 | @AllowNull(false) | ||
55 | @Column | ||
56 | status: VideoChangeOwnershipStatus | ||
57 | |||
58 | @ForeignKey(() => AccountModel) | ||
59 | @Column | ||
60 | initiatorAccountId: number | ||
61 | |||
62 | @BelongsTo(() => AccountModel, { | ||
63 | foreignKey: { | ||
64 | name: 'initiatorAccountId', | ||
65 | allowNull: false | ||
66 | }, | ||
67 | onDelete: 'cascade' | ||
68 | }) | ||
69 | Initiator: AccountModel | ||
70 | |||
71 | @ForeignKey(() => AccountModel) | ||
72 | @Column | ||
73 | nextOwnerAccountId: number | ||
74 | |||
75 | @BelongsTo(() => AccountModel, { | ||
76 | foreignKey: { | ||
77 | name: 'nextOwnerAccountId', | ||
78 | allowNull: false | ||
79 | }, | ||
80 | onDelete: 'cascade' | ||
81 | }) | ||
82 | NextOwner: AccountModel | ||
83 | |||
84 | @ForeignKey(() => VideoModel) | ||
85 | @Column | ||
86 | videoId: number | ||
87 | |||
88 | @BelongsTo(() => VideoModel, { | ||
89 | foreignKey: { | ||
90 | allowNull: false | ||
91 | }, | ||
92 | onDelete: 'cascade' | ||
93 | }) | ||
94 | Video: VideoModel | ||
95 | |||
96 | static listForApi (nextOwnerId: number, start: number, count: number, sort: string) { | ||
97 | return VideoChangeOwnershipModel.scope(ScopeNames.FULL).findAndCountAll({ | ||
98 | offset: start, | ||
99 | limit: count, | ||
100 | order: getSort(sort), | ||
101 | where: { | ||
102 | nextOwnerAccountId: nextOwnerId | ||
103 | } | ||
104 | }) | ||
105 | .then(({ rows, count }) => ({ total: count, data: rows })) | ||
106 | } | ||
107 | |||
108 | static load (id: number) { | ||
109 | return VideoChangeOwnershipModel.scope(ScopeNames.FULL).findById(id) | ||
110 | } | ||
111 | |||
112 | toFormattedJSON (): VideoChangeOwnership { | ||
113 | return { | ||
114 | id: this.id, | ||
115 | status: this.status, | ||
116 | initiatorAccount: this.Initiator.toFormattedJSON(), | ||
117 | nextOwnerAccount: this.NextOwner.toFormattedJSON(), | ||
118 | video: { | ||
119 | id: this.Video.id, | ||
120 | uuid: this.Video.uuid, | ||
121 | url: this.Video.url, | ||
122 | name: this.Video.name | ||
123 | }, | ||
124 | createdAt: this.createdAt | ||
125 | } | ||
126 | } | ||
127 | } | ||
diff --git a/server/tests/api/videos/video-change-ownership.ts b/server/tests/api/videos/video-change-ownership.ts new file mode 100644 index 000000000..275be40be --- /dev/null +++ b/server/tests/api/videos/video-change-ownership.ts | |||
@@ -0,0 +1,262 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { | ||
6 | acceptChangeOwnership, | ||
7 | changeVideoOwnership, | ||
8 | createUser, | ||
9 | flushTests, | ||
10 | getMyUserInformation, | ||
11 | getVideoChangeOwnershipList, | ||
12 | getVideosList, | ||
13 | killallServers, | ||
14 | refuseChangeOwnership, | ||
15 | runServer, | ||
16 | ServerInfo, | ||
17 | setAccessTokensToServers, | ||
18 | uploadVideo, | ||
19 | userLogin | ||
20 | } from '../../utils' | ||
21 | import { waitJobs } from '../../utils/server/jobs' | ||
22 | import { User } from '../../../../shared/models/users' | ||
23 | |||
24 | const expect = chai.expect | ||
25 | |||
26 | describe('Test video change ownership - nominal', function () { | ||
27 | let server: ServerInfo = undefined | ||
28 | const firstUser = { | ||
29 | username: 'first', | ||
30 | password: 'My great password' | ||
31 | } | ||
32 | const secondUser = { | ||
33 | username: 'second', | ||
34 | password: 'My other password' | ||
35 | } | ||
36 | let firstUserAccessToken = '' | ||
37 | let secondUserAccessToken = '' | ||
38 | let lastRequestChangeOwnershipId = undefined | ||
39 | |||
40 | before(async function () { | ||
41 | this.timeout(50000) | ||
42 | |||
43 | // Run one server | ||
44 | await flushTests() | ||
45 | server = await runServer(1) | ||
46 | await setAccessTokensToServers([server]) | ||
47 | |||
48 | const videoQuota = 42000000 | ||
49 | await createUser(server.url, server.accessToken, firstUser.username, firstUser.password, videoQuota) | ||
50 | await createUser(server.url, server.accessToken, secondUser.username, secondUser.password, videoQuota) | ||
51 | |||
52 | firstUserAccessToken = await userLogin(server, firstUser) | ||
53 | secondUserAccessToken = await userLogin(server, secondUser) | ||
54 | |||
55 | // Upload some videos on the server | ||
56 | const video1Attributes = { | ||
57 | name: 'my super name', | ||
58 | description: 'my super description' | ||
59 | } | ||
60 | await uploadVideo(server.url, firstUserAccessToken, video1Attributes) | ||
61 | |||
62 | await waitJobs(server) | ||
63 | |||
64 | const res = await getVideosList(server.url) | ||
65 | const videos = res.body.data | ||
66 | |||
67 | expect(videos.length).to.equal(1) | ||
68 | |||
69 | server.video = videos.find(video => video.name === 'my super name') | ||
70 | }) | ||
71 | |||
72 | it('Should not have video change ownership', async function () { | ||
73 | const resFirstUser = await getVideoChangeOwnershipList(server.url, firstUserAccessToken) | ||
74 | |||
75 | expect(resFirstUser.body.total).to.equal(0) | ||
76 | expect(resFirstUser.body.data).to.be.an('array') | ||
77 | expect(resFirstUser.body.data.length).to.equal(0) | ||
78 | |||
79 | const resSecondUser = await getVideoChangeOwnershipList(server.url, secondUserAccessToken) | ||
80 | |||
81 | expect(resSecondUser.body.total).to.equal(0) | ||
82 | expect(resSecondUser.body.data).to.be.an('array') | ||
83 | expect(resSecondUser.body.data.length).to.equal(0) | ||
84 | }) | ||
85 | |||
86 | it('Should send a request to change ownership of a video', async function () { | ||
87 | this.timeout(15000) | ||
88 | |||
89 | await changeVideoOwnership(server.url, firstUserAccessToken, server.video.id, secondUser.username) | ||
90 | }) | ||
91 | |||
92 | it('Should only return a request to change ownership for the second user', async function () { | ||
93 | const resFirstUser = await getVideoChangeOwnershipList(server.url, firstUserAccessToken) | ||
94 | |||
95 | expect(resFirstUser.body.total).to.equal(0) | ||
96 | expect(resFirstUser.body.data).to.be.an('array') | ||
97 | expect(resFirstUser.body.data.length).to.equal(0) | ||
98 | |||
99 | const resSecondUser = await getVideoChangeOwnershipList(server.url, secondUserAccessToken) | ||
100 | |||
101 | expect(resSecondUser.body.total).to.equal(1) | ||
102 | expect(resSecondUser.body.data).to.be.an('array') | ||
103 | expect(resSecondUser.body.data.length).to.equal(1) | ||
104 | |||
105 | lastRequestChangeOwnershipId = resSecondUser.body.data[0].id | ||
106 | }) | ||
107 | |||
108 | it('Should accept the same change ownership request without crashing', async function () { | ||
109 | this.timeout(10000) | ||
110 | |||
111 | await changeVideoOwnership(server.url, firstUserAccessToken, server.video.id, secondUser.username) | ||
112 | }) | ||
113 | |||
114 | it('Should not create multiple change ownership requests while one is waiting', async function () { | ||
115 | this.timeout(10000) | ||
116 | |||
117 | const resSecondUser = await getVideoChangeOwnershipList(server.url, secondUserAccessToken) | ||
118 | |||
119 | expect(resSecondUser.body.total).to.equal(1) | ||
120 | expect(resSecondUser.body.data).to.be.an('array') | ||
121 | expect(resSecondUser.body.data.length).to.equal(1) | ||
122 | }) | ||
123 | |||
124 | it('Should not be possible to refuse the change of ownership from first user', async function () { | ||
125 | this.timeout(10000) | ||
126 | |||
127 | await refuseChangeOwnership(server.url, firstUserAccessToken, lastRequestChangeOwnershipId, 403) | ||
128 | }) | ||
129 | |||
130 | it('Should be possible to refuse the change of ownership from second user', async function () { | ||
131 | this.timeout(10000) | ||
132 | |||
133 | await refuseChangeOwnership(server.url, secondUserAccessToken, lastRequestChangeOwnershipId) | ||
134 | }) | ||
135 | |||
136 | it('Should send a new request to change ownership of a video', async function () { | ||
137 | this.timeout(15000) | ||
138 | |||
139 | await changeVideoOwnership(server.url, firstUserAccessToken, server.video.id, secondUser.username) | ||
140 | }) | ||
141 | |||
142 | it('Should return two requests to change ownership for the second user', async function () { | ||
143 | const resFirstUser = await getVideoChangeOwnershipList(server.url, firstUserAccessToken) | ||
144 | |||
145 | expect(resFirstUser.body.total).to.equal(0) | ||
146 | expect(resFirstUser.body.data).to.be.an('array') | ||
147 | expect(resFirstUser.body.data.length).to.equal(0) | ||
148 | |||
149 | const resSecondUser = await getVideoChangeOwnershipList(server.url, secondUserAccessToken) | ||
150 | |||
151 | expect(resSecondUser.body.total).to.equal(2) | ||
152 | expect(resSecondUser.body.data).to.be.an('array') | ||
153 | expect(resSecondUser.body.data.length).to.equal(2) | ||
154 | |||
155 | lastRequestChangeOwnershipId = resSecondUser.body.data[0].id | ||
156 | }) | ||
157 | |||
158 | it('Should not be possible to accept the change of ownership from first user', async function () { | ||
159 | this.timeout(10000) | ||
160 | |||
161 | const secondUserInformationResponse = await getMyUserInformation(server.url, secondUserAccessToken) | ||
162 | const secondUserInformation: User = secondUserInformationResponse.body | ||
163 | const channelId = secondUserInformation.videoChannels[0].id | ||
164 | await acceptChangeOwnership(server.url, firstUserAccessToken, lastRequestChangeOwnershipId, channelId, 403) | ||
165 | }) | ||
166 | |||
167 | it('Should be possible to accept the change of ownership from second user', async function () { | ||
168 | this.timeout(10000) | ||
169 | |||
170 | const secondUserInformationResponse = await getMyUserInformation(server.url, secondUserAccessToken) | ||
171 | const secondUserInformation: User = secondUserInformationResponse.body | ||
172 | const channelId = secondUserInformation.videoChannels[0].id | ||
173 | await acceptChangeOwnership(server.url, secondUserAccessToken, lastRequestChangeOwnershipId, channelId) | ||
174 | }) | ||
175 | |||
176 | after(async function () { | ||
177 | killallServers([server]) | ||
178 | }) | ||
179 | }) | ||
180 | |||
181 | describe('Test video change ownership - quota too small', function () { | ||
182 | let server: ServerInfo = undefined | ||
183 | const firstUser = { | ||
184 | username: 'first', | ||
185 | password: 'My great password' | ||
186 | } | ||
187 | const secondUser = { | ||
188 | username: 'second', | ||
189 | password: 'My other password' | ||
190 | } | ||
191 | let firstUserAccessToken = '' | ||
192 | let secondUserAccessToken = '' | ||
193 | let lastRequestChangeOwnershipId = undefined | ||
194 | |||
195 | before(async function () { | ||
196 | this.timeout(50000) | ||
197 | |||
198 | // Run one server | ||
199 | await flushTests() | ||
200 | server = await runServer(1) | ||
201 | await setAccessTokensToServers([server]) | ||
202 | |||
203 | const videoQuota = 42000000 | ||
204 | const limitedVideoQuota = 10 | ||
205 | await createUser(server.url, server.accessToken, firstUser.username, firstUser.password, videoQuota) | ||
206 | await createUser(server.url, server.accessToken, secondUser.username, secondUser.password, limitedVideoQuota) | ||
207 | |||
208 | firstUserAccessToken = await userLogin(server, firstUser) | ||
209 | secondUserAccessToken = await userLogin(server, secondUser) | ||
210 | |||
211 | // Upload some videos on the server | ||
212 | const video1Attributes = { | ||
213 | name: 'my super name', | ||
214 | description: 'my super description' | ||
215 | } | ||
216 | await uploadVideo(server.url, firstUserAccessToken, video1Attributes) | ||
217 | |||
218 | await waitJobs(server) | ||
219 | |||
220 | const res = await getVideosList(server.url) | ||
221 | const videos = res.body.data | ||
222 | |||
223 | expect(videos.length).to.equal(1) | ||
224 | |||
225 | server.video = videos.find(video => video.name === 'my super name') | ||
226 | }) | ||
227 | |||
228 | it('Should send a request to change ownership of a video', async function () { | ||
229 | this.timeout(15000) | ||
230 | |||
231 | await changeVideoOwnership(server.url, firstUserAccessToken, server.video.id, secondUser.username) | ||
232 | }) | ||
233 | |||
234 | it('Should only return a request to change ownership for the second user', async function () { | ||
235 | const resFirstUser = await getVideoChangeOwnershipList(server.url, firstUserAccessToken) | ||
236 | |||
237 | expect(resFirstUser.body.total).to.equal(0) | ||
238 | expect(resFirstUser.body.data).to.be.an('array') | ||
239 | expect(resFirstUser.body.data.length).to.equal(0) | ||
240 | |||
241 | const resSecondUser = await getVideoChangeOwnershipList(server.url, secondUserAccessToken) | ||
242 | |||
243 | expect(resSecondUser.body.total).to.equal(1) | ||
244 | expect(resSecondUser.body.data).to.be.an('array') | ||
245 | expect(resSecondUser.body.data.length).to.equal(1) | ||
246 | |||
247 | lastRequestChangeOwnershipId = resSecondUser.body.data[0].id | ||
248 | }) | ||
249 | |||
250 | it('Should not be possible to accept the change of ownership from second user because of exceeded quota', async function () { | ||
251 | this.timeout(10000) | ||
252 | |||
253 | const secondUserInformationResponse = await getMyUserInformation(server.url, secondUserAccessToken) | ||
254 | const secondUserInformation: User = secondUserInformationResponse.body | ||
255 | const channelId = secondUserInformation.videoChannels[0].id | ||
256 | await acceptChangeOwnership(server.url, secondUserAccessToken, lastRequestChangeOwnershipId, channelId, 403) | ||
257 | }) | ||
258 | |||
259 | after(async function () { | ||
260 | killallServers([server]) | ||
261 | }) | ||
262 | }) | ||
diff --git a/server/tests/utils/index.ts b/server/tests/utils/index.ts index 391db18cf..897389824 100644 --- a/server/tests/utils/index.ts +++ b/server/tests/utils/index.ts | |||
@@ -13,5 +13,6 @@ export * from './videos/video-abuses' | |||
13 | export * from './videos/video-blacklist' | 13 | export * from './videos/video-blacklist' |
14 | export * from './videos/video-channels' | 14 | export * from './videos/video-channels' |
15 | export * from './videos/videos' | 15 | export * from './videos/videos' |
16 | export * from './videos/video-change-ownership' | ||
16 | export * from './feeds/feeds' | 17 | export * from './feeds/feeds' |
17 | export * from './search/videos' | 18 | export * from './search/videos' |
diff --git a/server/tests/utils/videos/video-change-ownership.ts b/server/tests/utils/videos/video-change-ownership.ts new file mode 100644 index 000000000..f288692ea --- /dev/null +++ b/server/tests/utils/videos/video-change-ownership.ts | |||
@@ -0,0 +1,54 @@ | |||
1 | import * as request from 'supertest' | ||
2 | |||
3 | function changeVideoOwnership (url: string, token: string, videoId: number | string, username) { | ||
4 | const path = '/api/v1/videos/' + videoId + '/give-ownership' | ||
5 | |||
6 | return request(url) | ||
7 | .post(path) | ||
8 | .set('Accept', 'application/json') | ||
9 | .set('Authorization', 'Bearer ' + token) | ||
10 | .send({ username }) | ||
11 | .expect(204) | ||
12 | } | ||
13 | |||
14 | function getVideoChangeOwnershipList (url: string, token: string) { | ||
15 | const path = '/api/v1/videos/ownership' | ||
16 | |||
17 | return request(url) | ||
18 | .get(path) | ||
19 | .query({ sort: '-createdAt' }) | ||
20 | .set('Accept', 'application/json') | ||
21 | .set('Authorization', 'Bearer ' + token) | ||
22 | .expect(200) | ||
23 | .expect('Content-Type', /json/) | ||
24 | } | ||
25 | |||
26 | function acceptChangeOwnership (url: string, token: string, ownershipId: string, channelId: number, expectedStatus = 204) { | ||
27 | const path = '/api/v1/videos/ownership/' + ownershipId + '/accept' | ||
28 | |||
29 | return request(url) | ||
30 | .post(path) | ||
31 | .set('Accept', 'application/json') | ||
32 | .set('Authorization', 'Bearer ' + token) | ||
33 | .send({ channelId }) | ||
34 | .expect(expectedStatus) | ||
35 | } | ||
36 | |||
37 | function refuseChangeOwnership (url: string, token: string, ownershipId: string, expectedStatus = 204) { | ||
38 | const path = '/api/v1/videos/ownership/' + ownershipId + '/refuse' | ||
39 | |||
40 | return request(url) | ||
41 | .post(path) | ||
42 | .set('Accept', 'application/json') | ||
43 | .set('Authorization', 'Bearer ' + token) | ||
44 | .expect(expectedStatus) | ||
45 | } | ||
46 | |||
47 | // --------------------------------------------------------------------------- | ||
48 | |||
49 | export { | ||
50 | changeVideoOwnership, | ||
51 | getVideoChangeOwnershipList, | ||
52 | acceptChangeOwnership, | ||
53 | refuseChangeOwnership | ||
54 | } | ||