]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Add reason when banning a user
authorChocobozzz <me@florianbigard.com>
Wed, 8 Aug 2018 15:36:10 +0000 (17:36 +0200)
committerChocobozzz <me@florianbigard.com>
Wed, 8 Aug 2018 15:44:22 +0000 (17:44 +0200)
24 files changed:
client/src/app/+admin/users/user-list/user-list.component.html
client/src/app/+admin/users/user-list/user-list.component.ts
client/src/app/shared/buttons/action-dropdown.component.html [new file with mode: 0644]
client/src/app/shared/buttons/action-dropdown.component.scss [new file with mode: 0644]
client/src/app/shared/buttons/action-dropdown.component.ts [new file with mode: 0644]
client/src/app/shared/buttons/button.component.scss [moved from client/src/app/shared/misc/button.component.scss with 100% similarity]
client/src/app/shared/buttons/delete-button.component.html [new file with mode: 0644]
client/src/app/shared/buttons/delete-button.component.ts [moved from client/src/app/shared/misc/delete-button.component.ts with 89% similarity]
client/src/app/shared/buttons/edit-button.component.html [moved from client/src/app/shared/misc/edit-button.component.html with 50% similarity]
client/src/app/shared/buttons/edit-button.component.ts [moved from client/src/app/shared/misc/edit-button.component.ts with 90% similarity]
client/src/app/shared/misc/delete-button.component.html [deleted file]
client/src/app/shared/shared.module.ts
client/src/app/shared/users/user.model.ts
server/controllers/api/users.ts
server/helpers/custom-validators/users.ts
server/initializers/constants.ts
server/initializers/migrations/0245-user-blocked.ts
server/lib/emailer.ts
server/middlewares/validators/users.ts
server/models/account/user.ts
server/models/video/video-abuse.ts
server/tests/api/server/email.ts
server/tests/utils/users/users.ts
shared/models/users/user.model.ts

index 166fafef0ff8064dc4b899105d0fce2cae8a7a19..ef5a6c6489646a216bd5b80791ecab36df04e6d6 100644 (file)
@@ -30,8 +30,9 @@
       <td>{{ user.roleLabel }}</td>
       <td>{{ user.createdAt }}</td>
       <td class="action-cell">
-        <my-edit-button [routerLink]="getRouterUserEditLink(user)"></my-edit-button>
-        <my-delete-button (click)="removeUser(user)"></my-delete-button>
+        <my-action-dropdown i18n-label label="Actions" [actions]="userActions" [entry]="user"></my-action-dropdown>
+        <!--<my-edit-button [routerLink]="getRouterUserEditLink(user)"></my-edit-button>-->
+        <!--<my-delete-button (click)="removeUser(user)"></my-delete-button>-->
       </td>
     </tr>
   </ng-template>
index ab25608c1775765689c5695386214b4e1acb69c9..3c83859e0ed963278b9e9c4596d4deeb48408131 100644 (file)
@@ -5,6 +5,7 @@ import { ConfirmService } from '../../../core'
 import { RestPagination, RestTable, User } from '../../../shared'
 import { UserService } from '../shared'
 import { I18n } from '@ngx-translate/i18n-polyfill'
+import { DropdownAction } from '@app/shared/buttons/action-dropdown.component'
 
 @Component({
   selector: 'my-user-list',
@@ -17,6 +18,7 @@ export class UserListComponent extends RestTable implements OnInit {
   rowsPerPage = 10
   sort: SortMeta = { field: 'createdAt', order: 1 }
   pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
+  userActions: DropdownAction<User>[] = []
 
   constructor (
     private notificationsService: NotificationsService,
@@ -25,6 +27,17 @@ export class UserListComponent extends RestTable implements OnInit {
     private i18n: I18n
   ) {
     super()
+
+    this.userActions = [
+      {
+        type: 'edit',
+        linkBuilder: this.getRouterUserEditLink
+      },
+      {
+        type: 'delete',
+        handler: user => this.removeUser(user)
+      }
+    ]
   }
 
   ngOnInit () {
diff --git a/client/src/app/shared/buttons/action-dropdown.component.html b/client/src/app/shared/buttons/action-dropdown.component.html
new file mode 100644 (file)
index 0000000..c87ba4c
--- /dev/null
@@ -0,0 +1,16 @@
+<div class="dropdown-root" dropdown container="body" dropup="true" placement="right" role="button">
+  <div class="action-button" dropdownToggle>
+    <span class="icon icon-action"></span>
+  </div>
+
+  <ul *dropdownMenu class="dropdown-menu" id="more-menu" role="menu" aria-labelledby="single-button">
+    <li role="menuitem" *ngFor="let action of actions">
+      <my-delete-button *ngIf="action.type === 'delete'" [label]="action.label" (click)="action.handler(entry)"></my-delete-button>
+      <my-edit-button *ngIf="action.type === 'edit'" [label]="action.label" [routerLink]="action.linkBuilder(entry)"></my-edit-button>
+
+      <a *ngIf="action.type === 'custom'" class="dropdown-item" href="#" (click)="action.handler(entry)">
+        <span *ngIf="action.iconClass" class="icon" [ngClass]="action.iconClass"></span> <ng-container>{{ action.label }}</ng-container>
+      </a>
+    </li>
+  </ul>
+</div>
\ No newline at end of file
diff --git a/client/src/app/shared/buttons/action-dropdown.component.scss b/client/src/app/shared/buttons/action-dropdown.component.scss
new file mode 100644 (file)
index 0000000..cc459b9
--- /dev/null
@@ -0,0 +1,21 @@
+@import '_variables';
+@import '_mixins';
+
+.action-button {
+  @include peertube-button;
+  @include grey-button;
+
+  &:hover, &:active, &:focus {
+    background-color: $grey-color;
+  }
+
+  display: inline-block;
+  padding: 0 10px;
+
+  .icon-action {
+    @include icon(21px);
+
+    background-image: url('../../../assets/images/video/more.svg');
+    top: -1px;
+  }
+}
\ No newline at end of file
diff --git a/client/src/app/shared/buttons/action-dropdown.component.ts b/client/src/app/shared/buttons/action-dropdown.component.ts
new file mode 100644 (file)
index 0000000..407d24b
--- /dev/null
@@ -0,0 +1,20 @@
+import { Component, Input } from '@angular/core'
+
+export type DropdownAction<T> = {
+  type: 'custom' | 'delete' | 'edit'
+  label?: string
+  handler?: (T) => any
+  linkBuilder?: (T) => (string | number)[]
+  iconClass?: string
+}
+
+@Component({
+  selector: 'my-action-dropdown',
+  styleUrls: [ './action-dropdown.component.scss' ],
+  templateUrl: './action-dropdown.component.html'
+})
+
+export class ActionDropdownComponent<T> {
+  @Input() actions: DropdownAction<T>[] = []
+  @Input() entry: T
+}
diff --git a/client/src/app/shared/buttons/delete-button.component.html b/client/src/app/shared/buttons/delete-button.component.html
new file mode 100644 (file)
index 0000000..7924902
--- /dev/null
@@ -0,0 +1,6 @@
+<span class="action-button action-button-delete" [title]="label" role="button">
+  <span class="icon icon-delete-grey"></span>
+
+  <span class="button-label" *ngIf="label">{{ label }}</span>
+  <span class="button-label" i18n *ngIf="!label">Delete</span>
+</span>
similarity index 89%
rename from client/src/app/shared/misc/delete-button.component.ts
rename to client/src/app/shared/buttons/delete-button.component.ts
index 2ffd98212727f1327b402e64902dcb4a2bdce871..cd2bcccdf9e31163b1b2d4d6a34cff854573db41 100644 (file)
@@ -7,5 +7,5 @@ import { Component, Input } from '@angular/core'
 })
 
 export class DeleteButtonComponent {
-  @Input() label = 'Delete'
+  @Input() label: string
 }
similarity index 50%
rename from client/src/app/shared/misc/edit-button.component.html
rename to client/src/app/shared/buttons/edit-button.component.html
index 78fbc326e6a28140926d09ddb70b29681d1fcb94..7efc54ce72a9bd6a6e375bb44363c575fdadebbb 100644 (file)
@@ -1,4 +1,6 @@
 <a class="action-button action-button-edit" [routerLink]="routerLink" title="Edit">
   <span class="icon icon-edit"></span>
-  <span i18n class="button-label">Edit</span>
+
+  <span class="button-label" *ngIf="label">{{ label }}</span>
+  <span i18n class="button-label" *ngIf="!label">Edit</span>
 </a>
similarity index 90%
rename from client/src/app/shared/misc/edit-button.component.ts
rename to client/src/app/shared/buttons/edit-button.component.ts
index 201a618ecdda8df5620d7883d630bde36b4003fe..7abaacc26b9e0751c009ca83fbe292ce0f747870 100644 (file)
@@ -7,5 +7,6 @@ import { Component, Input } from '@angular/core'
 })
 
 export class EditButtonComponent {
+  @Input() label: string
   @Input() routerLink = []
 }
diff --git a/client/src/app/shared/misc/delete-button.component.html b/client/src/app/shared/misc/delete-button.component.html
deleted file mode 100644 (file)
index 7387d0a..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-<span class="action-button action-button-delete" [title]="label">
-  <span class="icon icon-delete-grey"></span>
-  <span class="button-label">{{ label }}</span>
-</span>
index 62ce97102746d98b84ecb9b2b9e963cdadd77e68..94de3af9f42a2d0965a801444dda70e0aeba36f8 100644 (file)
@@ -17,8 +17,8 @@ import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes'
 import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared'
 
 import { AUTH_INTERCEPTOR_PROVIDER } from './auth'
-import { DeleteButtonComponent } from './misc/delete-button.component'
-import { EditButtonComponent } from './misc/edit-button.component'
+import { DeleteButtonComponent } from './buttons/delete-button.component'
+import { EditButtonComponent } from './buttons/edit-button.component'
 import { FromNowPipe } from './misc/from-now.pipe'
 import { LoaderComponent } from './misc/loader.component'
 import { NumberFormatterPipe } from './misc/number-formatter.pipe'
@@ -52,6 +52,7 @@ import { VideoCaptionsValidatorsService } from '@app/shared/forms/form-validator
 import { VideoCaptionService } from '@app/shared/video-caption'
 import { PeertubeCheckboxComponent } from '@app/shared/forms/peertube-checkbox.component'
 import { VideoImportService } from '@app/shared/video-import/video-import.service'
+import { ActionDropdownComponent } from '@app/shared/buttons/action-dropdown.component'
 
 @NgModule({
   imports: [
@@ -78,6 +79,7 @@ import { VideoImportService } from '@app/shared/video-import/video-import.servic
     VideoFeedComponent,
     DeleteButtonComponent,
     EditButtonComponent,
+    ActionDropdownComponent,
     NumberFormatterPipe,
     ObjectLengthPipe,
     FromNowPipe,
@@ -110,6 +112,7 @@ import { VideoImportService } from '@app/shared/video-import/video-import.servic
     VideoFeedComponent,
     DeleteButtonComponent,
     EditButtonComponent,
+    ActionDropdownComponent,
     MarkdownTextareaComponent,
     InfiniteScrollerDirective,
     HelpComponent,
index 581ea785964d2dee22ac251d5f2571f0adb7ca91..2748001d046c57eb4718d913feb82218f8f94ac3 100644 (file)
@@ -7,7 +7,6 @@ import {
   VideoChannel
 } from '../../../../../shared'
 import { NSFWPolicyType } from '../../../../../shared/models/videos/nsfw-policy.type'
-import { Actor } from '@app/shared/actor/actor.model'
 import { Account } from '@app/shared/account/account.model'
 import { Avatar } from '../../../../../shared/models/avatars/avatar.model'
 
@@ -22,6 +21,9 @@ export type UserConstructorHash = {
   createdAt?: Date,
   account?: AccountServerModel,
   videoChannels?: VideoChannel[]
+
+  blocked?: boolean
+  blockedReason?: string
 }
 export class User implements UserServerModel {
   id: number
@@ -35,35 +37,26 @@ export class User implements UserServerModel {
   videoChannels: VideoChannel[]
   createdAt: Date
 
+  blocked: boolean
+  blockedReason?: string
+
   constructor (hash: UserConstructorHash) {
     this.id = hash.id
     this.username = hash.username
     this.email = hash.email
     this.role = hash.role
 
+    this.videoChannels = hash.videoChannels
+    this.videoQuota = hash.videoQuota
+    this.nsfwPolicy = hash.nsfwPolicy
+    this.autoPlayVideo = hash.autoPlayVideo
+    this.createdAt = hash.createdAt
+    this.blocked = hash.blocked
+    this.blockedReason = hash.blockedReason
+
     if (hash.account !== undefined) {
       this.account = new Account(hash.account)
     }
-
-    if (hash.videoChannels !== undefined) {
-      this.videoChannels = hash.videoChannels
-    }
-
-    if (hash.videoQuota !== undefined) {
-      this.videoQuota = hash.videoQuota
-    }
-
-    if (hash.nsfwPolicy !== undefined) {
-      this.nsfwPolicy = hash.nsfwPolicy
-    }
-
-    if (hash.autoPlayVideo !== undefined) {
-      this.autoPlayVideo = hash.autoPlayVideo
-    }
-
-    if (hash.createdAt !== undefined) {
-      this.createdAt = hash.createdAt
-    }
   }
 
   get accountAvatarUrl () {
index 8f429d0b5f128089b18b5487ec578f1eaac53fb2..0e2be71231b75adc02bc9642936c780518ded90e 100644 (file)
@@ -302,8 +302,9 @@ async function unblockUser (req: express.Request, res: express.Response, next: e
 
 async function blockUser (req: express.Request, res: express.Response, next: express.NextFunction) {
   const user: UserModel = res.locals.user
+  const reason = req.body.reason
 
-  await changeUserBlock(res, user, true)
+  await changeUserBlock(res, user, true, reason)
 
   return res.status(204).end()
 }
@@ -454,10 +455,11 @@ function success (req: express.Request, res: express.Response, next: express.Nex
   res.end()
 }
 
-async function changeUserBlock (res: express.Response, user: UserModel, block: boolean) {
+async function changeUserBlock (res: express.Response, user: UserModel, block: boolean, reason?: string) {
   const oldUserAuditView = new UserAuditView(user.toFormattedJSON())
 
   user.blocked = block
+  user.blockedReason = reason || null
 
   await sequelizeTypescript.transaction(async t => {
     await OAuthTokenModel.deleteUserToken(user.id, t)
@@ -465,6 +467,8 @@ async function changeUserBlock (res: express.Response, user: UserModel, block: b
     await user.save({ transaction: t })
   })
 
+  await Emailer.Instance.addUserBlockJob(user, block, reason)
+
   auditLogger.update(
     res.locals.oauth.token.User.Account.Actor.getIdentifier(),
     new UserAuditView(user.toFormattedJSON()),
index 4a0d79ae58fff1c9fff770fbd61802cd198356f6..c3cdefd4edfbd812ee648977f593c4526acbe3d2 100644 (file)
@@ -42,6 +42,10 @@ function isUserBlockedValid (value: any) {
   return isBooleanValid(value)
 }
 
+function isUserBlockedReasonValid (value: any) {
+  return value === null || (exists(value) && validator.isLength(value, CONSTRAINTS_FIELDS.USERS.BLOCKED_REASON))
+}
+
 function isUserRoleValid (value: any) {
   return exists(value) && validator.isInt('' + value) && UserRole[value] !== undefined
 }
@@ -59,6 +63,7 @@ function isAvatarFile (files: { [ fieldname: string ]: Express.Multer.File[] } |
 export {
   isUserBlockedValid,
   isUserPasswordValid,
+  isUserBlockedReasonValid,
   isUserRoleValid,
   isUserVideoQuotaValid,
   isUserUsernameValid,
index 0a651beed353958e7476160d89392ebac9b931c7..ea561b686faa701a6ff8742227f26ae6d41ccfaa 100644 (file)
@@ -254,7 +254,8 @@ const CONSTRAINTS_FIELDS = {
     DESCRIPTION: { min: 3, max: 250 }, // Length
     USERNAME: { min: 3, max: 20 }, // Length
     PASSWORD: { min: 6, max: 255 }, // Length
-    VIDEO_QUOTA: { min: -1 }
+    VIDEO_QUOTA: { min: -1 },
+    BLOCKED_REASON: { min: 3, max: 250 } // Length
   },
   VIDEO_ABUSES: {
     REASON: { min: 2, max: 300 } // Length
index 67afea5ed3ee8ed95c2cb3d57fefd9187287923f..5a04ecd2b2bcccb0b0e6bf36453ebd81770b4685 100644 (file)
@@ -1,8 +1,5 @@
 import * as Sequelize from 'sequelize'
-import { createClient } from 'redis'
-import { CONFIG } from '../constants'
-import { JobQueue } from '../../lib/job-queue'
-import { initDatabaseModels } from '../database'
+import { CONSTRAINTS_FIELDS } from '../constants'
 
 async function up (utils: {
   transaction: Sequelize.Transaction
@@ -31,6 +28,15 @@ async function up (utils: {
     }
     await utils.queryInterface.changeColumn('user', 'blocked', data)
   }
+
+  {
+    const data = {
+      type: Sequelize.STRING(CONSTRAINTS_FIELDS.USERS.BLOCKED_REASON.max),
+      allowNull: true,
+      defaultValue: null
+    }
+    await utils.queryInterface.addColumn('user', 'blockedReason', data)
+  }
 }
 
 function down (options) {
index ded321bf70c4acd186c0225779a10810d7af5d56..3faeffd7705c3c58bd5b96c0987cbaa01781ee36 100644 (file)
@@ -89,7 +89,7 @@ class Emailer {
     return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
   }
 
-  async addVideoAbuseReport (videoId: number) {
+  async addVideoAbuseReportJob (videoId: number) {
     const video = await VideoModel.load(videoId)
     if (!video) throw new Error('Unknown Video id during Abuse report.')
 
@@ -108,6 +108,27 @@ class Emailer {
     return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
   }
 
+  addUserBlockJob (user: UserModel, blocked: boolean, reason?: string) {
+    const reasonString = reason ? ` for the following reason: ${reason}` : ''
+    const blockedWord = blocked ? 'blocked' : 'unblocked'
+    const blockedString = `Your account ${user.username} on ${CONFIG.WEBSERVER.HOST} has been ${blockedWord}${reasonString}.`
+
+    const text = 'Hi,\n\n' +
+      blockedString +
+      '\n\n' +
+      'Cheers,\n' +
+      `PeerTube.`
+
+    const to = user.email
+    const emailPayload: EmailPayload = {
+      to: [ to ],
+      subject: '[PeerTube] Account ' + blockedWord,
+      text
+    }
+
+    return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
+  }
+
   sendMail (to: string[], subject: string, text: string) {
     if (!this.transporter) {
       throw new Error('Cannot send mail because SMTP is not configured.')
index 94d8ab53bf8c6f622727e272b3bfed6275152214..771c414a023d10e8b5403938e751247e72d497db 100644 (file)
@@ -5,7 +5,7 @@ import { body, param } from 'express-validator/check'
 import { omit } from 'lodash'
 import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc'
 import {
-  isUserAutoPlayVideoValid,
+  isUserAutoPlayVideoValid, isUserBlockedReasonValid,
   isUserDescriptionValid,
   isUserDisplayNameValid,
   isUserNSFWPolicyValid,
@@ -76,9 +76,10 @@ const usersRemoveValidator = [
 
 const usersBlockingValidator = [
   param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
+  body('reason').optional().custom(isUserBlockedReasonValid).withMessage('Should have a valid blocking reason'),
 
   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
-    logger.debug('Checking usersRemove parameters', { parameters: req.params })
+    logger.debug('Checking usersBlocking parameters', { parameters: req.params })
 
     if (areValidationErrors(req, res)) return
     if (!await checkUserIdExist(req.params.id, res)) return
index ea6d63312314fcdc264c72da1c3358465ef89c54..81b0651fdf915fc3193f8b62752991ce5c559b9a 100644 (file)
@@ -21,6 +21,7 @@ import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared'
 import { User, UserRole } from '../../../shared/models/users'
 import {
   isUserAutoPlayVideoValid,
+  isUserBlockedReasonValid,
   isUserBlockedValid,
   isUserNSFWPolicyValid,
   isUserPasswordValid,
@@ -107,6 +108,12 @@ export class UserModel extends Model<UserModel> {
   @Column
   blocked: boolean
 
+  @AllowNull(true)
+  @Default(null)
+  @Is('UserBlockedReason', value => throwIfNotValid(value, isUserBlockedReasonValid, 'blocked reason'))
+  @Column
+  blockedReason: string
+
   @AllowNull(false)
   @Is('UserRole', value => throwIfNotValid(value, isUserRoleValid, 'role'))
   @Column
@@ -284,6 +291,8 @@ export class UserModel extends Model<UserModel> {
       roleLabel: USER_ROLE_LABELS[ this.role ],
       videoQuota: this.videoQuota,
       createdAt: this.createdAt,
+      blocked: this.blocked,
+      blockedReason: this.blockedReason,
       account: this.Account.toFormattedJSON(),
       videoChannels: []
     }
index a6319bb79186314d8304fb416b88aae2079b412f..39f0c2cb2c6ae605e733d8d70b8762b4f110f0c9 100644 (file)
@@ -57,7 +57,7 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
 
   @AfterCreate
   static sendEmailNotification (instance: VideoAbuseModel) {
-    return Emailer.Instance.addVideoAbuseReport(instance.videoId)
+    return Emailer.Instance.addVideoAbuseReportJob(instance.videoId)
   }
 
   static listForApi (start: number, count: number, sort: string) {
index 4be013c84e9f3f170d7be2090c36c081618822af..65d6a759f6507915142cfb36982ed12d844de3e5 100644 (file)
@@ -2,7 +2,17 @@
 
 import * as chai from 'chai'
 import 'mocha'
-import { askResetPassword, createUser, reportVideoAbuse, resetPassword, runServer, uploadVideo, userLogin, wait } from '../../utils'
+import {
+  askResetPassword,
+  blockUser,
+  createUser,
+  reportVideoAbuse,
+  resetPassword,
+  runServer,
+  unblockUser,
+  uploadVideo,
+  userLogin
+} from '../../utils'
 import { flushTests, killallServers, ServerInfo, setAccessTokensToServers } from '../../utils/index'
 import { mockSmtpServer } from '../../utils/miscs/email'
 import { waitJobs } from '../../utils/server/jobs'
@@ -112,6 +122,42 @@ describe('Test emails', function () {
     })
   })
 
+  describe('When blocking/unblocking user', async function () {
+    it('Should send the notification email when blocking a user', async function () {
+      this.timeout(10000)
+
+      const reason = 'my super bad reason'
+      await blockUser(server.url, userId, server.accessToken, 204, reason)
+
+      await waitJobs(server)
+      expect(emails).to.have.lengthOf(3)
+
+      const email = emails[2]
+
+      expect(email['from'][0]['address']).equal('test-admin@localhost')
+      expect(email['to'][0]['address']).equal('user_1@example.com')
+      expect(email['subject']).contains(' blocked')
+      expect(email['text']).contains(' blocked')
+      expect(email['text']).contains(reason)
+    })
+
+    it('Should send the notification email when unblocking a user', async function () {
+      this.timeout(10000)
+
+      await unblockUser(server.url, userId, server.accessToken, 204)
+
+      await waitJobs(server)
+      expect(emails).to.have.lengthOf(4)
+
+      const email = emails[3]
+
+      expect(email['from'][0]['address']).equal('test-admin@localhost')
+      expect(email['to'][0]['address']).equal('user_1@example.com')
+      expect(email['subject']).contains(' unblocked')
+      expect(email['text']).contains(' unblocked')
+    })
+  })
+
   after(async function () {
     killallServers([ server ])
   })
index 7e15fc86ec012d46f092df132242f097a76f19c5..f786de6e31726e9845435b984f757b721314a161 100644 (file)
@@ -134,11 +134,14 @@ function removeUser (url: string, userId: number | string, accessToken: string,
           .expect(expectedStatus)
 }
 
-function blockUser (url: string, userId: number | string, accessToken: string, expectedStatus = 204) {
+function blockUser (url: string, userId: number | string, accessToken: string, expectedStatus = 204, reason?: string) {
   const path = '/api/v1/users'
+  let body: any
+  if (reason) body = { reason }
 
   return request(url)
     .post(path + '/' + userId + '/block')
+    .send(body)
     .set('Accept', 'application/json')
     .set('Authorization', 'Bearer ' + accessToken)
     .expect(expectedStatus)
index 188e29ede5fda28d8f002db85b133dba25e8baf0..d3085267f713f648057895c42a03454b7abbb962 100644 (file)
@@ -14,4 +14,7 @@ export interface User {
   createdAt: Date
   account: Account
   videoChannels?: VideoChannel[]
+
+  blocked: boolean
+  blockedReason?: string
 }