give admins access to edit all channels

closes #4598
This commit is contained in:
kontrollanten 2021-12-06 05:54:26 +01:00
parent 37cb07eae2
commit 43df00a30d
16 changed files with 178 additions and 164 deletions

View File

@ -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>

View File

@ -1,7 +1,5 @@
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { MyVideoChannelUpdateComponent } from './my-video-channel-update.component'
import { MyVideoChannelCreateComponent } from './my-video-channel-create.component'
import { MyVideoChannelsComponent } from './my-video-channels.component'
const myVideoChannelsRoutes: Routes = [
@ -13,24 +11,6 @@ const myVideoChannelsRoutes: Routes = [
title: $localize`My video channels`
}
}
},
{
path: 'create',
component: MyVideoChannelCreateComponent,
data: {
meta: {
title: $localize`Create a new video channel`
}
}
},
{
path: 'update/:videoChannelId',
component: MyVideoChannelUpdateComponent,
data: {
meta: {
title: $localize`Update video channel`
}
}
}
]

View File

@ -9,7 +9,7 @@
<div class="video-channels-header d-flex justify-content-between">
<my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter>
<a class="create-button" routerLink="create">
<a class="create-button" routerLink="/c/@create">
<my-global-icon iconName="add" aria-hidden="true"></my-global-icon>
<ng-container i18n>Create video channel</ng-container>
</a>
@ -37,7 +37,7 @@
<div i18n class="video-channel-videos">{videoChannel.videosCount, plural, =0 {No videos} =1 {1 video} other {{{ videoChannel.videosCount }} videos}}</div>
<div class="video-channel-buttons">
<my-edit-button label [routerLink]="[ 'update', videoChannel.nameWithHost ]"></my-edit-button>
<my-edit-button label [routerLink]="[ '/c', videoChannel.nameWithHost, 'update' ]"></my-edit-button>
<my-delete-button label (click)="deleteVideoChannel(videoChannel)"></my-delete-button>
</div>

View File

@ -1,11 +1,8 @@
import { ChartModule } from 'primeng/chart'
import { NgModule } from '@angular/core'
import { SharedActorImageEditModule } from '@app/shared/shared-actor-image-edit'
import { SharedFormModule } from '@app/shared/shared-forms'
import { SharedGlobalIconModule } from '@app/shared/shared-icons'
import { SharedMainModule } from '@app/shared/shared-main'
import { MyVideoChannelCreateComponent } from './my-video-channel-create.component'
import { MyVideoChannelUpdateComponent } from './my-video-channel-update.component'
import { MyVideoChannelsRoutingModule } from './my-video-channels-routing.module'
import { MyVideoChannelsComponent } from './my-video-channels.component'
import { SharedActorImageModule } from '@app/shared/shared-actor-image/shared-actor-image.module'
@ -19,14 +16,11 @@ import { SharedActorImageModule } from '@app/shared/shared-actor-image/shared-ac
SharedMainModule,
SharedFormModule,
SharedGlobalIconModule,
SharedActorImageEditModule,
SharedActorImageModule
],
declarations: [
MyVideoChannelsComponent,
MyVideoChannelCreateComponent,
MyVideoChannelUpdateComponent
MyVideoChannelsComponent
],
exports: [],

View File

@ -12,11 +12,11 @@ import {
import { FormValidatorService } from '@app/shared/shared-forms'
import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
import { HttpStatusCode, VideoChannelCreate } from '@shared/models'
import { MyVideoChannelEdit } from './my-video-channel-edit'
import { MyVideoChannelEdit } from './video-channel-edit'
@Component({
templateUrl: './my-video-channel-edit.component.html',
styleUrls: [ './my-video-channel-edit.component.scss' ]
templateUrl: './video-channel-edit.component.html',
styleUrls: [ './video-channel-edit.component.scss' ]
})
export class MyVideoChannelCreateComponent extends MyVideoChannelEdit implements OnInit {
error: string

View File

@ -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>

View File

@ -1,6 +1,10 @@
@use '_variables' as *;
@use '_mixins' as *;
.margin-content {
padding-top: 20px;
}
label {
font-weight: $font-regular;
font-size: 100%;

View File

@ -12,14 +12,14 @@ import {
import { FormValidatorService } from '@app/shared/shared-forms'
import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
import { HTMLServerConfig, VideoChannelUpdate } from '@shared/models'
import { MyVideoChannelEdit } from './my-video-channel-edit'
import { MyVideoChannelEdit } from './video-channel-edit'
@Component({
selector: 'my-video-channel-update',
templateUrl: './my-video-channel-edit.component.html',
styleUrls: [ './my-video-channel-edit.component.scss' ]
templateUrl: './video-channel-edit.component.html',
styleUrls: [ './video-channel-edit.component.scss' ]
})
export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements OnInit, OnDestroy {
export class VideoChannelUpdateComponent extends MyVideoChannelEdit implements OnInit, OnDestroy {
error: string
videoChannel: VideoChannel
@ -50,9 +50,9 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
})
this.paramsSub = this.route.params.subscribe(routeParams => {
const videoChannelId = routeParams['videoChannelId']
const videoChannelName = routeParams['videoChannelName']
this.videoChannelService.getVideoChannel(videoChannelId)
this.videoChannelService.getVideoChannel(videoChannelName)
.subscribe({
next: videoChannelToUpdate => {
this.videoChannel = videoChannelToUpdate
@ -95,7 +95,7 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
this.notifier.success($localize`Video channel ${videoChannelUpdate.displayName} updated.`)
this.router.navigate([ '/my-library', 'video-channels' ])
this.router.navigate([ '/c', this.videoChannel.name ])
},
error: err => {

View File

@ -1,10 +1,21 @@
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { MyVideoChannelCreateComponent } from './video-channel-edit/video-channel-create.component'
import { VideoChannelUpdateComponent } from './video-channel-edit/video-channel-update.component'
import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component'
import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component'
import { VideoChannelsComponent } from './video-channels.component'
const videoChannelsRoutes: Routes = [
{
path: '@create',
component: MyVideoChannelCreateComponent,
data: {
meta: {
title: $localize`Create a new video channel`
}
}
},
{
path: ':videoChannelName',
component: VideoChannelsComponent,
@ -37,6 +48,16 @@ const videoChannelsRoutes: Routes = [
}
}
]
},
{
path: ':videoChannelName/update',
component: VideoChannelUpdateComponent,
data: {
meta: {
title: $localize`Update video channel`
}
}
}
]

View File

@ -6,11 +6,11 @@
<div class="channel-info">
<ng-template #buttonsTemplate>
<a *ngIf="isManageable()" [routerLink]="[ '/my-library/video-channels/update', videoChannel.nameWithHost ]" class="peertube-button-link orange-button" i18n>
<a *ngIf="isManageable()" [routerLink]="[ 'update' ]" class="peertube-button-link orange-button" i18n>
Manage channel
</a>
<my-subscribe-button *ngIf="!isManageable()" #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button>
<my-subscribe-button *ngIf="!isOwner()" #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button>
<button *ngIf="videoChannel.support" (click)="showSupportModal()" class="support-button peertube-button orange-button-inverted">
<my-global-icon iconName="support" aria-hidden="true"></my-global-icon>

View File

@ -7,7 +7,7 @@ import { AuthService, MarkdownService, Notifier, RestExtractor, ScreenService }
import { ListOverflowItem, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main'
import { SupportModalComponent } from '@app/shared/shared-support-modal'
import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
import { HttpStatusCode } from '@shared/models'
import { HttpStatusCode, UserRight } from '@shared/models'
@Component({
templateUrl: './video-channels.component.html',
@ -93,10 +93,14 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
return this.authService.isLoggedIn()
}
isOwner () {
return this.videoChannel?.ownerAccount.userId === this.authService.getUser().id
}
isManageable () {
if (!this.isUserLoggedIn()) return false
return this.videoChannel?.ownerAccount.userId === this.authService.getUser().id
return this.isOwner() || this.authService.getUser().hasRight(UserRight.MANAGE_VIDEO_CHANNELS)
}
activateCopiedMessage () {

View File

@ -11,6 +11,9 @@ import { VideoChannelVideosComponent } from './video-channel-videos/video-channe
import { VideoChannelsRoutingModule } from './video-channels-routing.module'
import { VideoChannelsComponent } from './video-channels.component'
import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module'
import { MyVideoChannelCreateComponent } from './video-channel-edit/video-channel-create.component'
import { VideoChannelUpdateComponent } from './video-channel-edit/video-channel-update.component'
import { SharedActorImageEditModule } from '@app/shared/shared-actor-image-edit'
@NgModule({
imports: [
@ -23,13 +26,16 @@ import { SharedActorImageModule } from '../shared/shared-actor-image/shared-acto
SharedUserSubscriptionModule,
SharedGlobalIconModule,
SharedSupportModal,
SharedActorImageModule
SharedActorImageModule,
SharedActorImageEditModule
],
declarations: [
VideoChannelsComponent,
VideoChannelVideosComponent,
VideoChannelPlaylistsComponent
VideoChannelPlaylistsComponent,
MyVideoChannelCreateComponent,
VideoChannelUpdateComponent
],
exports: [

View File

@ -24,6 +24,7 @@ import {
asyncRetryTransactionMiddleware,
authenticate,
commonVideosFiltersValidator,
ensureUserCanManageChannel,
optionalAuthenticate,
paginationValidator,
setDefaultPagination,
@ -74,7 +75,7 @@ videoChannelRouter.post('/:nameWithHost/avatar/pick',
authenticate,
reqAvatarFile,
asyncMiddleware(videoChannelsNameWithHostValidator),
ensureAuthUserOwnsChannelValidator,
ensureUserCanManageChannel,
updateAvatarValidator,
asyncMiddleware(updateVideoChannelAvatar)
)
@ -83,7 +84,7 @@ videoChannelRouter.post('/:nameWithHost/banner/pick',
authenticate,
reqBannerFile,
asyncMiddleware(videoChannelsNameWithHostValidator),
ensureAuthUserOwnsChannelValidator,
ensureUserCanManageChannel,
updateBannerValidator,
asyncMiddleware(updateVideoChannelBanner)
)
@ -91,21 +92,21 @@ videoChannelRouter.post('/:nameWithHost/banner/pick',
videoChannelRouter.delete('/:nameWithHost/avatar',
authenticate,
asyncMiddleware(videoChannelsNameWithHostValidator),
ensureAuthUserOwnsChannelValidator,
ensureUserCanManageChannel,
asyncMiddleware(deleteVideoChannelAvatar)
)
videoChannelRouter.delete('/:nameWithHost/banner',
authenticate,
asyncMiddleware(videoChannelsNameWithHostValidator),
ensureAuthUserOwnsChannelValidator,
ensureUserCanManageChannel,
asyncMiddleware(deleteVideoChannelBanner)
)
videoChannelRouter.put('/:nameWithHost',
authenticate,
asyncMiddleware(videoChannelsNameWithHostValidator),
ensureAuthUserOwnsChannelValidator,
ensureUserCanManageChannel,
videoChannelsUpdateValidator,
asyncRetryTransactionMiddleware(updateVideoChannel)
)

View File

@ -20,8 +20,26 @@ function ensureUserHasRight (userRight: UserRight) {
}
}
function ensureUserCanManageChannel (req: express.Request, res: express.Response, next: express.NextFunction) {
const user = res.locals.oauth.token.user
const isUserOwner = res.locals.videoChannel.Account.userId !== user.id
if (isUserOwner && user.hasRight(UserRight.MANAGE_VIDEO_CHANNELS) === false) {
const message = `User ${user.username} does not have right to manage channel ${req.params.nameWithHost}.`
logger.info(message)
return res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message
})
}
return next()
}
// ---------------------------------------------------------------------------
export {
ensureUserHasRight
ensureUserHasRight,
ensureUserCanManageChannel
}

View File

@ -41,5 +41,7 @@ export const enum UserRight {
MANAGE_VIDEOS_REDUNDANCIES,
MANAGE_VIDEO_FILES,
RUN_VIDEO_TRANSCODING
RUN_VIDEO_TRANSCODING,
MANAGE_VIDEO_CHANNELS
}