+++ /dev/null
-<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>
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 = [
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`
- }
- }
}
]
<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>
<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>
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'
SharedMainModule,
SharedFormModule,
SharedGlobalIconModule,
- SharedActorImageEditModule,
SharedActorImageModule
],
declarations: [
- MyVideoChannelsComponent,
- MyVideoChannelCreateComponent,
- MyVideoChannelUpdateComponent
+ MyVideoChannelsComponent
],
exports: [],
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
--- /dev/null
+<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
+
+<div class="margin-content">
+ <form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
+
+ <div class="form-row"> <!-- channel grid -->
+ <div class="form-group col-12 col-lg-4 col-xl-3">
+ <div *ngIf="isCreation()" class="video-channel-title" i18n>NEW CHANNEL</div>
+ <div *ngIf="!isCreation() && videoChannel" class="video-channel-title" i18n>CHANNEL</div>
+ </div>
+
+ <div class="form-group col-12 col-lg-8 col-xl-9">
+ <h6 i18n>Banner image of the channel</h6>
+
+ <my-actor-banner-edit
+ *ngIf="videoChannel" [previewImage]="isCreation()"
+ [actor]="videoChannel" (bannerChange)="onBannerChange($event)" (bannerDelete)="onBannerDelete()"
+ ></my-actor-banner-edit>
+
+ <my-actor-avatar-edit
+ *ngIf="videoChannel" [previewImage]="isCreation()"
+ [actor]="videoChannel" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"
+ [displayUsername]="!isCreation()" [displaySubscribers]="!isCreation()"
+ ></my-actor-avatar-edit>
+
+ <div class="form-group" *ngIf="isCreation()">
+ <label i18n for="name">Name</label>
+ <div class="input-group">
+ <input
+ type="text" id="name" i18n-placeholder placeholder="Example: my_channel"
+ formControlName="name" [ngClass]="{ 'input-error': formErrors['name'] }" class="form-control"
+ >
+ <div class="input-group-append">
+ <span class="input-group-text">@{{ instanceHost }}</span>
+ </div>
+ </div>
+ <div *ngIf="formErrors['name']" class="form-error">
+ {{ formErrors['name'] }}
+ </div>
+ </div>
+
+ <div class="form-group">
+ <label i18n for="display-name">Display name</label>
+ <input
+ type="text" id="display-name" class="form-control"
+ formControlName="display-name" [ngClass]="{ 'input-error': formErrors['display-name'] }"
+ >
+ <div *ngIf="formErrors['display-name']" class="form-error">
+ {{ formErrors['display-name'] }}
+ </div>
+ </div>
+
+ <div class="form-group">
+ <label i18n for="description">Description</label>
+ <textarea
+ id="description" formControlName="description" class="form-control"
+ [ngClass]="{ 'input-error': formErrors['description'] }"
+ ></textarea>
+ <div *ngIf="formErrors.description" class="form-error">
+ {{ formErrors.description }}
+ </div>
+ </div>
+
+ <div class="form-group">
+ <label for="support">Support</label>
+ <my-help
+ helpType="markdownEnhanced" i18n-preHtml preHtml="Short text to tell people how they can support the channel (membership platform...).<br /><br />
+ When a video is uploaded in this channel, the video support field will be automatically filled by this text."
+ ></my-help>
+ <my-markdown-textarea
+ id="support" formControlName="support" textareaMaxWidth="500px" markdownType="enhanced"
+ [classes]="{ 'input-error': formErrors['support'] }"
+ ></my-markdown-textarea>
+ <div *ngIf="formErrors.support" class="form-error">
+ {{ formErrors.support }}
+ </div>
+ </div>
+
+ <div class="form-group" *ngIf="isBulkUpdateVideosDisplayed()">
+ <my-peertube-checkbox
+ inputName="bulkVideosSupportUpdate" formControlName="bulkVideosSupportUpdate"
+ i18n-labelText labelText="Overwrite support field of all videos of this channel"
+ ></my-peertube-checkbox>
+ </div>
+
+ </div>
+ </div>
+
+ <div class="form-row"> <!-- submit placement block -->
+ <div class="col-md-7 col-xl-5"></div>
+ <div class="col-md-5 col-xl-5 d-inline-flex">
+ <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
+ </div>
+ </div>
+ </form>
+</div>
\ No newline at end of file
@use '_variables' as *;
@use '_mixins' as *;
+.margin-content {
+ padding-top: 20px;
+}
+
label {
font-weight: $font-regular;
font-size: 100%;
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
})
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
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 => {
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,
}
}
]
+ },
+ {
+ path: ':videoChannelName/update',
+ component: VideoChannelUpdateComponent,
+
+ data: {
+ meta: {
+ title: $localize`Update video channel`
+ }
+ }
}
]
<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>
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',
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 () {
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: [
SharedUserSubscriptionModule,
SharedGlobalIconModule,
SharedSupportModal,
- SharedActorImageModule
+ SharedActorImageModule,
+ SharedActorImageEditModule
],
declarations: [
VideoChannelsComponent,
VideoChannelVideosComponent,
- VideoChannelPlaylistsComponent
+ VideoChannelPlaylistsComponent,
+ MyVideoChannelCreateComponent,
+ VideoChannelUpdateComponent
],
exports: [
asyncRetryTransactionMiddleware,
authenticate,
commonVideosFiltersValidator,
+ ensureUserCanManageChannel,
optionalAuthenticate,
paginationValidator,
setDefaultPagination,
authenticate,
reqAvatarFile,
asyncMiddleware(videoChannelsNameWithHostValidator),
- ensureAuthUserOwnsChannelValidator,
+ ensureUserCanManageChannel,
updateAvatarValidator,
asyncMiddleware(updateVideoChannelAvatar)
)
authenticate,
reqBannerFile,
asyncMiddleware(videoChannelsNameWithHostValidator),
- ensureAuthUserOwnsChannelValidator,
+ ensureUserCanManageChannel,
updateBannerValidator,
asyncMiddleware(updateVideoChannelBanner)
)
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)
)
}
}
+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
}
MANAGE_VIDEOS_REDUNDANCIES,
MANAGE_VIDEO_FILES,
- RUN_VIDEO_TRANSCODING
+ RUN_VIDEO_TRANSCODING,
+
+ MANAGE_VIDEO_CHANNELS
}