* 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
import { Component, OnInit } from '@angular/core'
-import { Router } from '@angular/router'
+import { Router, ActivatedRoute } from '@angular/router'
import { AuthService, Notifier, ServerService } from '@app/core'
import { UserCreate, UserRole } from '../../../../../../shared'
import { UserEdit } from './user-edit'
protected configService: ConfigService,
protected auth: AuthService,
private userValidatorsService: UserValidatorsService,
+ private route: ActivatedRoute,
private router: Router,
private notifier: Notifier,
private userService: UserService,
this.buildForm({
username: this.userValidatorsService.USER_USERNAME,
email: this.userValidatorsService.USER_EMAIL,
- password: this.userValidatorsService.USER_PASSWORD,
+ password: this.isPasswordOptional() ? this.userValidatorsService.USER_PASSWORD_OPTIONAL : this.userValidatorsService.USER_PASSWORD,
role: this.userValidatorsService.USER_ROLE,
videoQuota: this.userValidatorsService.USER_VIDEO_QUOTA,
videoQuotaDaily: this.userValidatorsService.USER_VIDEO_QUOTA_DAILY,
return true
}
+ isPasswordOptional () {
+ const serverConfig = this.route.snapshot.data.serverConfig
+ return serverConfig.email.enabled
+ }
+
getFormButtonTitle () {
return this.i18n('Create user')
}
<div class="form-group" *ngIf="isCreation()">
<label i18n for="password">Password</label>
+ <my-help *ngIf="isPasswordOptional()">
+ <ng-template ptTemplate="customHtml">
+ <ng-container i18n>
+ If you leave the password empty, an email will be sent to the user.
+ </ng-container>
+ </ng-template>
+ </my-help>
<input
type="password" id="password" autocomplete="new-password"
formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }"
return false
}
+ isPasswordOptional () {
+ return false
+ }
+
getFormButtonTitle () {
return this.i18n('Update user')
}
import { UsersComponent } from './users.component'
import { UserCreateComponent, UserUpdateComponent } from './user-edit'
import { UserListComponent } from './user-list'
+import { ServerConfigResolver } from '@app/core/routing/server-config-resolver.service'
export const UsersRoutes: Routes = [
{
meta: {
title: 'Create a user'
}
+ },
+ resolve: {
+ serverConfig: ServerConfigResolver
}
},
{
readonly USER_USERNAME: BuildFormValidator
readonly USER_EMAIL: BuildFormValidator
readonly USER_PASSWORD: BuildFormValidator
+ readonly USER_PASSWORD_OPTIONAL: BuildFormValidator
readonly USER_CONFIRM_PASSWORD: BuildFormValidator
readonly USER_VIDEO_QUOTA: BuildFormValidator
readonly USER_VIDEO_QUOTA_DAILY: BuildFormValidator
}
}
+ this.USER_PASSWORD_OPTIONAL = {
+ VALIDATORS: [
+ Validators.minLength(6),
+ Validators.maxLength(255)
+ ],
+ MESSAGES: {
+ 'minlength': this.i18n('Password must be at least 6 characters long.'),
+ 'maxlength': this.i18n('Password cannot be more than 255 characters long.')
+ }
+ }
+
this.USER_CONFIRM_PASSWORD = {
VALIDATORS: [],
MESSAGES: {
import * as RateLimit from 'express-rate-limit'
import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared'
import { logger } from '../../../helpers/logger'
-import { getFormattedObjects } from '../../../helpers/utils'
+import { generateRandomString, getFormattedObjects } from '../../../helpers/utils'
import { WEBSERVER } from '../../../initializers/constants'
import { Emailer } from '../../../lib/emailer'
import { Redis } from '../../../lib/redis'
adminFlags: body.adminFlags || UserAdminFlag.NONE
}) as MUser
+ // NB: due to the validator usersAddValidator, password==='' can only be true if we can send the mail.
+ const createPassword = userToCreate.password === ''
+ if (createPassword) {
+ userToCreate.password = await generateRandomString(20)
+ }
+
const { user, account, videoChannel } = await createUserAccountAndChannelAndPlaylist({ userToCreate: userToCreate })
auditLogger.create(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()))
logger.info('User %s with its channel and account created.', body.username)
+ if (createPassword) {
+ // this will send an email for newly created users, so then can set their first password.
+ logger.info('Sending to user %s a create password email', body.username)
+ const verificationString = await Redis.Instance.setCreatePasswordVerificationString(user.id)
+ const url = WEBSERVER.URL + '/reset-password?userId=' + user.id + '&verificationString=' + verificationString
+ await Emailer.Instance.addPasswordCreateEmailJob(userToCreate.username, user.email, url)
+ }
+
Hooks.runAction('action:api.user.created', { body, user, account, videoChannel })
return res.json({
import { CONSTRAINTS_FIELDS, NSFW_POLICY_TYPES } from '../../initializers/constants'
import { exists, isArray, isBooleanValid, isFileValid } from './misc'
import { values } from 'lodash'
+import { CONFIG } from '../../initializers/config'
const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS
return validator.isLength(value, USERS_CONSTRAINTS_FIELDS.PASSWORD)
}
+function isUserPasswordValidOrEmpty (value: string) {
+ // Empty password is only possible if emailing is enabled.
+ if (value === '') {
+ return !!CONFIG.SMTP.HOSTNAME && !!CONFIG.SMTP.PORT
+ }
+ return isUserPasswordValid(value)
+}
+
function isUserVideoQuotaValid (value: string) {
return exists(value) && validator.isInt(value + '', USERS_CONSTRAINTS_FIELDS.VIDEO_QUOTA)
}
isUserVideosHistoryEnabledValid,
isUserBlockedValid,
isUserPasswordValid,
+ isUserPasswordValidOrEmpty,
isUserVideoLanguages,
isUserBlockedReasonValid,
isUserRoleValid,
const BCRYPT_SALT_SIZE = 10
const USER_PASSWORD_RESET_LIFETIME = 60000 * 60 // 60 minutes
+const USER_PASSWORD_CREATE_LIFETIME = 60000 * 60 * 24 * 7 // 7 days
const USER_EMAIL_VERIFY_LIFETIME = 60000 * 60 // 60 minutes
LRU_CACHE,
JOB_REQUEST_TIMEOUT,
USER_PASSWORD_RESET_LIFETIME,
+ USER_PASSWORD_CREATE_LIFETIME,
MEMOIZE_TTL,
USER_EMAIL_VERIFY_LIFETIME,
OVERVIEWS,
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
}
+ addPasswordCreateEmailJob (username: string, to: string, resetPasswordUrl: string) {
+ const text = 'Hi,\n\n' +
+ `Welcome to your ${WEBSERVER.HOST} PeerTube instance. Your username is: ${username}.\n\n` +
+ `Please set your password by following this link: ${resetPasswordUrl} (this link will expire within seven days).\n\n` +
+ 'Cheers,\n' +
+ `${CONFIG.EMAIL.BODY.SIGNATURE}`
+
+ const emailPayload: EmailPayload = {
+ to: [ to ],
+ subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'New PeerTube account password',
+ text
+ }
+
+ return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
+ }
+
addVerifyEmailJob (to: string, verifyEmailUrl: string) {
const text = 'Welcome to PeerTube,\n\n' +
`To start using PeerTube on ${WEBSERVER.HOST} you must verify your email! ` +
CONTACT_FORM_LIFETIME,
USER_EMAIL_VERIFY_LIFETIME,
USER_PASSWORD_RESET_LIFETIME,
+ USER_PASSWORD_CREATE_LIFETIME,
VIDEO_VIEW_LIFETIME,
WEBSERVER
} from '../initializers/constants'
return generatedString
}
+ async setCreatePasswordVerificationString (userId: number) {
+ const generatedString = await generateRandomString(32)
+
+ await this.setValue(this.generateResetPasswordKey(userId), generatedString, USER_PASSWORD_CREATE_LIFETIME)
+
+ return generatedString
+ }
+
async getResetPasswordLink (userId: number) {
return this.getValue(this.generateResetPasswordKey(userId))
}
isUserDisplayNameValid,
isUserNSFWPolicyValid,
isUserPasswordValid,
+ isUserPasswordValidOrEmpty,
isUserRoleValid,
isUserUsernameValid,
isUserVideoLanguages,
const usersAddValidator = [
body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'),
- body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'),
+ body('password').custom(isUserPasswordValidOrEmpty).withMessage('Should have a valid password'),
body('email').isEmail().withMessage('Should have a valid email'),
body('videoQuota').custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'),
body('videoQuotaDaily').custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'),
getMyUserVideoRating,
getUsersList,
immutableAssign,
+ killallServers,
makeGetRequest,
makePostBodyRequest,
makePutBodyRequest,
makeUploadRequest,
registerUser,
removeUser,
+ reRunServer,
ServerInfo,
setAccessTokensToServers,
unblockUser,
import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
import { expect } from 'chai'
import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
+import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
describe('Test users API validators', function () {
const path = '/api/v1/users/'
let serverWithRegistrationDisabled: ServerInfo
let userAccessToken = ''
let moderatorAccessToken = ''
+ let emailPort: number
+ let overrideConfig: Object
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let channelId: number
before(async function () {
this.timeout(30000)
+ const emails: object[] = []
+ emailPort = await MockSmtpServer.Instance.collectEmails(emails)
+
+ overrideConfig = { signup: { limit: 8 } }
+
{
const res = await Promise.all([
- flushAndRunServer(1, { signup: { limit: 7 } }),
+ flushAndRunServer(1, overrideConfig),
flushAndRunServer(2)
])
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
})
+ it('Should fail with empty password and no smtp configured', async function () {
+ const fields = immutableAssign(baseCorrectParams, { password: '' })
+
+ await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
+ })
+
+ it('Should succeed with no password on a server with smtp enabled', async function () {
+ this.timeout(10000)
+
+ killallServers([ server ])
+
+ const config = immutableAssign(overrideConfig, {
+ smtp: {
+ hostname: 'localhost',
+ port: emailPort
+ }
+ })
+ await reRunServer(server, config)
+
+ const fields = immutableAssign(baseCorrectParams, {
+ password: '',
+ username: 'create_password',
+ email: 'create_password@example.com'
+ })
+
+ await makePostBodyRequest({
+ url: server.url,
+ path: path,
+ token: server.accessToken,
+ fields,
+ statusCodeExpected: 200
+ })
+ })
+
it('Should fail with invalid admin flags', async function () {
const fields = immutableAssign(baseCorrectParams, { adminFlags: 'toto' })
})
after(async function () {
+ MockSmtpServer.Instance.kill()
+
await cleanupTests([ server, serverWithRegistrationDisabled ])
})
})
describe('Test emails', function () {
let server: ServerInfo
let userId: number
+ let userId2: number
let userAccessToken: string
let videoUUID: string
let videoUserUUID: string
let verificationString: string
+ let verificationString2: string
const emails: object[] = []
const user = {
username: 'user_1',
})
})
+ describe('When creating a user without password', function () {
+ it('Should send a create password email', async function () {
+ this.timeout(10000)
+
+ await createUser({
+ url: server.url,
+ accessToken: server.accessToken,
+ username: 'create_password',
+ password: ''
+ })
+
+ await waitJobs(server)
+ expect(emails).to.have.lengthOf(2)
+
+ const email = emails[1]
+
+ expect(email['from'][0]['name']).equal('localhost:' + server.port)
+ expect(email['from'][0]['address']).equal('test-admin@localhost')
+ expect(email['to'][0]['address']).equal('create_password@example.com')
+ expect(email['subject']).contains('account')
+ expect(email['subject']).contains('password')
+
+ const verificationStringMatches = /verificationString=([a-z0-9]+)/.exec(email['text'])
+ expect(verificationStringMatches).not.to.be.null
+
+ verificationString2 = verificationStringMatches[1]
+ expect(verificationString2).to.have.length.above(2)
+
+ const userIdMatches = /userId=([0-9]+)/.exec(email['text'])
+ expect(userIdMatches).not.to.be.null
+
+ userId2 = parseInt(userIdMatches[1], 10)
+ })
+
+ it('Should not reset the password with an invalid verification string', async function () {
+ await resetPassword(server.url, userId2, verificationString2 + 'c', 'newly_created_password', 403)
+ })
+
+ it('Should reset the password', async function () {
+ await resetPassword(server.url, userId2, verificationString2, 'newly_created_password')
+ })
+
+ it('Should login with this new password', async function () {
+ await userLogin(server, {
+ username: 'create_password',
+ password: 'newly_created_password'
+ })
+ })
+ })
+
describe('When creating a video abuse', function () {
it('Should send the notification email', async function () {
this.timeout(10000)
await reportVideoAbuse(server.url, server.accessToken, videoUUID, reason)
await waitJobs(server)
- expect(emails).to.have.lengthOf(2)
+ expect(emails).to.have.lengthOf(3)
- const email = emails[1]
+ const email = emails[2]
expect(email['from'][0]['name']).equal('localhost:' + server.port)
expect(email['from'][0]['address']).equal('test-admin@localhost')
await blockUser(server.url, userId, server.accessToken, 204, reason)
await waitJobs(server)
- expect(emails).to.have.lengthOf(3)
+ expect(emails).to.have.lengthOf(4)
- const email = emails[2]
+ const email = emails[3]
expect(email['from'][0]['name']).equal('localhost:' + server.port)
expect(email['from'][0]['address']).equal('test-admin@localhost')
await unblockUser(server.url, userId, server.accessToken, 204)
await waitJobs(server)
- expect(emails).to.have.lengthOf(4)
+ expect(emails).to.have.lengthOf(5)
- const email = emails[3]
+ const email = emails[4]
expect(email['from'][0]['name']).equal('localhost:' + server.port)
expect(email['from'][0]['address']).equal('test-admin@localhost')
await addVideoToBlacklist(server.url, server.accessToken, videoUserUUID, reason)
await waitJobs(server)
- expect(emails).to.have.lengthOf(5)
+ expect(emails).to.have.lengthOf(6)
- const email = emails[4]
+ const email = emails[5]
expect(email['from'][0]['name']).equal('localhost:' + server.port)
expect(email['from'][0]['address']).equal('test-admin@localhost')
await removeVideoFromBlacklist(server.url, server.accessToken, videoUserUUID)
await waitJobs(server)
- expect(emails).to.have.lengthOf(6)
+ expect(emails).to.have.lengthOf(7)
- const email = emails[5]
+ const email = emails[6]
expect(email['from'][0]['name']).equal('localhost:' + server.port)
expect(email['from'][0]['address']).equal('test-admin@localhost')
await askSendVerifyEmail(server.url, 'user_1@example.com')
await waitJobs(server)
- expect(emails).to.have.lengthOf(7)
+ expect(emails).to.have.lengthOf(8)
- const email = emails[6]
+ const email = emails[7]
expect(email['from'][0]['name']).equal('localhost:' + server.port)
expect(email['from'][0]['address']).equal('test-admin@localhost')
description: 'The user username '
password:
type: string
- description: 'The user password '
+ description: 'The user password. If the smtp server is configured, you can leave empty and an email will be sent '
email:
type: string
description: 'The user email '