]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Give moderators access to edit channels (#4608)
authorkontrollanten <6680299+kontrollanten@users.noreply.github.com>
Mon, 13 Dec 2021 14:29:13 +0000 (15:29 +0100)
committerGitHub <noreply@github.com>
Mon, 13 Dec 2021 14:29:13 +0000 (15:29 +0100)
* give admins access to edit all channels

closes #4598

* test(channels): +admin update another users channel

* Fix tests

* fix(server): delete another users channel

Since the channel owner isn't necessary the auth user we need to check
the right account whether it's the last video or not.

* REMOVE_ANY_VIDEO_CHANNEL > MANAGE_ANY_VIDEO_CHANNEL

Merge REMOVE_ANY_VIDEO_CHANNEL and MANY_VIDEO_CHANNELS to
MANAGE_ANY_VIDEO_CHANNEL.

* user-right: moderator can't manage admins channel

* client: MyVideoChannelCreateComponent > VideoChannelCreateComponent

* client: MyVideoChannelEdit > VideoChannelEdit

* Revert "user-right: moderator can't manage admins channel"

This reverts commit 2c627c154e2bfe6af2e0f45efb27faf4117572f3.

* server: clean dupl validator functionality

* fix ensureUserCanManageChannel usage

It's not async anymore.

* server: merge channel validator middleares

ensureAuthUserOwnsChannelValidator & ensureUserCanManageChannel gets
merged into one middleware.

* client(VideoChannelEdit): redirect to prev route

* fix(VideoChannels): handle anon users

* client: new routes for create/update channel

* Refactor channel validators

Co-authored-by: Chocobozzz <me@florianbigard.com>
27 files changed:
client/src/app/+manage/manage-routing.module.ts [new file with mode: 0644]
client/src/app/+manage/manage.module.ts [new file with mode: 0644]
client/src/app/+manage/video-channel-edit/video-channel-create.component.ts [moved from client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts with 91% similarity]
client/src/app/+manage/video-channel-edit/video-channel-edit.component.html [new file with mode: 0644]
client/src/app/+manage/video-channel-edit/video-channel-edit.component.scss [moved from client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss with 96% similarity]
client/src/app/+manage/video-channel-edit/video-channel-edit.ts [moved from client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts with 85% similarity]
client/src/app/+manage/video-channel-edit/video-channel-update.component.ts [moved from client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts with 89% similarity]
client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html [deleted file]
client/src/app/+my-library/+my-video-channels/my-video-channels-routing.module.ts
client/src/app/+my-library/+my-video-channels/my-video-channels.component.html
client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts
client/src/app/+video-channels/video-channels.component.html
client/src/app/+video-channels/video-channels.component.ts
client/src/app/+video-channels/video-channels.module.ts
client/src/app/app-routing.module.ts
client/src/app/core/routing/redirect.service.ts
server/controllers/activitypub/client.ts
server/controllers/activitypub/inbox.ts
server/controllers/activitypub/outbox.ts
server/controllers/api/video-channel.ts
server/middlewares/validators/shared/video-channels.ts
server/middlewares/validators/users.ts
server/middlewares/validators/videos/video-channels.ts
server/tests/api/videos/video-channels.ts
shared/core-utils/users/user-role.ts
shared/extra-utils/users/users-command.ts
shared/models/users/user-right.enum.ts

diff --git a/client/src/app/+manage/manage-routing.module.ts b/client/src/app/+manage/manage-routing.module.ts
new file mode 100644 (file)
index 0000000..14ae4f1
--- /dev/null
@@ -0,0 +1,31 @@
+import { NgModule } from '@angular/core'
+import { RouterModule, Routes } from '@angular/router'
+import { VideoChannelCreateComponent } from './video-channel-edit/video-channel-create.component'
+import { VideoChannelUpdateComponent } from './video-channel-edit/video-channel-update.component'
+
+const manageRoutes: Routes = [
+  {
+    path: 'create',
+    component: VideoChannelCreateComponent,
+    data: {
+      meta: {
+        title: $localize`Create a new video channel`
+      }
+    }
+  },
+  {
+    path: 'update/:videoChannelName',
+    component: VideoChannelUpdateComponent,
+    data: {
+      meta: {
+        title: $localize`Update video channel`
+      }
+    }
+  }
+]
+
+@NgModule({
+  imports: [ RouterModule.forChild(manageRoutes) ],
+  exports: [ RouterModule ]
+})
+export class ManageRoutingModule {}
diff --git a/client/src/app/+manage/manage.module.ts b/client/src/app/+manage/manage.module.ts
new file mode 100644 (file)
index 0000000..28939ec
--- /dev/null
@@ -0,0 +1,31 @@
+import { NgModule } from '@angular/core'
+import { SharedFormModule } from '@app/shared/shared-forms'
+import { SharedGlobalIconModule } from '@app/shared/shared-icons'
+import { SharedMainModule } from '@app/shared/shared-main'
+import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module'
+import { SharedActorImageEditModule } from '@app/shared/shared-actor-image-edit'
+import { VideoChannelCreateComponent } from './video-channel-edit/video-channel-create.component'
+import { VideoChannelUpdateComponent } from './video-channel-edit/video-channel-update.component'
+import { ManageRoutingModule } from './manage-routing.module'
+
+@NgModule({
+  imports: [
+    ManageRoutingModule,
+    SharedMainModule,
+    SharedFormModule,
+    SharedGlobalIconModule,
+    SharedActorImageModule,
+    SharedActorImageEditModule
+  ],
+
+  declarations: [
+    VideoChannelCreateComponent,
+    VideoChannelUpdateComponent
+  ],
+
+  exports: [
+  ],
+
+  providers: []
+})
+export class ManageModule { }
similarity index 91%
rename from client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts
rename to client/src/app/+manage/video-channel-edit/video-channel-create.component.ts
index fd00720d817cbb7f00928ba48f274c4c18c7ef0b..5f8e0278e3c2fd78c85c78e2115c7c684d376ebc 100644 (file)
@@ -12,13 +12,13 @@ import {
 import { FormValidatorService } from '@app/shared/shared-forms'
 import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
 import { HttpStatusCode, VideoChannelCreate } from '@shared/models'
-import { MyVideoChannelEdit } from './my-video-channel-edit'
+import { VideoChannelEdit } from './video-channel-edit'
 
 @Component({
-  templateUrl: './my-video-channel-edit.component.html',
-  styleUrls: [ './my-video-channel-edit.component.scss' ]
+  templateUrl: './video-channel-edit.component.html',
+  styleUrls: [ './video-channel-edit.component.scss' ]
 })
-export class MyVideoChannelCreateComponent extends MyVideoChannelEdit implements OnInit {
+export class VideoChannelCreateComponent extends VideoChannelEdit implements OnInit {
   error: string
   videoChannel = new VideoChannel({})
 
diff --git a/client/src/app/+manage/video-channel-edit/video-channel-edit.component.html b/client/src/app/+manage/video-channel-edit/video-channel-edit.component.html
new file mode 100644 (file)
index 0000000..3751747
--- /dev/null
@@ -0,0 +1,96 @@
+<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
+
+<div class="margin-content">
+  <form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
+
+    <div class="form-row"> <!-- channel grid -->
+      <div class="form-group col-12 col-lg-4 col-xl-3">
+        <div *ngIf="isCreation()" class="video-channel-title" i18n>NEW CHANNEL</div>
+        <div *ngIf="!isCreation() && videoChannel" class="video-channel-title" i18n>CHANNEL</div>
+      </div>
+  
+      <div class="form-group col-12 col-lg-8 col-xl-9">
+        <h6 i18n>Banner image of the channel</h6>
+  
+        <my-actor-banner-edit
+          *ngIf="videoChannel" [previewImage]="isCreation()"
+          [actor]="videoChannel" (bannerChange)="onBannerChange($event)" (bannerDelete)="onBannerDelete()"
+        ></my-actor-banner-edit>
+  
+        <my-actor-avatar-edit
+          *ngIf="videoChannel" [previewImage]="isCreation()"
+          [actor]="videoChannel" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"
+          [displayUsername]="!isCreation()" [displaySubscribers]="!isCreation()"
+        ></my-actor-avatar-edit>
+  
+        <div class="form-group" *ngIf="isCreation()">
+          <label i18n for="name">Name</label>
+          <div class="input-group">
+            <input
+              type="text" id="name" i18n-placeholder placeholder="Example: my_channel"
+              formControlName="name" [ngClass]="{ 'input-error': formErrors['name'] }" class="form-control"
+            >
+            <div class="input-group-append">
+              <span class="input-group-text">@{{ instanceHost }}</span>
+            </div>
+          </div>
+          <div *ngIf="formErrors['name']" class="form-error">
+            {{ formErrors['name'] }}
+          </div>
+        </div>
+  
+        <div class="form-group">
+          <label i18n for="display-name">Display name</label>
+          <input
+            type="text" id="display-name" class="form-control"
+            formControlName="display-name" [ngClass]="{ 'input-error': formErrors['display-name'] }"
+          >
+          <div *ngIf="formErrors['display-name']" class="form-error">
+            {{ formErrors['display-name'] }}
+          </div>
+        </div>
+  
+        <div class="form-group">
+          <label i18n for="description">Description</label>
+          <textarea
+            id="description" formControlName="description" class="form-control"
+            [ngClass]="{ 'input-error': formErrors['description'] }"
+          ></textarea>
+          <div *ngIf="formErrors.description" class="form-error">
+            {{ formErrors.description }}
+          </div>
+        </div>
+  
+        <div class="form-group">
+          <label for="support">Support</label>
+          <my-help
+            helpType="markdownEnhanced" i18n-preHtml preHtml="Short text to tell people how they can support the channel (membership platform...).<br /><br />
+      When a video is uploaded in this channel, the video support field will be automatically filled by this text."
+          ></my-help>
+          <my-markdown-textarea
+              id="support" formControlName="support" textareaMaxWidth="500px" markdownType="enhanced"
+              [classes]="{ 'input-error': formErrors['support'] }"
+          ></my-markdown-textarea>
+          <div *ngIf="formErrors.support" class="form-error">
+            {{ formErrors.support }}
+          </div>
+        </div>
+  
+        <div class="form-group" *ngIf="isBulkUpdateVideosDisplayed()">
+          <my-peertube-checkbox
+            inputName="bulkVideosSupportUpdate" formControlName="bulkVideosSupportUpdate"
+            i18n-labelText labelText="Overwrite support field of all videos of this channel"
+          ></my-peertube-checkbox>
+        </div>
+  
+      </div>
+    </div>
+  
+    <div class="form-row"> <!-- submit placement block -->
+      <div class="col-md-7 col-xl-5"></div>
+      <div class="col-md-5 col-xl-5 d-inline-flex">
+        <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
+      </div>
+    </div>
+  </form>  
+</div>
\ No newline at end of file
similarity index 96%
rename from client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss
rename to client/src/app/+manage/video-channel-edit/video-channel-edit.component.scss
index d8bfe71b6af87e3ae330b38ec043b5d3179623c5..d010d6277e1b53bc6ca8275e5382b566ecfd75d4 100644 (file)
@@ -1,6 +1,10 @@
 @use '_variables' as *;
 @use '_mixins' as *;
 
+.margin-content {
+  padding-top: 20px;
+}
+
 label {
   font-weight: $font-regular;
   font-size: 100%;
similarity index 85%
rename from client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts
rename to client/src/app/+manage/video-channel-edit/video-channel-edit.ts
index 33bb90f145578192cd5938179ebbac7c54e8e258..963b4cbbe24f5962e43fe24a57d8d1db9dc7d668 100644 (file)
@@ -1,7 +1,7 @@
 import { FormReactive } from '@app/shared/shared-forms'
 import { VideoChannel } from '@app/shared/shared-main'
 
-export abstract class MyVideoChannelEdit extends FormReactive {
+export abstract class VideoChannelEdit extends FormReactive {
   videoChannel: VideoChannel
 
   abstract isCreation (): boolean
similarity index 89%
rename from client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts
rename to client/src/app/+manage/video-channel-edit/video-channel-update.component.ts
index f9521b8b51afae1b852519be77d1a9e55effc9c3..21b6167b225814ea9acf3b703eb1a7eeb51f79a3 100644 (file)
@@ -2,7 +2,7 @@ import { Subscription } from 'rxjs'
 import { HttpErrorResponse } from '@angular/common/http'
 import { Component, OnDestroy, OnInit } from '@angular/core'
 import { ActivatedRoute, Router } from '@angular/router'
-import { AuthService, Notifier, ServerService } from '@app/core'
+import { AuthService, Notifier, RedirectService, ServerService } from '@app/core'
 import { genericUploadErrorHandler } from '@app/helpers'
 import {
   VIDEO_CHANNEL_DESCRIPTION_VALIDATOR,
@@ -12,14 +12,14 @@ import {
 import { FormValidatorService } from '@app/shared/shared-forms'
 import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
 import { HTMLServerConfig, VideoChannelUpdate } from '@shared/models'
-import { MyVideoChannelEdit } from './my-video-channel-edit'
+import { VideoChannelEdit } from './video-channel-edit'
 
 @Component({
   selector: 'my-video-channel-update',
-  templateUrl: './my-video-channel-edit.component.html',
-  styleUrls: [ './my-video-channel-edit.component.scss' ]
+  templateUrl: './video-channel-edit.component.html',
+  styleUrls: [ './video-channel-edit.component.scss' ]
 })
-export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements OnInit, OnDestroy {
+export class VideoChannelUpdateComponent extends VideoChannelEdit implements OnInit, OnDestroy {
   error: string
   videoChannel: VideoChannel
 
@@ -34,7 +34,8 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
     private router: Router,
     private route: ActivatedRoute,
     private videoChannelService: VideoChannelService,
-    private serverService: ServerService
+    private serverService: ServerService,
+    private redirectService: RedirectService
   ) {
     super()
   }
@@ -50,9 +51,9 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
     })
 
     this.paramsSub = this.route.params.subscribe(routeParams => {
-      const videoChannelId = routeParams['videoChannelId']
+      const videoChannelName = routeParams['videoChannelName']
 
-      this.videoChannelService.getVideoChannel(videoChannelId)
+      this.videoChannelService.getVideoChannel(videoChannelName)
         .subscribe({
           next: videoChannelToUpdate => {
             this.videoChannel = videoChannelToUpdate
@@ -95,7 +96,7 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
 
           this.notifier.success($localize`Video channel ${videoChannelUpdate.displayName} updated.`)
 
-          this.router.navigate([ '/my-library', 'video-channels' ])
+          this.redirectService.redirectToPreviousRoute([ '/c', this.videoChannel.name ])
         },
 
         error: err => {
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html
deleted file mode 100644 (file)
index 2910dff..0000000
+++ /dev/null
@@ -1,112 +0,0 @@
-<nav aria-label="breadcrumb">
-  <ol class="breadcrumb">
-    <li class="breadcrumb-item">
-      <a routerLink="/my-library/video-channels" i18n>My Channels</a>
-    </li>
-
-    <ng-container *ngIf="isCreation()">
-      <li class="breadcrumb-item active" i18n>Create</li>
-    </ng-container>
-    <ng-container *ngIf="!isCreation()">
-      <li class="breadcrumb-item active" i18n>Edit</li>
-      <li class="breadcrumb-item active" aria-current="page">
-        <a *ngIf="videoChannel" [routerLink]="[ '/my-library/video-channels/update', videoChannel?.nameWithHost ]">{{ videoChannel?.displayName }}</a>
-      </li>
-    </ng-container>
-  </ol>
-</nav>
-
-<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
-
-<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
-
-  <div class="form-row"> <!-- channel grid -->
-    <div class="form-group col-12 col-lg-4 col-xl-3">
-      <div *ngIf="isCreation()" class="video-channel-title" i18n>NEW CHANNEL</div>
-      <div *ngIf="!isCreation() && videoChannel" class="video-channel-title" i18n>CHANNEL</div>
-    </div>
-
-    <div class="form-group col-12 col-lg-8 col-xl-9">
-      <h6 i18n>Banner image of your channel</h6>
-
-      <my-actor-banner-edit
-        *ngIf="videoChannel" [previewImage]="isCreation()"
-        [actor]="videoChannel" (bannerChange)="onBannerChange($event)" (bannerDelete)="onBannerDelete()"
-      ></my-actor-banner-edit>
-
-      <my-actor-avatar-edit
-        *ngIf="videoChannel" [previewImage]="isCreation()"
-        [actor]="videoChannel" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"
-        [displayUsername]="!isCreation()" [displaySubscribers]="!isCreation()"
-      ></my-actor-avatar-edit>
-
-      <div class="form-group" *ngIf="isCreation()">
-        <label i18n for="name">Name</label>
-        <div class="input-group">
-          <input
-            type="text" id="name" i18n-placeholder placeholder="Example: my_channel"
-            formControlName="name" [ngClass]="{ 'input-error': formErrors['name'] }" class="form-control"
-          >
-          <div class="input-group-append">
-            <span class="input-group-text">@{{ instanceHost }}</span>
-          </div>
-        </div>
-        <div *ngIf="formErrors['name']" class="form-error">
-          {{ formErrors['name'] }}
-        </div>
-      </div>
-
-      <div class="form-group">
-        <label i18n for="display-name">Display name</label>
-        <input
-          type="text" id="display-name" class="form-control"
-          formControlName="display-name" [ngClass]="{ 'input-error': formErrors['display-name'] }"
-        >
-        <div *ngIf="formErrors['display-name']" class="form-error">
-          {{ formErrors['display-name'] }}
-        </div>
-      </div>
-
-      <div class="form-group">
-        <label i18n for="description">Description</label>
-        <textarea
-          id="description" formControlName="description" class="form-control"
-          [ngClass]="{ 'input-error': formErrors['description'] }"
-        ></textarea>
-        <div *ngIf="formErrors.description" class="form-error">
-          {{ formErrors.description }}
-        </div>
-      </div>
-
-      <div class="form-group">
-        <label for="support">Support</label>
-        <my-help
-          helpType="markdownEnhanced" i18n-preHtml preHtml="Short text to tell people how they can support your channel (membership platform...).<br /><br />
-    When you will upload a video in this channel, the video support field will be automatically filled by this text."
-        ></my-help>
-        <my-markdown-textarea
-            id="support" formControlName="support" textareaMaxWidth="500px" markdownType="enhanced"
-            [classes]="{ 'input-error': formErrors['support'] }"
-        ></my-markdown-textarea>
-        <div *ngIf="formErrors.support" class="form-error">
-          {{ formErrors.support }}
-        </div>
-      </div>
-
-      <div class="form-group" *ngIf="isBulkUpdateVideosDisplayed()">
-        <my-peertube-checkbox
-          inputName="bulkVideosSupportUpdate" formControlName="bulkVideosSupportUpdate"
-          i18n-labelText labelText="Overwrite support field of all videos of this channel"
-        ></my-peertube-checkbox>
-      </div>
-
-    </div>
-  </div>
-
-  <div class="form-row"> <!-- submit placement block -->
-    <div class="col-md-7 col-xl-5"></div>
-    <div class="col-md-5 col-xl-5 d-inline-flex">
-      <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
-    </div>
-  </div>
-</form>
index 6b8efad0b0f2e7c7feffec8be4d98ad316530883..b4962ed356fd25ca0a86c97b21508ef819d5fd0a 100644 (file)
@@ -1,7 +1,5 @@
 import { NgModule } from '@angular/core'
 import { RouterModule, Routes } from '@angular/router'
-import { MyVideoChannelUpdateComponent } from './my-video-channel-update.component'
-import { MyVideoChannelCreateComponent } from './my-video-channel-create.component'
 import { MyVideoChannelsComponent } from './my-video-channels.component'
 
 const myVideoChannelsRoutes: Routes = [
@@ -16,21 +14,11 @@ const myVideoChannelsRoutes: Routes = [
   },
   {
     path: 'create',
-    component: MyVideoChannelCreateComponent,
-    data: {
-      meta: {
-        title: $localize`Create a new video channel`
-      }
-    }
+    redirectTo: '/manage/create'
   },
   {
-    path: 'update/:videoChannelId',
-    component: MyVideoChannelUpdateComponent,
-    data: {
-      meta: {
-        title: $localize`Update video channel`
-      }
-    }
+    path: 'update/:videoChannelName',
+    redirectTo: '/manage/update/:videoChannelName'
   }
 ]
 
index bbe583971ab9c76277fb8e0a0aa8442b9f2837c6..77947315bbb38fd6872247ed1c213fb870d83323 100644 (file)
@@ -9,7 +9,7 @@
 <div class="video-channels-header d-flex justify-content-between">
   <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter>
 
-  <a class="create-button" routerLink="create">
+  <a class="create-button" routerLink="/manage/create">
     <my-global-icon iconName="add" aria-hidden="true"></my-global-icon>
     <ng-container i18n>Create video channel</ng-container>
   </a>
@@ -37,7 +37,7 @@
       <div i18n class="video-channel-videos">{videoChannel.videosCount, plural, =0 {No videos} =1 {1 video} other {{{ videoChannel.videosCount }} videos}}</div>
 
       <div class="video-channel-buttons">
-        <my-edit-button label [routerLink]="[ 'update', videoChannel.nameWithHost ]"></my-edit-button>
+        <my-edit-button label [routerLink]="[ '/manage/update', videoChannel.nameWithHost ]"></my-edit-button>
         <my-delete-button label (click)="deleteVideoChannel(videoChannel)"></my-delete-button>
       </div>
 
index c775bfdee46e931a917092394dea19b7316c213e..a17eb9f1069d3a1c43befcbfb98c688ca604416f 100644 (file)
@@ -1,11 +1,8 @@
 import { ChartModule } from 'primeng/chart'
 import { NgModule } from '@angular/core'
-import { SharedActorImageEditModule } from '@app/shared/shared-actor-image-edit'
 import { SharedFormModule } from '@app/shared/shared-forms'
 import { SharedGlobalIconModule } from '@app/shared/shared-icons'
 import { SharedMainModule } from '@app/shared/shared-main'
-import { MyVideoChannelCreateComponent } from './my-video-channel-create.component'
-import { MyVideoChannelUpdateComponent } from './my-video-channel-update.component'
 import { MyVideoChannelsRoutingModule } from './my-video-channels-routing.module'
 import { MyVideoChannelsComponent } from './my-video-channels.component'
 import { SharedActorImageModule } from '@app/shared/shared-actor-image/shared-actor-image.module'
@@ -19,14 +16,11 @@ import { SharedActorImageModule } from '@app/shared/shared-actor-image/shared-ac
     SharedMainModule,
     SharedFormModule,
     SharedGlobalIconModule,
-    SharedActorImageEditModule,
     SharedActorImageModule
   ],
 
   declarations: [
-    MyVideoChannelsComponent,
-    MyVideoChannelCreateComponent,
-    MyVideoChannelUpdateComponent
+    MyVideoChannelsComponent
   ],
 
   exports: [],
index aec2e373c246a5e69bccc503a16dcab3933d18c3..212e2f8671454e0d60f6cd02e34263726d21c41a 100644 (file)
@@ -6,11 +6,11 @@
   <div class="channel-info">
 
     <ng-template #buttonsTemplate>
-      <a *ngIf="isManageable()" [routerLink]="[ '/my-library/video-channels/update', videoChannel.nameWithHost ]" class="peertube-button-link orange-button" i18n>
+      <a *ngIf="isManageable()" [routerLink]="[ '/manage/update', videoChannel.nameWithHost  ]" class="peertube-button-link orange-button" i18n>
         Manage channel
       </a>
 
-      <my-subscribe-button *ngIf="!isManageable()" #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button>
+      <my-subscribe-button *ngIf="!isOwner()" #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button>
 
       <button *ngIf="videoChannel.support" (click)="showSupportModal()" class="support-button peertube-button orange-button-inverted">
         <my-global-icon iconName="support" aria-hidden="true"></my-global-icon>
index ebb991f4ee3a4fd5f63e175b12efc5fc0f5681f5..82c52d23919ec3a5fbc9b8c8103381dfd898dced 100644 (file)
@@ -8,7 +8,7 @@ import { Account, ListOverflowItem, VideoChannel, VideoChannelService, VideoServ
 import { BlocklistService } from '@app/shared/shared-moderation'
 import { SupportModalComponent } from '@app/shared/shared-support-modal'
 import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
-import { HttpStatusCode } from '@shared/models'
+import { HttpStatusCode, UserRight } from '@shared/models'
 
 @Component({
   templateUrl: './video-channels.component.html',
@@ -98,12 +98,18 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
     return this.authService.isLoggedIn()
   }
 
-  isManageable () {
+  isOwner () {
     if (!this.isUserLoggedIn()) return false
 
     return this.videoChannel?.ownerAccount.userId === this.authService.getUser().id
   }
 
+  isManageable () {
+    if (!this.isUserLoggedIn()) return false
+
+    return this.isOwner() || this.authService.getUser().hasRight(UserRight.MANAGE_ANY_VIDEO_CHANNEL)
+  }
+
   activateCopiedMessage () {
     this.notifier.success($localize`Username copied`)
   }
index 76aaecf83b075c2f5228bb241392ad2020b3bd71..aef3ed0a37f0f5ff9804272c0f5d0d7ecfc147b8 100644 (file)
@@ -12,6 +12,7 @@ import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-
 import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component'
 import { VideoChannelsRoutingModule } from './video-channels-routing.module'
 import { VideoChannelsComponent } from './video-channels.component'
+import { SharedActorImageEditModule } from '@app/shared/shared-actor-image-edit'
 
 @NgModule({
   imports: [
@@ -25,6 +26,7 @@ import { VideoChannelsComponent } from './video-channels.component'
     SharedGlobalIconModule,
     SharedSupportModal,
     SharedActorImageModule,
+    SharedActorImageEditModule,
     SharedModerationModule
   ],
 
index 42328d83ddff099ee0279015764bbd0eaf4c351a..b5afc9c92fb4dc0d21d1768c0c1d4867250d1e7d 100644 (file)
@@ -56,7 +56,11 @@ const routes: Routes = [
     loadChildren: () => import('./+video-channels/video-channels.module').then(m => m.VideoChannelsModule),
     canActivateChild: [ MetaGuard ]
   },
-
+  {
+    path: 'manage',
+    loadChildren: () => import('./+manage/manage.module').then(m => m.ManageModule),
+    canActivateChild: [ MetaGuard ]
+  },
   {
     path: 'p',
     loadChildren: () => import('./+plugin-pages/plugin-pages.module').then(m => m.PluginPagesModule),
index 17d9d1358b3a0db443a0f6b5a06eba3102cabfec..571476d1de529583b793c08ca5a2f8b3cd1fb65d 100644 (file)
@@ -46,7 +46,7 @@ export class RedirectService {
     return this.defaultTrendingAlgorithm
   }
 
-  redirectToPreviousRoute () {
+  redirectToPreviousRoute (fallbackRoute: string[] = null) {
     const exceptions = [
       '/verify-account',
       '/reset-password'
@@ -57,6 +57,10 @@ export class RedirectService {
       if (!isException) return this.router.navigateByUrl(this.previousUrl)
     }
 
+    if (fallbackRoute) {
+      return this.router.navigate(fallbackRoute)
+    }
+
     return this.redirectToHomepage()
   }
 
index c4e3cec6b4833da1a0effda48acee07f89cb008b..4e6bd5e25adc445a665baeaf8461cd17774612ee 100644 (file)
@@ -20,7 +20,8 @@ import {
   asyncMiddleware,
   executeIfActivityPub,
   localAccountValidator,
-  localVideoChannelValidator,
+  videoChannelsNameWithHostValidator,
+  ensureIsLocalChannel,
   videosCustomGetValidator,
   videosShareValidator
 } from '../../middlewares'
@@ -123,24 +124,28 @@ activityPubClientRouter.get('/videos/watch/:videoId/comments/:commentId/activity
 )
 
 activityPubClientRouter.get(
-  [ '/video-channels/:name', '/video-channels/:name/videos', '/c/:name', '/c/:name/videos' ],
+  [ '/video-channels/:nameWithHost', '/video-channels/:nameWithHost/videos', '/c/:nameWithHost', '/c/:nameWithHost/videos' ],
   executeIfActivityPub,
-  asyncMiddleware(localVideoChannelValidator),
+  asyncMiddleware(videoChannelsNameWithHostValidator),
+  ensureIsLocalChannel,
   videoChannelController
 )
-activityPubClientRouter.get('/video-channels/:name/followers',
+activityPubClientRouter.get('/video-channels/:nameWithHost/followers',
   executeIfActivityPub,
-  asyncMiddleware(localVideoChannelValidator),
+  asyncMiddleware(videoChannelsNameWithHostValidator),
+  ensureIsLocalChannel,
   asyncMiddleware(videoChannelFollowersController)
 )
-activityPubClientRouter.get('/video-channels/:name/following',
+activityPubClientRouter.get('/video-channels/:nameWithHost/following',
   executeIfActivityPub,
-  asyncMiddleware(localVideoChannelValidator),
+  asyncMiddleware(videoChannelsNameWithHostValidator),
+  ensureIsLocalChannel,
   asyncMiddleware(videoChannelFollowingController)
 )
-activityPubClientRouter.get('/video-channels/:name/playlists',
+activityPubClientRouter.get('/video-channels/:nameWithHost/playlists',
   executeIfActivityPub,
-  asyncMiddleware(localVideoChannelValidator),
+  asyncMiddleware(videoChannelsNameWithHostValidator),
+  ensureIsLocalChannel,
   asyncMiddleware(videoChannelPlaylistsController)
 )
 
index ece4edff0c00e5961b5d05b7c8d4f7ee491f5550..5995b8f3ad5e2c00ae6b643274df79e79c50d55f 100644 (file)
@@ -4,7 +4,14 @@ import { Activity, ActivityPubCollection, ActivityPubOrderedCollection, RootActi
 import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
 import { isActivityValid } from '../../helpers/custom-validators/activitypub/activity'
 import { logger } from '../../helpers/logger'
-import { asyncMiddleware, checkSignature, localAccountValidator, localVideoChannelValidator, signatureValidator } from '../../middlewares'
+import {
+  asyncMiddleware,
+  checkSignature,
+  ensureIsLocalChannel,
+  localAccountValidator,
+  signatureValidator,
+  videoChannelsNameWithHostValidator
+} from '../../middlewares'
 import { activityPubValidator } from '../../middlewares/validators/activitypub/activity'
 
 const inboxRouter = express.Router()
@@ -23,10 +30,11 @@ inboxRouter.post('/accounts/:name/inbox',
   asyncMiddleware(activityPubValidator),
   inboxController
 )
-inboxRouter.post('/video-channels/:name/inbox',
+inboxRouter.post('/video-channels/:nameWithHost/inbox',
   signatureValidator,
   asyncMiddleware(checkSignature),
-  asyncMiddleware(localVideoChannelValidator),
+  asyncMiddleware(videoChannelsNameWithHostValidator),
+  ensureIsLocalChannel,
   asyncMiddleware(activityPubValidator),
   inboxController
 )
index bdf9d138bfd6f035e88bd50eea5ef2a5e485cbb2..cdef8e9699b495825b5e80cbac60f2272abb939f 100644 (file)
@@ -1,15 +1,15 @@
 import express from 'express'
+import { MActorLight } from '@server/types/models'
 import { Activity } from '../../../shared/models/activitypub/activity'
 import { VideoPrivacy } from '../../../shared/models/videos'
 import { activityPubCollectionPagination, activityPubContextify } from '../../helpers/activitypub'
 import { logger } from '../../helpers/logger'
-import { buildAnnounceActivity, buildCreateActivity } from '../../lib/activitypub/send'
 import { buildAudience } from '../../lib/activitypub/audience'
-import { asyncMiddleware, localAccountValidator, localVideoChannelValidator } from '../../middlewares'
+import { buildAnnounceActivity, buildCreateActivity } from '../../lib/activitypub/send'
+import { asyncMiddleware, ensureIsLocalChannel, localAccountValidator, videoChannelsNameWithHostValidator } from '../../middlewares'
+import { apPaginationValidator } from '../../middlewares/validators/activitypub'
 import { VideoModel } from '../../models/video/video'
 import { activityPubResponse } from './utils'
-import { MActorLight } from '@server/types/models'
-import { apPaginationValidator } from '../../middlewares/validators/activitypub'
 
 const outboxRouter = express.Router()
 
@@ -19,9 +19,10 @@ outboxRouter.get('/accounts/:name/outbox',
   asyncMiddleware(outboxController)
 )
 
-outboxRouter.get('/video-channels/:name/outbox',
+outboxRouter.get('/video-channels/:nameWithHost/outbox',
   apPaginationValidator,
-  localVideoChannelValidator,
+  asyncMiddleware(videoChannelsNameWithHostValidator),
+  ensureIsLocalChannel,
   asyncMiddleware(outboxController)
 )
 
index d1a1e6473d5b6a788913f2cd24faec0dea3d13af..abb777e086a23c6ce696b610c9c3b5d4cce90bbf 100644 (file)
@@ -24,6 +24,7 @@ import {
   asyncRetryTransactionMiddleware,
   authenticate,
   commonVideosFiltersValidator,
+  ensureCanManageChannel,
   optionalAuthenticate,
   paginationValidator,
   setDefaultPagination,
@@ -36,7 +37,7 @@ import {
   videoPlaylistsSortValidator
 } from '../../middlewares'
 import {
-  ensureAuthUserOwnsChannelValidator,
+  ensureIsLocalChannel,
   videoChannelsFollowersSortValidator,
   videoChannelsListValidator,
   videoChannelsNameWithHostValidator,
@@ -74,7 +75,8 @@ videoChannelRouter.post('/:nameWithHost/avatar/pick',
   authenticate,
   reqAvatarFile,
   asyncMiddleware(videoChannelsNameWithHostValidator),
-  ensureAuthUserOwnsChannelValidator,
+  ensureIsLocalChannel,
+  ensureCanManageChannel,
   updateAvatarValidator,
   asyncMiddleware(updateVideoChannelAvatar)
 )
@@ -83,7 +85,8 @@ videoChannelRouter.post('/:nameWithHost/banner/pick',
   authenticate,
   reqBannerFile,
   asyncMiddleware(videoChannelsNameWithHostValidator),
-  ensureAuthUserOwnsChannelValidator,
+  ensureIsLocalChannel,
+  ensureCanManageChannel,
   updateBannerValidator,
   asyncMiddleware(updateVideoChannelBanner)
 )
@@ -91,27 +94,33 @@ videoChannelRouter.post('/:nameWithHost/banner/pick',
 videoChannelRouter.delete('/:nameWithHost/avatar',
   authenticate,
   asyncMiddleware(videoChannelsNameWithHostValidator),
-  ensureAuthUserOwnsChannelValidator,
+  ensureIsLocalChannel,
+  ensureCanManageChannel,
   asyncMiddleware(deleteVideoChannelAvatar)
 )
 
 videoChannelRouter.delete('/:nameWithHost/banner',
   authenticate,
   asyncMiddleware(videoChannelsNameWithHostValidator),
-  ensureAuthUserOwnsChannelValidator,
+  ensureIsLocalChannel,
+  ensureCanManageChannel,
   asyncMiddleware(deleteVideoChannelBanner)
 )
 
 videoChannelRouter.put('/:nameWithHost',
   authenticate,
   asyncMiddleware(videoChannelsNameWithHostValidator),
-  ensureAuthUserOwnsChannelValidator,
+  ensureIsLocalChannel,
+  ensureCanManageChannel,
   videoChannelsUpdateValidator,
   asyncRetryTransactionMiddleware(updateVideoChannel)
 )
 
 videoChannelRouter.delete('/:nameWithHost',
   authenticate,
+  asyncMiddleware(videoChannelsNameWithHostValidator),
+  ensureIsLocalChannel,
+  ensureCanManageChannel,
   asyncMiddleware(videoChannelsRemoveValidator),
   asyncRetryTransactionMiddleware(removeVideoChannel)
 )
@@ -145,7 +154,7 @@ videoChannelRouter.get('/:nameWithHost/videos',
 videoChannelRouter.get('/:nameWithHost/followers',
   authenticate,
   asyncMiddleware(videoChannelsNameWithHostValidator),
-  ensureAuthUserOwnsChannelValidator,
+  ensureCanManageChannel,
   paginationValidator,
   videoChannelsFollowersSortValidator,
   setDefaultSort,
index 7c0c89267b074396a7a892f7818a148e07d57d57..bed9f5dbe7d2109d3a5209ff5dba3599ecbe99f2 100644 (file)
@@ -3,12 +3,6 @@ import { VideoChannelModel } from '@server/models/video/video-channel'
 import { MChannelBannerAccountDefault } from '@server/types/models'
 import { HttpStatusCode } from '@shared/models'
 
-async function doesLocalVideoChannelNameExist (name: string, res: express.Response) {
-  const videoChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(name)
-
-  return processVideoChannelExist(videoChannel, res)
-}
-
 async function doesVideoChannelIdExist (id: number, res: express.Response) {
   const videoChannel = await VideoChannelModel.loadAndPopulateAccount(+id)
 
@@ -24,7 +18,6 @@ async function doesVideoChannelNameWithHostExist (nameWithDomain: string, res: e
 // ---------------------------------------------------------------------------
 
 export {
-  doesLocalVideoChannelNameExist,
   doesVideoChannelIdExist,
   doesVideoChannelNameWithHostExist
 }
index 33b31d54b83c93317f664d6ea186efacbc27b04f..7a6b2ce57f86122bc42b25e460a7c0bac44e3ef3 100644 (file)
@@ -3,7 +3,7 @@ import { body, param, query } from 'express-validator'
 import { omit } from 'lodash'
 import { Hooks } from '@server/lib/plugins/hooks'
 import { MUserDefault } from '@server/types/models'
-import { HttpStatusCode, UserRegister, UserRole } from '@shared/models'
+import { HttpStatusCode, UserRegister, UserRight, UserRole } from '@shared/models'
 import { isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
 import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
 import {
@@ -490,14 +490,17 @@ const ensureAuthUserOwnsAccountValidator = [
   }
 ]
 
-const ensureAuthUserOwnsChannelValidator = [
+const ensureCanManageChannel = [
   (req: express.Request, res: express.Response, next: express.NextFunction) => {
-    const user = res.locals.oauth.token.User
+    const user = res.locals.oauth.token.user
+    const isUserOwner = res.locals.videoChannel.Account.userId === user.id
+
+    if (!isUserOwner && user.hasRight(UserRight.MANAGE_ANY_VIDEO_CHANNEL) === false) {
+      const message = `User ${user.username} does not have right to manage channel ${req.params.nameWithHost}.`
 
-    if (res.locals.videoChannel.Account.userId !== user.id) {
       return res.fail({
         status: HttpStatusCode.FORBIDDEN_403,
-        message: 'Only owner of this video channel can access this ressource'
+        message
       })
     }
 
@@ -542,8 +545,8 @@ export {
   usersVerifyEmailValidator,
   userAutocompleteValidator,
   ensureAuthUserOwnsAccountValidator,
-  ensureAuthUserOwnsChannelValidator,
-  ensureCanManageUser
+  ensureCanManageUser,
+  ensureCanManageChannel
 }
 
 // ---------------------------------------------------------------------------
index edce48c7f76b4df23dbd1f5e7318b022122455a3..3bfdebbb149895871cb5cfc813b97be1b6fa0873 100644 (file)
@@ -1,7 +1,7 @@
 import express from 'express'
 import { body, param, query } from 'express-validator'
-import { MChannelAccountDefault, MUser } from '@server/types/models'
-import { UserRight } from '../../../../shared'
+import { CONFIG } from '@server/initializers/config'
+import { MChannelAccountDefault } from '@server/types/models'
 import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
 import { isBooleanValid, toBooleanOrNull } from '../../../helpers/custom-validators/misc'
 import {
@@ -13,8 +13,7 @@ import {
 import { logger } from '../../../helpers/logger'
 import { ActorModel } from '../../../models/actor/actor'
 import { VideoChannelModel } from '../../../models/video/video-channel'
-import { areValidationErrors, doesLocalVideoChannelNameExist, doesVideoChannelNameWithHostExist } from '../shared'
-import { CONFIG } from '@server/initializers/config'
+import { areValidationErrors, doesVideoChannelNameWithHostExist } from '../shared'
 
 const videoChannelsAddValidator = [
   body('name').custom(isVideoChannelUsernameValid).withMessage('Should have a valid channel name'),
@@ -71,16 +70,10 @@ const videoChannelsUpdateValidator = [
 ]
 
 const videoChannelsRemoveValidator = [
-  param('nameWithHost').exists().withMessage('Should have an video channel name with host'),
-
   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
     logger.debug('Checking videoChannelsRemove parameters', { parameters: req.params })
 
-    if (areValidationErrors(req, res)) return
-    if (!await doesVideoChannelNameWithHostExist(req.params.nameWithHost, res)) return
-
-    if (!checkUserCanDeleteVideoChannel(res.locals.oauth.token.User, res.locals.videoChannel, res)) return
-    if (!await checkVideoChannelIsNotTheLastOne(res)) return
+    if (!await checkVideoChannelIsNotTheLastOne(res.locals.videoChannel, res)) return
 
     return next()
   }
@@ -100,14 +93,14 @@ const videoChannelsNameWithHostValidator = [
   }
 ]
 
-const localVideoChannelValidator = [
-  param('name').custom(isVideoChannelDisplayNameValid).withMessage('Should have a valid video channel name'),
-
-  async (req: express.Request, res: express.Response, next: express.NextFunction) => {
-    logger.debug('Checking localVideoChannelValidator parameters', { parameters: req.params })
-
-    if (areValidationErrors(req, res)) return
-    if (!await doesLocalVideoChannelNameExist(req.params.name, res)) return
+const ensureIsLocalChannel = [
+  (req: express.Request, res: express.Response, next: express.NextFunction) => {
+    if (res.locals.videoChannel.Actor.isOwned() === false) {
+      return res.fail({
+        status: HttpStatusCode.FORBIDDEN_403,
+        message: 'This channel is not owned.'
+      })
+    }
 
     return next()
   }
@@ -144,38 +137,15 @@ export {
   videoChannelsUpdateValidator,
   videoChannelsRemoveValidator,
   videoChannelsNameWithHostValidator,
+  ensureIsLocalChannel,
   videoChannelsListValidator,
-  localVideoChannelValidator,
   videoChannelStatsValidator
 }
 
 // ---------------------------------------------------------------------------
 
-function checkUserCanDeleteVideoChannel (user: MUser, videoChannel: MChannelAccountDefault, res: express.Response) {
-  if (videoChannel.Actor.isOwned() === false) {
-    res.fail({
-      status: HttpStatusCode.FORBIDDEN_403,
-      message: 'Cannot remove video channel of another server.'
-    })
-    return false
-  }
-
-  // Check if the user can delete the video channel
-  // The user can delete it if s/he is an admin
-  // Or if s/he is the video channel's account
-  if (user.hasRight(UserRight.REMOVE_ANY_VIDEO_CHANNEL) === false && videoChannel.Account.userId !== user.id) {
-    res.fail({
-      status: HttpStatusCode.FORBIDDEN_403,
-      message: 'Cannot remove video channel of another user'
-    })
-    return false
-  }
-
-  return true
-}
-
-async function checkVideoChannelIsNotTheLastOne (res: express.Response) {
-  const count = await VideoChannelModel.countByAccount(res.locals.oauth.token.User.Account.id)
+async function checkVideoChannelIsNotTheLastOne (videoChannel: MChannelAccountDefault, res: express.Response) {
+  const count = await VideoChannelModel.countByAccount(videoChannel.Account.id)
 
   if (count <= 1) {
     res.fail({
index c25754eb6a92d3c2a5f8c75e69705f5bd4b4ad82..6ab5faa077cda9b1348c655ce4137d420b02f342 100644 (file)
@@ -33,6 +33,7 @@ describe('Test video channels', function () {
   let totoChannel: number
   let videoUUID: string
   let accountName: string
+  let secondUserChannelName: string
 
   const avatarPaths: { [ port: number ]: string } = {}
   const bannerPaths: { [ port: number ]: string } = {}
@@ -219,6 +220,35 @@ describe('Test video channels', function () {
     }
   })
 
+  it('Should update another accounts video channel', async function () {
+    this.timeout(15000)
+
+    const result = await servers[0].users.generate('second_user')
+    secondUserChannelName = result.userChannelName
+
+    await servers[0].videos.quickUpload({ name: 'video', token: result.token })
+
+    const videoChannelAttributes = {
+      displayName: 'video channel updated',
+      description: 'video channel description updated',
+      support: 'support updated'
+    }
+
+    await servers[0].channels.update({ channelName: secondUserChannelName, attributes: videoChannelAttributes })
+
+    await waitJobs(servers)
+  })
+
+  it('Should have another accounts video channel updated', async function () {
+    for (const server of servers) {
+      const body = await server.channels.get({ channelName: `${secondUserChannelName}@${servers[0].host}` })
+
+      expect(body.displayName).to.equal('video channel updated')
+      expect(body.description).to.equal('video channel description updated')
+      expect(body.support).to.equal('support updated')
+    }
+  })
+
   it('Should update the channel support field and update videos too', async function () {
     this.timeout(35000)
 
@@ -368,12 +398,13 @@ describe('Test video channels', function () {
   })
 
   it('Should have video channel deleted', async function () {
-    const body = await servers[0].channels.list({ start: 0, count: 10 })
+    const body = await servers[0].channels.list({ start: 0, count: 10, sort: 'createdAt' })
 
-    expect(body.total).to.equal(1)
+    expect(body.total).to.equal(2)
     expect(body.data).to.be.an('array')
-    expect(body.data).to.have.lengthOf(1)
+    expect(body.data).to.have.lengthOf(2)
     expect(body.data[0].displayName).to.equal('Main root channel')
+    expect(body.data[1].displayName).to.equal('video channel updated')
   })
 
   it('Should create the main channel with an uuid if there is a conflict', async function () {
index 81cba1dad0fc3ac9fd4f2a59aa727c5d60460a38..cc757d779e9176b3bf634c1e36244f43f3c645b7 100644 (file)
@@ -14,8 +14,8 @@ const userRoleRights: { [ id in UserRole ]: UserRight[] } = {
   [UserRole.MODERATOR]: [
     UserRight.MANAGE_VIDEO_BLACKLIST,
     UserRight.MANAGE_ABUSES,
+    UserRight.MANAGE_ANY_VIDEO_CHANNEL,
     UserRight.REMOVE_ANY_VIDEO,
-    UserRight.REMOVE_ANY_VIDEO_CHANNEL,
     UserRight.REMOVE_ANY_VIDEO_PLAYLIST,
     UserRight.REMOVE_ANY_VIDEO_COMMENT,
     UserRight.UPDATE_ANY_VIDEO,
index 2a10e4fc8d2fe3baa676bab49f56d3824d54b91e..90c5f218340aefe38c9764f41761be7a6d077ab6 100644 (file)
@@ -202,7 +202,8 @@ export class UsersCommand extends AbstractCommand {
     return {
       token,
       userId: user.id,
-      userChannelId: me.videoChannels[0].id
+      userChannelId: me.videoChannels[0].id,
+      userChannelName: me.videoChannels[0].name
     }
   }
 
index 6415ca6f2ec9b6e6f7ddd897d6e46cab62776537..668535f4eb86d12f74d344206ba8bf2bcbe016fe 100644 (file)
@@ -22,9 +22,9 @@ export const enum UserRight {
   MANAGE_SERVERS_BLOCKLIST,
 
   MANAGE_VIDEO_BLACKLIST,
+  MANAGE_ANY_VIDEO_CHANNEL,
 
   REMOVE_ANY_VIDEO,
-  REMOVE_ANY_VIDEO_CHANNEL,
   REMOVE_ANY_VIDEO_PLAYLIST,
   REMOVE_ANY_VIDEO_COMMENT,