From e590b4a512617bbf63595b684386f68abea7d8b8 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 28 May 2019 10:46:32 +0200 Subject: [PATCH] Add ability to specify channel on registration --- server/controllers/api/users/index.ts | 5 ++-- server/initializers/installer.ts | 2 +- server/lib/user.ts | 34 +++++++++++++--------- server/middlewares/validators/users.ts | 22 ++++++++++++++ server/tests/api/check-params/users.ts | 28 ++++++++++++++++-- server/tests/api/users/users.ts | 14 +++++++-- shared/extra-utils/users/users.ts | 27 +++++++++++++++-- shared/models/users/user-register.model.ts | 10 +++++++ 8 files changed, 120 insertions(+), 22 deletions(-) create mode 100644 shared/models/users/user-register.model.ts diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts index 0aafba66e..a04f77841 100644 --- a/server/controllers/api/users/index.ts +++ b/server/controllers/api/users/index.ts @@ -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) diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts index 33970f0fa..e14554ede 100644 --- a/server/initializers/installer.ts +++ b/server/initializers/installer.ts @@ -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) } diff --git a/server/lib/user.ts b/server/lib/user.ts index 7badb3e72..d9fd89e15 100644 --- a/server/lib/user.ts +++ b/server/lib/user.ts @@ -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 + } +} diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index 6d8cd7894..b58dcc0d6 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts @@ -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() } ] diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts index 5935104a5..d26032ea5 100644 --- a/server/tests/api/check-params/users.ts +++ b/server/tests/api/check-params/users.ts @@ -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 }) }) diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts index c1a24b838..9d2ef786f 100644 --- a/server/tests/api/users/users.ts +++ b/server/tests/api/users/users.ts @@ -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) diff --git a/shared/extra-utils/users/users.ts b/shared/extra-utils/users/users.ts index 2bd37b8be..c00da19e0 100644 --- a/shared/extra-utils/users/users.ts +++ b/shared/extra-utils/users/users.ts @@ -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 index 000000000..ce5c9c3d2 --- /dev/null +++ b/shared/models/users/user-register.model.ts @@ -0,0 +1,10 @@ +export interface UserRegister { + username: string + password: string + email: string + + channel?: { + name: string + displayName: string + } +}