Add user/instance block by users in the client
authorChocobozzz <me@florianbigard.com>
Fri, 12 Oct 2018 15:26:40 +0000 (17:26 +0200)
committerChocobozzz <me@florianbigard.com>
Tue, 16 Oct 2018 14:41:36 +0000 (16:41 +0200)
33 files changed:
client/src/app/+accounts/accounts.component.html
client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html
client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html
client/src/app/+admin/users/user-list/user-list.component.html
client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.html [new file with mode: 0644]
client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.scss [new file with mode: 0644]
client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.ts [new file with mode: 0644]
client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.html [new file with mode: 0644]
client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.scss [new file with mode: 0644]
client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.ts [new file with mode: 0644]
client/src/app/+my-account/my-account-routing.module.ts
client/src/app/+my-account/my-account.component.html
client/src/app/+my-account/my-account.component.scss
client/src/app/+my-account/my-account.component.ts
client/src/app/+my-account/my-account.module.ts
client/src/app/shared/account/account.model.ts
client/src/app/shared/blocklist/account-block.model.ts [new file with mode: 0644]
client/src/app/shared/blocklist/blocklist.service.ts [new file with mode: 0644]
client/src/app/shared/blocklist/index.ts [new file with mode: 0644]
client/src/app/shared/buttons/action-dropdown.component.html
client/src/app/shared/buttons/action-dropdown.component.scss
client/src/app/shared/moderation/user-moderation-dropdown.component.html
client/src/app/shared/moderation/user-moderation-dropdown.component.ts
client/src/app/shared/shared.module.ts
server/controllers/api/users/my-blocklist.ts
server/lib/blocklist.ts
server/middlewares/validators/blocklist.ts
server/models/account/account-blocklist.ts
server/models/server/server-blocklist.ts
server/tests/api/check-params/blocklist.ts
server/tests/api/users/account-blocklist.ts
shared/models/blocklist/account-block.model.ts
shared/models/blocklist/server-block.model.ts

index 036e794d268bae2f5fd2d9ded01d7f216fe36383..60dbcdf1db31c41c62a330d2f3d7cddc67d33c82 100644 (file)
           <div class="actor-name">{{ account.nameWithHost }}</div>
 
           <span *ngIf="user?.blocked" [ngbTooltip]="user.blockedReason" class="badge badge-danger" i18n>Banned</span>
+          <span *ngIf="account.muted" class="badge badge-danger" i18n>Muted</span>
+          <span *ngIf="account.mutedServer" class="badge badge-danger" i18n>Instance muted</span>
 
-          <my-user-moderation-dropdown buttonSize="small" [user]="user" (userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()">
+          <my-user-moderation-dropdown
+            buttonSize="small" [account]="account" [user]="user"
+            (userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()"
+          >
           </my-user-moderation-dropdown>
         </div>
         <div i18n class="actor-followers">{{ account.followersCount }} subscribers</div>
index 287ab3e46a58607c1808de17747850347c500e49..0374b70ef26c7c0e06fe44298565721e71bb19b8 100644 (file)
@@ -9,7 +9,7 @@
       <th i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
       <th i18n>Video</th>
       <th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th>
-      <th style="width: 50px;"></th>
+      <th style="width: 120px;"></th>
     </tr>
   </ng-template>
 
index 0585e0490ce20b2dd9292229f2c9518f941f279c..ff4543b975334a41fe4b4cebbbe55b85b8adfa4e 100644 (file)
@@ -8,7 +8,7 @@
       <th i18n pSortableColumn="name">Video name <p-sortIcon field="name"></p-sortIcon></th>
       <th i18n>Sensitive</th>
       <th i18n pSortableColumn="createdAt">Date <p-sortIcon field="createdAt"></p-sortIcon></th>
-      <th style="width: 50px;"></th>
+      <th style="width: 120px;"></th>
     </tr>
   </ng-template>
 
index afa9ccfe4319f7c922fd80eaf6484fb6730221e9..eb8d30e17f6a3ac10bc403709f132ad04a50a5a3 100644 (file)
       </td>
 
       <td>
-        {{ user.username }}
-        <span *ngIf="user.blocked" class="banned-info">(banned)</span>
+        <a i18n-title title="Go to the account page" target="_blank" rel="noopener noreferrer" [routerLink]="[ '/accounts/' + user.username ]">
+          {{ user.username }}
+          <span i18n *ngIf="user.blocked" class="banned-info">(banned)</span>
+        </a>
       </td>
       <td>{{ user.email }}</td>
       <td>{{ user.videoQuotaUsed }} / {{ user.videoQuota }}</td>
diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.html b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.html
new file mode 100644 (file)
index 0000000..a96a11f
--- /dev/null
@@ -0,0 +1,26 @@
+<div class="admin-sub-header">
+  <div i18n class="form-sub-title">Muted accounts</div>
+</div>
+
+<p-table
+  [value]="blockedAccounts" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
+  [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
+>
+
+  <ng-template pTemplate="header">
+    <tr>
+      <th i18n>Account</th>
+      <th i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
+    </tr>
+  </ng-template>
+
+  <ng-template pTemplate="body" let-accountBlock>
+    <tr>
+      <td>{{ accountBlock.blockedAccount.nameWithHost }}</td>
+      <td>{{ accountBlock.createdAt }}</td>
+      <td class="action-cell">
+        <button class="unblock-button" (click)="unblockAccount(accountBlock)" i18n>Unmute</button>
+      </td>
+    </tr>
+  </ng-template>
+</p-table>
diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.scss b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.scss
new file mode 100644 (file)
index 0000000..6028b75
--- /dev/null
@@ -0,0 +1,7 @@
+@import '_variables';
+@import '_mixins';
+
+.unblock-button {
+  @include peertube-button;
+  @include grey-button;
+}
\ No newline at end of file
diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.ts b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.ts
new file mode 100644 (file)
index 0000000..fbad284
--- /dev/null
@@ -0,0 +1,59 @@
+import { Component, OnInit } from '@angular/core'
+import { NotificationsService } from 'angular2-notifications'
+import { I18n } from '@ngx-translate/i18n-polyfill'
+import { RestPagination, RestTable } from '@app/shared'
+import { SortMeta } from 'primeng/components/common/sortmeta'
+import { BlocklistService, AccountBlock } from '@app/shared/blocklist'
+
+@Component({
+  selector: 'my-account-blocklist',
+  styleUrls: [ './my-account-blocklist.component.scss' ],
+  templateUrl: './my-account-blocklist.component.html'
+})
+export class MyAccountBlocklistComponent extends RestTable implements OnInit {
+  blockedAccounts: AccountBlock[] = []
+  totalRecords = 0
+  rowsPerPage = 10
+  sort: SortMeta = { field: 'createdAt', order: -1 }
+  pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
+
+  constructor (
+    private notificationsService: NotificationsService,
+    private blocklistService: BlocklistService,
+    private i18n: I18n
+  ) {
+    super()
+  }
+
+  ngOnInit () {
+    this.initialize()
+  }
+
+  unblockAccount (accountBlock: AccountBlock) {
+    const blockedAccount = accountBlock.blockedAccount
+
+    this.blocklistService.unblockAccountByUser(blockedAccount)
+        .subscribe(
+          () => {
+            this.notificationsService.success(
+              this.i18n('Success'),
+              this.i18n('Account {{nameWithHost}} unmuted.', { nameWithHost: blockedAccount.nameWithHost })
+            )
+
+            this.loadData()
+          }
+        )
+  }
+
+  protected loadData () {
+    return this.blocklistService.getUserAccountBlocklist(this.pagination, this.sort)
+      .subscribe(
+        resultList => {
+          this.blockedAccounts = resultList.data
+          this.totalRecords = resultList.total
+        },
+
+        err => this.notificationsService.error(this.i18n('Error'), err.message)
+      )
+  }
+}
diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.html b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.html
new file mode 100644 (file)
index 0000000..6803347
--- /dev/null
@@ -0,0 +1,27 @@
+<div class="admin-sub-header">
+  <div i18n class="form-sub-title">Muted instances</div>
+</div>
+
+<p-table
+  [value]="blockedAccounts" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
+  [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
+>
+
+  <ng-template pTemplate="header">
+    <tr>
+      <th i18n>Instance</th>
+      <th i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
+      <th></th>
+    </tr>
+  </ng-template>
+
+  <ng-template pTemplate="body" let-serverBlock>
+    <tr>
+      <td>{{ serverBlock.blockedServer.host }}</td>
+      <td>{{ serverBlock.createdAt }}</td>
+      <td class="action-cell">
+        <button class="unblock-button" (click)="unblockServer(serverBlock)" i18n>Unmute</button>
+      </td>
+    </tr>
+  </ng-template>
+</p-table>
diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.scss b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.scss
new file mode 100644 (file)
index 0000000..6028b75
--- /dev/null
@@ -0,0 +1,7 @@
+@import '_variables';
+@import '_mixins';
+
+.unblock-button {
+  @include peertube-button;
+  @include grey-button;
+}
\ No newline at end of file
diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.ts b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.ts
new file mode 100644 (file)
index 0000000..b994c2c
--- /dev/null
@@ -0,0 +1,60 @@
+import { Component, OnInit } from '@angular/core'
+import { NotificationsService } from 'angular2-notifications'
+import { I18n } from '@ngx-translate/i18n-polyfill'
+import { RestPagination, RestTable } from '@app/shared'
+import { SortMeta } from 'primeng/components/common/sortmeta'
+import { ServerBlock } from '../../../../../shared'
+import { BlocklistService } from '@app/shared/blocklist'
+
+@Component({
+  selector: 'my-account-server-blocklist',
+  styleUrls: [ './my-account-server-blocklist.component.scss' ],
+  templateUrl: './my-account-server-blocklist.component.html'
+})
+export class MyAccountServerBlocklistComponent extends RestTable implements OnInit {
+  blockedAccounts: ServerBlock[] = []
+  totalRecords = 0
+  rowsPerPage = 10
+  sort: SortMeta = { field: 'createdAt', order: -1 }
+  pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
+
+  constructor (
+    private notificationsService: NotificationsService,
+    private blocklistService: BlocklistService,
+    private i18n: I18n
+  ) {
+    super()
+  }
+
+  ngOnInit () {
+    this.initialize()
+  }
+
+  unblockServer (serverBlock: ServerBlock) {
+    const host = serverBlock.blockedServer.host
+
+    this.blocklistService.unblockServerByUser(host)
+      .subscribe(
+        () => {
+          this.notificationsService.success(
+            this.i18n('Success'),
+            this.i18n('Instance {{host}} unmuted.', { host })
+          )
+
+          this.loadData()
+        }
+      )
+  }
+
+  protected loadData () {
+    return this.blocklistService.getUserServerBlocklist(this.pagination, this.sort)
+      .subscribe(
+        resultList => {
+          this.blockedAccounts = resultList.data
+          this.totalRecords = resultList.total
+        },
+
+        err => this.notificationsService.error(this.i18n('Error'), err.message)
+      )
+  }
+}
index 4b2168e35721ea3b0642768fa60e745a1f86e050..49f9c94a768fa6f76743001ebb0da13b60b0859d 100644 (file)
@@ -11,6 +11,8 @@ import { MyAccountVideoChannelUpdateComponent } from '@app/+my-account/my-accoun
 import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component'
 import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component'
 import { MyAccountOwnershipComponent } from '@app/+my-account/my-account-ownership/my-account-ownership.component'
+import { MyAccountBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-blocklist.component'
+import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-server-blocklist.component'
 
 const myAccountRoutes: Routes = [
   {
@@ -94,6 +96,24 @@ const myAccountRoutes: Routes = [
             title: 'Ownership changes'
           }
         }
+      },
+      {
+        path: 'blocklist/accounts',
+        component: MyAccountBlocklistComponent,
+        data: {
+          meta: {
+            title: 'Accounts blocklist'
+          }
+        }
+      },
+      {
+        path: 'blocklist/servers',
+        component: MyAccountServerBlocklistComponent,
+        data: {
+          meta: {
+            title: 'Instances blocklist'
+          }
+        }
       }
     ]
   }
index b602fd69fa4eff9906db6f8d26c14919428de5a4..41333c25a78787728dab52aca0957c03b8168369 100644 (file)
       </div>
     </div>
 
-    <a i18n routerLink="/my-account/ownership" routerLinkActive="active" class="title-page">Ownership changes</a>
+    <div ngbDropdown class="misc">
+      <span role="button" class="title-page" [ngClass]="{ active: miscLabel !== '' }" ngbDropdownToggle>
+        <ng-container i18n>Misc</ng-container>
+        <ng-container *ngIf="miscLabel"> - {{ miscLabel }}</ng-container>
+      </span>
+
+      <div ngbDropdownMenu>
+        <a class="dropdown-item" i18n routerLink="/my-account/blocklist/accounts">Muted accounts</a>
+
+        <a class="dropdown-item" i18n routerLink="/my-account/blocklist/servers">Muted instances</a>
+
+        <a class="dropdown-item" i18n routerLink="/my-account/ownership">Ownership changes</a>
+      </div>
+    </div>
+
   </div>
 
   <div class="margin-content">
index 20b2639b5c3fdc4c40f0a73d7248f6e11b565adc..6243c6dcfe21806368e4b98c579445d0c016bdfc 100644 (file)
@@ -1,4 +1,4 @@
-.my-library {
+.my-library, .misc {
   span[role=button] {
     cursor: pointer;
   }
index bad60a8fb750e6b9bd4598accb4ae6d9faad1b11..d728caf0758ded3dea274d8f6941241b4017e24b 100644 (file)
@@ -13,6 +13,7 @@ import { Subscription } from 'rxjs'
 export class MyAccountComponent implements OnInit, OnDestroy {
 
   libraryLabel = ''
+  miscLabel = ''
 
   private routeSub: Subscription
 
@@ -23,11 +24,11 @@ export class MyAccountComponent implements OnInit, OnDestroy {
   ) {}
 
   ngOnInit () {
-    this.updateLibraryLabel(this.router.url)
+    this.updateLabels(this.router.url)
 
     this.routeSub = this.router.events
         .pipe(filter(event => event instanceof NavigationStart))
-        .subscribe((event: NavigationStart) => this.updateLibraryLabel(event.url))
+        .subscribe((event: NavigationStart) => this.updateLabels(event.url))
   }
 
   ngOnDestroy () {
@@ -40,7 +41,7 @@ export class MyAccountComponent implements OnInit, OnDestroy {
     return importConfig.http.enabled || importConfig.torrent.enabled
   }
 
-  private updateLibraryLabel (url: string) {
+  private updateLabels (url: string) {
     const [ path ] = url.split('?')
 
     if (path.startsWith('/my-account/video-channels')) {
@@ -54,5 +55,13 @@ export class MyAccountComponent implements OnInit, OnDestroy {
     } else {
       this.libraryLabel = ''
     }
+
+    if (path.startsWith('/my-account/blocklist/accounts')) {
+      this.miscLabel = this.i18n('Muted accounts')
+    } else if (path.startsWith('/my-account/blocklist/servers')) {
+      this.miscLabel = this.i18n('Muted instances')
+    } else {
+      this.miscLabel = ''
+    }
   }
 }
index ad21162a8eee229d342751c8b4e02d529968aae7..017ebd57d8773500baeb375b55eb0bf699533e5c 100644 (file)
@@ -19,6 +19,8 @@ import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-i
 import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component'
 import { MyAccountDangerZoneComponent } from '@app/+my-account/my-account-settings/my-account-danger-zone'
 import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component'
+import { MyAccountBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-blocklist.component'
+import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-server-blocklist.component'
 
 @NgModule({
   imports: [
@@ -45,7 +47,9 @@ import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-sub
     ActorAvatarInfoComponent,
     MyAccountVideoImportsComponent,
     MyAccountDangerZoneComponent,
-    MyAccountSubscriptionsComponent
+    MyAccountSubscriptionsComponent,
+    MyAccountBlocklistComponent,
+    MyAccountServerBlocklistComponent
   ],
 
   exports: [
index 42f2cfeafbbe57a9d0a68abaad5e451993def60d..0aba9428ac112c62841453fdf3ecbd3b012c88e2 100644 (file)
@@ -5,6 +5,8 @@ export class Account extends Actor implements ServerAccount {
   displayName: string
   description: string
   nameWithHost: string
+  muted: boolean
+  mutedServer: boolean
 
   userId?: number
 
@@ -15,5 +17,8 @@ export class Account extends Actor implements ServerAccount {
     this.description = hash.description
     this.userId = hash.userId
     this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host)
+
+    this.muted = false
+    this.mutedServer = false
   }
 }
diff --git a/client/src/app/shared/blocklist/account-block.model.ts b/client/src/app/shared/blocklist/account-block.model.ts
new file mode 100644 (file)
index 0000000..336680f
--- /dev/null
@@ -0,0 +1,14 @@
+import { AccountBlock as AccountBlockServer } from '../../../../../shared'
+import { Account } from '../account/account.model'
+
+export class AccountBlock implements AccountBlockServer {
+  byAccount: Account
+  blockedAccount: Account
+  createdAt: Date | string
+
+  constructor (block: AccountBlockServer) {
+    this.byAccount = new Account(block.byAccount)
+    this.blockedAccount = new Account(block.blockedAccount)
+    this.createdAt = block.createdAt
+  }
+}
\ No newline at end of file
diff --git a/client/src/app/shared/blocklist/blocklist.service.ts b/client/src/app/shared/blocklist/blocklist.service.ts
new file mode 100644 (file)
index 0000000..d9c3182
--- /dev/null
@@ -0,0 +1,79 @@
+import { Injectable } from '@angular/core'
+import { environment } from '../../../environments/environment'
+import { HttpClient, HttpParams } from '@angular/common/http'
+import { RestExtractor, RestPagination, RestService } from '../rest'
+import { SortMeta } from 'primeng/api'
+import { catchError, map } from 'rxjs/operators'
+import { AccountBlock as AccountBlockServer, ResultList, ServerBlock } from '../../../../../shared'
+import { Account } from '@app/shared/account/account.model'
+import { AccountBlock } from '@app/shared/blocklist/account-block.model'
+
+@Injectable()
+export class BlocklistService {
+  static BASE_USER_BLOCKLIST_URL = environment.apiUrl + '/api/v1/users/me/blocklist'
+
+  constructor (
+    private authHttp: HttpClient,
+    private restExtractor: RestExtractor,
+    private restService: RestService
+  ) { }
+
+  /*********************** User -> Account blocklist ***********************/
+
+  getUserAccountBlocklist (pagination: RestPagination, sort: SortMeta) {
+    let params = new HttpParams()
+    params = this.restService.addRestGetParams(params, pagination, sort)
+
+    return this.authHttp.get<ResultList<AccountBlock>>(BlocklistService.BASE_USER_BLOCKLIST_URL + '/accounts', { params })
+               .pipe(
+                 map(res => this.restExtractor.convertResultListDateToHuman(res)),
+                 map(res => this.restExtractor.applyToResultListData(res, this.formatAccountBlock.bind(this))),
+                 catchError(err => this.restExtractor.handleError(err))
+               )
+  }
+
+  blockAccountByUser (account: Account) {
+    const body = { accountName: account.nameWithHost }
+
+    return this.authHttp.post(BlocklistService.BASE_USER_BLOCKLIST_URL + '/accounts', body)
+               .pipe(catchError(err => this.restExtractor.handleError(err)))
+  }
+
+  unblockAccountByUser (account: Account) {
+    const path = BlocklistService.BASE_USER_BLOCKLIST_URL + '/accounts/' + account.nameWithHost
+
+    return this.authHttp.delete(path)
+               .pipe(catchError(err => this.restExtractor.handleError(err)))
+  }
+
+  /*********************** User -> Server blocklist ***********************/
+
+  getUserServerBlocklist (pagination: RestPagination, sort: SortMeta) {
+    let params = new HttpParams()
+    params = this.restService.addRestGetParams(params, pagination, sort)
+
+    return this.authHttp.get<ResultList<ServerBlock>>(BlocklistService.BASE_USER_BLOCKLIST_URL + '/servers', { params })
+               .pipe(
+                 map(res => this.restExtractor.convertResultListDateToHuman(res)),
+                 catchError(err => this.restExtractor.handleError(err))
+               )
+  }
+
+  blockServerByUser (host: string) {
+    const body = { host }
+
+    return this.authHttp.post(BlocklistService.BASE_USER_BLOCKLIST_URL + '/servers', body)
+               .pipe(catchError(err => this.restExtractor.handleError(err)))
+  }
+
+  unblockServerByUser (host: string) {
+    const path = BlocklistService.BASE_USER_BLOCKLIST_URL + '/servers/' + host
+
+    return this.authHttp.delete(path)
+               .pipe(catchError(err => this.restExtractor.handleError(err)))
+  }
+
+  private formatAccountBlock (accountBlock: AccountBlockServer) {
+    return new AccountBlock(accountBlock)
+  }
+}
diff --git a/client/src/app/shared/blocklist/index.ts b/client/src/app/shared/blocklist/index.ts
new file mode 100644 (file)
index 0000000..8cf6a55
--- /dev/null
@@ -0,0 +1,2 @@
+export * from './blocklist.service'
+export * from './account-block.model'
\ No newline at end of file
index 11162742474c8058d8ec165493ed58d0369f5ce5..48230d6d80d2274b7aa6e2c4a36cc3c3206fbb22 100644 (file)
@@ -9,13 +9,13 @@
 
   <div ngbDropdownMenu class="dropdown-menu">
     <ng-container *ngFor="let action of actions">
-      <div class="dropdown-item" *ngIf="action.isDisplayed === undefined || action.isDisplayed(entry) === true">
+      <ng-container *ngIf="action.isDisplayed === undefined || action.isDisplayed(entry) === true">
         <a *ngIf="action.linkBuilder" class="dropdown-item" [routerLink]="action.linkBuilder(entry)">{{ action.label }}</a>
 
-        <span *ngIf="!action.linkBuilder" class="custom-action" class="dropdown-item" (click)="action.handler(entry)" role="button">
+        <span *ngIf="!action.linkBuilder" class="custom-action dropdown-item" (click)="action.handler(entry)" role="button">
           {{ action.label }}
         </span>
-      </div>
+      </ng-container>
     </ng-container>
   </div>
 </div>
\ No newline at end of file
index 0a9aa7b0460156cec2f483699b47eec09e589101..92c4d1d2c075296759f669f74fc5ed1bdec92e77 100644 (file)
   .dropdown-item {
     cursor: pointer;
     color: #000 !important;
+
+    a, span {
+      display: block;
+      width: 100%;
+    }
   }
 }
\ No newline at end of file
index 01db7cd4a40b92cb47836ab0e0b38de40c46f5bd..7367a7e59e13618d5244980d76a4a0567bf25e2a 100644 (file)
@@ -1,5 +1,8 @@
-<ng-container *ngIf="user && userActions.length !== 0">
+<ng-container *ngIf="userActions.length !== 0">
   <my-user-ban-modal #userBanModal (userBanned)="onUserBanned()"></my-user-ban-modal>
 
-  <my-action-dropdown [actions]="userActions" [entry]="user" [buttonSize]="buttonSize" [placement]="placement"></my-action-dropdown>
+  <my-action-dropdown
+    [actions]="userActions" [entry]="{ user: user, account: account }"
+    [buttonSize]="buttonSize" [placement]="placement"
+  ></my-action-dropdown>
 </ng-container>
\ No newline at end of file
index 105c99d8b8df6133e2595e9bcd9708691fcb288d..2f4a55f3795e435bc6afe678fc8f0c4df51a1f27 100644 (file)
@@ -1,4 +1,4 @@
-import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
+import { Component, EventEmitter, Input, OnChanges, Output, ViewChild } from '@angular/core'
 import { NotificationsService } from 'angular2-notifications'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 import { DropdownAction } from '@app/shared/buttons/action-dropdown.component'
@@ -6,16 +6,20 @@ import { UserBanModalComponent } from '@app/shared/moderation/user-ban-modal.com
 import { UserService } from '@app/shared/users'
 import { AuthService, ConfirmService } from '@app/core'
 import { User, UserRight } from '../../../../../shared/models/users'
+import { Account } from '@app/shared/account/account.model'
+import { BlocklistService } from '@app/shared/blocklist'
 
 @Component({
   selector: 'my-user-moderation-dropdown',
   templateUrl: './user-moderation-dropdown.component.html',
   styleUrls: [ './user-moderation-dropdown.component.scss' ]
 })
-export class UserModerationDropdownComponent implements OnInit {
+export class UserModerationDropdownComponent implements OnChanges {
   @ViewChild('userBanModal') userBanModal: UserBanModalComponent
 
   @Input() user: User
+  @Input() account: Account
+
   @Input() buttonSize: 'normal' | 'small' = 'normal'
   @Input() placement = 'left'
 
@@ -29,10 +33,11 @@ export class UserModerationDropdownComponent implements OnInit {
     private notificationsService: NotificationsService,
     private confirmService: ConfirmService,
     private userService: UserService,
+    private blocklistService: BlocklistService,
     private i18n: I18n
   ) { }
 
-  ngOnInit () {
+  ngOnChanges () {
     this.buildActions()
   }
 
@@ -92,6 +97,74 @@ export class UserModerationDropdownComponent implements OnInit {
     )
   }
 
+  blockAccountByUser (account: Account) {
+    this.blocklistService.blockAccountByUser(account)
+        .subscribe(
+          () => {
+            this.notificationsService.success(
+              this.i18n('Success'),
+              this.i18n('Account {{nameWithHost}} muted.', { nameWithHost: account.nameWithHost })
+            )
+
+            this.account.muted = true
+            this.userChanged.emit()
+          },
+
+          err => this.notificationsService.error(this.i18n('Error'), err.message)
+        )
+  }
+
+  unblockAccountByUser (account: Account) {
+    this.blocklistService.unblockAccountByUser(account)
+        .subscribe(
+          () => {
+            this.notificationsService.success(
+              this.i18n('Success'),
+              this.i18n('Account {{nameWithHost}} unmuted.', { nameWithHost: account.nameWithHost })
+            )
+
+            this.account.muted = false
+            this.userChanged.emit()
+          },
+
+          err => this.notificationsService.error(this.i18n('Error'), err.message)
+        )
+  }
+
+  blockServerByUser (host: string) {
+    this.blocklistService.blockServerByUser(host)
+        .subscribe(
+          () => {
+            this.notificationsService.success(
+              this.i18n('Success'),
+              this.i18n('Instance {{host}} muted.', { host })
+            )
+
+            this.account.mutedServer = true
+            this.userChanged.emit()
+          },
+
+          err => this.notificationsService.error(this.i18n('Error'), err.message)
+        )
+  }
+
+  unblockServerByUser (host: string) {
+    this.blocklistService.unblockServerByUser(host)
+        .subscribe(
+          () => {
+            this.notificationsService.success(
+              this.i18n('Success'),
+              this.i18n('Instance {{host}} unmuted.', { host })
+            )
+
+            this.account.mutedServer = false
+            this.userChanged.emit()
+          },
+
+          err => this.notificationsService.error(this.i18n('Error'), err.message)
+        )
+  }
+
   getRouterUserEditLink (user: User) {
     return [ '/admin', 'users', 'update', user.id ]
   }
@@ -102,25 +175,53 @@ export class UserModerationDropdownComponent implements OnInit {
     if (this.authService.isLoggedIn()) {
       const authUser = this.authService.getUser()
 
-      if (authUser.hasRight(UserRight.MANAGE_USERS)) {
+      if (this.user && authUser.id === this.user.id) return
+
+      if (this.user && authUser.hasRight(UserRight.MANAGE_USERS)) {
         this.userActions = this.userActions.concat([
           {
             label: this.i18n('Edit'),
-            linkBuilder: this.getRouterUserEditLink
+            linkBuilder: ({ user }) => this.getRouterUserEditLink(user)
           },
           {
             label: this.i18n('Delete'),
-            handler: user => this.removeUser(user)
+            handler: ({ user }) => this.removeUser(user)
           },
           {
             label: this.i18n('Ban'),
-            handler: user => this.openBanUserModal(user),
-            isDisplayed: user => !user.blocked
+            handler: ({ user }) => this.openBanUserModal(user),
+            isDisplayed: ({ user }) => !user.muted
           },
           {
             label: this.i18n('Unban'),
-            handler: user => this.unbanUser(user),
-            isDisplayed: user => user.blocked
+            handler: ({ user }) => this.unbanUser(user),
+            isDisplayed: ({ user }) => user.muted
+          }
+        ])
+      }
+
+      // User actions on accounts/servers
+      if (this.account) {
+        this.userActions = this.userActions.concat([
+          {
+            label: this.i18n('Mute this account'),
+            isDisplayed: ({ account }) => account.muted === false,
+            handler: ({ account }) => this.blockAccountByUser(account)
+          },
+          {
+            label: this.i18n('Unmute this account'),
+            isDisplayed: ({ account }) => account.muted === true,
+            handler: ({ account }) => this.unblockAccountByUser(account)
+          },
+          {
+            label: this.i18n('Mute the instance'),
+            isDisplayed: ({ account }) => !account.userId && account.mutedServer === false,
+            handler: ({ account }) => this.blockServerByUser(account.host)
+          },
+          {
+            label: this.i18n('Unmute the instance'),
+            isDisplayed: ({ account }) => !account.userId && account.mutedServer === true,
+            handler: ({ account }) => this.unblockServerByUser(account.host)
           }
         ])
       }
index 9647a7966288f86d6f4147217055e45d2bf2b1d1..40e05fcc7ba1cdfa39be2968bb6088fd99d72ba8 100644 (file)
@@ -58,6 +58,7 @@ import { InstanceFeaturesTableComponent } from '@app/shared/instance/instance-fe
 import { OverviewService } from '@app/shared/overview'
 import { UserBanModalComponent } from '@app/shared/moderation'
 import { UserModerationDropdownComponent } from '@app/shared/moderation/user-moderation-dropdown.component'
+import { BlocklistService } from '@app/shared/blocklist'
 
 @NgModule({
   imports: [
@@ -172,6 +173,7 @@ import { UserModerationDropdownComponent } from '@app/shared/moderation/user-mod
     OverviewService,
     VideoChangeOwnershipValidatorsService,
     VideoAcceptOwnershipValidatorsService,
+    BlocklistService,
 
     I18nPrimengCalendarService,
     ScreenService,
index e955ffde9c2c9a198538a8e980e9006f1aa5ca6d..95a4105ec31d380ce60da830ba7756bbc83ce3aa 100644 (file)
@@ -6,7 +6,6 @@ import {
   asyncRetryTransactionMiddleware,
   authenticate,
   paginationValidator,
-  serverGetValidator,
   setDefaultPagination,
   setDefaultSort,
   unblockAccountByAccountValidator
@@ -14,6 +13,7 @@ import {
 import {
   accountsBlocklistSortValidator,
   blockAccountByAccountValidator,
+  blockServerByAccountValidator,
   serversBlocklistSortValidator,
   unblockServerByAccountValidator
 } from '../../../middlewares/validators'
@@ -58,7 +58,7 @@ myBlocklistRouter.get('/me/blocklist/servers',
 
 myBlocklistRouter.post('/me/blocklist/servers',
   authenticate,
-  asyncMiddleware(serverGetValidator),
+  asyncMiddleware(blockServerByAccountValidator),
   asyncRetryTransactionMiddleware(blockServer)
 )
 
index 394c2453705c139054a839372807ec482da53762..1633e500cb34964b9d9252adfc43c6f41ce6c9b3 100644 (file)
@@ -4,7 +4,7 @@ import { ServerBlocklistModel } from '../models/server/server-blocklist'
 
 function addAccountInBlocklist (byAccountId: number, targetAccountId: number) {
   return sequelizeTypescript.transaction(async t => {
-    return AccountBlocklistModel.create({
+    return AccountBlocklistModel.upsert({
       accountId: byAccountId,
       targetAccountId: targetAccountId
     }, { transaction: t })
@@ -13,7 +13,7 @@ function addAccountInBlocklist (byAccountId: number, targetAccountId: number) {
 
 function addServerInBlocklist (byAccountId: number, targetServerId: number) {
   return sequelizeTypescript.transaction(async t => {
-    return ServerBlocklistModel.create({
+    return ServerBlocklistModel.upsert({
       accountId: byAccountId,
       targetServerId
     }, { transaction: t })
index 9dbd5e51275cd609d215c578d7e63ef589e3c352..25c054d6b2504eb3216b9daa45107785f42151a2 100644 (file)
@@ -1,4 +1,4 @@
-import { param, body } from 'express-validator/check'
+import { body, param } from 'express-validator/check'
 import * as express from 'express'
 import { logger } from '../../helpers/logger'
 import { areValidationErrors } from './utils'
@@ -7,6 +7,8 @@ import { UserModel } from '../../models/account/user'
 import { AccountBlocklistModel } from '../../models/account/account-blocklist'
 import { isHostValid } from '../../helpers/custom-validators/servers'
 import { ServerBlocklistModel } from '../../models/server/server-blocklist'
+import { ServerModel } from '../../models/server/server'
+import { CONFIG } from '../../initializers'
 
 const blockAccountByAccountValidator = [
   body('accountName').exists().withMessage('Should have an account name with host'),
@@ -17,6 +19,17 @@ const blockAccountByAccountValidator = [
     if (areValidationErrors(req, res)) return
     if (!await isAccountNameWithHostExist(req.body.accountName, res)) return
 
+    const user = res.locals.oauth.token.User as UserModel
+    const accountToBlock = res.locals.account
+
+    if (user.Account.id === accountToBlock.id) {
+      res.status(409)
+         .send({ error: 'You cannot block yourself.' })
+         .end()
+
+      return
+    }
+
     return next()
   }
 ]
@@ -38,6 +51,35 @@ const unblockAccountByAccountValidator = [
   }
 ]
 
+const blockServerByAccountValidator = [
+  body('host').custom(isHostValid).withMessage('Should have a valid host'),
+
+  async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+    logger.debug('Checking serverGetValidator parameters', { parameters: req.body })
+
+    if (areValidationErrors(req, res)) return
+
+    const host: string = req.body.host
+
+    if (host === CONFIG.WEBSERVER.HOST) {
+      return res.status(409)
+        .send({ error: 'You cannot block your own server.' })
+        .end()
+    }
+
+    const server = await ServerModel.loadByHost(host)
+    if (!server) {
+      return res.status(404)
+                .send({ error: 'Server host not found.' })
+                .end()
+    }
+
+    res.locals.server = server
+
+    return next()
+  }
+]
+
 const unblockServerByAccountValidator = [
   param('host').custom(isHostValid).withMessage('Should have an account name with host'),
 
@@ -56,6 +98,7 @@ const unblockServerByAccountValidator = [
 // ---------------------------------------------------------------------------
 
 export {
+  blockServerByAccountValidator,
   blockAccountByAccountValidator,
   unblockAccountByAccountValidator,
   unblockServerByAccountValidator
index bacd122e877fc0286368b5ea055ad7fd5cef7128..fa281923547be9764d1395d474a25c0b13fe9666 100644 (file)
@@ -18,7 +18,7 @@ enum ScopeNames {
       {
         model: () => AccountModel,
         required: true,
-        as: 'AccountBlocked'
+        as: 'BlockedAccount'
       }
     ]
   }
@@ -67,10 +67,10 @@ export class AccountBlocklistModel extends Model<AccountBlocklistModel> {
       name: 'targetAccountId',
       allowNull: false
     },
-    as: 'AccountBlocked',
+    as: 'BlockedAccount',
     onDelete: 'CASCADE'
   })
-  AccountBlocked: AccountModel
+  BlockedAccount: AccountModel
 
   static loadByAccountAndTarget (accountId: number, targetAccountId: number) {
     const query = {
@@ -104,7 +104,7 @@ export class AccountBlocklistModel extends Model<AccountBlocklistModel> {
   toFormattedJSON (): AccountBlock {
     return {
       byAccount: this.ByAccount.toFormattedJSON(),
-      accountBlocked: this.AccountBlocked.toFormattedJSON(),
+      blockedAccount: this.BlockedAccount.toFormattedJSON(),
       createdAt: this.createdAt
     }
   }
index 705ed2c6b9027704a498ac9f7d373136bc963f42..450f27152069449057dc9be0c795876699f53b24 100644 (file)
@@ -72,7 +72,7 @@ export class ServerBlocklistModel extends Model<ServerBlocklistModel> {
     },
     onDelete: 'CASCADE'
   })
-  ServerBlocked: ServerModel
+  BlockedServer: ServerModel
 
   static loadByAccountAndHost (accountId: number, host: string) {
     const query = {
@@ -114,7 +114,7 @@ export class ServerBlocklistModel extends Model<ServerBlocklistModel> {
   toFormattedJSON (): ServerBlock {
     return {
       byAccount: this.ByAccount.toFormattedJSON(),
-      serverBlocked: this.ServerBlocked.toFormattedJSON(),
+      blockedServer: this.BlockedServer.toFormattedJSON(),
       createdAt: this.createdAt
     }
   }
index 8117c46a6292115236144ad7e8995d97f6227550..d24d9323fefcb6af0320d2e71fc6eebadf82ff3d 100644 (file)
@@ -85,6 +85,16 @@ describe('Test blocklist API validators', function () {
           })
         })
 
+        it('Should fail to block ourselves', async function () {
+          await makePostBodyRequest({
+            url: server.url,
+            token: server.accessToken,
+            path,
+            fields: { accountName: 'root' },
+            statusCodeExpected: 409
+          })
+        })
+
         it('Should succeed with the correct params', async function () {
           await makePostBodyRequest({
             url: server.url,
@@ -170,6 +180,16 @@ describe('Test blocklist API validators', function () {
           })
         })
 
+        it('Should fail with our own server', async function () {
+          await makePostBodyRequest({
+            url: server.url,
+            token: server.accessToken,
+            path,
+            fields: { host: 'localhost:9001' },
+            statusCodeExpected: 409
+          })
+        })
+
         it('Should succeed with the correct params', async function () {
           await makePostBodyRequest({
             url: server.url,
index 00ad51461361af39f3cd8ad23768cf6255d13b57..026971331a11f18b325e0747138176e0d268d77f 100644 (file)
@@ -183,9 +183,9 @@ describe('Test accounts blocklist', function () {
         const block = blocks[0]
         expect(block.byAccount.displayName).to.equal('root')
         expect(block.byAccount.name).to.equal('root')
-        expect(block.accountBlocked.displayName).to.equal('user2')
-        expect(block.accountBlocked.name).to.equal('user2')
-        expect(block.accountBlocked.host).to.equal('localhost:9002')
+        expect(block.blockedAccount.displayName).to.equal('user2')
+        expect(block.blockedAccount.name).to.equal('user2')
+        expect(block.blockedAccount.host).to.equal('localhost:9002')
       }
 
       {
@@ -197,9 +197,9 @@ describe('Test accounts blocklist', function () {
         const block = blocks[0]
         expect(block.byAccount.displayName).to.equal('root')
         expect(block.byAccount.name).to.equal('root')
-        expect(block.accountBlocked.displayName).to.equal('user1')
-        expect(block.accountBlocked.name).to.equal('user1')
-        expect(block.accountBlocked.host).to.equal('localhost:9001')
+        expect(block.blockedAccount.displayName).to.equal('user1')
+        expect(block.blockedAccount.name).to.equal('user1')
+        expect(block.blockedAccount.host).to.equal('localhost:9001')
       }
     })
 
@@ -267,7 +267,7 @@ describe('Test accounts blocklist', function () {
       const block = blocks[0]
       expect(block.byAccount.displayName).to.equal('root')
       expect(block.byAccount.name).to.equal('root')
-      expect(block.serverBlocked.host).to.equal('localhost:9002')
+      expect(block.blockedServer.host).to.equal('localhost:9002')
     })
 
     it('Should unblock the remote server', async function () {
index d6f8840c56139a4e3ed884064a0a5ca8421a332b..a942ed614e81d3bfbd93dcd211927bdf03c339e1 100644 (file)
@@ -2,6 +2,6 @@ import { Account } from '../actors'
 
 export interface AccountBlock {
   byAccount: Account
-  accountBlocked: Account
+  blockedAccount: Account
   createdAt: Date | string
 }
index efba672bd0336e8379ed3d1d6dc99e3a3b576365..a8b8af0b7fa984a885ddd3d5fd74d36ed303d1f5 100644 (file)
@@ -2,7 +2,7 @@ import { Account } from '../actors'
 
 export interface ServerBlock {
   byAccount: Account
-  serverBlocked: {
+  blockedServer: {
     host: string
   }
   createdAt: Date | string