aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/users/index.ts16
-rw-r--r--server/helpers/custom-validators/users.ts10
-rw-r--r--server/initializers/constants.ts2
-rw-r--r--server/lib/emailer.ts16
-rw-r--r--server/lib/redis.ts9
-rw-r--r--server/middlewares/validators/users.ts3
-rw-r--r--server/tests/api/check-params/users.ts48
-rw-r--r--server/tests/api/server/email.ts76
8 files changed, 165 insertions, 15 deletions
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'
2import * as RateLimit from 'express-rate-limit' 2import * as RateLimit from 'express-rate-limit'
3import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared' 3import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared'
4import { logger } from '../../../helpers/logger' 4import { logger } from '../../../helpers/logger'
5import { getFormattedObjects } from '../../../helpers/utils' 5import { generateRandomString, getFormattedObjects } from '../../../helpers/utils'
6import { WEBSERVER } from '../../../initializers/constants' 6import { WEBSERVER } from '../../../initializers/constants'
7import { Emailer } from '../../../lib/emailer' 7import { Emailer } from '../../../lib/emailer'
8import { Redis } from '../../../lib/redis' 8import { 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'
3import { CONSTRAINTS_FIELDS, NSFW_POLICY_TYPES } from '../../initializers/constants' 3import { CONSTRAINTS_FIELDS, NSFW_POLICY_TYPES } from '../../initializers/constants'
4import { exists, isArray, isBooleanValid, isFileValid } from './misc' 4import { exists, isArray, isBooleanValid, isFileValid } from './misc'
5import { values } from 'lodash' 5import { values } from 'lodash'
6import { CONFIG } from '../../initializers/config'
6 7
7const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS 8const 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
14function 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
13function isUserVideoQuotaValid (value: string) { 22function 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
502const BCRYPT_SALT_SIZE = 10 502const BCRYPT_SALT_SIZE = 10
503 503
504const USER_PASSWORD_RESET_LIFETIME = 60000 * 60 // 60 minutes 504const USER_PASSWORD_RESET_LIFETIME = 60000 * 60 // 60 minutes
505const USER_PASSWORD_CREATE_LIFETIME = 60000 * 60 * 24 * 7 // 7 days
505 506
506const USER_EMAIL_VERIFY_LIFETIME = 60000 * 60 // 60 minutes 507const 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
40const usersAddValidator = [ 41const 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'
39import { waitJobs } from '../../../../shared/extra-utils/server/jobs' 41import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
40import { expect } from 'chai' 42import { expect } from 'chai'
41import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' 43import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
44import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
42 45
43describe('Test users API validators', function () { 46describe('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
28describe('Test emails', function () { 28describe('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')