diff options
author | John Livingston <38844060+JohnXLivingston@users.noreply.github.com> | 2020-02-17 10:16:52 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-02-17 10:16:52 +0100 |
commit | 45f1bd72a08998c60a9dd68ff069cea9de39161c (patch) | |
tree | 79e484bd7fd38fe97c84fdb00a164534f43941e9 | |
parent | c5621bd23bce038671cd81149a0aa5e238558b67 (diff) | |
download | PeerTube-45f1bd72a08998c60a9dd68ff069cea9de39161c.tar.gz PeerTube-45f1bd72a08998c60a9dd68ff069cea9de39161c.tar.zst PeerTube-45f1bd72a08998c60a9dd68ff069cea9de39161c.zip |
Creating a user with an empty password will send an email to let him set his password (#2479)
* Creating a user with an empty password will send an email to let him set his password
* Consideration of Chocobozzz's comments
* Tips for optional password
* API documentation
* Fix circular imports
* Tests
-rw-r--r-- | client/src/app/+admin/users/user-edit/user-create.component.ts | 10 | ||||
-rw-r--r-- | client/src/app/+admin/users/user-edit/user-edit.component.html | 7 | ||||
-rw-r--r-- | client/src/app/+admin/users/user-edit/user-update.component.ts | 4 | ||||
-rw-r--r-- | client/src/app/+admin/users/users.routes.ts | 4 | ||||
-rw-r--r-- | client/src/app/shared/forms/form-validators/user-validators.service.ts | 12 | ||||
-rw-r--r-- | server/controllers/api/users/index.ts | 16 | ||||
-rw-r--r-- | server/helpers/custom-validators/users.ts | 10 | ||||
-rw-r--r-- | server/initializers/constants.ts | 2 | ||||
-rw-r--r-- | server/lib/emailer.ts | 16 | ||||
-rw-r--r-- | server/lib/redis.ts | 9 | ||||
-rw-r--r-- | server/middlewares/validators/users.ts | 3 | ||||
-rw-r--r-- | server/tests/api/check-params/users.ts | 48 | ||||
-rw-r--r-- | server/tests/api/server/email.ts | 76 | ||||
-rw-r--r-- | support/doc/api/openapi.yaml | 2 |
14 files changed, 201 insertions, 18 deletions
diff --git a/client/src/app/+admin/users/user-edit/user-create.component.ts b/client/src/app/+admin/users/user-edit/user-create.component.ts index e726ec4d7..1769c0de0 100644 --- a/client/src/app/+admin/users/user-edit/user-create.component.ts +++ b/client/src/app/+admin/users/user-edit/user-create.component.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { Router } from '@angular/router' | 2 | import { Router, ActivatedRoute } from '@angular/router' |
3 | import { AuthService, Notifier, ServerService } from '@app/core' | 3 | import { AuthService, Notifier, ServerService } from '@app/core' |
4 | import { UserCreate, UserRole } from '../../../../../../shared' | 4 | import { UserCreate, UserRole } from '../../../../../../shared' |
5 | import { UserEdit } from './user-edit' | 5 | import { UserEdit } from './user-edit' |
@@ -23,6 +23,7 @@ export class UserCreateComponent extends UserEdit implements OnInit { | |||
23 | protected configService: ConfigService, | 23 | protected configService: ConfigService, |
24 | protected auth: AuthService, | 24 | protected auth: AuthService, |
25 | private userValidatorsService: UserValidatorsService, | 25 | private userValidatorsService: UserValidatorsService, |
26 | private route: ActivatedRoute, | ||
26 | private router: Router, | 27 | private router: Router, |
27 | private notifier: Notifier, | 28 | private notifier: Notifier, |
28 | private userService: UserService, | 29 | private userService: UserService, |
@@ -45,7 +46,7 @@ export class UserCreateComponent extends UserEdit implements OnInit { | |||
45 | this.buildForm({ | 46 | this.buildForm({ |
46 | username: this.userValidatorsService.USER_USERNAME, | 47 | username: this.userValidatorsService.USER_USERNAME, |
47 | email: this.userValidatorsService.USER_EMAIL, | 48 | email: this.userValidatorsService.USER_EMAIL, |
48 | password: this.userValidatorsService.USER_PASSWORD, | 49 | password: this.isPasswordOptional() ? this.userValidatorsService.USER_PASSWORD_OPTIONAL : this.userValidatorsService.USER_PASSWORD, |
49 | role: this.userValidatorsService.USER_ROLE, | 50 | role: this.userValidatorsService.USER_ROLE, |
50 | videoQuota: this.userValidatorsService.USER_VIDEO_QUOTA, | 51 | videoQuota: this.userValidatorsService.USER_VIDEO_QUOTA, |
51 | videoQuotaDaily: this.userValidatorsService.USER_VIDEO_QUOTA_DAILY, | 52 | videoQuotaDaily: this.userValidatorsService.USER_VIDEO_QUOTA_DAILY, |
@@ -78,6 +79,11 @@ export class UserCreateComponent extends UserEdit implements OnInit { | |||
78 | return true | 79 | return true |
79 | } | 80 | } |
80 | 81 | ||
82 | isPasswordOptional () { | ||
83 | const serverConfig = this.route.snapshot.data.serverConfig | ||
84 | return serverConfig.email.enabled | ||
85 | } | ||
86 | |||
81 | getFormButtonTitle () { | 87 | getFormButtonTitle () { |
82 | return this.i18n('Create user') | 88 | return this.i18n('Create user') |
83 | } | 89 | } |
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.html b/client/src/app/+admin/users/user-edit/user-edit.component.html index 4ff4d0d12..2aca5ddca 100644 --- a/client/src/app/+admin/users/user-edit/user-edit.component.html +++ b/client/src/app/+admin/users/user-edit/user-edit.component.html | |||
@@ -29,6 +29,13 @@ | |||
29 | 29 | ||
30 | <div class="form-group" *ngIf="isCreation()"> | 30 | <div class="form-group" *ngIf="isCreation()"> |
31 | <label i18n for="password">Password</label> | 31 | <label i18n for="password">Password</label> |
32 | <my-help *ngIf="isPasswordOptional()"> | ||
33 | <ng-template ptTemplate="customHtml"> | ||
34 | <ng-container i18n> | ||
35 | If you leave the password empty, an email will be sent to the user. | ||
36 | </ng-container> | ||
37 | </ng-template> | ||
38 | </my-help> | ||
32 | <input | 39 | <input |
33 | type="password" id="password" autocomplete="new-password" | 40 | type="password" id="password" autocomplete="new-password" |
34 | formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }" | 41 | formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }" |
diff --git a/client/src/app/+admin/users/user-edit/user-update.component.ts b/client/src/app/+admin/users/user-edit/user-update.component.ts index d1682a99d..1ab2e9dbf 100644 --- a/client/src/app/+admin/users/user-edit/user-update.component.ts +++ b/client/src/app/+admin/users/user-edit/user-update.component.ts | |||
@@ -92,6 +92,10 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { | |||
92 | return false | 92 | return false |
93 | } | 93 | } |
94 | 94 | ||
95 | isPasswordOptional () { | ||
96 | return false | ||
97 | } | ||
98 | |||
95 | getFormButtonTitle () { | 99 | getFormButtonTitle () { |
96 | return this.i18n('Update user') | 100 | return this.i18n('Update user') |
97 | } | 101 | } |
diff --git a/client/src/app/+admin/users/users.routes.ts b/client/src/app/+admin/users/users.routes.ts index 8b3791bd3..2d4f9305e 100644 --- a/client/src/app/+admin/users/users.routes.ts +++ b/client/src/app/+admin/users/users.routes.ts | |||
@@ -5,6 +5,7 @@ import { UserRight } from '../../../../../shared' | |||
5 | import { UsersComponent } from './users.component' | 5 | import { UsersComponent } from './users.component' |
6 | import { UserCreateComponent, UserUpdateComponent } from './user-edit' | 6 | import { UserCreateComponent, UserUpdateComponent } from './user-edit' |
7 | import { UserListComponent } from './user-list' | 7 | import { UserListComponent } from './user-list' |
8 | import { ServerConfigResolver } from '@app/core/routing/server-config-resolver.service' | ||
8 | 9 | ||
9 | export const UsersRoutes: Routes = [ | 10 | export const UsersRoutes: Routes = [ |
10 | { | 11 | { |
@@ -36,6 +37,9 @@ export const UsersRoutes: Routes = [ | |||
36 | meta: { | 37 | meta: { |
37 | title: 'Create a user' | 38 | title: 'Create a user' |
38 | } | 39 | } |
40 | }, | ||
41 | resolve: { | ||
42 | serverConfig: ServerConfigResolver | ||
39 | } | 43 | } |
40 | }, | 44 | }, |
41 | { | 45 | { |
diff --git a/client/src/app/shared/forms/form-validators/user-validators.service.ts b/client/src/app/shared/forms/form-validators/user-validators.service.ts index 4dff3e422..13b9228d4 100644 --- a/client/src/app/shared/forms/form-validators/user-validators.service.ts +++ b/client/src/app/shared/forms/form-validators/user-validators.service.ts | |||
@@ -8,6 +8,7 @@ export class UserValidatorsService { | |||
8 | readonly USER_USERNAME: BuildFormValidator | 8 | readonly USER_USERNAME: BuildFormValidator |
9 | readonly USER_EMAIL: BuildFormValidator | 9 | readonly USER_EMAIL: BuildFormValidator |
10 | readonly USER_PASSWORD: BuildFormValidator | 10 | readonly USER_PASSWORD: BuildFormValidator |
11 | readonly USER_PASSWORD_OPTIONAL: BuildFormValidator | ||
11 | readonly USER_CONFIRM_PASSWORD: BuildFormValidator | 12 | readonly USER_CONFIRM_PASSWORD: BuildFormValidator |
12 | readonly USER_VIDEO_QUOTA: BuildFormValidator | 13 | readonly USER_VIDEO_QUOTA: BuildFormValidator |
13 | readonly USER_VIDEO_QUOTA_DAILY: BuildFormValidator | 14 | readonly USER_VIDEO_QUOTA_DAILY: BuildFormValidator |
@@ -56,6 +57,17 @@ export class UserValidatorsService { | |||
56 | } | 57 | } |
57 | } | 58 | } |
58 | 59 | ||
60 | this.USER_PASSWORD_OPTIONAL = { | ||
61 | VALIDATORS: [ | ||
62 | Validators.minLength(6), | ||
63 | Validators.maxLength(255) | ||
64 | ], | ||
65 | MESSAGES: { | ||
66 | 'minlength': this.i18n('Password must be at least 6 characters long.'), | ||
67 | 'maxlength': this.i18n('Password cannot be more than 255 characters long.') | ||
68 | } | ||
69 | } | ||
70 | |||
59 | this.USER_CONFIRM_PASSWORD = { | 71 | this.USER_CONFIRM_PASSWORD = { |
60 | VALIDATORS: [], | 72 | VALIDATORS: [], |
61 | MESSAGES: { | 73 | MESSAGES: { |
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts index 0b7012537..98eb2beed 100644 --- a/server/controllers/api/users/index.ts +++ b/server/controllers/api/users/index.ts | |||
@@ -2,7 +2,7 @@ import * as express from 'express' | |||
2 | import * as RateLimit from 'express-rate-limit' | 2 | import * as RateLimit from 'express-rate-limit' |
3 | import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared' | 3 | import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared' |
4 | import { logger } from '../../../helpers/logger' | 4 | import { logger } from '../../../helpers/logger' |
5 | import { getFormattedObjects } from '../../../helpers/utils' | 5 | import { generateRandomString, getFormattedObjects } from '../../../helpers/utils' |
6 | import { WEBSERVER } from '../../../initializers/constants' | 6 | import { WEBSERVER } from '../../../initializers/constants' |
7 | import { Emailer } from '../../../lib/emailer' | 7 | import { Emailer } from '../../../lib/emailer' |
8 | import { Redis } from '../../../lib/redis' | 8 | import { Redis } from '../../../lib/redis' |
@@ -197,11 +197,25 @@ async function createUser (req: express.Request, res: express.Response) { | |||
197 | adminFlags: body.adminFlags || UserAdminFlag.NONE | 197 | adminFlags: body.adminFlags || UserAdminFlag.NONE |
198 | }) as MUser | 198 | }) as MUser |
199 | 199 | ||
200 | // NB: due to the validator usersAddValidator, password==='' can only be true if we can send the mail. | ||
201 | const createPassword = userToCreate.password === '' | ||
202 | if (createPassword) { | ||
203 | userToCreate.password = await generateRandomString(20) | ||
204 | } | ||
205 | |||
200 | const { user, account, videoChannel } = await createUserAccountAndChannelAndPlaylist({ userToCreate: userToCreate }) | 206 | const { user, account, videoChannel } = await createUserAccountAndChannelAndPlaylist({ userToCreate: userToCreate }) |
201 | 207 | ||
202 | auditLogger.create(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON())) | 208 | auditLogger.create(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON())) |
203 | logger.info('User %s with its channel and account created.', body.username) | 209 | logger.info('User %s with its channel and account created.', body.username) |
204 | 210 | ||
211 | if (createPassword) { | ||
212 | // this will send an email for newly created users, so then can set their first password. | ||
213 | logger.info('Sending to user %s a create password email', body.username) | ||
214 | const verificationString = await Redis.Instance.setCreatePasswordVerificationString(user.id) | ||
215 | const url = WEBSERVER.URL + '/reset-password?userId=' + user.id + '&verificationString=' + verificationString | ||
216 | await Emailer.Instance.addPasswordCreateEmailJob(userToCreate.username, user.email, url) | ||
217 | } | ||
218 | |||
205 | Hooks.runAction('action:api.user.created', { body, user, account, videoChannel }) | 219 | Hooks.runAction('action:api.user.created', { body, user, account, videoChannel }) |
206 | 220 | ||
207 | return res.json({ | 221 | return res.json({ |
diff --git a/server/helpers/custom-validators/users.ts b/server/helpers/custom-validators/users.ts index b4d5751e7..63673bee2 100644 --- a/server/helpers/custom-validators/users.ts +++ b/server/helpers/custom-validators/users.ts | |||
@@ -3,6 +3,7 @@ import { UserRole } from '../../../shared' | |||
3 | import { CONSTRAINTS_FIELDS, NSFW_POLICY_TYPES } from '../../initializers/constants' | 3 | import { CONSTRAINTS_FIELDS, NSFW_POLICY_TYPES } from '../../initializers/constants' |
4 | import { exists, isArray, isBooleanValid, isFileValid } from './misc' | 4 | import { exists, isArray, isBooleanValid, isFileValid } from './misc' |
5 | import { values } from 'lodash' | 5 | import { values } from 'lodash' |
6 | import { CONFIG } from '../../initializers/config' | ||
6 | 7 | ||
7 | const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS | 8 | const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS |
8 | 9 | ||
@@ -10,6 +11,14 @@ function isUserPasswordValid (value: string) { | |||
10 | return validator.isLength(value, USERS_CONSTRAINTS_FIELDS.PASSWORD) | 11 | return validator.isLength(value, USERS_CONSTRAINTS_FIELDS.PASSWORD) |
11 | } | 12 | } |
12 | 13 | ||
14 | function isUserPasswordValidOrEmpty (value: string) { | ||
15 | // Empty password is only possible if emailing is enabled. | ||
16 | if (value === '') { | ||
17 | return !!CONFIG.SMTP.HOSTNAME && !!CONFIG.SMTP.PORT | ||
18 | } | ||
19 | return isUserPasswordValid(value) | ||
20 | } | ||
21 | |||
13 | function isUserVideoQuotaValid (value: string) { | 22 | function isUserVideoQuotaValid (value: string) { |
14 | return exists(value) && validator.isInt(value + '', USERS_CONSTRAINTS_FIELDS.VIDEO_QUOTA) | 23 | return exists(value) && validator.isInt(value + '', USERS_CONSTRAINTS_FIELDS.VIDEO_QUOTA) |
15 | } | 24 | } |
@@ -103,6 +112,7 @@ export { | |||
103 | isUserVideosHistoryEnabledValid, | 112 | isUserVideosHistoryEnabledValid, |
104 | isUserBlockedValid, | 113 | isUserBlockedValid, |
105 | isUserPasswordValid, | 114 | isUserPasswordValid, |
115 | isUserPasswordValidOrEmpty, | ||
106 | isUserVideoLanguages, | 116 | isUserVideoLanguages, |
107 | isUserBlockedReasonValid, | 117 | isUserBlockedReasonValid, |
108 | isUserRoleValid, | 118 | isUserRoleValid, |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 311d371a7..3da06402c 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -502,6 +502,7 @@ let PRIVATE_RSA_KEY_SIZE = 2048 | |||
502 | const BCRYPT_SALT_SIZE = 10 | 502 | const BCRYPT_SALT_SIZE = 10 |
503 | 503 | ||
504 | const USER_PASSWORD_RESET_LIFETIME = 60000 * 60 // 60 minutes | 504 | const USER_PASSWORD_RESET_LIFETIME = 60000 * 60 // 60 minutes |
505 | const USER_PASSWORD_CREATE_LIFETIME = 60000 * 60 * 24 * 7 // 7 days | ||
505 | 506 | ||
506 | const USER_EMAIL_VERIFY_LIFETIME = 60000 * 60 // 60 minutes | 507 | const USER_EMAIL_VERIFY_LIFETIME = 60000 * 60 // 60 minutes |
507 | 508 | ||
@@ -764,6 +765,7 @@ export { | |||
764 | LRU_CACHE, | 765 | LRU_CACHE, |
765 | JOB_REQUEST_TIMEOUT, | 766 | JOB_REQUEST_TIMEOUT, |
766 | USER_PASSWORD_RESET_LIFETIME, | 767 | USER_PASSWORD_RESET_LIFETIME, |
768 | USER_PASSWORD_CREATE_LIFETIME, | ||
767 | MEMOIZE_TTL, | 769 | MEMOIZE_TTL, |
768 | USER_EMAIL_VERIFY_LIFETIME, | 770 | USER_EMAIL_VERIFY_LIFETIME, |
769 | OVERVIEWS, | 771 | OVERVIEWS, |
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts index 9ce6186b1..0f74d2a8c 100644 --- a/server/lib/emailer.ts +++ b/server/lib/emailer.ts | |||
@@ -384,6 +384,22 @@ class Emailer { | |||
384 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 384 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
385 | } | 385 | } |
386 | 386 | ||
387 | addPasswordCreateEmailJob (username: string, to: string, resetPasswordUrl: string) { | ||
388 | const text = 'Hi,\n\n' + | ||
389 | `Welcome to your ${WEBSERVER.HOST} PeerTube instance. Your username is: ${username}.\n\n` + | ||
390 | `Please set your password by following this link: ${resetPasswordUrl} (this link will expire within seven days).\n\n` + | ||
391 | 'Cheers,\n' + | ||
392 | `${CONFIG.EMAIL.BODY.SIGNATURE}` | ||
393 | |||
394 | const emailPayload: EmailPayload = { | ||
395 | to: [ to ], | ||
396 | subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'New PeerTube account password', | ||
397 | text | ||
398 | } | ||
399 | |||
400 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
401 | } | ||
402 | |||
387 | addVerifyEmailJob (to: string, verifyEmailUrl: string) { | 403 | addVerifyEmailJob (to: string, verifyEmailUrl: string) { |
388 | const text = 'Welcome to PeerTube,\n\n' + | 404 | const text = 'Welcome to PeerTube,\n\n' + |
389 | `To start using PeerTube on ${WEBSERVER.HOST} you must verify your email! ` + | 405 | `To start using PeerTube on ${WEBSERVER.HOST} you must verify your email! ` + |
diff --git a/server/lib/redis.ts b/server/lib/redis.ts index 0c5dbdd3e..b4cd6f8e7 100644 --- a/server/lib/redis.ts +++ b/server/lib/redis.ts | |||
@@ -6,6 +6,7 @@ import { | |||
6 | CONTACT_FORM_LIFETIME, | 6 | CONTACT_FORM_LIFETIME, |
7 | USER_EMAIL_VERIFY_LIFETIME, | 7 | USER_EMAIL_VERIFY_LIFETIME, |
8 | USER_PASSWORD_RESET_LIFETIME, | 8 | USER_PASSWORD_RESET_LIFETIME, |
9 | USER_PASSWORD_CREATE_LIFETIME, | ||
9 | VIDEO_VIEW_LIFETIME, | 10 | VIDEO_VIEW_LIFETIME, |
10 | WEBSERVER | 11 | WEBSERVER |
11 | } from '../initializers/constants' | 12 | } from '../initializers/constants' |
@@ -74,6 +75,14 @@ class Redis { | |||
74 | return generatedString | 75 | return generatedString |
75 | } | 76 | } |
76 | 77 | ||
78 | async setCreatePasswordVerificationString (userId: number) { | ||
79 | const generatedString = await generateRandomString(32) | ||
80 | |||
81 | await this.setValue(this.generateResetPasswordKey(userId), generatedString, USER_PASSWORD_CREATE_LIFETIME) | ||
82 | |||
83 | return generatedString | ||
84 | } | ||
85 | |||
77 | async getResetPasswordLink (userId: number) { | 86 | async getResetPasswordLink (userId: number) { |
78 | return this.getValue(this.generateResetPasswordKey(userId)) | 87 | return this.getValue(this.generateResetPasswordKey(userId)) |
79 | } | 88 | } |
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index 5d52b5804..adc67a046 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts | |||
@@ -14,6 +14,7 @@ import { | |||
14 | isUserDisplayNameValid, | 14 | isUserDisplayNameValid, |
15 | isUserNSFWPolicyValid, | 15 | isUserNSFWPolicyValid, |
16 | isUserPasswordValid, | 16 | isUserPasswordValid, |
17 | isUserPasswordValidOrEmpty, | ||
17 | isUserRoleValid, | 18 | isUserRoleValid, |
18 | isUserUsernameValid, | 19 | isUserUsernameValid, |
19 | isUserVideoLanguages, | 20 | isUserVideoLanguages, |
@@ -39,7 +40,7 @@ import { Hooks } from '@server/lib/plugins/hooks' | |||
39 | 40 | ||
40 | const usersAddValidator = [ | 41 | const usersAddValidator = [ |
41 | body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'), | 42 | body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'), |
42 | body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'), | 43 | body('password').custom(isUserPasswordValidOrEmpty).withMessage('Should have a valid password'), |
43 | body('email').isEmail().withMessage('Should have a valid email'), | 44 | body('email').isEmail().withMessage('Should have a valid email'), |
44 | body('videoQuota').custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'), | 45 | body('videoQuota').custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'), |
45 | body('videoQuotaDaily').custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'), | 46 | body('videoQuotaDaily').custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'), |
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts index f448bb2a6..4d597f0a3 100644 --- a/server/tests/api/check-params/users.ts +++ b/server/tests/api/check-params/users.ts | |||
@@ -16,12 +16,14 @@ import { | |||
16 | getMyUserVideoRating, | 16 | getMyUserVideoRating, |
17 | getUsersList, | 17 | getUsersList, |
18 | immutableAssign, | 18 | immutableAssign, |
19 | killallServers, | ||
19 | makeGetRequest, | 20 | makeGetRequest, |
20 | makePostBodyRequest, | 21 | makePostBodyRequest, |
21 | makePutBodyRequest, | 22 | makePutBodyRequest, |
22 | makeUploadRequest, | 23 | makeUploadRequest, |
23 | registerUser, | 24 | registerUser, |
24 | removeUser, | 25 | removeUser, |
26 | reRunServer, | ||
25 | ServerInfo, | 27 | ServerInfo, |
26 | setAccessTokensToServers, | 28 | setAccessTokensToServers, |
27 | unblockUser, | 29 | unblockUser, |
@@ -39,6 +41,7 @@ import { VideoPrivacy } from '../../../../shared/models/videos' | |||
39 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | 41 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' |
40 | import { expect } from 'chai' | 42 | import { expect } from 'chai' |
41 | import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' | 43 | import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' |
44 | import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email' | ||
42 | 45 | ||
43 | describe('Test users API validators', function () { | 46 | describe('Test users API validators', function () { |
44 | const path = '/api/v1/users/' | 47 | const path = '/api/v1/users/' |
@@ -50,6 +53,8 @@ describe('Test users API validators', function () { | |||
50 | let serverWithRegistrationDisabled: ServerInfo | 53 | let serverWithRegistrationDisabled: ServerInfo |
51 | let userAccessToken = '' | 54 | let userAccessToken = '' |
52 | let moderatorAccessToken = '' | 55 | let moderatorAccessToken = '' |
56 | let emailPort: number | ||
57 | let overrideConfig: Object | ||
53 | // eslint-disable-next-line @typescript-eslint/no-unused-vars | 58 | // eslint-disable-next-line @typescript-eslint/no-unused-vars |
54 | let channelId: number | 59 | let channelId: number |
55 | 60 | ||
@@ -58,9 +63,14 @@ describe('Test users API validators', function () { | |||
58 | before(async function () { | 63 | before(async function () { |
59 | this.timeout(30000) | 64 | this.timeout(30000) |
60 | 65 | ||
66 | const emails: object[] = [] | ||
67 | emailPort = await MockSmtpServer.Instance.collectEmails(emails) | ||
68 | |||
69 | overrideConfig = { signup: { limit: 8 } } | ||
70 | |||
61 | { | 71 | { |
62 | const res = await Promise.all([ | 72 | const res = await Promise.all([ |
63 | flushAndRunServer(1, { signup: { limit: 7 } }), | 73 | flushAndRunServer(1, overrideConfig), |
64 | flushAndRunServer(2) | 74 | flushAndRunServer(2) |
65 | ]) | 75 | ]) |
66 | 76 | ||
@@ -229,6 +239,40 @@ describe('Test users API validators', function () { | |||
229 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | 239 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) |
230 | }) | 240 | }) |
231 | 241 | ||
242 | it('Should fail with empty password and no smtp configured', async function () { | ||
243 | const fields = immutableAssign(baseCorrectParams, { password: '' }) | ||
244 | |||
245 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
246 | }) | ||
247 | |||
248 | it('Should succeed with no password on a server with smtp enabled', async function () { | ||
249 | this.timeout(10000) | ||
250 | |||
251 | killallServers([ server ]) | ||
252 | |||
253 | const config = immutableAssign(overrideConfig, { | ||
254 | smtp: { | ||
255 | hostname: 'localhost', | ||
256 | port: emailPort | ||
257 | } | ||
258 | }) | ||
259 | await reRunServer(server, config) | ||
260 | |||
261 | const fields = immutableAssign(baseCorrectParams, { | ||
262 | password: '', | ||
263 | username: 'create_password', | ||
264 | email: 'create_password@example.com' | ||
265 | }) | ||
266 | |||
267 | await makePostBodyRequest({ | ||
268 | url: server.url, | ||
269 | path: path, | ||
270 | token: server.accessToken, | ||
271 | fields, | ||
272 | statusCodeExpected: 200 | ||
273 | }) | ||
274 | }) | ||
275 | |||
232 | it('Should fail with invalid admin flags', async function () { | 276 | it('Should fail with invalid admin flags', async function () { |
233 | const fields = immutableAssign(baseCorrectParams, { adminFlags: 'toto' }) | 277 | const fields = immutableAssign(baseCorrectParams, { adminFlags: 'toto' }) |
234 | 278 | ||
@@ -1102,6 +1146,8 @@ describe('Test users API validators', function () { | |||
1102 | }) | 1146 | }) |
1103 | 1147 | ||
1104 | after(async function () { | 1148 | after(async function () { |
1149 | MockSmtpServer.Instance.kill() | ||
1150 | |||
1105 | await cleanupTests([ server, serverWithRegistrationDisabled ]) | 1151 | await cleanupTests([ server, serverWithRegistrationDisabled ]) |
1106 | }) | 1152 | }) |
1107 | }) | 1153 | }) |
diff --git a/server/tests/api/server/email.ts b/server/tests/api/server/email.ts index f18859e5d..95b64a459 100644 --- a/server/tests/api/server/email.ts +++ b/server/tests/api/server/email.ts | |||
@@ -28,10 +28,12 @@ const expect = chai.expect | |||
28 | describe('Test emails', function () { | 28 | describe('Test emails', function () { |
29 | let server: ServerInfo | 29 | let server: ServerInfo |
30 | let userId: number | 30 | let userId: number |
31 | let userId2: number | ||
31 | let userAccessToken: string | 32 | let userAccessToken: string |
32 | let videoUUID: string | 33 | let videoUUID: string |
33 | let videoUserUUID: string | 34 | let videoUserUUID: string |
34 | let verificationString: string | 35 | let verificationString: string |
36 | let verificationString2: string | ||
35 | const emails: object[] = [] | 37 | const emails: object[] = [] |
36 | const user = { | 38 | const user = { |
37 | username: 'user_1', | 39 | username: 'user_1', |
@@ -122,6 +124,56 @@ describe('Test emails', function () { | |||
122 | }) | 124 | }) |
123 | }) | 125 | }) |
124 | 126 | ||
127 | describe('When creating a user without password', function () { | ||
128 | it('Should send a create password email', async function () { | ||
129 | this.timeout(10000) | ||
130 | |||
131 | await createUser({ | ||
132 | url: server.url, | ||
133 | accessToken: server.accessToken, | ||
134 | username: 'create_password', | ||
135 | password: '' | ||
136 | }) | ||
137 | |||
138 | await waitJobs(server) | ||
139 | expect(emails).to.have.lengthOf(2) | ||
140 | |||
141 | const email = emails[1] | ||
142 | |||
143 | expect(email['from'][0]['name']).equal('localhost:' + server.port) | ||
144 | expect(email['from'][0]['address']).equal('test-admin@localhost') | ||
145 | expect(email['to'][0]['address']).equal('create_password@example.com') | ||
146 | expect(email['subject']).contains('account') | ||
147 | expect(email['subject']).contains('password') | ||
148 | |||
149 | const verificationStringMatches = /verificationString=([a-z0-9]+)/.exec(email['text']) | ||
150 | expect(verificationStringMatches).not.to.be.null | ||
151 | |||
152 | verificationString2 = verificationStringMatches[1] | ||
153 | expect(verificationString2).to.have.length.above(2) | ||
154 | |||
155 | const userIdMatches = /userId=([0-9]+)/.exec(email['text']) | ||
156 | expect(userIdMatches).not.to.be.null | ||
157 | |||
158 | userId2 = parseInt(userIdMatches[1], 10) | ||
159 | }) | ||
160 | |||
161 | it('Should not reset the password with an invalid verification string', async function () { | ||
162 | await resetPassword(server.url, userId2, verificationString2 + 'c', 'newly_created_password', 403) | ||
163 | }) | ||
164 | |||
165 | it('Should reset the password', async function () { | ||
166 | await resetPassword(server.url, userId2, verificationString2, 'newly_created_password') | ||
167 | }) | ||
168 | |||
169 | it('Should login with this new password', async function () { | ||
170 | await userLogin(server, { | ||
171 | username: 'create_password', | ||
172 | password: 'newly_created_password' | ||
173 | }) | ||
174 | }) | ||
175 | }) | ||
176 | |||
125 | describe('When creating a video abuse', function () { | 177 | describe('When creating a video abuse', function () { |
126 | it('Should send the notification email', async function () { | 178 | it('Should send the notification email', async function () { |
127 | this.timeout(10000) | 179 | this.timeout(10000) |
@@ -130,9 +182,9 @@ describe('Test emails', function () { | |||
130 | await reportVideoAbuse(server.url, server.accessToken, videoUUID, reason) | 182 | await reportVideoAbuse(server.url, server.accessToken, videoUUID, reason) |
131 | 183 | ||
132 | await waitJobs(server) | 184 | await waitJobs(server) |
133 | expect(emails).to.have.lengthOf(2) | 185 | expect(emails).to.have.lengthOf(3) |
134 | 186 | ||
135 | const email = emails[1] | 187 | const email = emails[2] |
136 | 188 | ||
137 | expect(email['from'][0]['name']).equal('localhost:' + server.port) | 189 | expect(email['from'][0]['name']).equal('localhost:' + server.port) |
138 | expect(email['from'][0]['address']).equal('test-admin@localhost') | 190 | expect(email['from'][0]['address']).equal('test-admin@localhost') |
@@ -151,9 +203,9 @@ describe('Test emails', function () { | |||
151 | await blockUser(server.url, userId, server.accessToken, 204, reason) | 203 | await blockUser(server.url, userId, server.accessToken, 204, reason) |
152 | 204 | ||
153 | await waitJobs(server) | 205 | await waitJobs(server) |
154 | expect(emails).to.have.lengthOf(3) | 206 | expect(emails).to.have.lengthOf(4) |
155 | 207 | ||
156 | const email = emails[2] | 208 | const email = emails[3] |
157 | 209 | ||
158 | expect(email['from'][0]['name']).equal('localhost:' + server.port) | 210 | expect(email['from'][0]['name']).equal('localhost:' + server.port) |
159 | expect(email['from'][0]['address']).equal('test-admin@localhost') | 211 | expect(email['from'][0]['address']).equal('test-admin@localhost') |
@@ -169,9 +221,9 @@ describe('Test emails', function () { | |||
169 | await unblockUser(server.url, userId, server.accessToken, 204) | 221 | await unblockUser(server.url, userId, server.accessToken, 204) |
170 | 222 | ||
171 | await waitJobs(server) | 223 | await waitJobs(server) |
172 | expect(emails).to.have.lengthOf(4) | 224 | expect(emails).to.have.lengthOf(5) |
173 | 225 | ||
174 | const email = emails[3] | 226 | const email = emails[4] |
175 | 227 | ||
176 | expect(email['from'][0]['name']).equal('localhost:' + server.port) | 228 | expect(email['from'][0]['name']).equal('localhost:' + server.port) |
177 | expect(email['from'][0]['address']).equal('test-admin@localhost') | 229 | expect(email['from'][0]['address']).equal('test-admin@localhost') |
@@ -189,9 +241,9 @@ describe('Test emails', function () { | |||
189 | await addVideoToBlacklist(server.url, server.accessToken, videoUserUUID, reason) | 241 | await addVideoToBlacklist(server.url, server.accessToken, videoUserUUID, reason) |
190 | 242 | ||
191 | await waitJobs(server) | 243 | await waitJobs(server) |
192 | expect(emails).to.have.lengthOf(5) | 244 | expect(emails).to.have.lengthOf(6) |
193 | 245 | ||
194 | const email = emails[4] | 246 | const email = emails[5] |
195 | 247 | ||
196 | expect(email['from'][0]['name']).equal('localhost:' + server.port) | 248 | expect(email['from'][0]['name']).equal('localhost:' + server.port) |
197 | expect(email['from'][0]['address']).equal('test-admin@localhost') | 249 | expect(email['from'][0]['address']).equal('test-admin@localhost') |
@@ -207,9 +259,9 @@ describe('Test emails', function () { | |||
207 | await removeVideoFromBlacklist(server.url, server.accessToken, videoUserUUID) | 259 | await removeVideoFromBlacklist(server.url, server.accessToken, videoUserUUID) |
208 | 260 | ||
209 | await waitJobs(server) | 261 | await waitJobs(server) |
210 | expect(emails).to.have.lengthOf(6) | 262 | expect(emails).to.have.lengthOf(7) |
211 | 263 | ||
212 | const email = emails[5] | 264 | const email = emails[6] |
213 | 265 | ||
214 | expect(email['from'][0]['name']).equal('localhost:' + server.port) | 266 | expect(email['from'][0]['name']).equal('localhost:' + server.port) |
215 | expect(email['from'][0]['address']).equal('test-admin@localhost') | 267 | expect(email['from'][0]['address']).equal('test-admin@localhost') |
@@ -227,9 +279,9 @@ describe('Test emails', function () { | |||
227 | await askSendVerifyEmail(server.url, 'user_1@example.com') | 279 | await askSendVerifyEmail(server.url, 'user_1@example.com') |
228 | 280 | ||
229 | await waitJobs(server) | 281 | await waitJobs(server) |
230 | expect(emails).to.have.lengthOf(7) | 282 | expect(emails).to.have.lengthOf(8) |
231 | 283 | ||
232 | const email = emails[6] | 284 | const email = emails[7] |
233 | 285 | ||
234 | expect(email['from'][0]['name']).equal('localhost:' + server.port) | 286 | expect(email['from'][0]['name']).equal('localhost:' + server.port) |
235 | expect(email['from'][0]['address']).equal('test-admin@localhost') | 287 | expect(email['from'][0]['address']).equal('test-admin@localhost') |
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml index 180f65bcf..40f7e0cdd 100644 --- a/support/doc/api/openapi.yaml +++ b/support/doc/api/openapi.yaml | |||
@@ -2781,7 +2781,7 @@ components: | |||
2781 | description: 'The user username ' | 2781 | description: 'The user username ' |
2782 | password: | 2782 | password: |
2783 | type: string | 2783 | type: string |
2784 | description: 'The user password ' | 2784 | description: 'The user password. If the smtp server is configured, you can leave empty and an email will be sent ' |
2785 | email: | 2785 | email: |
2786 | type: string | 2786 | type: string |
2787 | description: 'The user email ' | 2787 | description: 'The user email ' |