diff --git a/server/controllers/api/users.ts b/server/controllers/api/users.ts index 891056912..c80f27a23 100644 --- a/server/controllers/api/users.ts +++ b/server/controllers/api/users.ts @@ -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 account = res.locals.oauth.token.user.Account - 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 }) - - await sendUpdateActor(user.Account, t) - - return updatedActor.Avatar - }) + const avatar = await updateActorAvatarFile(avatarPhysicalFile, account.Actor, account) return res .json({ diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts index 61e72125f..1707732ee 100644 --- a/server/controllers/api/video-channel.ts +++ b/server/controllers/api/video-channel.ts @@ -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 index 000000000..7fdef008c --- /dev/null +++ b/server/lib/avatar.ts @@ -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 index 000000000..f346ea92f --- /dev/null +++ b/server/middlewares/validators/avatar.ts @@ -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 +} diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index 55a08a648..8ca9763a1 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts @@ -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 } diff --git a/server/middlewares/validators/video-channels.ts b/server/middlewares/validators/video-channels.ts index a5be5f114..7f65f7290 100644 --- a/server/middlewares/validators/video-channels.ts +++ b/server/middlewares/validators/video-channels.ts @@ -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'), diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts index 28537315e..e1954c64f 100644 --- a/server/tests/api/check-params/users.ts +++ b/server/tests/api/check-params/users.ts @@ -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 = { diff --git a/server/tests/api/check-params/video-channels.ts b/server/tests/api/check-params/video-channels.ts index 5080af2c9..7b05e5882 100644 --- a/server/tests/api/check-params/video-channels.ts +++ b/server/tests/api/check-params/video-channels.ts @@ -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({ diff --git a/server/tests/api/videos/video-channels.ts b/server/tests/api/videos/video-channels.ts index ad543e2d6..e4e3ce9d9 100644 --- a/server/tests/api/videos/video-channels.ts +++ b/server/tests/api/videos/video-channels.ts @@ -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) diff --git a/server/tests/utils/requests/requests.ts b/server/tests/utils/requests/requests.ts index b6195089d..ebde692cd 100644 --- a/server/tests/utils/requests/requests.ts +++ b/server/tests/utils/requests/requests.ts @@ -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 } diff --git a/server/tests/utils/users/users.ts b/server/tests/utils/users/users.ts index 34d50f7ad..37b15f64a 100644 --- a/server/tests/utils/users/users.ts +++ b/server/tests/utils/users/users.ts @@ -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: { diff --git a/server/tests/utils/videos/video-channels.ts b/server/tests/utils/videos/video-channels.ts index a064598f4..3ca39469c 100644 --- a/server/tests/utils/videos/video-channels.ts +++ b/server/tests/utils/videos/video-channels.ts @@ -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,