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