Add ability to specify channel on registration
authorChocobozzz <me@florianbigard.com>
Tue, 28 May 2019 08:46:32 +0000 (10:46 +0200)
committerChocobozzz <me@florianbigard.com>
Tue, 28 May 2019 08:46:32 +0000 (10:46 +0200)
server/controllers/api/users/index.ts
server/initializers/installer.ts
server/lib/user.ts
server/middlewares/validators/users.ts
server/tests/api/check-params/users.ts
server/tests/api/users/users.ts
shared/extra-utils/users/users.ts
shared/models/users/user-register.model.ts [new file with mode: 0644]

index 0aafba66e6f0f3f6f9fac87e28f4bb1ea5b6d32e..a04f778417742f9402bc9beffcbfd9cb1d4fcbd2 100644 (file)
@@ -46,6 +46,7 @@ import { mySubscriptionsRouter } from './my-subscriptions'
 import { CONFIG } from '../../../initializers/config'
 import { sequelizeTypescript } from '../../../initializers/database'
 import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
+import { UserRegister } from '../../../../shared/models/users/user-register.model'
 
 const auditLogger = auditLoggerFactory('users')
 
@@ -197,7 +198,7 @@ async function createUser (req: express.Request, res: express.Response) {
 }
 
 async function registerUser (req: express.Request, res: express.Response) {
-  const body: UserCreate = req.body
+  const body: UserRegister = req.body
 
   const userToCreate = new UserModel({
     username: body.username,
@@ -211,7 +212,7 @@ async function registerUser (req: express.Request, res: express.Response) {
     emailVerified: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION ? false : null
   })
 
-  const { user } = await createUserAccountAndChannelAndPlaylist(userToCreate)
+  const { user } = await createUserAccountAndChannelAndPlaylist(userToCreate, body.channel)
 
   auditLogger.create(body.username, new UserAuditView(user.toFormattedJSON()))
   logger.info('User %s with its channel and account registered.', body.username)
index 33970f0fae9195b4cc46c2e4107e93eb8956db7e..e14554ede5191c0df5376b2b2a67dcfbac0b83c0 100644 (file)
@@ -146,7 +146,7 @@ async function createOAuthAdminIfNotExist () {
   }
   const user = new UserModel(userData)
 
-  await createUserAccountAndChannelAndPlaylist(user, validatePassword)
+  await createUserAccountAndChannelAndPlaylist(user, undefined, validatePassword)
   logger.info('Username: ' + username)
   logger.info('User password: ' + password)
 }
index 7badb3e72e41c2d4dadc0cfdbd0a674428c502e0..d9fd89e1594f77f70959d060e8c19f081330cd9b 100644 (file)
@@ -13,7 +13,8 @@ import { UserNotificationSetting, UserNotificationSettingValue } from '../../sha
 import { createWatchLaterPlaylist } from './video-playlist'
 import { sequelizeTypescript } from '../initializers/database'
 
-async function createUserAccountAndChannelAndPlaylist (userToCreate: UserModel, validateUser = true) {
+type ChannelNames = { name: string, displayName: string }
+async function createUserAccountAndChannelAndPlaylist (userToCreate: UserModel, channelNames?: ChannelNames, validateUser = true) {
   const { user, account, videoChannel } = await sequelizeTypescript.transaction(async t => {
     const userOptions = {
       transaction: t,
@@ -26,18 +27,8 @@ async function createUserAccountAndChannelAndPlaylist (userToCreate: UserModel,
     const accountCreated = await createLocalAccountWithoutKeys(userCreated.username, userCreated.id, null, t)
     userCreated.Account = accountCreated
 
-    let channelName = userCreated.username + '_channel'
-
-    // Conflict, generate uuid instead
-    const actor = await ActorModel.loadLocalByName(channelName)
-    if (actor) channelName = uuidv4()
-
-    const videoChannelDisplayName = `Main ${userCreated.username} channel`
-    const videoChannelInfo = {
-      name: channelName,
-      displayName: videoChannelDisplayName
-    }
-    const videoChannel = await createVideoChannel(videoChannelInfo, accountCreated, t)
+    const channelAttributes = await buildChannelAttributes(userCreated, channelNames)
+    const videoChannel = await createVideoChannel(channelAttributes, accountCreated, t)
 
     const videoPlaylist = await createWatchLaterPlaylist(accountCreated, t)
 
@@ -116,3 +107,20 @@ function createDefaultUserNotificationSettings (user: UserModel, t: Sequelize.Tr
 
   return UserNotificationSettingModel.create(values, { transaction: t })
 }
+
+async function buildChannelAttributes (user: UserModel, channelNames?: ChannelNames) {
+  if (channelNames) return channelNames
+
+  let channelName = user.username + '_channel'
+
+  // Conflict, generate uuid instead
+  const actor = await ActorModel.loadLocalByName(channelName)
+  if (actor) channelName = uuidv4()
+
+  const videoChannelDisplayName = `Main ${user.username} channel`
+
+  return {
+    name: channelName,
+    displayName: videoChannelDisplayName
+  }
+}
index 6d8cd7894b39358b3fa94fe67f4f5ea947c0db6b..b58dcc0d6adacc6b64c91b28c4ab529ea7f89761 100644 (file)
@@ -25,6 +25,10 @@ import { Redis } from '../../lib/redis'
 import { UserModel } from '../../models/account/user'
 import { areValidationErrors } from './utils'
 import { ActorModel } from '../../models/activitypub/actor'
+import { isActorPreferredUsernameValid } from '../../helpers/custom-validators/activitypub/actor'
+import { isVideoChannelNameValid } from '../../helpers/custom-validators/video-channels'
+import { UserCreate } from '../../../shared/models/users'
+import { UserRegister } from '../../../shared/models/users/user-register.model'
 
 const usersAddValidator = [
   body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'),
@@ -49,6 +53,8 @@ const usersRegisterValidator = [
   body('username').custom(isUserUsernameValid).withMessage('Should have a valid username'),
   body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'),
   body('email').isEmail().withMessage('Should have a valid email'),
+  body('channel.name').optional().custom(isActorPreferredUsernameValid).withMessage('Should have a valid channel name'),
+  body('channel.displayName').optional().custom(isVideoChannelNameValid).withMessage('Should have a valid display name'),
 
   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
     logger.debug('Checking usersRegister parameters', { parameters: omit(req.body, 'password') })
@@ -56,6 +62,22 @@ const usersRegisterValidator = [
     if (areValidationErrors(req, res)) return
     if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
 
+    const body: UserRegister = req.body
+    if (body.channel) {
+      if (!body.channel.name || !body.channel.displayName) {
+        return res.status(400)
+          .send({ error: 'Channel is optional but if you specify it, channel.name and channel.displayName are required.' })
+          .end()
+      }
+
+      const existing = await ActorModel.loadLocalByName(body.channel.name)
+      if (existing) {
+        return res.status(409)
+                  .send({ error: `Channel with name ${body.channel.name} already exists.` })
+                  .end()
+      }
+    }
+
     return next()
   }
 ]
index 5935104a58cfb8898c57158c610fde4c559ff56c..d26032ea5139cf5472ed08a1b205d0e41aa95fa3 100644 (file)
@@ -6,6 +6,7 @@ import { join } from 'path'
 import { UserRole, VideoImport, VideoImportState } from '../../../../shared'
 
 import {
+  addVideoChannel,
   blockUser,
   cleanupTests,
   createUser,
@@ -638,7 +639,7 @@ describe('Test users API validators', function () {
     })
   })
 
-  describe('When register a new user', function () {
+  describe('When registering a new user', function () {
     const registrationPath = path + '/register'
     const baseCorrectParams = {
       username: 'user3',
@@ -724,12 +725,35 @@ describe('Test users API validators', function () {
       })
     })
 
+    it('Should fail with a bad channel name', async function () {
+      const fields = immutableAssign(baseCorrectParams, { channel: { name: '[]azf', displayName: 'toto' } })
+
+      await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields })
+    })
+
+    it('Should fail with a bad channel display name', async function () {
+      const fields = immutableAssign(baseCorrectParams, { channel: { name: 'toto', displayName: '' } })
+
+      await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields })
+    })
+
+    it('Should fail with an existing channel', async function () {
+      const videoChannelAttributesArg = { name: 'existing_channel', displayName: 'hello', description: 'super description' }
+      await addVideoChannel(server.url, server.accessToken, videoChannelAttributesArg)
+
+      const fields = immutableAssign(baseCorrectParams, { channel: { name: 'existing_channel', displayName: 'toto' } })
+
+      await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields, statusCodeExpected: 409 })
+    })
+
     it('Should succeed with the correct params', async function () {
+      const fields = immutableAssign(baseCorrectParams, { channel: { name: 'super_channel', displayName: 'toto' } })
+
       await makePostBodyRequest({
         url: server.url,
         path: registrationPath,
         token: server.accessToken,
-        fields: baseCorrectParams,
+        fields: fields,
         statusCodeExpected: 204
       })
     })
index c1a24b8387598d9276a3d74649eb6c73d9165a52..9d2ef786f1da14303ce6f43fd6ca3146f81053d3 100644 (file)
@@ -31,7 +31,8 @@ import {
   updateMyUser,
   updateUser,
   uploadVideo,
-  userLogin
+  userLogin,
+  registerUserWithChannel, getVideoChannel
 } from '../../../../shared/extra-utils'
 import { follow } from '../../../../shared/extra-utils/server/follows'
 import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
@@ -617,7 +618,10 @@ describe('Test users', function () {
 
   describe('Registering a new user', function () {
     it('Should register a new user', async function () {
-      await registerUser(server.url, 'user_15', 'my super password')
+      const user = { username: 'user_15', password: 'my super password' }
+      const channel = { name: 'my_user_15_channel', displayName: 'my channel rocks' }
+
+      await registerUserWithChannel({ url: server.url, user, channel })
     })
 
     it('Should be able to login with this registered user', async function () {
@@ -636,6 +640,12 @@ describe('Test users', function () {
       expect(user.videoQuota).to.equal(5 * 1024 * 1024)
     })
 
+    it('Should have created the channel', async function () {
+      const res = await getVideoChannel(server.url, 'my_user_15_channel')
+
+      expect(res.body.displayName).to.equal('my channel rocks')
+    })
+
     it('Should remove me', async function () {
       {
         const res = await getUsersList(server.url, server.accessToken)
index 2bd37b8be26300506764c37e0c15c3c2b3466709..c00da19e01f17a8720f7ada4d38994e972b72dbf 100644 (file)
@@ -1,10 +1,11 @@
 import * as request from 'supertest'
-import { makePostBodyRequest, makePutBodyRequest, updateAvatarRequest } from '../requests/requests'
+import { makeGetRequest, makePostBodyRequest, makePutBodyRequest, updateAvatarRequest } from '../requests/requests'
 
-import { UserRole } from '../../index'
+import { UserCreate, UserRole } from '../../index'
 import { NSFWPolicyType } from '../../models/videos/nsfw-policy.type'
 import { ServerInfo, userLogin } from '..'
 import { UserAdminFlag } from '../../models/users/user-flag.model'
+import { UserRegister } from '../../models/users/user-register.model'
 
 type CreateUserArgs = { url: string,
   accessToken: string,
@@ -70,6 +71,27 @@ function registerUser (url: string, username: string, password: string, specialS
           .expect(specialStatus)
 }
 
+function registerUserWithChannel (options: {
+  url: string,
+  user: { username: string, password: string },
+  channel: { name: string, displayName: string }
+}) {
+  const path = '/api/v1/users/register'
+  const body: UserRegister = {
+    username: options.user.username,
+    password: options.user.password,
+    email: options.user.username + '@example.com',
+    channel: options.channel
+  }
+
+  return makePostBodyRequest({
+    url: options.url,
+    path,
+    fields: body,
+    statusCodeExpected: 204
+  })
+}
+
 function getMyUserInformation (url: string, accessToken: string, specialStatus = 200) {
   const path = '/api/v1/users/me'
 
@@ -312,6 +334,7 @@ export {
   getMyUserInformation,
   getMyUserVideoRating,
   deleteMe,
+  registerUserWithChannel,
   getMyUserVideoQuotaUsed,
   getUsersList,
   getUsersListPaginationAndSort,
diff --git a/shared/models/users/user-register.model.ts b/shared/models/users/user-register.model.ts
new file mode 100644 (file)
index 0000000..ce5c9c3
--- /dev/null
@@ -0,0 +1,10 @@
+export interface UserRegister {
+  username: string
+  password: string
+  email: string
+
+  channel?: {
+    name: string
+    displayName: string
+  }
+}