diff options
-rw-r--r-- | server/controllers/api/users/index.ts | 5 | ||||
-rw-r--r-- | server/initializers/installer.ts | 2 | ||||
-rw-r--r-- | server/lib/user.ts | 34 | ||||
-rw-r--r-- | server/middlewares/validators/users.ts | 22 | ||||
-rw-r--r-- | server/tests/api/check-params/users.ts | 28 | ||||
-rw-r--r-- | server/tests/api/users/users.ts | 14 | ||||
-rw-r--r-- | shared/extra-utils/users/users.ts | 27 | ||||
-rw-r--r-- | shared/models/users/user-register.model.ts | 10 |
8 files changed, 120 insertions, 22 deletions
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' | |||
46 | import { CONFIG } from '../../../initializers/config' | 46 | import { CONFIG } from '../../../initializers/config' |
47 | import { sequelizeTypescript } from '../../../initializers/database' | 47 | import { sequelizeTypescript } from '../../../initializers/database' |
48 | import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' | 48 | import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' |
49 | import { UserRegister } from '../../../../shared/models/users/user-register.model' | ||
49 | 50 | ||
50 | const auditLogger = auditLoggerFactory('users') | 51 | const auditLogger = auditLoggerFactory('users') |
51 | 52 | ||
@@ -197,7 +198,7 @@ async function createUser (req: express.Request, res: express.Response) { | |||
197 | } | 198 | } |
198 | 199 | ||
199 | async function registerUser (req: express.Request, res: express.Response) { | 200 | async function registerUser (req: express.Request, res: express.Response) { |
200 | const body: UserCreate = req.body | 201 | const body: UserRegister = req.body |
201 | 202 | ||
202 | const userToCreate = new UserModel({ | 203 | const userToCreate = new UserModel({ |
203 | username: body.username, | 204 | username: body.username, |
@@ -211,7 +212,7 @@ async function registerUser (req: express.Request, res: express.Response) { | |||
211 | emailVerified: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION ? false : null | 212 | emailVerified: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION ? false : null |
212 | }) | 213 | }) |
213 | 214 | ||
214 | const { user } = await createUserAccountAndChannelAndPlaylist(userToCreate) | 215 | const { user } = await createUserAccountAndChannelAndPlaylist(userToCreate, body.channel) |
215 | 216 | ||
216 | auditLogger.create(body.username, new UserAuditView(user.toFormattedJSON())) | 217 | auditLogger.create(body.username, new UserAuditView(user.toFormattedJSON())) |
217 | logger.info('User %s with its channel and account registered.', body.username) | 218 | 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 () { | |||
146 | } | 146 | } |
147 | const user = new UserModel(userData) | 147 | const user = new UserModel(userData) |
148 | 148 | ||
149 | await createUserAccountAndChannelAndPlaylist(user, validatePassword) | 149 | await createUserAccountAndChannelAndPlaylist(user, undefined, validatePassword) |
150 | logger.info('Username: ' + username) | 150 | logger.info('Username: ' + username) |
151 | logger.info('User password: ' + password) | 151 | logger.info('User password: ' + password) |
152 | } | 152 | } |
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 | |||
13 | import { createWatchLaterPlaylist } from './video-playlist' | 13 | import { createWatchLaterPlaylist } from './video-playlist' |
14 | import { sequelizeTypescript } from '../initializers/database' | 14 | import { sequelizeTypescript } from '../initializers/database' |
15 | 15 | ||
16 | async function createUserAccountAndChannelAndPlaylist (userToCreate: UserModel, validateUser = true) { | 16 | type ChannelNames = { name: string, displayName: string } |
17 | async function createUserAccountAndChannelAndPlaylist (userToCreate: UserModel, channelNames?: ChannelNames, validateUser = true) { | ||
17 | const { user, account, videoChannel } = await sequelizeTypescript.transaction(async t => { | 18 | const { user, account, videoChannel } = await sequelizeTypescript.transaction(async t => { |
18 | const userOptions = { | 19 | const userOptions = { |
19 | transaction: t, | 20 | transaction: t, |
@@ -26,18 +27,8 @@ async function createUserAccountAndChannelAndPlaylist (userToCreate: UserModel, | |||
26 | const accountCreated = await createLocalAccountWithoutKeys(userCreated.username, userCreated.id, null, t) | 27 | const accountCreated = await createLocalAccountWithoutKeys(userCreated.username, userCreated.id, null, t) |
27 | userCreated.Account = accountCreated | 28 | userCreated.Account = accountCreated |
28 | 29 | ||
29 | let channelName = userCreated.username + '_channel' | 30 | const channelAttributes = await buildChannelAttributes(userCreated, channelNames) |
30 | 31 | const videoChannel = await createVideoChannel(channelAttributes, accountCreated, t) | |
31 | // Conflict, generate uuid instead | ||
32 | const actor = await ActorModel.loadLocalByName(channelName) | ||
33 | if (actor) channelName = uuidv4() | ||
34 | |||
35 | const videoChannelDisplayName = `Main ${userCreated.username} channel` | ||
36 | const videoChannelInfo = { | ||
37 | name: channelName, | ||
38 | displayName: videoChannelDisplayName | ||
39 | } | ||
40 | const videoChannel = await createVideoChannel(videoChannelInfo, accountCreated, t) | ||
41 | 32 | ||
42 | const videoPlaylist = await createWatchLaterPlaylist(accountCreated, t) | 33 | const videoPlaylist = await createWatchLaterPlaylist(accountCreated, t) |
43 | 34 | ||
@@ -116,3 +107,20 @@ function createDefaultUserNotificationSettings (user: UserModel, t: Sequelize.Tr | |||
116 | 107 | ||
117 | return UserNotificationSettingModel.create(values, { transaction: t }) | 108 | return UserNotificationSettingModel.create(values, { transaction: t }) |
118 | } | 109 | } |
110 | |||
111 | async function buildChannelAttributes (user: UserModel, channelNames?: ChannelNames) { | ||
112 | if (channelNames) return channelNames | ||
113 | |||
114 | let channelName = user.username + '_channel' | ||
115 | |||
116 | // Conflict, generate uuid instead | ||
117 | const actor = await ActorModel.loadLocalByName(channelName) | ||
118 | if (actor) channelName = uuidv4() | ||
119 | |||
120 | const videoChannelDisplayName = `Main ${user.username} channel` | ||
121 | |||
122 | return { | ||
123 | name: channelName, | ||
124 | displayName: videoChannelDisplayName | ||
125 | } | ||
126 | } | ||
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' | |||
25 | import { UserModel } from '../../models/account/user' | 25 | import { UserModel } from '../../models/account/user' |
26 | import { areValidationErrors } from './utils' | 26 | import { areValidationErrors } from './utils' |
27 | import { ActorModel } from '../../models/activitypub/actor' | 27 | import { ActorModel } from '../../models/activitypub/actor' |
28 | import { isActorPreferredUsernameValid } from '../../helpers/custom-validators/activitypub/actor' | ||
29 | import { isVideoChannelNameValid } from '../../helpers/custom-validators/video-channels' | ||
30 | import { UserCreate } from '../../../shared/models/users' | ||
31 | import { UserRegister } from '../../../shared/models/users/user-register.model' | ||
28 | 32 | ||
29 | const usersAddValidator = [ | 33 | const usersAddValidator = [ |
30 | body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'), | 34 | body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'), |
@@ -49,6 +53,8 @@ const usersRegisterValidator = [ | |||
49 | body('username').custom(isUserUsernameValid).withMessage('Should have a valid username'), | 53 | body('username').custom(isUserUsernameValid).withMessage('Should have a valid username'), |
50 | body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'), | 54 | body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'), |
51 | body('email').isEmail().withMessage('Should have a valid email'), | 55 | body('email').isEmail().withMessage('Should have a valid email'), |
56 | body('channel.name').optional().custom(isActorPreferredUsernameValid).withMessage('Should have a valid channel name'), | ||
57 | body('channel.displayName').optional().custom(isVideoChannelNameValid).withMessage('Should have a valid display name'), | ||
52 | 58 | ||
53 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 59 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
54 | logger.debug('Checking usersRegister parameters', { parameters: omit(req.body, 'password') }) | 60 | logger.debug('Checking usersRegister parameters', { parameters: omit(req.body, 'password') }) |
@@ -56,6 +62,22 @@ const usersRegisterValidator = [ | |||
56 | if (areValidationErrors(req, res)) return | 62 | if (areValidationErrors(req, res)) return |
57 | if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return | 63 | if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return |
58 | 64 | ||
65 | const body: UserRegister = req.body | ||
66 | if (body.channel) { | ||
67 | if (!body.channel.name || !body.channel.displayName) { | ||
68 | return res.status(400) | ||
69 | .send({ error: 'Channel is optional but if you specify it, channel.name and channel.displayName are required.' }) | ||
70 | .end() | ||
71 | } | ||
72 | |||
73 | const existing = await ActorModel.loadLocalByName(body.channel.name) | ||
74 | if (existing) { | ||
75 | return res.status(409) | ||
76 | .send({ error: `Channel with name ${body.channel.name} already exists.` }) | ||
77 | .end() | ||
78 | } | ||
79 | } | ||
80 | |||
59 | return next() | 81 | return next() |
60 | } | 82 | } |
61 | ] | 83 | ] |
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' | |||
6 | import { UserRole, VideoImport, VideoImportState } from '../../../../shared' | 6 | import { UserRole, VideoImport, VideoImportState } from '../../../../shared' |
7 | 7 | ||
8 | import { | 8 | import { |
9 | addVideoChannel, | ||
9 | blockUser, | 10 | blockUser, |
10 | cleanupTests, | 11 | cleanupTests, |
11 | createUser, | 12 | createUser, |
@@ -638,7 +639,7 @@ describe('Test users API validators', function () { | |||
638 | }) | 639 | }) |
639 | }) | 640 | }) |
640 | 641 | ||
641 | describe('When register a new user', function () { | 642 | describe('When registering a new user', function () { |
642 | const registrationPath = path + '/register' | 643 | const registrationPath = path + '/register' |
643 | const baseCorrectParams = { | 644 | const baseCorrectParams = { |
644 | username: 'user3', | 645 | username: 'user3', |
@@ -724,12 +725,35 @@ describe('Test users API validators', function () { | |||
724 | }) | 725 | }) |
725 | }) | 726 | }) |
726 | 727 | ||
728 | it('Should fail with a bad channel name', async function () { | ||
729 | const fields = immutableAssign(baseCorrectParams, { channel: { name: '[]azf', displayName: 'toto' } }) | ||
730 | |||
731 | await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields }) | ||
732 | }) | ||
733 | |||
734 | it('Should fail with a bad channel display name', async function () { | ||
735 | const fields = immutableAssign(baseCorrectParams, { channel: { name: 'toto', displayName: '' } }) | ||
736 | |||
737 | await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields }) | ||
738 | }) | ||
739 | |||
740 | it('Should fail with an existing channel', async function () { | ||
741 | const videoChannelAttributesArg = { name: 'existing_channel', displayName: 'hello', description: 'super description' } | ||
742 | await addVideoChannel(server.url, server.accessToken, videoChannelAttributesArg) | ||
743 | |||
744 | const fields = immutableAssign(baseCorrectParams, { channel: { name: 'existing_channel', displayName: 'toto' } }) | ||
745 | |||
746 | await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields, statusCodeExpected: 409 }) | ||
747 | }) | ||
748 | |||
727 | it('Should succeed with the correct params', async function () { | 749 | it('Should succeed with the correct params', async function () { |
750 | const fields = immutableAssign(baseCorrectParams, { channel: { name: 'super_channel', displayName: 'toto' } }) | ||
751 | |||
728 | await makePostBodyRequest({ | 752 | await makePostBodyRequest({ |
729 | url: server.url, | 753 | url: server.url, |
730 | path: registrationPath, | 754 | path: registrationPath, |
731 | token: server.accessToken, | 755 | token: server.accessToken, |
732 | fields: baseCorrectParams, | 756 | fields: fields, |
733 | statusCodeExpected: 204 | 757 | statusCodeExpected: 204 |
734 | }) | 758 | }) |
735 | }) | 759 | }) |
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 { | |||
31 | updateMyUser, | 31 | updateMyUser, |
32 | updateUser, | 32 | updateUser, |
33 | uploadVideo, | 33 | uploadVideo, |
34 | userLogin | 34 | userLogin, |
35 | registerUserWithChannel, getVideoChannel | ||
35 | } from '../../../../shared/extra-utils' | 36 | } from '../../../../shared/extra-utils' |
36 | import { follow } from '../../../../shared/extra-utils/server/follows' | 37 | import { follow } from '../../../../shared/extra-utils/server/follows' |
37 | import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' | 38 | import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' |
@@ -617,7 +618,10 @@ describe('Test users', function () { | |||
617 | 618 | ||
618 | describe('Registering a new user', function () { | 619 | describe('Registering a new user', function () { |
619 | it('Should register a new user', async function () { | 620 | it('Should register a new user', async function () { |
620 | await registerUser(server.url, 'user_15', 'my super password') | 621 | const user = { username: 'user_15', password: 'my super password' } |
622 | const channel = { name: 'my_user_15_channel', displayName: 'my channel rocks' } | ||
623 | |||
624 | await registerUserWithChannel({ url: server.url, user, channel }) | ||
621 | }) | 625 | }) |
622 | 626 | ||
623 | it('Should be able to login with this registered user', async function () { | 627 | it('Should be able to login with this registered user', async function () { |
@@ -636,6 +640,12 @@ describe('Test users', function () { | |||
636 | expect(user.videoQuota).to.equal(5 * 1024 * 1024) | 640 | expect(user.videoQuota).to.equal(5 * 1024 * 1024) |
637 | }) | 641 | }) |
638 | 642 | ||
643 | it('Should have created the channel', async function () { | ||
644 | const res = await getVideoChannel(server.url, 'my_user_15_channel') | ||
645 | |||
646 | expect(res.body.displayName).to.equal('my channel rocks') | ||
647 | }) | ||
648 | |||
639 | it('Should remove me', async function () { | 649 | it('Should remove me', async function () { |
640 | { | 650 | { |
641 | const res = await getUsersList(server.url, server.accessToken) | 651 | 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 @@ | |||
1 | import * as request from 'supertest' | 1 | import * as request from 'supertest' |
2 | import { makePostBodyRequest, makePutBodyRequest, updateAvatarRequest } from '../requests/requests' | 2 | import { makeGetRequest, makePostBodyRequest, makePutBodyRequest, updateAvatarRequest } from '../requests/requests' |
3 | 3 | ||
4 | import { UserRole } from '../../index' | 4 | import { UserCreate, UserRole } from '../../index' |
5 | import { NSFWPolicyType } from '../../models/videos/nsfw-policy.type' | 5 | import { NSFWPolicyType } from '../../models/videos/nsfw-policy.type' |
6 | import { ServerInfo, userLogin } from '..' | 6 | import { ServerInfo, userLogin } from '..' |
7 | import { UserAdminFlag } from '../../models/users/user-flag.model' | 7 | import { UserAdminFlag } from '../../models/users/user-flag.model' |
8 | import { UserRegister } from '../../models/users/user-register.model' | ||
8 | 9 | ||
9 | type CreateUserArgs = { url: string, | 10 | type CreateUserArgs = { url: string, |
10 | accessToken: string, | 11 | accessToken: string, |
@@ -70,6 +71,27 @@ function registerUser (url: string, username: string, password: string, specialS | |||
70 | .expect(specialStatus) | 71 | .expect(specialStatus) |
71 | } | 72 | } |
72 | 73 | ||
74 | function registerUserWithChannel (options: { | ||
75 | url: string, | ||
76 | user: { username: string, password: string }, | ||
77 | channel: { name: string, displayName: string } | ||
78 | }) { | ||
79 | const path = '/api/v1/users/register' | ||
80 | const body: UserRegister = { | ||
81 | username: options.user.username, | ||
82 | password: options.user.password, | ||
83 | email: options.user.username + '@example.com', | ||
84 | channel: options.channel | ||
85 | } | ||
86 | |||
87 | return makePostBodyRequest({ | ||
88 | url: options.url, | ||
89 | path, | ||
90 | fields: body, | ||
91 | statusCodeExpected: 204 | ||
92 | }) | ||
93 | } | ||
94 | |||
73 | function getMyUserInformation (url: string, accessToken: string, specialStatus = 200) { | 95 | function getMyUserInformation (url: string, accessToken: string, specialStatus = 200) { |
74 | const path = '/api/v1/users/me' | 96 | const path = '/api/v1/users/me' |
75 | 97 | ||
@@ -312,6 +334,7 @@ export { | |||
312 | getMyUserInformation, | 334 | getMyUserInformation, |
313 | getMyUserVideoRating, | 335 | getMyUserVideoRating, |
314 | deleteMe, | 336 | deleteMe, |
337 | registerUserWithChannel, | ||
315 | getMyUserVideoQuotaUsed, | 338 | getMyUserVideoQuotaUsed, |
316 | getUsersList, | 339 | getUsersList, |
317 | getUsersListPaginationAndSort, | 340 | 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 @@ | |||
1 | export interface UserRegister { | ||
2 | username: string | ||
3 | password: string | ||
4 | email: string | ||
5 | |||
6 | channel?: { | ||
7 | name: string | ||
8 | displayName: string | ||
9 | } | ||
10 | } | ||