diff options
Diffstat (limited to 'server')
-rw-r--r-- | server/controllers/api/users.ts | 8 | ||||
-rw-r--r-- | server/helpers/custom-validators/users.ts | 5 | ||||
-rw-r--r-- | server/initializers/constants.ts | 3 | ||||
-rw-r--r-- | server/initializers/migrations/0245-user-blocked.ts | 14 | ||||
-rw-r--r-- | server/lib/emailer.ts | 23 | ||||
-rw-r--r-- | server/middlewares/validators/users.ts | 5 | ||||
-rw-r--r-- | server/models/account/user.ts | 9 | ||||
-rw-r--r-- | server/models/video/video-abuse.ts | 2 | ||||
-rw-r--r-- | server/tests/api/server/email.ts | 48 | ||||
-rw-r--r-- | server/tests/utils/users/users.ts | 5 |
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 | ||
303 | async function blockUser (req: express.Request, res: express.Response, next: express.NextFunction) { | 303 | async 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 | ||
457 | async function changeUserBlock (res: express.Response, user: UserModel, block: boolean) { | 458 | async 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 | ||
45 | function isUserBlockedReasonValid (value: any) { | ||
46 | return value === null || (exists(value) && validator.isLength(value, CONSTRAINTS_FIELDS.USERS.BLOCKED_REASON)) | ||
47 | } | ||
48 | |||
45 | function isUserRoleValid (value: any) { | 49 | function 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[] } | | |||
59 | export { | 63 | export { |
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 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { createClient } from 'redis' | 2 | import { CONSTRAINTS_FIELDS } from '../constants' |
3 | import { CONFIG } from '../constants' | ||
4 | import { JobQueue } from '../../lib/job-queue' | ||
5 | import { initDatabaseModels } from '../database' | ||
6 | 3 | ||
7 | async function up (utils: { | 4 | async 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 | ||
36 | function down (options) { | 42 | function 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' | |||
5 | import { omit } from 'lodash' | 5 | import { omit } from 'lodash' |
6 | import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' | 6 | import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' |
7 | import { | 7 | import { |
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 | ||
77 | const usersBlockingValidator = [ | 77 | const 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' | |||
21 | import { User, UserRole } from '../../../shared/models/users' | 21 | import { User, UserRole } from '../../../shared/models/users' |
22 | import { | 22 | import { |
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 | ||
3 | import * as chai from 'chai' | 3 | import * as chai from 'chai' |
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { askResetPassword, createUser, reportVideoAbuse, resetPassword, runServer, uploadVideo, userLogin, wait } from '../../utils' | 5 | import { |
6 | askResetPassword, | ||
7 | blockUser, | ||
8 | createUser, | ||
9 | reportVideoAbuse, | ||
10 | resetPassword, | ||
11 | runServer, | ||
12 | unblockUser, | ||
13 | uploadVideo, | ||
14 | userLogin | ||
15 | } from '../../utils' | ||
6 | import { flushTests, killallServers, ServerInfo, setAccessTokensToServers } from '../../utils/index' | 16 | import { flushTests, killallServers, ServerInfo, setAccessTokensToServers } from '../../utils/index' |
7 | import { mockSmtpServer } from '../../utils/miscs/email' | 17 | import { mockSmtpServer } from '../../utils/miscs/email' |
8 | import { waitJobs } from '../../utils/server/jobs' | 18 | import { 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 | ||
137 | function blockUser (url: string, userId: number | string, accessToken: string, expectedStatus = 204) { | 137 | function 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) |