--- /dev/null
+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 {}
--- /dev/null
+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 { }
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 { VideoChannelEdit } 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 {
+export class VideoChannelCreateComponent extends VideoChannelEdit implements OnInit {
error: string
videoChannel = new VideoChannel({})
--- /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 { FormReactive } from '@app/shared/shared-forms'
import { VideoChannel } from '@app/shared/shared-main'
-export abstract class MyVideoChannelEdit extends FormReactive {
+export abstract class VideoChannelEdit extends FormReactive {
videoChannel: VideoChannel
abstract isCreation (): boolean
import { HttpErrorResponse } from '@angular/common/http'
import { Component, OnDestroy, OnInit } from '@angular/core'
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 {
VIDEO_CHANNEL_DESCRIPTION_VALIDATOR,
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 { VideoChannelEdit } 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 VideoChannelEdit implements OnInit, OnDestroy {
error: string
videoChannel: VideoChannel
private router: Router,
private route: ActivatedRoute,
private videoChannelService: VideoChannelService,
- private serverService: ServerService
+ private serverService: ServerService,
+ private redirectService: RedirectService
) {
super()
}
})
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.redirectService.redirectToPreviousRoute([ '/c', this.videoChannel.name ])
},
error: err => {
+++ /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 = [
},
{
path: 'create',
- component: MyVideoChannelCreateComponent,
- data: {
- meta: {
- title: $localize`Create a new video channel`
- }
- }
+ redirectTo: '/manage/create'
},
{
- path: 'update/:videoChannelId',
- component: MyVideoChannelUpdateComponent,
- data: {
- meta: {
- title: $localize`Update video channel`
- }
- }
+ path: 'update/:videoChannelName',
+ redirectTo: '/manage/update/:videoChannelName'
}
]
<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="/manage/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]="[ '/manage/update', videoChannel.nameWithHost ]"></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: [],
<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]="[ '/manage/update', videoChannel.nameWithHost ]" 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 { BlocklistService } from '@app/shared/shared-moderation'
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()
}
- isManageable () {
+ isOwner () {
if (!this.isUserLoggedIn()) return false
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 () {
this.notifier.success($localize`Username copied`)
}
import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component'
import { VideoChannelsRoutingModule } from './video-channels-routing.module'
import { VideoChannelsComponent } from './video-channels.component'
+import { SharedActorImageEditModule } from '@app/shared/shared-actor-image-edit'
@NgModule({
imports: [
SharedGlobalIconModule,
SharedSupportModal,
SharedActorImageModule,
+ SharedActorImageEditModule,
SharedModerationModule
],
loadChildren: () => import('./+video-channels/video-channels.module').then(m => m.VideoChannelsModule),
canActivateChild: [ MetaGuard ]
},
-
+ {
+ path: 'manage',
+ loadChildren: () => import('./+manage/manage.module').then(m => m.ManageModule),
+ canActivateChild: [ MetaGuard ]
+ },
{
path: 'p',
loadChildren: () => import('./+plugin-pages/plugin-pages.module').then(m => m.PluginPagesModule),
return this.defaultTrendingAlgorithm
}
- redirectToPreviousRoute () {
+ redirectToPreviousRoute (fallbackRoute: string[] = null) {
const exceptions = [
'/verify-account',
'/reset-password'
if (!isException) return this.router.navigateByUrl(this.previousUrl)
}
+ if (fallbackRoute) {
+ return this.router.navigate(fallbackRoute)
+ }
+
return this.redirectToHomepage()
}
asyncMiddleware,
executeIfActivityPub,
localAccountValidator,
- localVideoChannelValidator,
+ videoChannelsNameWithHostValidator,
+ ensureIsLocalChannel,
videosCustomGetValidator,
videosShareValidator
} from '../../middlewares'
)
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,
- asyncMiddleware(localVideoChannelValidator),
+ asyncMiddleware(videoChannelsNameWithHostValidator),
+ ensureIsLocalChannel,
videoChannelController
)
-activityPubClientRouter.get('/video-channels/:name/followers',
+activityPubClientRouter.get('/video-channels/:nameWithHost/followers',
executeIfActivityPub,
- asyncMiddleware(localVideoChannelValidator),
+ asyncMiddleware(videoChannelsNameWithHostValidator),
+ ensureIsLocalChannel,
asyncMiddleware(videoChannelFollowersController)
)
-activityPubClientRouter.get('/video-channels/:name/following',
+activityPubClientRouter.get('/video-channels/:nameWithHost/following',
executeIfActivityPub,
- asyncMiddleware(localVideoChannelValidator),
+ asyncMiddleware(videoChannelsNameWithHostValidator),
+ ensureIsLocalChannel,
asyncMiddleware(videoChannelFollowingController)
)
-activityPubClientRouter.get('/video-channels/:name/playlists',
+activityPubClientRouter.get('/video-channels/:nameWithHost/playlists',
executeIfActivityPub,
- asyncMiddleware(localVideoChannelValidator),
+ asyncMiddleware(videoChannelsNameWithHostValidator),
+ ensureIsLocalChannel,
asyncMiddleware(videoChannelPlaylistsController)
)
import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
import { isActivityValid } from '../../helpers/custom-validators/activitypub/activity'
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'
const inboxRouter = express.Router()
asyncMiddleware(activityPubValidator),
inboxController
)
-inboxRouter.post('/video-channels/:name/inbox',
+inboxRouter.post('/video-channels/:nameWithHost/inbox',
signatureValidator,
asyncMiddleware(checkSignature),
- asyncMiddleware(localVideoChannelValidator),
+ asyncMiddleware(videoChannelsNameWithHostValidator),
+ ensureIsLocalChannel,
asyncMiddleware(activityPubValidator),
inboxController
)
import express from 'express'
+import { MActorLight } from '@server/types/models'
import { Activity } from '../../../shared/models/activitypub/activity'
import { VideoPrivacy } from '../../../shared/models/videos'
import { activityPubCollectionPagination, activityPubContextify } from '../../helpers/activitypub'
import { logger } from '../../helpers/logger'
-import { buildAnnounceActivity, buildCreateActivity } from '../../lib/activitypub/send'
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 { activityPubResponse } from './utils'
-import { MActorLight } from '@server/types/models'
-import { apPaginationValidator } from '../../middlewares/validators/activitypub'
const outboxRouter = express.Router()
asyncMiddleware(outboxController)
)
-outboxRouter.get('/video-channels/:name/outbox',
+outboxRouter.get('/video-channels/:nameWithHost/outbox',
apPaginationValidator,
- localVideoChannelValidator,
+ asyncMiddleware(videoChannelsNameWithHostValidator),
+ ensureIsLocalChannel,
asyncMiddleware(outboxController)
)
asyncRetryTransactionMiddleware,
authenticate,
commonVideosFiltersValidator,
+ ensureCanManageChannel,
optionalAuthenticate,
paginationValidator,
setDefaultPagination,
videoPlaylistsSortValidator
} from '../../middlewares'
import {
- ensureAuthUserOwnsChannelValidator,
+ ensureIsLocalChannel,
videoChannelsFollowersSortValidator,
videoChannelsListValidator,
videoChannelsNameWithHostValidator,
authenticate,
reqAvatarFile,
asyncMiddleware(videoChannelsNameWithHostValidator),
- ensureAuthUserOwnsChannelValidator,
+ ensureIsLocalChannel,
+ ensureCanManageChannel,
updateAvatarValidator,
asyncMiddleware(updateVideoChannelAvatar)
)
authenticate,
reqBannerFile,
asyncMiddleware(videoChannelsNameWithHostValidator),
- ensureAuthUserOwnsChannelValidator,
+ ensureIsLocalChannel,
+ ensureCanManageChannel,
updateBannerValidator,
asyncMiddleware(updateVideoChannelBanner)
)
videoChannelRouter.delete('/:nameWithHost/avatar',
authenticate,
asyncMiddleware(videoChannelsNameWithHostValidator),
- ensureAuthUserOwnsChannelValidator,
+ ensureIsLocalChannel,
+ ensureCanManageChannel,
asyncMiddleware(deleteVideoChannelAvatar)
)
videoChannelRouter.delete('/:nameWithHost/banner',
authenticate,
asyncMiddleware(videoChannelsNameWithHostValidator),
- ensureAuthUserOwnsChannelValidator,
+ ensureIsLocalChannel,
+ ensureCanManageChannel,
asyncMiddleware(deleteVideoChannelBanner)
)
videoChannelRouter.put('/:nameWithHost',
authenticate,
asyncMiddleware(videoChannelsNameWithHostValidator),
- ensureAuthUserOwnsChannelValidator,
+ ensureIsLocalChannel,
+ ensureCanManageChannel,
videoChannelsUpdateValidator,
asyncRetryTransactionMiddleware(updateVideoChannel)
)
videoChannelRouter.delete('/:nameWithHost',
authenticate,
+ asyncMiddleware(videoChannelsNameWithHostValidator),
+ ensureIsLocalChannel,
+ ensureCanManageChannel,
asyncMiddleware(videoChannelsRemoveValidator),
asyncRetryTransactionMiddleware(removeVideoChannel)
)
videoChannelRouter.get('/:nameWithHost/followers',
authenticate,
asyncMiddleware(videoChannelsNameWithHostValidator),
- ensureAuthUserOwnsChannelValidator,
+ ensureCanManageChannel,
paginationValidator,
videoChannelsFollowersSortValidator,
setDefaultSort,
import { MChannelBannerAccountDefault } from '@server/types/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) {
const videoChannel = await VideoChannelModel.loadAndPopulateAccount(+id)
// ---------------------------------------------------------------------------
export {
- doesLocalVideoChannelNameExist,
doesVideoChannelIdExist,
doesVideoChannelNameWithHostExist
}
import { omit } from 'lodash'
import { Hooks } from '@server/lib/plugins/hooks'
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 { isThemeNameValid } from '../../helpers/custom-validators/plugins'
import {
}
]
-const ensureAuthUserOwnsChannelValidator = [
+const ensureCanManageChannel = [
(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({
status: HttpStatusCode.FORBIDDEN_403,
- message: 'Only owner of this video channel can access this ressource'
+ message
})
}
usersVerifyEmailValidator,
userAutocompleteValidator,
ensureAuthUserOwnsAccountValidator,
- ensureAuthUserOwnsChannelValidator,
- ensureCanManageUser
+ ensureCanManageUser,
+ ensureCanManageChannel
}
// ---------------------------------------------------------------------------
import express from 'express'
import { body, param, query } from 'express-validator'
-import { MChannelAccountDefault, MUser } from '@server/types/models'
-import { UserRight } from '../../../../shared'
+import { CONFIG } from '@server/initializers/config'
+import { MChannelAccountDefault } from '@server/types/models'
import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
import { isBooleanValid, toBooleanOrNull } from '../../../helpers/custom-validators/misc'
import {
import { logger } from '../../../helpers/logger'
import { ActorModel } from '../../../models/actor/actor'
import { VideoChannelModel } from '../../../models/video/video-channel'
-import { areValidationErrors, doesLocalVideoChannelNameExist, doesVideoChannelNameWithHostExist } from '../shared'
-import { CONFIG } from '@server/initializers/config'
+import { areValidationErrors, doesVideoChannelNameWithHostExist } from '../shared'
const videoChannelsAddValidator = [
body('name').custom(isVideoChannelUsernameValid).withMessage('Should have a valid channel name'),
]
const videoChannelsRemoveValidator = [
- param('nameWithHost').exists().withMessage('Should have an video channel name with host'),
-
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking videoChannelsRemove parameters', { parameters: req.params })
- if (areValidationErrors(req, 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
+ if (!await checkVideoChannelIsNotTheLastOne(res.locals.videoChannel, res)) return
return next()
}
}
]
-const localVideoChannelValidator = [
- param('name').custom(isVideoChannelDisplayNameValid).withMessage('Should have a valid video channel name'),
-
- async (req: express.Request, res: express.Response, next: express.NextFunction) => {
- logger.debug('Checking localVideoChannelValidator parameters', { parameters: req.params })
-
- if (areValidationErrors(req, res)) return
- if (!await doesLocalVideoChannelNameExist(req.params.name, res)) return
+const ensureIsLocalChannel = [
+ (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ if (res.locals.videoChannel.Actor.isOwned() === false) {
+ return res.fail({
+ status: HttpStatusCode.FORBIDDEN_403,
+ message: 'This channel is not owned.'
+ })
+ }
return next()
}
videoChannelsUpdateValidator,
videoChannelsRemoveValidator,
videoChannelsNameWithHostValidator,
+ ensureIsLocalChannel,
videoChannelsListValidator,
- localVideoChannelValidator,
videoChannelStatsValidator
}
// ---------------------------------------------------------------------------
-function checkUserCanDeleteVideoChannel (user: MUser, videoChannel: MChannelAccountDefault, res: express.Response) {
- if (videoChannel.Actor.isOwned() === false) {
- 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)
+async function checkVideoChannelIsNotTheLastOne (videoChannel: MChannelAccountDefault, res: express.Response) {
+ const count = await VideoChannelModel.countByAccount(videoChannel.Account.id)
if (count <= 1) {
res.fail({
let totoChannel: number
let videoUUID: string
let accountName: string
+ let secondUserChannelName: string
const avatarPaths: { [ port: number ]: string } = {}
const bannerPaths: { [ port: number ]: string } = {}
}
})
+ 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 () {
this.timeout(35000)
})
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.have.lengthOf(1)
+ expect(body.data).to.have.lengthOf(2)
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 () {
[UserRole.MODERATOR]: [
UserRight.MANAGE_VIDEO_BLACKLIST,
UserRight.MANAGE_ABUSES,
+ UserRight.MANAGE_ANY_VIDEO_CHANNEL,
UserRight.REMOVE_ANY_VIDEO,
- UserRight.REMOVE_ANY_VIDEO_CHANNEL,
UserRight.REMOVE_ANY_VIDEO_PLAYLIST,
UserRight.REMOVE_ANY_VIDEO_COMMENT,
UserRight.UPDATE_ANY_VIDEO,
return {
token,
userId: user.id,
- userChannelId: me.videoChannels[0].id
+ userChannelId: me.videoChannels[0].id,
+ userChannelName: me.videoChannels[0].name
}
}
MANAGE_SERVERS_BLOCKLIST,
MANAGE_VIDEO_BLACKLIST,
+ MANAGE_ANY_VIDEO_CHANNEL,
REMOVE_ANY_VIDEO,
- REMOVE_ANY_VIDEO_CHANNEL,
REMOVE_ANY_VIDEO_PLAYLIST,
REMOVE_ANY_VIDEO_COMMENT,