diff options
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 @@ | |||
1 | import { NgModule } from '@angular/core' | ||
2 | import { RouterModule, Routes } from '@angular/router' | ||
3 | import { VideoChannelCreateComponent } from './video-channel-edit/video-channel-create.component' | ||
4 | import { VideoChannelUpdateComponent } from './video-channel-edit/video-channel-update.component' | ||
5 | |||
6 | const 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 | }) | ||
31 | 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 index 000000000..28939ec5a --- /dev/null +++ b/client/src/app/+manage/manage.module.ts | |||
@@ -0,0 +1,31 @@ | |||
1 | import { NgModule } from '@angular/core' | ||
2 | import { SharedFormModule } from '@app/shared/shared-forms' | ||
3 | import { SharedGlobalIconModule } from '@app/shared/shared-icons' | ||
4 | import { SharedMainModule } from '@app/shared/shared-main' | ||
5 | import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module' | ||
6 | import { SharedActorImageEditModule } from '@app/shared/shared-actor-image-edit' | ||
7 | import { VideoChannelCreateComponent } from './video-channel-edit/video-channel-create.component' | ||
8 | import { VideoChannelUpdateComponent } from './video-channel-edit/video-channel-update.component' | ||
9 | import { 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 | }) | ||
31 | export 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 { | |||
12 | import { FormValidatorService } from '@app/shared/shared-forms' | 12 | import { FormValidatorService } from '@app/shared/shared-forms' |
13 | import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' | 13 | import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' |
14 | import { HttpStatusCode, VideoChannelCreate } from '@shared/models' | 14 | import { HttpStatusCode, VideoChannelCreate } from '@shared/models' |
15 | import { MyVideoChannelEdit } from './my-video-channel-edit' | 15 | import { 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 | }) |
21 | export class MyVideoChannelCreateComponent extends MyVideoChannelEdit implements OnInit { | 21 | export 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 | |||
4 | label { | 8 | label { |
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 @@ | |||
1 | import { FormReactive } from '@app/shared/shared-forms' | 1 | import { FormReactive } from '@app/shared/shared-forms' |
2 | import { VideoChannel } from '@app/shared/shared-main' | 2 | import { VideoChannel } from '@app/shared/shared-main' |
3 | 3 | ||
4 | export abstract class MyVideoChannelEdit extends FormReactive { | 4 | export 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' | |||
2 | import { HttpErrorResponse } from '@angular/common/http' | 2 | import { HttpErrorResponse } from '@angular/common/http' |
3 | import { Component, OnDestroy, OnInit } from '@angular/core' | 3 | import { Component, OnDestroy, OnInit } from '@angular/core' |
4 | import { ActivatedRoute, Router } from '@angular/router' | 4 | import { ActivatedRoute, Router } from '@angular/router' |
5 | import { AuthService, Notifier, ServerService } from '@app/core' | 5 | import { AuthService, Notifier, RedirectService, ServerService } from '@app/core' |
6 | import { genericUploadErrorHandler } from '@app/helpers' | 6 | import { genericUploadErrorHandler } from '@app/helpers' |
7 | import { | 7 | import { |
8 | VIDEO_CHANNEL_DESCRIPTION_VALIDATOR, | 8 | VIDEO_CHANNEL_DESCRIPTION_VALIDATOR, |
@@ -12,14 +12,14 @@ import { | |||
12 | import { FormValidatorService } from '@app/shared/shared-forms' | 12 | import { FormValidatorService } from '@app/shared/shared-forms' |
13 | import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' | 13 | import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' |
14 | import { HTMLServerConfig, VideoChannelUpdate } from '@shared/models' | 14 | import { HTMLServerConfig, VideoChannelUpdate } from '@shared/models' |
15 | import { MyVideoChannelEdit } from './my-video-channel-edit' | 15 | import { 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 | }) |
22 | export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements OnInit, OnDestroy { | 22 | export 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 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { RouterModule, Routes } from '@angular/router' | 2 | import { RouterModule, Routes } from '@angular/router' |
3 | import { MyVideoChannelUpdateComponent } from './my-video-channel-update.component' | ||
4 | import { MyVideoChannelCreateComponent } from './my-video-channel-create.component' | ||
5 | import { MyVideoChannelsComponent } from './my-video-channels.component' | 3 | import { MyVideoChannelsComponent } from './my-video-channels.component' |
6 | 4 | ||
7 | const myVideoChannelsRoutes: Routes = [ | 5 | const 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 @@ | |||
1 | import { ChartModule } from 'primeng/chart' | 1 | import { ChartModule } from 'primeng/chart' |
2 | import { NgModule } from '@angular/core' | 2 | import { NgModule } from '@angular/core' |
3 | import { SharedActorImageEditModule } from '@app/shared/shared-actor-image-edit' | ||
4 | import { SharedFormModule } from '@app/shared/shared-forms' | 3 | import { SharedFormModule } from '@app/shared/shared-forms' |
5 | import { SharedGlobalIconModule } from '@app/shared/shared-icons' | 4 | import { SharedGlobalIconModule } from '@app/shared/shared-icons' |
6 | import { SharedMainModule } from '@app/shared/shared-main' | 5 | import { SharedMainModule } from '@app/shared/shared-main' |
7 | import { MyVideoChannelCreateComponent } from './my-video-channel-create.component' | ||
8 | import { MyVideoChannelUpdateComponent } from './my-video-channel-update.component' | ||
9 | import { MyVideoChannelsRoutingModule } from './my-video-channels-routing.module' | 6 | import { MyVideoChannelsRoutingModule } from './my-video-channels-routing.module' |
10 | import { MyVideoChannelsComponent } from './my-video-channels.component' | 7 | import { MyVideoChannelsComponent } from './my-video-channels.component' |
11 | import { SharedActorImageModule } from '@app/shared/shared-actor-image/shared-actor-image.module' | 8 | 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 | |||
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 | |||
8 | import { BlocklistService } from '@app/shared/shared-moderation' | 8 | import { BlocklistService } from '@app/shared/shared-moderation' |
9 | import { SupportModalComponent } from '@app/shared/shared-support-modal' | 9 | import { SupportModalComponent } from '@app/shared/shared-support-modal' |
10 | import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' | 10 | import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' |
11 | import { HttpStatusCode } from '@shared/models' | 11 | import { 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- | |||
12 | import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component' | 12 | import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component' |
13 | import { VideoChannelsRoutingModule } from './video-channels-routing.module' | 13 | import { VideoChannelsRoutingModule } from './video-channels-routing.module' |
14 | import { VideoChannelsComponent } from './video-channels.component' | 14 | import { VideoChannelsComponent } from './video-channels.component' |
15 | import { 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 | ||
125 | activityPubClientRouter.get( | 126 | activityPubClientRouter.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 | ) |
131 | activityPubClientRouter.get('/video-channels/:name/followers', | 133 | activityPubClientRouter.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 | ) |
136 | activityPubClientRouter.get('/video-channels/:name/following', | 139 | activityPubClientRouter.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 | ) |
141 | activityPubClientRouter.get('/video-channels/:name/playlists', | 145 | activityPubClientRouter.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 | |||
4 | import { HttpStatusCode } from '../../../shared/models/http/http-error-codes' | 4 | import { HttpStatusCode } from '../../../shared/models/http/http-error-codes' |
5 | import { isActivityValid } from '../../helpers/custom-validators/activitypub/activity' | 5 | import { isActivityValid } from '../../helpers/custom-validators/activitypub/activity' |
6 | import { logger } from '../../helpers/logger' | 6 | import { logger } from '../../helpers/logger' |
7 | import { asyncMiddleware, checkSignature, localAccountValidator, localVideoChannelValidator, signatureValidator } from '../../middlewares' | 7 | import { |
8 | asyncMiddleware, | ||
9 | checkSignature, | ||
10 | ensureIsLocalChannel, | ||
11 | localAccountValidator, | ||
12 | signatureValidator, | ||
13 | videoChannelsNameWithHostValidator | ||
14 | } from '../../middlewares' | ||
8 | import { activityPubValidator } from '../../middlewares/validators/activitypub/activity' | 15 | import { activityPubValidator } from '../../middlewares/validators/activitypub/activity' |
9 | 16 | ||
10 | const inboxRouter = express.Router() | 17 | const inboxRouter = express.Router() |
@@ -23,10 +30,11 @@ inboxRouter.post('/accounts/:name/inbox', | |||
23 | asyncMiddleware(activityPubValidator), | 30 | asyncMiddleware(activityPubValidator), |
24 | inboxController | 31 | inboxController |
25 | ) | 32 | ) |
26 | inboxRouter.post('/video-channels/:name/inbox', | 33 | inboxRouter.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 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { MActorLight } from '@server/types/models' | ||
2 | import { Activity } from '../../../shared/models/activitypub/activity' | 3 | import { Activity } from '../../../shared/models/activitypub/activity' |
3 | import { VideoPrivacy } from '../../../shared/models/videos' | 4 | import { VideoPrivacy } from '../../../shared/models/videos' |
4 | import { activityPubCollectionPagination, activityPubContextify } from '../../helpers/activitypub' | 5 | import { activityPubCollectionPagination, activityPubContextify } from '../../helpers/activitypub' |
5 | import { logger } from '../../helpers/logger' | 6 | import { logger } from '../../helpers/logger' |
6 | import { buildAnnounceActivity, buildCreateActivity } from '../../lib/activitypub/send' | ||
7 | import { buildAudience } from '../../lib/activitypub/audience' | 7 | import { buildAudience } from '../../lib/activitypub/audience' |
8 | import { asyncMiddleware, localAccountValidator, localVideoChannelValidator } from '../../middlewares' | 8 | import { buildAnnounceActivity, buildCreateActivity } from '../../lib/activitypub/send' |
9 | import { asyncMiddleware, ensureIsLocalChannel, localAccountValidator, videoChannelsNameWithHostValidator } from '../../middlewares' | ||
10 | import { apPaginationValidator } from '../../middlewares/validators/activitypub' | ||
9 | import { VideoModel } from '../../models/video/video' | 11 | import { VideoModel } from '../../models/video/video' |
10 | import { activityPubResponse } from './utils' | 12 | import { activityPubResponse } from './utils' |
11 | import { MActorLight } from '@server/types/models' | ||
12 | import { apPaginationValidator } from '../../middlewares/validators/activitypub' | ||
13 | 13 | ||
14 | const outboxRouter = express.Router() | 14 | const outboxRouter = express.Router() |
15 | 15 | ||
@@ -19,9 +19,10 @@ outboxRouter.get('/accounts/:name/outbox', | |||
19 | asyncMiddleware(outboxController) | 19 | asyncMiddleware(outboxController) |
20 | ) | 20 | ) |
21 | 21 | ||
22 | outboxRouter.get('/video-channels/:name/outbox', | 22 | outboxRouter.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' |
38 | import { | 39 | import { |
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', | |||
91 | videoChannelRouter.delete('/:nameWithHost/avatar', | 94 | videoChannelRouter.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 | ||
98 | videoChannelRouter.delete('/:nameWithHost/banner', | 102 | videoChannelRouter.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 | ||
105 | videoChannelRouter.put('/:nameWithHost', | 110 | videoChannelRouter.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 | ||
113 | videoChannelRouter.delete('/:nameWithHost', | 119 | videoChannelRouter.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', | |||
145 | videoChannelRouter.get('/:nameWithHost/followers', | 154 | videoChannelRouter.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' | |||
3 | import { MChannelBannerAccountDefault } from '@server/types/models' | 3 | import { MChannelBannerAccountDefault } from '@server/types/models' |
4 | import { HttpStatusCode } from '@shared/models' | 4 | import { HttpStatusCode } from '@shared/models' |
5 | 5 | ||
6 | async function doesLocalVideoChannelNameExist (name: string, res: express.Response) { | ||
7 | const videoChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(name) | ||
8 | |||
9 | return processVideoChannelExist(videoChannel, res) | ||
10 | } | ||
11 | |||
12 | async function doesVideoChannelIdExist (id: number, res: express.Response) { | 6 | async 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 | ||
26 | export { | 20 | export { |
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' | |||
3 | import { omit } from 'lodash' | 3 | import { omit } from 'lodash' |
4 | import { Hooks } from '@server/lib/plugins/hooks' | 4 | import { Hooks } from '@server/lib/plugins/hooks' |
5 | import { MUserDefault } from '@server/types/models' | 5 | import { MUserDefault } from '@server/types/models' |
6 | import { HttpStatusCode, UserRegister, UserRole } from '@shared/models' | 6 | import { HttpStatusCode, UserRegister, UserRight, UserRole } from '@shared/models' |
7 | import { isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' | 7 | import { isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' |
8 | import { isThemeNameValid } from '../../helpers/custom-validators/plugins' | 8 | import { isThemeNameValid } from '../../helpers/custom-validators/plugins' |
9 | import { | 9 | import { |
@@ -490,14 +490,17 @@ const ensureAuthUserOwnsAccountValidator = [ | |||
490 | } | 490 | } |
491 | ] | 491 | ] |
492 | 492 | ||
493 | const ensureAuthUserOwnsChannelValidator = [ | 493 | const 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 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { body, param, query } from 'express-validator' | 2 | import { body, param, query } from 'express-validator' |
3 | import { MChannelAccountDefault, MUser } from '@server/types/models' | 3 | import { CONFIG } from '@server/initializers/config' |
4 | import { UserRight } from '../../../../shared' | 4 | import { MChannelAccountDefault } from '@server/types/models' |
5 | import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' | 5 | import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' |
6 | import { isBooleanValid, toBooleanOrNull } from '../../../helpers/custom-validators/misc' | 6 | import { isBooleanValid, toBooleanOrNull } from '../../../helpers/custom-validators/misc' |
7 | import { | 7 | import { |
@@ -13,8 +13,7 @@ import { | |||
13 | import { logger } from '../../../helpers/logger' | 13 | import { logger } from '../../../helpers/logger' |
14 | import { ActorModel } from '../../../models/actor/actor' | 14 | import { ActorModel } from '../../../models/actor/actor' |
15 | import { VideoChannelModel } from '../../../models/video/video-channel' | 15 | import { VideoChannelModel } from '../../../models/video/video-channel' |
16 | import { areValidationErrors, doesLocalVideoChannelNameExist, doesVideoChannelNameWithHostExist } from '../shared' | 16 | import { areValidationErrors, doesVideoChannelNameWithHostExist } from '../shared' |
17 | import { CONFIG } from '@server/initializers/config' | ||
18 | 17 | ||
19 | const videoChannelsAddValidator = [ | 18 | const 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 | ||
73 | const videoChannelsRemoveValidator = [ | 72 | const 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 | ||
103 | const localVideoChannelValidator = [ | 96 | const 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 | ||
154 | function checkUserCanDeleteVideoChannel (user: MUser, videoChannel: MChannelAccountDefault, res: express.Response) { | 147 | async 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 | |||
177 | async 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 | ||