API: Add ability to update video channel avatar
authorChocobozzz <me@florianbigard.com>
Fri, 29 Jun 2018 09:29:23 +0000 (11:29 +0200)
committerChocobozzz <me@florianbigard.com>
Fri, 29 Jun 2018 09:29:23 +0000 (11:29 +0200)
12 files changed:
server/controllers/api/users.ts
server/controllers/api/video-channel.ts
server/lib/avatar.ts [new file with mode: 0644]
server/middlewares/validators/avatar.ts [new file with mode: 0644]
server/middlewares/validators/users.ts
server/middlewares/validators/video-channels.ts
server/tests/api/check-params/users.ts
server/tests/api/check-params/video-channels.ts
server/tests/api/videos/video-channels.ts
server/tests/utils/requests/requests.ts
server/tests/utils/users/users.ts
server/tests/utils/videos/video-channels.ts

index 89105691267786772d361956f22e92acbff9d327..c80f27a23c53b19b4dc96c91cc89ece3c7026873 100644 (file)
@@ -1,14 +1,10 @@
 import * as express from 'express'
 import 'multer'
-import { extname, join } from 'path'
-import * as uuidv4 from 'uuid/v4'
 import * as RateLimit from 'express-rate-limit'
 import { UserCreate, UserRight, UserRole, UserUpdate, UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../shared'
-import { processImage } from '../../helpers/image-utils'
 import { logger } from '../../helpers/logger'
 import { getFormattedObjects } from '../../helpers/utils'
-import { AVATARS_SIZE, CONFIG, IMAGE_MIMETYPE_EXT, RATES_LIMIT, sequelizeTypescript } from '../../initializers'
-import { updateActorAvatarInstance } from '../../lib/activitypub'
+import { CONFIG, IMAGE_MIMETYPE_EXT, RATES_LIMIT, sequelizeTypescript } from '../../initializers'
 import { sendUpdateActor } from '../../lib/activitypub/send'
 import { Emailer } from '../../lib/emailer'
 import { Redis } from '../../lib/redis'
@@ -33,12 +29,7 @@ import {
   usersUpdateValidator,
   usersVideoRatingValidator
 } from '../../middlewares'
-import {
-  usersAskResetPasswordValidator,
-  usersResetPasswordValidator,
-  usersUpdateMyAvatarValidator,
-  videosSortValidator
-} from '../../middlewares/validators'
+import { usersAskResetPasswordValidator, usersResetPasswordValidator, videosSortValidator } from '../../middlewares/validators'
 import { AccountVideoRateModel } from '../../models/account/account-video-rate'
 import { UserModel } from '../../models/account/user'
 import { OAuthTokenModel } from '../../models/oauth/oauth-token'
@@ -46,6 +37,8 @@ import { VideoModel } from '../../models/video/video'
 import { VideoSortField } from '../../../client/src/app/shared/video/sort-field.type'
 import { createReqFiles } from '../../helpers/express-utils'
 import { UserVideoQuota } from '../../../shared/models/users/user-video-quota.model'
+import { updateAvatarValidator } from '../../middlewares/validators/avatar'
+import { updateActorAvatarFile } from '../../lib/avatar'
 
 const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.AVATARS_DIR })
 const loginRateLimiter = new RateLimit({
@@ -121,7 +114,7 @@ usersRouter.put('/me',
 usersRouter.post('/me/avatar/pick',
   authenticate,
   reqAvatarFile,
-  usersUpdateMyAvatarValidator,
+  updateAvatarValidator,
   asyncMiddleware(updateMyAvatar)
 )
 
@@ -304,22 +297,9 @@ async function updateMe (req: express.Request, res: express.Response, next: expr
 
 async function updateMyAvatar (req: express.Request, res: express.Response, next: express.NextFunction) {
   const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ]
-  const user = res.locals.oauth.token.user
-  const actor = user.Account.Actor
-
-  const extension = extname(avatarPhysicalFile.filename)
-  const avatarName = uuidv4() + extension
-  const destination = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
-  await processImage(avatarPhysicalFile, destination, AVATARS_SIZE)
-
-  const avatar = await sequelizeTypescript.transaction(async t => {
-    const updatedActor = await updateActorAvatarInstance(actor, avatarName, t)
-    await updatedActor.save({ transaction: t })
+  const account = res.locals.oauth.token.user.Account
 
-    await sendUpdateActor(user.Account, t)
-
-    return updatedActor.Avatar
-  })
+  const avatar = await updateActorAvatarFile(avatarPhysicalFile, account.Actor, account)
 
   return res
     .json({
index 61e72125fba3b9ce3a067288b64f76a93c994189..1707732ee29a8a675d0e9c6e31ab70ef93c01bdb 100644 (file)
@@ -19,12 +19,16 @@ import { videosSortValidator } from '../../middlewares/validators'
 import { sendUpdateActor } from '../../lib/activitypub/send'
 import { VideoChannelCreate, VideoChannelUpdate } from '../../../shared'
 import { createVideoChannel } from '../../lib/video-channel'
-import { isNSFWHidden } from '../../helpers/express-utils'
+import { createReqFiles, isNSFWHidden } from '../../helpers/express-utils'
 import { setAsyncActorKeys } from '../../lib/activitypub'
 import { AccountModel } from '../../models/account/account'
-import { sequelizeTypescript } from '../../initializers'
+import { CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../initializers'
 import { logger } from '../../helpers/logger'
 import { VideoModel } from '../../models/video/video'
+import { updateAvatarValidator } from '../../middlewares/validators/avatar'
+import { updateActorAvatarFile } from '../../lib/avatar'
+
+const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.AVATARS_DIR })
 
 const videoChannelRouter = express.Router()
 
@@ -42,6 +46,15 @@ videoChannelRouter.post('/',
   asyncRetryTransactionMiddleware(addVideoChannel)
 )
 
+videoChannelRouter.post('/:id/avatar/pick',
+  authenticate,
+  reqAvatarFile,
+  // Check the rights
+  asyncMiddleware(videoChannelsUpdateValidator),
+  updateAvatarValidator,
+  asyncMiddleware(updateVideoChannelAvatar)
+)
+
 videoChannelRouter.put('/:id',
   authenticate,
   asyncMiddleware(videoChannelsUpdateValidator),
@@ -83,6 +96,19 @@ async function listVideoChannels (req: express.Request, res: express.Response, n
   return res.json(getFormattedObjects(resultList.data, resultList.total))
 }
 
+async function updateVideoChannelAvatar (req: express.Request, res: express.Response, next: express.NextFunction) {
+  const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ]
+  const videoChannel = res.locals.videoChannel
+
+  const avatar = await updateActorAvatarFile(avatarPhysicalFile, videoChannel.Actor, videoChannel)
+
+  return res
+    .json({
+      avatar: avatar.toFormattedJSON()
+    })
+    .end()
+}
+
 async function addVideoChannel (req: express.Request, res: express.Response) {
   const videoChannelInfo: VideoChannelCreate = req.body
   const account: AccountModel = res.locals.oauth.token.User.Account
diff --git a/server/lib/avatar.ts b/server/lib/avatar.ts
new file mode 100644 (file)
index 0000000..7fdef00
--- /dev/null
@@ -0,0 +1,34 @@
+import 'multer'
+import * as uuidv4 from 'uuid'
+import { sendUpdateActor } from './activitypub/send'
+import { AVATARS_SIZE, CONFIG, sequelizeTypescript } from '../initializers'
+import { updateActorAvatarInstance } from './activitypub'
+import { processImage } from '../helpers/image-utils'
+import { ActorModel } from '../models/activitypub/actor'
+import { AccountModel } from '../models/account/account'
+import { VideoChannelModel } from '../models/video/video-channel'
+import { extname, join } from 'path'
+
+async function updateActorAvatarFile (
+  avatarPhysicalFile: Express.Multer.File,
+  actor: ActorModel,
+  accountOrChannel: AccountModel | VideoChannelModel
+) {
+  const extension = extname(avatarPhysicalFile.filename)
+  const avatarName = uuidv4() + extension
+  const destination = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
+  await processImage(avatarPhysicalFile, destination, AVATARS_SIZE)
+
+  return sequelizeTypescript.transaction(async t => {
+    const updatedActor = await updateActorAvatarInstance(actor, avatarName, t)
+    await updatedActor.save({ transaction: t })
+
+    await sendUpdateActor(accountOrChannel, t)
+
+    return updatedActor.Avatar
+  })
+}
+
+export {
+  updateActorAvatarFile
+}
diff --git a/server/middlewares/validators/avatar.ts b/server/middlewares/validators/avatar.ts
new file mode 100644 (file)
index 0000000..f346ea9
--- /dev/null
@@ -0,0 +1,25 @@
+import * as express from 'express'
+import { body } from 'express-validator/check'
+import { isAvatarFile } from '../../helpers/custom-validators/users'
+import { areValidationErrors } from './utils'
+import { CONSTRAINTS_FIELDS } from '../../initializers'
+import { logger } from '../../helpers/logger'
+
+const updateAvatarValidator = [
+  body('avatarfile').custom((value, { req }) => isAvatarFile(req.files)).withMessage(
+    'This file is not supported or too large. Please, make sure it is of the following type : '
+    + CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME.join(', ')
+  ),
+
+  (req: express.Request, res: express.Response, next: express.NextFunction) => {
+    logger.debug('Checking updateAvatarValidator parameters', { files: req.files })
+
+    if (areValidationErrors(req, res)) return
+
+    return next()
+  }
+]
+
+export {
+  updateAvatarValidator
+}
index 55a08a64800dd822cc7ced26d42c6b336b059b5d..8ca9763a1ddf6b164aae734bc0f567728a0c6367 100644 (file)
@@ -5,9 +5,9 @@ import { body, param } from 'express-validator/check'
 import { omit } from 'lodash'
 import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc'
 import {
-  isAvatarFile,
   isUserAutoPlayVideoValid,
-  isUserDescriptionValid, isUserDisplayNameValid,
+  isUserDescriptionValid,
+  isUserDisplayNameValid,
   isUserNSFWPolicyValid,
   isUserPasswordValid,
   isUserRoleValid,
@@ -17,7 +17,6 @@ import {
 import { isVideoExist } from '../../helpers/custom-validators/videos'
 import { logger } from '../../helpers/logger'
 import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/utils'
-import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers'
 import { Redis } from '../../lib/redis'
 import { UserModel } from '../../models/account/user'
 import { areValidationErrors } from './utils'
@@ -116,21 +115,6 @@ const usersUpdateMeValidator = [
   }
 ]
 
-const usersUpdateMyAvatarValidator = [
-  body('avatarfile').custom((value, { req }) => isAvatarFile(req.files)).withMessage(
-    'This file is not supported or too large. Please, make sure it is of the following type : '
-    + CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME.join(', ')
-  ),
-
-  (req: express.Request, res: express.Response, next: express.NextFunction) => {
-    logger.debug('Checking usersUpdateMyAvatarValidator parameters', { files: req.files })
-
-    if (areValidationErrors(req, res)) return
-
-    return next()
-  }
-]
-
 const usersGetValidator = [
   param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
 
@@ -239,7 +223,6 @@ export {
   ensureUserRegistrationAllowed,
   ensureUserRegistrationAllowedForIP,
   usersGetValidator,
-  usersUpdateMyAvatarValidator,
   usersAskResetPasswordValidator,
   usersResetPasswordValidator
 }
index a5be5f1146b24ba59080fed0672d085be155d7b8..7f65f729085e521232d93ddec50267bc37e421a1 100644 (file)
@@ -13,6 +13,8 @@ import { logger } from '../../helpers/logger'
 import { UserModel } from '../../models/account/user'
 import { VideoChannelModel } from '../../models/video/video-channel'
 import { areValidationErrors } from './utils'
+import { isAvatarFile } from '../../helpers/custom-validators/users'
+import { CONSTRAINTS_FIELDS } from '../../initializers'
 
 const listVideoAccountChannelsValidator = [
   param('accountName').exists().withMessage('Should have a valid account name'),
index 28537315e1f03a3e98ef022d1521d9dbcc80b58b..e1954c64f53332485d134f506b0a84f0620dba1a 100644 (file)
@@ -304,6 +304,20 @@ describe('Test users API validators', function () {
       await makeUploadRequest({ url: server.url, path: path + '/me/avatar/pick', token: server.accessToken, fields, attaches })
     })
 
+    it('Should fail with an unauthenticated user', async function () {
+      const fields = {}
+      const attaches = {
+        'avatarfile': join(__dirname, '..', '..', 'fixtures', 'avatar.png')
+      }
+      await makeUploadRequest({
+        url: server.url,
+        path: path + '/me/avatar/pick',
+        fields,
+        attaches,
+        statusCodeExpected: 401
+      })
+    })
+
     it('Should succeed with the correct params', async function () {
       const fields = {}
       const attaches = {
index 5080af2c964db1d01e3a7c4f9e4e3bd698374ca8..7b05e5882df739907e01d177d0c3321b188bb0c7 100644 (file)
@@ -14,7 +14,7 @@ import {
   killallServers,
   makeGetRequest,
   makePostBodyRequest,
-  makePutBodyRequest,
+  makePutBodyRequest, makeUploadRequest,
   runServer,
   ServerInfo,
   setAccessTokensToServers,
@@ -22,6 +22,7 @@ import {
 } from '../../utils'
 import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params'
 import { User } from '../../../../shared/models/users'
+import { join } from "path"
 
 const expect = chai.expect
 
@@ -189,6 +190,59 @@ describe('Test video channels API validator', function () {
     })
   })
 
+  describe('When updating video channel avatar', function () {
+    let path: string
+
+    before(async function () {
+      path = videoChannelPath + '/' + videoChannelUUID
+    })
+
+    it('Should fail with an incorrect input file', async function () {
+      const fields = {}
+      const attaches = {
+        'avatarfile': join(__dirname, '..', '..', 'fixtures', 'video_short.mp4')
+      }
+      await makeUploadRequest({ url: server.url, path: path + '/avatar/pick', token: server.accessToken, fields, attaches })
+    })
+
+    it('Should fail with a big file', async function () {
+      const fields = {}
+      const attaches = {
+        'avatarfile': join(__dirname, '..', '..', 'fixtures', 'avatar-big.png')
+      }
+      await makeUploadRequest({ url: server.url, path: path + '/avatar/pick', token: server.accessToken, fields, attaches })
+    })
+
+    it('Should fail with an unauthenticated user', async function () {
+      const fields = {}
+      const attaches = {
+        'avatarfile': join(__dirname, '..', '..', 'fixtures', 'avatar.png')
+      }
+      await makeUploadRequest({
+        url: server.url,
+        path: path + '/avatar/pick',
+        fields,
+        attaches,
+        statusCodeExpected: 401
+      })
+    })
+
+    it('Should succeed with the correct params', async function () {
+      const fields = {}
+      const attaches = {
+        'avatarfile': join(__dirname, '..', '..', 'fixtures', 'avatar.png')
+      }
+      await makeUploadRequest({
+        url: server.url,
+        path: path + '/avatar/pick',
+        token: server.accessToken,
+        fields,
+        attaches,
+        statusCodeExpected: 200
+      })
+    })
+  })
+
   describe('When getting a video channel', function () {
     it('Should return the list of the video channels with nothing', async function () {
       const res = await makeGetRequest({
index ad543e2d6c31df9c908fd69732fc31b890559e94..e4e3ce9d9e78a2498b8d197e81eb4118e5fa6fd1 100644 (file)
@@ -3,7 +3,14 @@
 import * as chai from 'chai'
 import 'mocha'
 import { User, Video } from '../../../../shared/index'
-import { doubleFollow, flushAndRunMultipleServers, getVideoChannelVideos, updateVideo, uploadVideo } from '../../utils'
+import {
+  doubleFollow,
+  flushAndRunMultipleServers,
+  getVideoChannelVideos, testImage,
+  updateVideo,
+  updateVideoChannelAvatar,
+  uploadVideo, wait
+} from '../../utils'
 import {
   addVideoChannel,
   deleteVideoChannel,
@@ -159,6 +166,31 @@ describe('Test video channels', function () {
     }
   })
 
+  it('Should update video channel avatar', async function () {
+    this.timeout(5000)
+
+    const fixture = 'avatar.png'
+
+    await updateVideoChannelAvatar({
+      url: servers[0].url,
+      accessToken: servers[0].accessToken,
+      videoChannelId: secondVideoChannelId,
+      fixture
+    })
+
+    await waitJobs(servers)
+  })
+
+  it('Should have video channel avatar updated', async function () {
+    for (const server of servers) {
+      const res = await getVideoChannelsList(server.url, 0, 1, '-name')
+
+      const videoChannel = res.body.data.find(c => c.id === secondVideoChannelId)
+
+      await testImage(server.url, 'avatar-resized', videoChannel.avatar.path, '.png')
+    }
+  })
+
   it('Should get video channel', async function () {
     const res = await getVideoChannel(servers[0].url, secondVideoChannelId)
 
index b6195089dd169e95711b4e46474fc2e473a7865e..ebde692cd44ea6ef25a3373106dbd15664406baf 100644 (file)
@@ -1,5 +1,6 @@
 import * as request from 'supertest'
 import { buildAbsoluteFixturePath } from '../miscs/miscs'
+import { isAbsolute, join } from 'path'
 
 function makeGetRequest (options: {
   url: string,
@@ -45,7 +46,7 @@ function makeUploadRequest (options: {
   url: string,
   method?: 'POST' | 'PUT',
   path: string,
-  token: string,
+  token?: string,
   fields: { [ fieldName: string ]: any },
   attaches: { [ attachName: string ]: any },
   statusCodeExpected?: number
@@ -122,6 +123,29 @@ function makePutBodyRequest (options: {
             .expect(options.statusCodeExpected)
 }
 
+function updateAvatarRequest (options: {
+  url: string,
+  path: string,
+  accessToken: string,
+  fixture: string
+}) {
+  let filePath = ''
+  if (isAbsolute(options.fixture)) {
+    filePath = options.fixture
+  } else {
+    filePath = join(__dirname, '..', '..', 'fixtures', options.fixture)
+  }
+
+  return makeUploadRequest({
+    url: options.url,
+    path: options.path,
+    token: options.accessToken,
+    fields: {},
+    attaches: { avatarfile: filePath },
+    statusCodeExpected: 200
+  })
+}
+
 // ---------------------------------------------------------------------------
 
 export {
@@ -129,5 +153,6 @@ export {
   makeUploadRequest,
   makePostBodyRequest,
   makePutBodyRequest,
-  makeDeleteRequest
+  makeDeleteRequest,
+  updateAvatarRequest
 }
index 34d50f7adefc40a2959f05aa34708dc3b01d21f4..37b15f64a4e39753c2c7794b803d66158f60b618 100644 (file)
@@ -1,6 +1,5 @@
-import { isAbsolute, join } from 'path'
 import * as request from 'supertest'
-import { makePostBodyRequest, makeUploadRequest, makePutBodyRequest } from '../'
+import { makePostBodyRequest, makePutBodyRequest, updateAvatarRequest } from '../'
 
 import { UserRole } from '../../../../shared/index'
 import { NSFWPolicyType } from '../../../../shared/models/videos/nsfw-policy.type'
@@ -160,21 +159,8 @@ function updateMyAvatar (options: {
   fixture: string
 }) {
   const path = '/api/v1/users/me/avatar/pick'
-  let filePath = ''
-  if (isAbsolute(options.fixture)) {
-    filePath = options.fixture
-  } else {
-    filePath = join(__dirname, '..', '..', 'fixtures', options.fixture)
-  }
 
-  return makeUploadRequest({
-    url: options.url,
-    path,
-    token: options.accessToken,
-    fields: {},
-    attaches: { avatarfile: filePath },
-    statusCodeExpected: 200
-  })
+  return updateAvatarRequest(Object.assign(options, { path }))
 }
 
 function updateUser (options: {
index a064598f47a235e890dbf3fdbe314641e47402b6..3ca39469c5b7b88e2db27ae1e051d917497efa7d 100644 (file)
@@ -1,5 +1,6 @@
 import * as request from 'supertest'
 import { VideoChannelCreate, VideoChannelUpdate } from '../../../../shared/models/videos'
+import { updateAvatarRequest } from '../index'
 
 function getVideoChannelsList (url: string, start: number, count: number, sort?: string) {
   const path = '/api/v1/video-channels'
@@ -92,9 +93,22 @@ function getVideoChannel (url: string, channelId: number | string) {
     .expect('Content-Type', /json/)
 }
 
+function updateVideoChannelAvatar (options: {
+  url: string,
+  accessToken: string,
+  fixture: string,
+  videoChannelId: string | number
+}) {
+
+  const path = '/api/v1/video-channels/' + options.videoChannelId + '/avatar/pick'
+
+  return updateAvatarRequest(Object.assign(options, { path }))
+}
+
 // ---------------------------------------------------------------------------
 
 export {
+  updateVideoChannelAvatar,
   getVideoChannelsList,
   getAccountVideoChannelsList,
   addVideoChannel,