aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorkontrollanten <6680299+kontrollanten@users.noreply.github.com>2021-12-13 15:29:13 +0100
committerGitHub <noreply@github.com>2021-12-13 15:29:13 +0100
commita37e9e74ff07b057370d1ed6c0b391a02be8a6d2 (patch)
tree30d59e12518149a309bbd10bee1485f8be523c75
parent11e520b50d791a0dd48cbb2d0fc681b25eb7cd53 (diff)
downloadPeerTube-a37e9e74ff07b057370d1ed6c0b391a02be8a6d2.tar.gz
PeerTube-a37e9e74ff07b057370d1ed6c0b391a02be8a6d2.tar.zst
PeerTube-a37e9e74ff07b057370d1ed6c0b391a02be8a6d2.zip
Give moderators access to edit channels (#4608)
* 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>
-rw-r--r--client/src/app/+manage/manage-routing.module.ts31
-rw-r--r--client/src/app/+manage/manage.module.ts31
-rw-r--r--client/src/app/+manage/video-channel-edit/video-channel-create.component.ts (renamed from client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts)8
-rw-r--r--client/src/app/+manage/video-channel-edit/video-channel-edit.component.html96
-rw-r--r--client/src/app/+manage/video-channel-edit/video-channel-edit.component.scss (renamed from client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss)4
-rw-r--r--client/src/app/+manage/video-channel-edit/video-channel-edit.ts (renamed from client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts)2
-rw-r--r--client/src/app/+manage/video-channel-edit/video-channel-update.component.ts (renamed from client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts)19
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html112
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channels-routing.module.ts18
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channels.component.html4
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts8
-rw-r--r--client/src/app/+video-channels/video-channels.component.html4
-rw-r--r--client/src/app/+video-channels/video-channels.component.ts10
-rw-r--r--client/src/app/+video-channels/video-channels.module.ts2
-rw-r--r--client/src/app/app-routing.module.ts6
-rw-r--r--client/src/app/core/routing/redirect.service.ts6
-rw-r--r--server/controllers/activitypub/client.ts23
-rw-r--r--server/controllers/activitypub/inbox.ts14
-rw-r--r--server/controllers/activitypub/outbox.ts13
-rw-r--r--server/controllers/api/video-channel.ts23
-rw-r--r--server/middlewares/validators/shared/video-channels.ts7
-rw-r--r--server/middlewares/validators/users.ts17
-rw-r--r--server/middlewares/validators/videos/video-channels.ts60
-rw-r--r--server/tests/api/videos/video-channels.ts37
-rw-r--r--shared/core-utils/users/user-role.ts2
-rw-r--r--shared/extra-utils/users/users-command.ts3
-rw-r--r--shared/models/users/user-right.enum.ts2
27 files changed, 316 insertions, 246 deletions
diff --git a/client/src/app/+manage/manage-routing.module.ts b/client/src/app/+manage/manage-routing.module.ts
new file mode 100644
index 000000000..14ae4f1e0
--- /dev/null
+++ b/client/src/app/+manage/manage-routing.module.ts
@@ -0,0 +1,31 @@
1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router'
3import { VideoChannelCreateComponent } from './video-channel-edit/video-channel-create.component'
4import { VideoChannelUpdateComponent } from './video-channel-edit/video-channel-update.component'
5
6const manageRoutes: Routes = [
7 {
8 path: 'create',
9 component: VideoChannelCreateComponent,
10 data: {
11 meta: {
12 title: $localize`Create a new video channel`
13 }
14 }
15 },
16 {
17 path: 'update/:videoChannelName',
18 component: VideoChannelUpdateComponent,
19 data: {
20 meta: {
21 title: $localize`Update video channel`
22 }
23 }
24 }
25]
26
27@NgModule({
28 imports: [ RouterModule.forChild(manageRoutes) ],
29 exports: [ RouterModule ]
30})
31export class ManageRoutingModule {}
diff --git a/client/src/app/+manage/manage.module.ts b/client/src/app/+manage/manage.module.ts
new file mode 100644
index 000000000..28939ec5a
--- /dev/null
+++ b/client/src/app/+manage/manage.module.ts
@@ -0,0 +1,31 @@
1import { NgModule } from '@angular/core'
2import { SharedFormModule } from '@app/shared/shared-forms'
3import { SharedGlobalIconModule } from '@app/shared/shared-icons'
4import { SharedMainModule } from '@app/shared/shared-main'
5import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module'
6import { SharedActorImageEditModule } from '@app/shared/shared-actor-image-edit'
7import { VideoChannelCreateComponent } from './video-channel-edit/video-channel-create.component'
8import { VideoChannelUpdateComponent } from './video-channel-edit/video-channel-update.component'
9import { ManageRoutingModule } from './manage-routing.module'
10
11@NgModule({
12 imports: [
13 ManageRoutingModule,
14 SharedMainModule,
15 SharedFormModule,
16 SharedGlobalIconModule,
17 SharedActorImageModule,
18 SharedActorImageEditModule
19 ],
20
21 declarations: [
22 VideoChannelCreateComponent,
23 VideoChannelUpdateComponent
24 ],
25
26 exports: [
27 ],
28
29 providers: []
30})
31export class ManageModule { }
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts b/client/src/app/+manage/video-channel-edit/video-channel-create.component.ts
index fd00720d8..5f8e0278e 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts
+++ b/client/src/app/+manage/video-channel-edit/video-channel-create.component.ts
@@ -12,13 +12,13 @@ import {
12import { FormValidatorService } from '@app/shared/shared-forms' 12import { FormValidatorService } from '@app/shared/shared-forms'
13import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' 13import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
14import { HttpStatusCode, VideoChannelCreate } from '@shared/models' 14import { HttpStatusCode, VideoChannelCreate } from '@shared/models'
15import { MyVideoChannelEdit } from './my-video-channel-edit' 15import { VideoChannelEdit } from './video-channel-edit'
16 16
17@Component({ 17@Component({
18 templateUrl: './my-video-channel-edit.component.html', 18 templateUrl: './video-channel-edit.component.html',
19 styleUrls: [ './my-video-channel-edit.component.scss' ] 19 styleUrls: [ './video-channel-edit.component.scss' ]
20}) 20})
21export class MyVideoChannelCreateComponent extends MyVideoChannelEdit implements OnInit { 21export class VideoChannelCreateComponent extends VideoChannelEdit implements OnInit {
22 error: string 22 error: string
23 videoChannel = new VideoChannel({}) 23 videoChannel = new VideoChannel({})
24 24
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
index 000000000..3751747a9
--- /dev/null
+++ b/client/src/app/+manage/video-channel-edit/video-channel-edit.component.html
@@ -0,0 +1,96 @@
1<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
2
3<div class="margin-content">
4 <form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
5
6 <div class="form-row"> <!-- channel grid -->
7 <div class="form-group col-12 col-lg-4 col-xl-3">
8 <div *ngIf="isCreation()" class="video-channel-title" i18n>NEW CHANNEL</div>
9 <div *ngIf="!isCreation() && videoChannel" class="video-channel-title" i18n>CHANNEL</div>
10 </div>
11
12 <div class="form-group col-12 col-lg-8 col-xl-9">
13 <h6 i18n>Banner image of the channel</h6>
14
15 <my-actor-banner-edit
16 *ngIf="videoChannel" [previewImage]="isCreation()"
17 [actor]="videoChannel" (bannerChange)="onBannerChange($event)" (bannerDelete)="onBannerDelete()"
18 ></my-actor-banner-edit>
19
20 <my-actor-avatar-edit
21 *ngIf="videoChannel" [previewImage]="isCreation()"
22 [actor]="videoChannel" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"
23 [displayUsername]="!isCreation()" [displaySubscribers]="!isCreation()"
24 ></my-actor-avatar-edit>
25
26 <div class="form-group" *ngIf="isCreation()">
27 <label i18n for="name">Name</label>
28 <div class="input-group">
29 <input
30 type="text" id="name" i18n-placeholder placeholder="Example: my_channel"
31 formControlName="name" [ngClass]="{ 'input-error': formErrors['name'] }" class="form-control"
32 >
33 <div class="input-group-append">
34 <span class="input-group-text">@{{ instanceHost }}</span>
35 </div>
36 </div>
37 <div *ngIf="formErrors['name']" class="form-error">
38 {{ formErrors['name'] }}
39 </div>
40 </div>
41
42 <div class="form-group">
43 <label i18n for="display-name">Display name</label>
44 <input
45 type="text" id="display-name" class="form-control"
46 formControlName="display-name" [ngClass]="{ 'input-error': formErrors['display-name'] }"
47 >
48 <div *ngIf="formErrors['display-name']" class="form-error">
49 {{ formErrors['display-name'] }}
50 </div>
51 </div>
52
53 <div class="form-group">
54 <label i18n for="description">Description</label>
55 <textarea
56 id="description" formControlName="description" class="form-control"
57 [ngClass]="{ 'input-error': formErrors['description'] }"
58 ></textarea>
59 <div *ngIf="formErrors.description" class="form-error">
60 {{ formErrors.description }}
61 </div>
62 </div>
63
64 <div class="form-group">
65 <label for="support">Support</label>
66 <my-help
67 helpType="markdownEnhanced" i18n-preHtml preHtml="Short text to tell people how they can support the channel (membership platform...).<br /><br />
68 When a video is uploaded in this channel, the video support field will be automatically filled by this text."
69 ></my-help>
70 <my-markdown-textarea
71 id="support" formControlName="support" textareaMaxWidth="500px" markdownType="enhanced"
72 [classes]="{ 'input-error': formErrors['support'] }"
73 ></my-markdown-textarea>
74 <div *ngIf="formErrors.support" class="form-error">
75 {{ formErrors.support }}
76 </div>
77 </div>
78
79 <div class="form-group" *ngIf="isBulkUpdateVideosDisplayed()">
80 <my-peertube-checkbox
81 inputName="bulkVideosSupportUpdate" formControlName="bulkVideosSupportUpdate"
82 i18n-labelText labelText="Overwrite support field of all videos of this channel"
83 ></my-peertube-checkbox>
84 </div>
85
86 </div>
87 </div>
88
89 <div class="form-row"> <!-- submit placement block -->
90 <div class="col-md-7 col-xl-5"></div>
91 <div class="col-md-5 col-xl-5 d-inline-flex">
92 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
93 </div>
94 </div>
95 </form>
96</div> \ No newline at end of file
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss b/client/src/app/+manage/video-channel-edit/video-channel-edit.component.scss
index d8bfe71b6..d010d6277 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss
+++ b/client/src/app/+manage/video-channel-edit/video-channel-edit.component.scss
@@ -1,6 +1,10 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3 3
4.margin-content {
5 padding-top: 20px;
6}
7
4label { 8label {
5 font-weight: $font-regular; 9 font-weight: $font-regular;
6 font-size: 100%; 10 font-size: 100%;
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts b/client/src/app/+manage/video-channel-edit/video-channel-edit.ts
index 33bb90f14..963b4cbbe 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts
+++ b/client/src/app/+manage/video-channel-edit/video-channel-edit.ts
@@ -1,7 +1,7 @@
1import { FormReactive } from '@app/shared/shared-forms' 1import { FormReactive } from '@app/shared/shared-forms'
2import { VideoChannel } from '@app/shared/shared-main' 2import { VideoChannel } from '@app/shared/shared-main'
3 3
4export abstract class MyVideoChannelEdit extends FormReactive { 4export abstract class VideoChannelEdit extends FormReactive {
5 videoChannel: VideoChannel 5 videoChannel: VideoChannel
6 6
7 abstract isCreation (): boolean 7 abstract isCreation (): boolean
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts b/client/src/app/+manage/video-channel-edit/video-channel-update.component.ts
index f9521b8b5..21b6167b2 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts
+++ b/client/src/app/+manage/video-channel-edit/video-channel-update.component.ts
@@ -2,7 +2,7 @@ import { Subscription } from 'rxjs'
2import { HttpErrorResponse } from '@angular/common/http' 2import { HttpErrorResponse } from '@angular/common/http'
3import { Component, OnDestroy, OnInit } from '@angular/core' 3import { Component, OnDestroy, OnInit } from '@angular/core'
4import { ActivatedRoute, Router } from '@angular/router' 4import { ActivatedRoute, Router } from '@angular/router'
5import { AuthService, Notifier, ServerService } from '@app/core' 5import { AuthService, Notifier, RedirectService, ServerService } from '@app/core'
6import { genericUploadErrorHandler } from '@app/helpers' 6import { genericUploadErrorHandler } from '@app/helpers'
7import { 7import {
8 VIDEO_CHANNEL_DESCRIPTION_VALIDATOR, 8 VIDEO_CHANNEL_DESCRIPTION_VALIDATOR,
@@ -12,14 +12,14 @@ import {
12import { FormValidatorService } from '@app/shared/shared-forms' 12import { FormValidatorService } from '@app/shared/shared-forms'
13import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' 13import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
14import { HTMLServerConfig, VideoChannelUpdate } from '@shared/models' 14import { HTMLServerConfig, VideoChannelUpdate } from '@shared/models'
15import { MyVideoChannelEdit } from './my-video-channel-edit' 15import { VideoChannelEdit } from './video-channel-edit'
16 16
17@Component({ 17@Component({
18 selector: 'my-video-channel-update', 18 selector: 'my-video-channel-update',
19 templateUrl: './my-video-channel-edit.component.html', 19 templateUrl: './video-channel-edit.component.html',
20 styleUrls: [ './my-video-channel-edit.component.scss' ] 20 styleUrls: [ './video-channel-edit.component.scss' ]
21}) 21})
22export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements OnInit, OnDestroy { 22export class VideoChannelUpdateComponent extends VideoChannelEdit implements OnInit, OnDestroy {
23 error: string 23 error: string
24 videoChannel: VideoChannel 24 videoChannel: VideoChannel
25 25
@@ -34,7 +34,8 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
34 private router: Router, 34 private router: Router,
35 private route: ActivatedRoute, 35 private route: ActivatedRoute,
36 private videoChannelService: VideoChannelService, 36 private videoChannelService: VideoChannelService,
37 private serverService: ServerService 37 private serverService: ServerService,
38 private redirectService: RedirectService
38 ) { 39 ) {
39 super() 40 super()
40 } 41 }
@@ -50,9 +51,9 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
50 }) 51 })
51 52
52 this.paramsSub = this.route.params.subscribe(routeParams => { 53 this.paramsSub = this.route.params.subscribe(routeParams => {
53 const videoChannelId = routeParams['videoChannelId'] 54 const videoChannelName = routeParams['videoChannelName']
54 55
55 this.videoChannelService.getVideoChannel(videoChannelId) 56 this.videoChannelService.getVideoChannel(videoChannelName)
56 .subscribe({ 57 .subscribe({
57 next: videoChannelToUpdate => { 58 next: videoChannelToUpdate => {
58 this.videoChannel = videoChannelToUpdate 59 this.videoChannel = videoChannelToUpdate
@@ -95,7 +96,7 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
95 96
96 this.notifier.success($localize`Video channel ${videoChannelUpdate.displayName} updated.`) 97 this.notifier.success($localize`Video channel ${videoChannelUpdate.displayName} updated.`)
97 98
98 this.router.navigate([ '/my-library', 'video-channels' ]) 99 this.redirectService.redirectToPreviousRoute([ '/c', this.videoChannel.name ])
99 }, 100 },
100 101
101 error: err => { 102 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
index 2910dffad..000000000
--- a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html
+++ /dev/null
@@ -1,112 +0,0 @@
1<nav aria-label="breadcrumb">
2 <ol class="breadcrumb">
3 <li class="breadcrumb-item">
4 <a routerLink="/my-library/video-channels" i18n>My Channels</a>
5 </li>
6
7 <ng-container *ngIf="isCreation()">
8 <li class="breadcrumb-item active" i18n>Create</li>
9 </ng-container>
10 <ng-container *ngIf="!isCreation()">
11 <li class="breadcrumb-item active" i18n>Edit</li>
12 <li class="breadcrumb-item active" aria-current="page">
13 <a *ngIf="videoChannel" [routerLink]="[ '/my-library/video-channels/update', videoChannel?.nameWithHost ]">{{ videoChannel?.displayName }}</a>
14 </li>
15 </ng-container>
16 </ol>
17</nav>
18
19<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
20
21<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
22
23 <div class="form-row"> <!-- channel grid -->
24 <div class="form-group col-12 col-lg-4 col-xl-3">
25 <div *ngIf="isCreation()" class="video-channel-title" i18n>NEW CHANNEL</div>
26 <div *ngIf="!isCreation() && videoChannel" class="video-channel-title" i18n>CHANNEL</div>
27 </div>
28
29 <div class="form-group col-12 col-lg-8 col-xl-9">
30 <h6 i18n>Banner image of your channel</h6>
31
32 <my-actor-banner-edit
33 *ngIf="videoChannel" [previewImage]="isCreation()"
34 [actor]="videoChannel" (bannerChange)="onBannerChange($event)" (bannerDelete)="onBannerDelete()"
35 ></my-actor-banner-edit>
36
37 <my-actor-avatar-edit
38 *ngIf="videoChannel" [previewImage]="isCreation()"
39 [actor]="videoChannel" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"
40 [displayUsername]="!isCreation()" [displaySubscribers]="!isCreation()"
41 ></my-actor-avatar-edit>
42
43 <div class="form-group" *ngIf="isCreation()">
44 <label i18n for="name">Name</label>
45 <div class="input-group">
46 <input
47 type="text" id="name" i18n-placeholder placeholder="Example: my_channel"
48 formControlName="name" [ngClass]="{ 'input-error': formErrors['name'] }" class="form-control"
49 >
50 <div class="input-group-append">
51 <span class="input-group-text">@{{ instanceHost }}</span>
52 </div>
53 </div>
54 <div *ngIf="formErrors['name']" class="form-error">
55 {{ formErrors['name'] }}
56 </div>
57 </div>
58
59 <div class="form-group">
60 <label i18n for="display-name">Display name</label>
61 <input
62 type="text" id="display-name" class="form-control"
63 formControlName="display-name" [ngClass]="{ 'input-error': formErrors['display-name'] }"
64 >
65 <div *ngIf="formErrors['display-name']" class="form-error">
66 {{ formErrors['display-name'] }}
67 </div>
68 </div>
69
70 <div class="form-group">
71 <label i18n for="description">Description</label>
72 <textarea
73 id="description" formControlName="description" class="form-control"
74 [ngClass]="{ 'input-error': formErrors['description'] }"
75 ></textarea>
76 <div *ngIf="formErrors.description" class="form-error">
77 {{ formErrors.description }}
78 </div>
79 </div>
80
81 <div class="form-group">
82 <label for="support">Support</label>
83 <my-help
84 helpType="markdownEnhanced" i18n-preHtml preHtml="Short text to tell people how they can support your channel (membership platform...).<br /><br />
85 When you will upload a video in this channel, the video support field will be automatically filled by this text."
86 ></my-help>
87 <my-markdown-textarea
88 id="support" formControlName="support" textareaMaxWidth="500px" markdownType="enhanced"
89 [classes]="{ 'input-error': formErrors['support'] }"
90 ></my-markdown-textarea>
91 <div *ngIf="formErrors.support" class="form-error">
92 {{ formErrors.support }}
93 </div>
94 </div>
95
96 <div class="form-group" *ngIf="isBulkUpdateVideosDisplayed()">
97 <my-peertube-checkbox
98 inputName="bulkVideosSupportUpdate" formControlName="bulkVideosSupportUpdate"
99 i18n-labelText labelText="Overwrite support field of all videos of this channel"
100 ></my-peertube-checkbox>
101 </div>
102
103 </div>
104 </div>
105
106 <div class="form-row"> <!-- submit placement block -->
107 <div class="col-md-7 col-xl-5"></div>
108 <div class="col-md-5 col-xl-5 d-inline-flex">
109 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
110 </div>
111 </div>
112</form>
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels-routing.module.ts b/client/src/app/+my-library/+my-video-channels/my-video-channels-routing.module.ts
index 6b8efad0b..b4962ed35 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channels-routing.module.ts
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channels-routing.module.ts
@@ -1,7 +1,5 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { MyVideoChannelUpdateComponent } from './my-video-channel-update.component'
4import { MyVideoChannelCreateComponent } from './my-video-channel-create.component'
5import { MyVideoChannelsComponent } from './my-video-channels.component' 3import { MyVideoChannelsComponent } from './my-video-channels.component'
6 4
7const myVideoChannelsRoutes: Routes = [ 5const myVideoChannelsRoutes: Routes = [
@@ -16,21 +14,11 @@ const myVideoChannelsRoutes: Routes = [
16 }, 14 },
17 { 15 {
18 path: 'create', 16 path: 'create',
19 component: MyVideoChannelCreateComponent, 17 redirectTo: '/manage/create'
20 data: {
21 meta: {
22 title: $localize`Create a new video channel`
23 }
24 }
25 }, 18 },
26 { 19 {
27 path: 'update/:videoChannelId', 20 path: 'update/:videoChannelName',
28 component: MyVideoChannelUpdateComponent, 21 redirectTo: '/manage/update/:videoChannelName'
29 data: {
30 meta: {
31 title: $localize`Update video channel`
32 }
33 }
34 } 22 }
35] 23]
36 24
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html
index bbe583971..77947315b 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html
@@ -9,7 +9,7 @@
9<div class="video-channels-header d-flex justify-content-between"> 9<div class="video-channels-header d-flex justify-content-between">
10 <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter> 10 <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter>
11 11
12 <a class="create-button" routerLink="create"> 12 <a class="create-button" routerLink="/manage/create">
13 <my-global-icon iconName="add" aria-hidden="true"></my-global-icon> 13 <my-global-icon iconName="add" aria-hidden="true"></my-global-icon>
14 <ng-container i18n>Create video channel</ng-container> 14 <ng-container i18n>Create video channel</ng-container>
15 </a> 15 </a>
@@ -37,7 +37,7 @@
37 <div i18n class="video-channel-videos">{videoChannel.videosCount, plural, =0 {No videos} =1 {1 video} other {{{ videoChannel.videosCount }} videos}}</div> 37 <div i18n class="video-channel-videos">{videoChannel.videosCount, plural, =0 {No videos} =1 {1 video} other {{{ videoChannel.videosCount }} videos}}</div>
38 38
39 <div class="video-channel-buttons"> 39 <div class="video-channel-buttons">
40 <my-edit-button label [routerLink]="[ 'update', videoChannel.nameWithHost ]"></my-edit-button> 40 <my-edit-button label [routerLink]="[ '/manage/update', videoChannel.nameWithHost ]"></my-edit-button>
41 <my-delete-button label (click)="deleteVideoChannel(videoChannel)"></my-delete-button> 41 <my-delete-button label (click)="deleteVideoChannel(videoChannel)"></my-delete-button>
42 </div> 42 </div>
43 43
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts b/client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts
index c775bfdee..a17eb9f10 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts
@@ -1,11 +1,8 @@
1import { ChartModule } from 'primeng/chart' 1import { ChartModule } from 'primeng/chart'
2import { NgModule } from '@angular/core' 2import { NgModule } from '@angular/core'
3import { SharedActorImageEditModule } from '@app/shared/shared-actor-image-edit'
4import { SharedFormModule } from '@app/shared/shared-forms' 3import { SharedFormModule } from '@app/shared/shared-forms'
5import { SharedGlobalIconModule } from '@app/shared/shared-icons' 4import { SharedGlobalIconModule } from '@app/shared/shared-icons'
6import { SharedMainModule } from '@app/shared/shared-main' 5import { SharedMainModule } from '@app/shared/shared-main'
7import { MyVideoChannelCreateComponent } from './my-video-channel-create.component'
8import { MyVideoChannelUpdateComponent } from './my-video-channel-update.component'
9import { MyVideoChannelsRoutingModule } from './my-video-channels-routing.module' 6import { MyVideoChannelsRoutingModule } from './my-video-channels-routing.module'
10import { MyVideoChannelsComponent } from './my-video-channels.component' 7import { MyVideoChannelsComponent } from './my-video-channels.component'
11import { SharedActorImageModule } from '@app/shared/shared-actor-image/shared-actor-image.module' 8import { 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
19 SharedMainModule, 16 SharedMainModule,
20 SharedFormModule, 17 SharedFormModule,
21 SharedGlobalIconModule, 18 SharedGlobalIconModule,
22 SharedActorImageEditModule,
23 SharedActorImageModule 19 SharedActorImageModule
24 ], 20 ],
25 21
26 declarations: [ 22 declarations: [
27 MyVideoChannelsComponent, 23 MyVideoChannelsComponent
28 MyVideoChannelCreateComponent,
29 MyVideoChannelUpdateComponent
30 ], 24 ],
31 25
32 exports: [], 26 exports: [],
diff --git a/client/src/app/+video-channels/video-channels.component.html b/client/src/app/+video-channels/video-channels.component.html
index aec2e373c..212e2f867 100644
--- a/client/src/app/+video-channels/video-channels.component.html
+++ b/client/src/app/+video-channels/video-channels.component.html
@@ -6,11 +6,11 @@
6 <div class="channel-info"> 6 <div class="channel-info">
7 7
8 <ng-template #buttonsTemplate> 8 <ng-template #buttonsTemplate>
9 <a *ngIf="isManageable()" [routerLink]="[ '/my-library/video-channels/update', videoChannel.nameWithHost ]" class="peertube-button-link orange-button" i18n> 9 <a *ngIf="isManageable()" [routerLink]="[ '/manage/update', videoChannel.nameWithHost ]" class="peertube-button-link orange-button" i18n>
10 Manage channel 10 Manage channel
11 </a> 11 </a>
12 12
13 <my-subscribe-button *ngIf="!isManageable()" #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button> 13 <my-subscribe-button *ngIf="!isOwner()" #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button>
14 14
15 <button *ngIf="videoChannel.support" (click)="showSupportModal()" class="support-button peertube-button orange-button-inverted"> 15 <button *ngIf="videoChannel.support" (click)="showSupportModal()" class="support-button peertube-button orange-button-inverted">
16 <my-global-icon iconName="support" aria-hidden="true"></my-global-icon> 16 <my-global-icon iconName="support" aria-hidden="true"></my-global-icon>
diff --git a/client/src/app/+video-channels/video-channels.component.ts b/client/src/app/+video-channels/video-channels.component.ts
index ebb991f4e..82c52d239 100644
--- a/client/src/app/+video-channels/video-channels.component.ts
+++ b/client/src/app/+video-channels/video-channels.component.ts
@@ -8,7 +8,7 @@ import { Account, ListOverflowItem, VideoChannel, VideoChannelService, VideoServ
8import { BlocklistService } from '@app/shared/shared-moderation' 8import { BlocklistService } from '@app/shared/shared-moderation'
9import { SupportModalComponent } from '@app/shared/shared-support-modal' 9import { SupportModalComponent } from '@app/shared/shared-support-modal'
10import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' 10import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
11import { HttpStatusCode } from '@shared/models' 11import { HttpStatusCode, UserRight } from '@shared/models'
12 12
13@Component({ 13@Component({
14 templateUrl: './video-channels.component.html', 14 templateUrl: './video-channels.component.html',
@@ -98,12 +98,18 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
98 return this.authService.isLoggedIn() 98 return this.authService.isLoggedIn()
99 } 99 }
100 100
101 isManageable () { 101 isOwner () {
102 if (!this.isUserLoggedIn()) return false 102 if (!this.isUserLoggedIn()) return false
103 103
104 return this.videoChannel?.ownerAccount.userId === this.authService.getUser().id 104 return this.videoChannel?.ownerAccount.userId === this.authService.getUser().id
105 } 105 }
106 106
107 isManageable () {
108 if (!this.isUserLoggedIn()) return false
109
110 return this.isOwner() || this.authService.getUser().hasRight(UserRight.MANAGE_ANY_VIDEO_CHANNEL)
111 }
112
107 activateCopiedMessage () { 113 activateCopiedMessage () {
108 this.notifier.success($localize`Username copied`) 114 this.notifier.success($localize`Username copied`)
109 } 115 }
diff --git a/client/src/app/+video-channels/video-channels.module.ts b/client/src/app/+video-channels/video-channels.module.ts
index 76aaecf83..aef3ed0a3 100644
--- a/client/src/app/+video-channels/video-channels.module.ts
+++ b/client/src/app/+video-channels/video-channels.module.ts
@@ -12,6 +12,7 @@ import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-
12import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component' 12import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component'
13import { VideoChannelsRoutingModule } from './video-channels-routing.module' 13import { VideoChannelsRoutingModule } from './video-channels-routing.module'
14import { VideoChannelsComponent } from './video-channels.component' 14import { VideoChannelsComponent } from './video-channels.component'
15import { SharedActorImageEditModule } from '@app/shared/shared-actor-image-edit'
15 16
16@NgModule({ 17@NgModule({
17 imports: [ 18 imports: [
@@ -25,6 +26,7 @@ import { VideoChannelsComponent } from './video-channels.component'
25 SharedGlobalIconModule, 26 SharedGlobalIconModule,
26 SharedSupportModal, 27 SharedSupportModal,
27 SharedActorImageModule, 28 SharedActorImageModule,
29 SharedActorImageEditModule,
28 SharedModerationModule 30 SharedModerationModule
29 ], 31 ],
30 32
diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts
index 42328d83d..b5afc9c92 100644
--- a/client/src/app/app-routing.module.ts
+++ b/client/src/app/app-routing.module.ts
@@ -56,7 +56,11 @@ const routes: Routes = [
56 loadChildren: () => import('./+video-channels/video-channels.module').then(m => m.VideoChannelsModule), 56 loadChildren: () => import('./+video-channels/video-channels.module').then(m => m.VideoChannelsModule),
57 canActivateChild: [ MetaGuard ] 57 canActivateChild: [ MetaGuard ]
58 }, 58 },
59 59 {
60 path: 'manage',
61 loadChildren: () => import('./+manage/manage.module').then(m => m.ManageModule),
62 canActivateChild: [ MetaGuard ]
63 },
60 { 64 {
61 path: 'p', 65 path: 'p',
62 loadChildren: () => import('./+plugin-pages/plugin-pages.module').then(m => m.PluginPagesModule), 66 loadChildren: () => import('./+plugin-pages/plugin-pages.module').then(m => m.PluginPagesModule),
diff --git a/client/src/app/core/routing/redirect.service.ts b/client/src/app/core/routing/redirect.service.ts
index 17d9d1358..571476d1d 100644
--- a/client/src/app/core/routing/redirect.service.ts
+++ b/client/src/app/core/routing/redirect.service.ts
@@ -46,7 +46,7 @@ export class RedirectService {
46 return this.defaultTrendingAlgorithm 46 return this.defaultTrendingAlgorithm
47 } 47 }
48 48
49 redirectToPreviousRoute () { 49 redirectToPreviousRoute (fallbackRoute: string[] = null) {
50 const exceptions = [ 50 const exceptions = [
51 '/verify-account', 51 '/verify-account',
52 '/reset-password' 52 '/reset-password'
@@ -57,6 +57,10 @@ export class RedirectService {
57 if (!isException) return this.router.navigateByUrl(this.previousUrl) 57 if (!isException) return this.router.navigateByUrl(this.previousUrl)
58 } 58 }
59 59
60 if (fallbackRoute) {
61 return this.router.navigate(fallbackRoute)
62 }
63
60 return this.redirectToHomepage() 64 return this.redirectToHomepage()
61 } 65 }
62 66
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts
index c4e3cec6b..4e6bd5e25 100644
--- a/server/controllers/activitypub/client.ts
+++ b/server/controllers/activitypub/client.ts
@@ -20,7 +20,8 @@ import {
20 asyncMiddleware, 20 asyncMiddleware,
21 executeIfActivityPub, 21 executeIfActivityPub,
22 localAccountValidator, 22 localAccountValidator,
23 localVideoChannelValidator, 23 videoChannelsNameWithHostValidator,
24 ensureIsLocalChannel,
24 videosCustomGetValidator, 25 videosCustomGetValidator,
25 videosShareValidator 26 videosShareValidator
26} from '../../middlewares' 27} from '../../middlewares'
@@ -123,24 +124,28 @@ activityPubClientRouter.get('/videos/watch/:videoId/comments/:commentId/activity
123) 124)
124 125
125activityPubClientRouter.get( 126activityPubClientRouter.get(
126 [ '/video-channels/:name', '/video-channels/:name/videos', '/c/:name', '/c/:name/videos' ], 127 [ '/video-channels/:nameWithHost', '/video-channels/:nameWithHost/videos', '/c/:nameWithHost', '/c/:nameWithHost/videos' ],
127 executeIfActivityPub, 128 executeIfActivityPub,
128 asyncMiddleware(localVideoChannelValidator), 129 asyncMiddleware(videoChannelsNameWithHostValidator),
130 ensureIsLocalChannel,
129 videoChannelController 131 videoChannelController
130) 132)
131activityPubClientRouter.get('/video-channels/:name/followers', 133activityPubClientRouter.get('/video-channels/:nameWithHost/followers',
132 executeIfActivityPub, 134 executeIfActivityPub,
133 asyncMiddleware(localVideoChannelValidator), 135 asyncMiddleware(videoChannelsNameWithHostValidator),
136 ensureIsLocalChannel,
134 asyncMiddleware(videoChannelFollowersController) 137 asyncMiddleware(videoChannelFollowersController)
135) 138)
136activityPubClientRouter.get('/video-channels/:name/following', 139activityPubClientRouter.get('/video-channels/:nameWithHost/following',
137 executeIfActivityPub, 140 executeIfActivityPub,
138 asyncMiddleware(localVideoChannelValidator), 141 asyncMiddleware(videoChannelsNameWithHostValidator),
142 ensureIsLocalChannel,
139 asyncMiddleware(videoChannelFollowingController) 143 asyncMiddleware(videoChannelFollowingController)
140) 144)
141activityPubClientRouter.get('/video-channels/:name/playlists', 145activityPubClientRouter.get('/video-channels/:nameWithHost/playlists',
142 executeIfActivityPub, 146 executeIfActivityPub,
143 asyncMiddleware(localVideoChannelValidator), 147 asyncMiddleware(videoChannelsNameWithHostValidator),
148 ensureIsLocalChannel,
144 asyncMiddleware(videoChannelPlaylistsController) 149 asyncMiddleware(videoChannelPlaylistsController)
145) 150)
146 151
diff --git a/server/controllers/activitypub/inbox.ts b/server/controllers/activitypub/inbox.ts
index ece4edff0..5995b8f3a 100644
--- a/server/controllers/activitypub/inbox.ts
+++ b/server/controllers/activitypub/inbox.ts
@@ -4,7 +4,14 @@ import { Activity, ActivityPubCollection, ActivityPubOrderedCollection, RootActi
4import { HttpStatusCode } from '../../../shared/models/http/http-error-codes' 4import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
5import { isActivityValid } from '../../helpers/custom-validators/activitypub/activity' 5import { isActivityValid } from '../../helpers/custom-validators/activitypub/activity'
6import { logger } from '../../helpers/logger' 6import { logger } from '../../helpers/logger'
7import { asyncMiddleware, checkSignature, localAccountValidator, localVideoChannelValidator, signatureValidator } from '../../middlewares' 7import {
8 asyncMiddleware,
9 checkSignature,
10 ensureIsLocalChannel,
11 localAccountValidator,
12 signatureValidator,
13 videoChannelsNameWithHostValidator
14} from '../../middlewares'
8import { activityPubValidator } from '../../middlewares/validators/activitypub/activity' 15import { activityPubValidator } from '../../middlewares/validators/activitypub/activity'
9 16
10const inboxRouter = express.Router() 17const inboxRouter = express.Router()
@@ -23,10 +30,11 @@ inboxRouter.post('/accounts/:name/inbox',
23 asyncMiddleware(activityPubValidator), 30 asyncMiddleware(activityPubValidator),
24 inboxController 31 inboxController
25) 32)
26inboxRouter.post('/video-channels/:name/inbox', 33inboxRouter.post('/video-channels/:nameWithHost/inbox',
27 signatureValidator, 34 signatureValidator,
28 asyncMiddleware(checkSignature), 35 asyncMiddleware(checkSignature),
29 asyncMiddleware(localVideoChannelValidator), 36 asyncMiddleware(videoChannelsNameWithHostValidator),
37 ensureIsLocalChannel,
30 asyncMiddleware(activityPubValidator), 38 asyncMiddleware(activityPubValidator),
31 inboxController 39 inboxController
32) 40)
diff --git a/server/controllers/activitypub/outbox.ts b/server/controllers/activitypub/outbox.ts
index bdf9d138b..cdef8e969 100644
--- a/server/controllers/activitypub/outbox.ts
+++ b/server/controllers/activitypub/outbox.ts
@@ -1,15 +1,15 @@
1import express from 'express' 1import express from 'express'
2import { MActorLight } from '@server/types/models'
2import { Activity } from '../../../shared/models/activitypub/activity' 3import { Activity } from '../../../shared/models/activitypub/activity'
3import { VideoPrivacy } from '../../../shared/models/videos' 4import { VideoPrivacy } from '../../../shared/models/videos'
4import { activityPubCollectionPagination, activityPubContextify } from '../../helpers/activitypub' 5import { activityPubCollectionPagination, activityPubContextify } from '../../helpers/activitypub'
5import { logger } from '../../helpers/logger' 6import { logger } from '../../helpers/logger'
6import { buildAnnounceActivity, buildCreateActivity } from '../../lib/activitypub/send'
7import { buildAudience } from '../../lib/activitypub/audience' 7import { buildAudience } from '../../lib/activitypub/audience'
8import { asyncMiddleware, localAccountValidator, localVideoChannelValidator } from '../../middlewares' 8import { buildAnnounceActivity, buildCreateActivity } from '../../lib/activitypub/send'
9import { asyncMiddleware, ensureIsLocalChannel, localAccountValidator, videoChannelsNameWithHostValidator } from '../../middlewares'
10import { apPaginationValidator } from '../../middlewares/validators/activitypub'
9import { VideoModel } from '../../models/video/video' 11import { VideoModel } from '../../models/video/video'
10import { activityPubResponse } from './utils' 12import { activityPubResponse } from './utils'
11import { MActorLight } from '@server/types/models'
12import { apPaginationValidator } from '../../middlewares/validators/activitypub'
13 13
14const outboxRouter = express.Router() 14const outboxRouter = express.Router()
15 15
@@ -19,9 +19,10 @@ outboxRouter.get('/accounts/:name/outbox',
19 asyncMiddleware(outboxController) 19 asyncMiddleware(outboxController)
20) 20)
21 21
22outboxRouter.get('/video-channels/:name/outbox', 22outboxRouter.get('/video-channels/:nameWithHost/outbox',
23 apPaginationValidator, 23 apPaginationValidator,
24 localVideoChannelValidator, 24 asyncMiddleware(videoChannelsNameWithHostValidator),
25 ensureIsLocalChannel,
25 asyncMiddleware(outboxController) 26 asyncMiddleware(outboxController)
26) 27)
27 28
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts
index d1a1e6473..abb777e08 100644
--- a/server/controllers/api/video-channel.ts
+++ b/server/controllers/api/video-channel.ts
@@ -24,6 +24,7 @@ import {
24 asyncRetryTransactionMiddleware, 24 asyncRetryTransactionMiddleware,
25 authenticate, 25 authenticate,
26 commonVideosFiltersValidator, 26 commonVideosFiltersValidator,
27 ensureCanManageChannel,
27 optionalAuthenticate, 28 optionalAuthenticate,
28 paginationValidator, 29 paginationValidator,
29 setDefaultPagination, 30 setDefaultPagination,
@@ -36,7 +37,7 @@ import {
36 videoPlaylistsSortValidator 37 videoPlaylistsSortValidator
37} from '../../middlewares' 38} from '../../middlewares'
38import { 39import {
39 ensureAuthUserOwnsChannelValidator, 40 ensureIsLocalChannel,
40 videoChannelsFollowersSortValidator, 41 videoChannelsFollowersSortValidator,
41 videoChannelsListValidator, 42 videoChannelsListValidator,
42 videoChannelsNameWithHostValidator, 43 videoChannelsNameWithHostValidator,
@@ -74,7 +75,8 @@ videoChannelRouter.post('/:nameWithHost/avatar/pick',
74 authenticate, 75 authenticate,
75 reqAvatarFile, 76 reqAvatarFile,
76 asyncMiddleware(videoChannelsNameWithHostValidator), 77 asyncMiddleware(videoChannelsNameWithHostValidator),
77 ensureAuthUserOwnsChannelValidator, 78 ensureIsLocalChannel,
79 ensureCanManageChannel,
78 updateAvatarValidator, 80 updateAvatarValidator,
79 asyncMiddleware(updateVideoChannelAvatar) 81 asyncMiddleware(updateVideoChannelAvatar)
80) 82)
@@ -83,7 +85,8 @@ videoChannelRouter.post('/:nameWithHost/banner/pick',
83 authenticate, 85 authenticate,
84 reqBannerFile, 86 reqBannerFile,
85 asyncMiddleware(videoChannelsNameWithHostValidator), 87 asyncMiddleware(videoChannelsNameWithHostValidator),
86 ensureAuthUserOwnsChannelValidator, 88 ensureIsLocalChannel,
89 ensureCanManageChannel,
87 updateBannerValidator, 90 updateBannerValidator,
88 asyncMiddleware(updateVideoChannelBanner) 91 asyncMiddleware(updateVideoChannelBanner)
89) 92)
@@ -91,27 +94,33 @@ videoChannelRouter.post('/:nameWithHost/banner/pick',
91videoChannelRouter.delete('/:nameWithHost/avatar', 94videoChannelRouter.delete('/:nameWithHost/avatar',
92 authenticate, 95 authenticate,
93 asyncMiddleware(videoChannelsNameWithHostValidator), 96 asyncMiddleware(videoChannelsNameWithHostValidator),
94 ensureAuthUserOwnsChannelValidator, 97 ensureIsLocalChannel,
98 ensureCanManageChannel,
95 asyncMiddleware(deleteVideoChannelAvatar) 99 asyncMiddleware(deleteVideoChannelAvatar)
96) 100)
97 101
98videoChannelRouter.delete('/:nameWithHost/banner', 102videoChannelRouter.delete('/:nameWithHost/banner',
99 authenticate, 103 authenticate,
100 asyncMiddleware(videoChannelsNameWithHostValidator), 104 asyncMiddleware(videoChannelsNameWithHostValidator),
101 ensureAuthUserOwnsChannelValidator, 105 ensureIsLocalChannel,
106 ensureCanManageChannel,
102 asyncMiddleware(deleteVideoChannelBanner) 107 asyncMiddleware(deleteVideoChannelBanner)
103) 108)
104 109
105videoChannelRouter.put('/:nameWithHost', 110videoChannelRouter.put('/:nameWithHost',
106 authenticate, 111 authenticate,
107 asyncMiddleware(videoChannelsNameWithHostValidator), 112 asyncMiddleware(videoChannelsNameWithHostValidator),
108 ensureAuthUserOwnsChannelValidator, 113 ensureIsLocalChannel,
114 ensureCanManageChannel,
109 videoChannelsUpdateValidator, 115 videoChannelsUpdateValidator,
110 asyncRetryTransactionMiddleware(updateVideoChannel) 116 asyncRetryTransactionMiddleware(updateVideoChannel)
111) 117)
112 118
113videoChannelRouter.delete('/:nameWithHost', 119videoChannelRouter.delete('/:nameWithHost',
114 authenticate, 120 authenticate,
121 asyncMiddleware(videoChannelsNameWithHostValidator),
122 ensureIsLocalChannel,
123 ensureCanManageChannel,
115 asyncMiddleware(videoChannelsRemoveValidator), 124 asyncMiddleware(videoChannelsRemoveValidator),
116 asyncRetryTransactionMiddleware(removeVideoChannel) 125 asyncRetryTransactionMiddleware(removeVideoChannel)
117) 126)
@@ -145,7 +154,7 @@ videoChannelRouter.get('/:nameWithHost/videos',
145videoChannelRouter.get('/:nameWithHost/followers', 154videoChannelRouter.get('/:nameWithHost/followers',
146 authenticate, 155 authenticate,
147 asyncMiddleware(videoChannelsNameWithHostValidator), 156 asyncMiddleware(videoChannelsNameWithHostValidator),
148 ensureAuthUserOwnsChannelValidator, 157 ensureCanManageChannel,
149 paginationValidator, 158 paginationValidator,
150 videoChannelsFollowersSortValidator, 159 videoChannelsFollowersSortValidator,
151 setDefaultSort, 160 setDefaultSort,
diff --git a/server/middlewares/validators/shared/video-channels.ts b/server/middlewares/validators/shared/video-channels.ts
index 7c0c89267..bed9f5dbe 100644
--- a/server/middlewares/validators/shared/video-channels.ts
+++ b/server/middlewares/validators/shared/video-channels.ts
@@ -3,12 +3,6 @@ import { VideoChannelModel } from '@server/models/video/video-channel'
3import { MChannelBannerAccountDefault } from '@server/types/models' 3import { MChannelBannerAccountDefault } from '@server/types/models'
4import { HttpStatusCode } from '@shared/models' 4import { HttpStatusCode } from '@shared/models'
5 5
6async function doesLocalVideoChannelNameExist (name: string, res: express.Response) {
7 const videoChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(name)
8
9 return processVideoChannelExist(videoChannel, res)
10}
11
12async function doesVideoChannelIdExist (id: number, res: express.Response) { 6async function doesVideoChannelIdExist (id: number, res: express.Response) {
13 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(+id) 7 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(+id)
14 8
@@ -24,7 +18,6 @@ async function doesVideoChannelNameWithHostExist (nameWithDomain: string, res: e
24// --------------------------------------------------------------------------- 18// ---------------------------------------------------------------------------
25 19
26export { 20export {
27 doesLocalVideoChannelNameExist,
28 doesVideoChannelIdExist, 21 doesVideoChannelIdExist,
29 doesVideoChannelNameWithHostExist 22 doesVideoChannelNameWithHostExist
30} 23}
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts
index 33b31d54b..7a6b2ce57 100644
--- a/server/middlewares/validators/users.ts
+++ b/server/middlewares/validators/users.ts
@@ -3,7 +3,7 @@ import { body, param, query } from 'express-validator'
3import { omit } from 'lodash' 3import { omit } from 'lodash'
4import { Hooks } from '@server/lib/plugins/hooks' 4import { Hooks } from '@server/lib/plugins/hooks'
5import { MUserDefault } from '@server/types/models' 5import { MUserDefault } from '@server/types/models'
6import { HttpStatusCode, UserRegister, UserRole } from '@shared/models' 6import { HttpStatusCode, UserRegister, UserRight, UserRole } from '@shared/models'
7import { isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' 7import { isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
8import { isThemeNameValid } from '../../helpers/custom-validators/plugins' 8import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
9import { 9import {
@@ -490,14 +490,17 @@ const ensureAuthUserOwnsAccountValidator = [
490 } 490 }
491] 491]
492 492
493const ensureAuthUserOwnsChannelValidator = [ 493const ensureCanManageChannel = [
494 (req: express.Request, res: express.Response, next: express.NextFunction) => { 494 (req: express.Request, res: express.Response, next: express.NextFunction) => {
495 const user = res.locals.oauth.token.User 495 const user = res.locals.oauth.token.user
496 const isUserOwner = res.locals.videoChannel.Account.userId === user.id
497
498 if (!isUserOwner && user.hasRight(UserRight.MANAGE_ANY_VIDEO_CHANNEL) === false) {
499 const message = `User ${user.username} does not have right to manage channel ${req.params.nameWithHost}.`
496 500
497 if (res.locals.videoChannel.Account.userId !== user.id) {
498 return res.fail({ 501 return res.fail({
499 status: HttpStatusCode.FORBIDDEN_403, 502 status: HttpStatusCode.FORBIDDEN_403,
500 message: 'Only owner of this video channel can access this ressource' 503 message
501 }) 504 })
502 } 505 }
503 506
@@ -542,8 +545,8 @@ export {
542 usersVerifyEmailValidator, 545 usersVerifyEmailValidator,
543 userAutocompleteValidator, 546 userAutocompleteValidator,
544 ensureAuthUserOwnsAccountValidator, 547 ensureAuthUserOwnsAccountValidator,
545 ensureAuthUserOwnsChannelValidator, 548 ensureCanManageUser,
546 ensureCanManageUser 549 ensureCanManageChannel
547} 550}
548 551
549// --------------------------------------------------------------------------- 552// ---------------------------------------------------------------------------
diff --git a/server/middlewares/validators/videos/video-channels.ts b/server/middlewares/validators/videos/video-channels.ts
index edce48c7f..3bfdebbb1 100644
--- a/server/middlewares/validators/videos/video-channels.ts
+++ b/server/middlewares/validators/videos/video-channels.ts
@@ -1,7 +1,7 @@
1import express from 'express' 1import express from 'express'
2import { body, param, query } from 'express-validator' 2import { body, param, query } from 'express-validator'
3import { MChannelAccountDefault, MUser } from '@server/types/models' 3import { CONFIG } from '@server/initializers/config'
4import { UserRight } from '../../../../shared' 4import { MChannelAccountDefault } from '@server/types/models'
5import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' 5import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
6import { isBooleanValid, toBooleanOrNull } from '../../../helpers/custom-validators/misc' 6import { isBooleanValid, toBooleanOrNull } from '../../../helpers/custom-validators/misc'
7import { 7import {
@@ -13,8 +13,7 @@ import {
13import { logger } from '../../../helpers/logger' 13import { logger } from '../../../helpers/logger'
14import { ActorModel } from '../../../models/actor/actor' 14import { ActorModel } from '../../../models/actor/actor'
15import { VideoChannelModel } from '../../../models/video/video-channel' 15import { VideoChannelModel } from '../../../models/video/video-channel'
16import { areValidationErrors, doesLocalVideoChannelNameExist, doesVideoChannelNameWithHostExist } from '../shared' 16import { areValidationErrors, doesVideoChannelNameWithHostExist } from '../shared'
17import { CONFIG } from '@server/initializers/config'
18 17
19const videoChannelsAddValidator = [ 18const videoChannelsAddValidator = [
20 body('name').custom(isVideoChannelUsernameValid).withMessage('Should have a valid channel name'), 19 body('name').custom(isVideoChannelUsernameValid).withMessage('Should have a valid channel name'),
@@ -71,16 +70,10 @@ const videoChannelsUpdateValidator = [
71] 70]
72 71
73const videoChannelsRemoveValidator = [ 72const videoChannelsRemoveValidator = [
74 param('nameWithHost').exists().withMessage('Should have an video channel name with host'),
75
76 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 73 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
77 logger.debug('Checking videoChannelsRemove parameters', { parameters: req.params }) 74 logger.debug('Checking videoChannelsRemove parameters', { parameters: req.params })
78 75
79 if (areValidationErrors(req, res)) return 76 if (!await checkVideoChannelIsNotTheLastOne(res.locals.videoChannel, res)) return
80 if (!await doesVideoChannelNameWithHostExist(req.params.nameWithHost, res)) return
81
82 if (!checkUserCanDeleteVideoChannel(res.locals.oauth.token.User, res.locals.videoChannel, res)) return
83 if (!await checkVideoChannelIsNotTheLastOne(res)) return
84 77
85 return next() 78 return next()
86 } 79 }
@@ -100,14 +93,14 @@ const videoChannelsNameWithHostValidator = [
100 } 93 }
101] 94]
102 95
103const localVideoChannelValidator = [ 96const ensureIsLocalChannel = [
104 param('name').custom(isVideoChannelDisplayNameValid).withMessage('Should have a valid video channel name'), 97 (req: express.Request, res: express.Response, next: express.NextFunction) => {
105 98 if (res.locals.videoChannel.Actor.isOwned() === false) {
106 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 99 return res.fail({
107 logger.debug('Checking localVideoChannelValidator parameters', { parameters: req.params }) 100 status: HttpStatusCode.FORBIDDEN_403,
108 101 message: 'This channel is not owned.'
109 if (areValidationErrors(req, res)) return 102 })
110 if (!await doesLocalVideoChannelNameExist(req.params.name, res)) return 103 }
111 104
112 return next() 105 return next()
113 } 106 }
@@ -144,38 +137,15 @@ export {
144 videoChannelsUpdateValidator, 137 videoChannelsUpdateValidator,
145 videoChannelsRemoveValidator, 138 videoChannelsRemoveValidator,
146 videoChannelsNameWithHostValidator, 139 videoChannelsNameWithHostValidator,
140 ensureIsLocalChannel,
147 videoChannelsListValidator, 141 videoChannelsListValidator,
148 localVideoChannelValidator,
149 videoChannelStatsValidator 142 videoChannelStatsValidator
150} 143}
151 144
152// --------------------------------------------------------------------------- 145// ---------------------------------------------------------------------------
153 146
154function checkUserCanDeleteVideoChannel (user: MUser, videoChannel: MChannelAccountDefault, res: express.Response) { 147async function checkVideoChannelIsNotTheLastOne (videoChannel: MChannelAccountDefault, res: express.Response) {
155 if (videoChannel.Actor.isOwned() === false) { 148 const count = await VideoChannelModel.countByAccount(videoChannel.Account.id)
156 res.fail({
157 status: HttpStatusCode.FORBIDDEN_403,
158 message: 'Cannot remove video channel of another server.'
159 })
160 return false
161 }
162
163 // Check if the user can delete the video channel
164 // The user can delete it if s/he is an admin
165 // Or if s/he is the video channel's account
166 if (user.hasRight(UserRight.REMOVE_ANY_VIDEO_CHANNEL) === false && videoChannel.Account.userId !== user.id) {
167 res.fail({
168 status: HttpStatusCode.FORBIDDEN_403,
169 message: 'Cannot remove video channel of another user'
170 })
171 return false
172 }
173
174 return true
175}
176
177async function checkVideoChannelIsNotTheLastOne (res: express.Response) {
178 const count = await VideoChannelModel.countByAccount(res.locals.oauth.token.User.Account.id)
179 149
180 if (count <= 1) { 150 if (count <= 1) {
181 res.fail({ 151 res.fail({
diff --git a/server/tests/api/videos/video-channels.ts b/server/tests/api/videos/video-channels.ts
index c25754eb6..6ab5faa07 100644
--- a/server/tests/api/videos/video-channels.ts
+++ b/server/tests/api/videos/video-channels.ts
@@ -33,6 +33,7 @@ describe('Test video channels', function () {
33 let totoChannel: number 33 let totoChannel: number
34 let videoUUID: string 34 let videoUUID: string
35 let accountName: string 35 let accountName: string
36 let secondUserChannelName: string
36 37
37 const avatarPaths: { [ port: number ]: string } = {} 38 const avatarPaths: { [ port: number ]: string } = {}
38 const bannerPaths: { [ port: number ]: string } = {} 39 const bannerPaths: { [ port: number ]: string } = {}
@@ -219,6 +220,35 @@ describe('Test video channels', function () {
219 } 220 }
220 }) 221 })
221 222
223 it('Should update another accounts video channel', async function () {
224 this.timeout(15000)
225
226 const result = await servers[0].users.generate('second_user')
227 secondUserChannelName = result.userChannelName
228
229 await servers[0].videos.quickUpload({ name: 'video', token: result.token })
230
231 const videoChannelAttributes = {
232 displayName: 'video channel updated',
233 description: 'video channel description updated',
234 support: 'support updated'
235 }
236
237 await servers[0].channels.update({ channelName: secondUserChannelName, attributes: videoChannelAttributes })
238
239 await waitJobs(servers)
240 })
241
242 it('Should have another accounts video channel updated', async function () {
243 for (const server of servers) {
244 const body = await server.channels.get({ channelName: `${secondUserChannelName}@${servers[0].host}` })
245
246 expect(body.displayName).to.equal('video channel updated')
247 expect(body.description).to.equal('video channel description updated')
248 expect(body.support).to.equal('support updated')
249 }
250 })
251
222 it('Should update the channel support field and update videos too', async function () { 252 it('Should update the channel support field and update videos too', async function () {
223 this.timeout(35000) 253 this.timeout(35000)
224 254
@@ -368,12 +398,13 @@ describe('Test video channels', function () {
368 }) 398 })
369 399
370 it('Should have video channel deleted', async function () { 400 it('Should have video channel deleted', async function () {
371 const body = await servers[0].channels.list({ start: 0, count: 10 }) 401 const body = await servers[0].channels.list({ start: 0, count: 10, sort: 'createdAt' })
372 402
373 expect(body.total).to.equal(1) 403 expect(body.total).to.equal(2)
374 expect(body.data).to.be.an('array') 404 expect(body.data).to.be.an('array')
375 expect(body.data).to.have.lengthOf(1) 405 expect(body.data).to.have.lengthOf(2)
376 expect(body.data[0].displayName).to.equal('Main root channel') 406 expect(body.data[0].displayName).to.equal('Main root channel')
407 expect(body.data[1].displayName).to.equal('video channel updated')
377 }) 408 })
378 409
379 it('Should create the main channel with an uuid if there is a conflict', async function () { 410 it('Should create the main channel with an uuid if there is a conflict', async function () {
diff --git a/shared/core-utils/users/user-role.ts b/shared/core-utils/users/user-role.ts
index 81cba1dad..cc757d779 100644
--- a/shared/core-utils/users/user-role.ts
+++ b/shared/core-utils/users/user-role.ts
@@ -14,8 +14,8 @@ const userRoleRights: { [ id in UserRole ]: UserRight[] } = {
14 [UserRole.MODERATOR]: [ 14 [UserRole.MODERATOR]: [
15 UserRight.MANAGE_VIDEO_BLACKLIST, 15 UserRight.MANAGE_VIDEO_BLACKLIST,
16 UserRight.MANAGE_ABUSES, 16 UserRight.MANAGE_ABUSES,
17 UserRight.MANAGE_ANY_VIDEO_CHANNEL,
17 UserRight.REMOVE_ANY_VIDEO, 18 UserRight.REMOVE_ANY_VIDEO,
18 UserRight.REMOVE_ANY_VIDEO_CHANNEL,
19 UserRight.REMOVE_ANY_VIDEO_PLAYLIST, 19 UserRight.REMOVE_ANY_VIDEO_PLAYLIST,
20 UserRight.REMOVE_ANY_VIDEO_COMMENT, 20 UserRight.REMOVE_ANY_VIDEO_COMMENT,
21 UserRight.UPDATE_ANY_VIDEO, 21 UserRight.UPDATE_ANY_VIDEO,
diff --git a/shared/extra-utils/users/users-command.ts b/shared/extra-utils/users/users-command.ts
index 2a10e4fc8..90c5f2183 100644
--- a/shared/extra-utils/users/users-command.ts
+++ b/shared/extra-utils/users/users-command.ts
@@ -202,7 +202,8 @@ export class UsersCommand extends AbstractCommand {
202 return { 202 return {
203 token, 203 token,
204 userId: user.id, 204 userId: user.id,
205 userChannelId: me.videoChannels[0].id 205 userChannelId: me.videoChannels[0].id,
206 userChannelName: me.videoChannels[0].name
206 } 207 }
207 } 208 }
208 209
diff --git a/shared/models/users/user-right.enum.ts b/shared/models/users/user-right.enum.ts
index 6415ca6f2..668535f4e 100644
--- a/shared/models/users/user-right.enum.ts
+++ b/shared/models/users/user-right.enum.ts
@@ -22,9 +22,9 @@ export const enum UserRight {
22 MANAGE_SERVERS_BLOCKLIST, 22 MANAGE_SERVERS_BLOCKLIST,
23 23
24 MANAGE_VIDEO_BLACKLIST, 24 MANAGE_VIDEO_BLACKLIST,
25 MANAGE_ANY_VIDEO_CHANNEL,
25 26
26 REMOVE_ANY_VIDEO, 27 REMOVE_ANY_VIDEO,
27 REMOVE_ANY_VIDEO_CHANNEL,
28 REMOVE_ANY_VIDEO_PLAYLIST, 28 REMOVE_ANY_VIDEO_PLAYLIST,
29 REMOVE_ANY_VIDEO_COMMENT, 29 REMOVE_ANY_VIDEO_COMMENT,
30 30