aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/users.ts8
-rw-r--r--server/helpers/custom-validators/users.ts5
-rw-r--r--server/initializers/constants.ts3
-rw-r--r--server/initializers/migrations/0245-user-blocked.ts14
-rw-r--r--server/lib/emailer.ts23
-rw-r--r--server/middlewares/validators/users.ts5
-rw-r--r--server/models/account/user.ts9
-rw-r--r--server/models/video/video-abuse.ts2
-rw-r--r--server/tests/api/server/email.ts48
-rw-r--r--server/tests/utils/users/users.ts5
10 files changed, 109 insertions, 13 deletions
diff --git a/server/controllers/api/users.ts b/server/controllers/api/users.ts
index 8f429d0b5..0e2be7123 100644
--- a/server/controllers/api/users.ts
+++ b/server/controllers/api/users.ts
@@ -302,8 +302,9 @@ async function unblockUser (req: express.Request, res: express.Response, next: e
302 302
303async function blockUser (req: express.Request, res: express.Response, next: express.NextFunction) { 303async function blockUser (req: express.Request, res: express.Response, next: express.NextFunction) {
304 const user: UserModel = res.locals.user 304 const user: UserModel = res.locals.user
305 const reason = req.body.reason
305 306
306 await changeUserBlock(res, user, true) 307 await changeUserBlock(res, user, true, reason)
307 308
308 return res.status(204).end() 309 return res.status(204).end()
309} 310}
@@ -454,10 +455,11 @@ function success (req: express.Request, res: express.Response, next: express.Nex
454 res.end() 455 res.end()
455} 456}
456 457
457async function changeUserBlock (res: express.Response, user: UserModel, block: boolean) { 458async function changeUserBlock (res: express.Response, user: UserModel, block: boolean, reason?: string) {
458 const oldUserAuditView = new UserAuditView(user.toFormattedJSON()) 459 const oldUserAuditView = new UserAuditView(user.toFormattedJSON())
459 460
460 user.blocked = block 461 user.blocked = block
462 user.blockedReason = reason || null
461 463
462 await sequelizeTypescript.transaction(async t => { 464 await sequelizeTypescript.transaction(async t => {
463 await OAuthTokenModel.deleteUserToken(user.id, t) 465 await OAuthTokenModel.deleteUserToken(user.id, t)
@@ -465,6 +467,8 @@ async function changeUserBlock (res: express.Response, user: UserModel, block: b
465 await user.save({ transaction: t }) 467 await user.save({ transaction: t })
466 }) 468 })
467 469
470 await Emailer.Instance.addUserBlockJob(user, block, reason)
471
468 auditLogger.update( 472 auditLogger.update(
469 res.locals.oauth.token.User.Account.Actor.getIdentifier(), 473 res.locals.oauth.token.User.Account.Actor.getIdentifier(),
470 new UserAuditView(user.toFormattedJSON()), 474 new UserAuditView(user.toFormattedJSON()),
diff --git a/server/helpers/custom-validators/users.ts b/server/helpers/custom-validators/users.ts
index 4a0d79ae5..c3cdefd4e 100644
--- a/server/helpers/custom-validators/users.ts
+++ b/server/helpers/custom-validators/users.ts
@@ -42,6 +42,10 @@ function isUserBlockedValid (value: any) {
42 return isBooleanValid(value) 42 return isBooleanValid(value)
43} 43}
44 44
45function isUserBlockedReasonValid (value: any) {
46 return value === null || (exists(value) && validator.isLength(value, CONSTRAINTS_FIELDS.USERS.BLOCKED_REASON))
47}
48
45function isUserRoleValid (value: any) { 49function isUserRoleValid (value: any) {
46 return exists(value) && validator.isInt('' + value) && UserRole[value] !== undefined 50 return exists(value) && validator.isInt('' + value) && UserRole[value] !== undefined
47} 51}
@@ -59,6 +63,7 @@ function isAvatarFile (files: { [ fieldname: string ]: Express.Multer.File[] } |
59export { 63export {
60 isUserBlockedValid, 64 isUserBlockedValid,
61 isUserPasswordValid, 65 isUserPasswordValid,
66 isUserBlockedReasonValid,
62 isUserRoleValid, 67 isUserRoleValid,
63 isUserVideoQuotaValid, 68 isUserVideoQuotaValid,
64 isUserUsernameValid, 69 isUserUsernameValid,
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 0a651beed..ea561b686 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -254,7 +254,8 @@ const CONSTRAINTS_FIELDS = {
254 DESCRIPTION: { min: 3, max: 250 }, // Length 254 DESCRIPTION: { min: 3, max: 250 }, // Length
255 USERNAME: { min: 3, max: 20 }, // Length 255 USERNAME: { min: 3, max: 20 }, // Length
256 PASSWORD: { min: 6, max: 255 }, // Length 256 PASSWORD: { min: 6, max: 255 }, // Length
257 VIDEO_QUOTA: { min: -1 } 257 VIDEO_QUOTA: { min: -1 },
258 BLOCKED_REASON: { min: 3, max: 250 } // Length
258 }, 259 },
259 VIDEO_ABUSES: { 260 VIDEO_ABUSES: {
260 REASON: { min: 2, max: 300 } // Length 261 REASON: { min: 2, max: 300 } // Length
diff --git a/server/initializers/migrations/0245-user-blocked.ts b/server/initializers/migrations/0245-user-blocked.ts
index 67afea5ed..5a04ecd2b 100644
--- a/server/initializers/migrations/0245-user-blocked.ts
+++ b/server/initializers/migrations/0245-user-blocked.ts
@@ -1,8 +1,5 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import { createClient } from 'redis' 2import { CONSTRAINTS_FIELDS } from '../constants'
3import { CONFIG } from '../constants'
4import { JobQueue } from '../../lib/job-queue'
5import { initDatabaseModels } from '../database'
6 3
7async function up (utils: { 4async function up (utils: {
8 transaction: Sequelize.Transaction 5 transaction: Sequelize.Transaction
@@ -31,6 +28,15 @@ async function up (utils: {
31 } 28 }
32 await utils.queryInterface.changeColumn('user', 'blocked', data) 29 await utils.queryInterface.changeColumn('user', 'blocked', data)
33 } 30 }
31
32 {
33 const data = {
34 type: Sequelize.STRING(CONSTRAINTS_FIELDS.USERS.BLOCKED_REASON.max),
35 allowNull: true,
36 defaultValue: null
37 }
38 await utils.queryInterface.addColumn('user', 'blockedReason', data)
39 }
34} 40}
35 41
36function down (options) { 42function down (options) {
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts
index ded321bf7..3faeffd77 100644
--- a/server/lib/emailer.ts
+++ b/server/lib/emailer.ts
@@ -89,7 +89,7 @@ class Emailer {
89 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 89 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
90 } 90 }
91 91
92 async addVideoAbuseReport (videoId: number) { 92 async addVideoAbuseReportJob (videoId: number) {
93 const video = await VideoModel.load(videoId) 93 const video = await VideoModel.load(videoId)
94 if (!video) throw new Error('Unknown Video id during Abuse report.') 94 if (!video) throw new Error('Unknown Video id during Abuse report.')
95 95
@@ -108,6 +108,27 @@ class Emailer {
108 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 108 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
109 } 109 }
110 110
111 addUserBlockJob (user: UserModel, blocked: boolean, reason?: string) {
112 const reasonString = reason ? ` for the following reason: ${reason}` : ''
113 const blockedWord = blocked ? 'blocked' : 'unblocked'
114 const blockedString = `Your account ${user.username} on ${CONFIG.WEBSERVER.HOST} has been ${blockedWord}${reasonString}.`
115
116 const text = 'Hi,\n\n' +
117 blockedString +
118 '\n\n' +
119 'Cheers,\n' +
120 `PeerTube.`
121
122 const to = user.email
123 const emailPayload: EmailPayload = {
124 to: [ to ],
125 subject: '[PeerTube] Account ' + blockedWord,
126 text
127 }
128
129 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
130 }
131
111 sendMail (to: string[], subject: string, text: string) { 132 sendMail (to: string[], subject: string, text: string) {
112 if (!this.transporter) { 133 if (!this.transporter) {
113 throw new Error('Cannot send mail because SMTP is not configured.') 134 throw new Error('Cannot send mail because SMTP is not configured.')
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts
index 94d8ab53b..771c414a0 100644
--- a/server/middlewares/validators/users.ts
+++ b/server/middlewares/validators/users.ts
@@ -5,7 +5,7 @@ import { body, param } from 'express-validator/check'
5import { omit } from 'lodash' 5import { omit } from 'lodash'
6import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' 6import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc'
7import { 7import {
8 isUserAutoPlayVideoValid, 8 isUserAutoPlayVideoValid, isUserBlockedReasonValid,
9 isUserDescriptionValid, 9 isUserDescriptionValid,
10 isUserDisplayNameValid, 10 isUserDisplayNameValid,
11 isUserNSFWPolicyValid, 11 isUserNSFWPolicyValid,
@@ -76,9 +76,10 @@ const usersRemoveValidator = [
76 76
77const usersBlockingValidator = [ 77const usersBlockingValidator = [
78 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'), 78 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
79 body('reason').optional().custom(isUserBlockedReasonValid).withMessage('Should have a valid blocking reason'),
79 80
80 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 81 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
81 logger.debug('Checking usersRemove parameters', { parameters: req.params }) 82 logger.debug('Checking usersBlocking parameters', { parameters: req.params })
82 83
83 if (areValidationErrors(req, res)) return 84 if (areValidationErrors(req, res)) return
84 if (!await checkUserIdExist(req.params.id, res)) return 85 if (!await checkUserIdExist(req.params.id, res)) return
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index ea6d63312..81b0651fd 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -21,6 +21,7 @@ import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared'
21import { User, UserRole } from '../../../shared/models/users' 21import { User, UserRole } from '../../../shared/models/users'
22import { 22import {
23 isUserAutoPlayVideoValid, 23 isUserAutoPlayVideoValid,
24 isUserBlockedReasonValid,
24 isUserBlockedValid, 25 isUserBlockedValid,
25 isUserNSFWPolicyValid, 26 isUserNSFWPolicyValid,
26 isUserPasswordValid, 27 isUserPasswordValid,
@@ -107,6 +108,12 @@ export class UserModel extends Model<UserModel> {
107 @Column 108 @Column
108 blocked: boolean 109 blocked: boolean
109 110
111 @AllowNull(true)
112 @Default(null)
113 @Is('UserBlockedReason', value => throwIfNotValid(value, isUserBlockedReasonValid, 'blocked reason'))
114 @Column
115 blockedReason: string
116
110 @AllowNull(false) 117 @AllowNull(false)
111 @Is('UserRole', value => throwIfNotValid(value, isUserRoleValid, 'role')) 118 @Is('UserRole', value => throwIfNotValid(value, isUserRoleValid, 'role'))
112 @Column 119 @Column
@@ -284,6 +291,8 @@ export class UserModel extends Model<UserModel> {
284 roleLabel: USER_ROLE_LABELS[ this.role ], 291 roleLabel: USER_ROLE_LABELS[ this.role ],
285 videoQuota: this.videoQuota, 292 videoQuota: this.videoQuota,
286 createdAt: this.createdAt, 293 createdAt: this.createdAt,
294 blocked: this.blocked,
295 blockedReason: this.blockedReason,
287 account: this.Account.toFormattedJSON(), 296 account: this.Account.toFormattedJSON(),
288 videoChannels: [] 297 videoChannels: []
289 } 298 }
diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts
index a6319bb79..39f0c2cb2 100644
--- a/server/models/video/video-abuse.ts
+++ b/server/models/video/video-abuse.ts
@@ -57,7 +57,7 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
57 57
58 @AfterCreate 58 @AfterCreate
59 static sendEmailNotification (instance: VideoAbuseModel) { 59 static sendEmailNotification (instance: VideoAbuseModel) {
60 return Emailer.Instance.addVideoAbuseReport(instance.videoId) 60 return Emailer.Instance.addVideoAbuseReportJob(instance.videoId)
61 } 61 }
62 62
63 static listForApi (start: number, count: number, sort: string) { 63 static listForApi (start: number, count: number, sort: string) {
diff --git a/server/tests/api/server/email.ts b/server/tests/api/server/email.ts
index 4be013c84..65d6a759f 100644
--- a/server/tests/api/server/email.ts
+++ b/server/tests/api/server/email.ts
@@ -2,7 +2,17 @@
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { askResetPassword, createUser, reportVideoAbuse, resetPassword, runServer, uploadVideo, userLogin, wait } from '../../utils' 5import {
6 askResetPassword,
7 blockUser,
8 createUser,
9 reportVideoAbuse,
10 resetPassword,
11 runServer,
12 unblockUser,
13 uploadVideo,
14 userLogin
15} from '../../utils'
6import { flushTests, killallServers, ServerInfo, setAccessTokensToServers } from '../../utils/index' 16import { flushTests, killallServers, ServerInfo, setAccessTokensToServers } from '../../utils/index'
7import { mockSmtpServer } from '../../utils/miscs/email' 17import { mockSmtpServer } from '../../utils/miscs/email'
8import { waitJobs } from '../../utils/server/jobs' 18import { waitJobs } from '../../utils/server/jobs'
@@ -112,6 +122,42 @@ describe('Test emails', function () {
112 }) 122 })
113 }) 123 })
114 124
125 describe('When blocking/unblocking user', async function () {
126 it('Should send the notification email when blocking a user', async function () {
127 this.timeout(10000)
128
129 const reason = 'my super bad reason'
130 await blockUser(server.url, userId, server.accessToken, 204, reason)
131
132 await waitJobs(server)
133 expect(emails).to.have.lengthOf(3)
134
135 const email = emails[2]
136
137 expect(email['from'][0]['address']).equal('test-admin@localhost')
138 expect(email['to'][0]['address']).equal('user_1@example.com')
139 expect(email['subject']).contains(' blocked')
140 expect(email['text']).contains(' blocked')
141 expect(email['text']).contains(reason)
142 })
143
144 it('Should send the notification email when unblocking a user', async function () {
145 this.timeout(10000)
146
147 await unblockUser(server.url, userId, server.accessToken, 204)
148
149 await waitJobs(server)
150 expect(emails).to.have.lengthOf(4)
151
152 const email = emails[3]
153
154 expect(email['from'][0]['address']).equal('test-admin@localhost')
155 expect(email['to'][0]['address']).equal('user_1@example.com')
156 expect(email['subject']).contains(' unblocked')
157 expect(email['text']).contains(' unblocked')
158 })
159 })
160
115 after(async function () { 161 after(async function () {
116 killallServers([ server ]) 162 killallServers([ server ])
117 }) 163 })
diff --git a/server/tests/utils/users/users.ts b/server/tests/utils/users/users.ts
index 7e15fc86e..f786de6e3 100644
--- a/server/tests/utils/users/users.ts
+++ b/server/tests/utils/users/users.ts
@@ -134,11 +134,14 @@ function removeUser (url: string, userId: number | string, accessToken: string,
134 .expect(expectedStatus) 134 .expect(expectedStatus)
135} 135}
136 136
137function blockUser (url: string, userId: number | string, accessToken: string, expectedStatus = 204) { 137function blockUser (url: string, userId: number | string, accessToken: string, expectedStatus = 204, reason?: string) {
138 const path = '/api/v1/users' 138 const path = '/api/v1/users'
139 let body: any
140 if (reason) body = { reason }
139 141
140 return request(url) 142 return request(url)
141 .post(path + '/' + userId + '/block') 143 .post(path + '/' + userId + '/block')
144 .send(body)
142 .set('Accept', 'application/json') 145 .set('Accept', 'application/json')
143 .set('Authorization', 'Bearer ' + accessToken) 146 .set('Authorization', 'Bearer ' + accessToken)
144 .expect(expectedStatus) 147 .expect(expectedStatus)