From 40346ead2b0b7afa475aef057d3673b6c7574b7a Mon Sep 17 00:00:00 2001
From: Wicklow <123956049+wickloww@users.noreply.github.com>
Date: Thu, 29 Jun 2023 07:48:55 +0000
Subject: Feature/password protected videos (#5836)
* Add server endpoints
* Refactoring test suites
* Update server and add openapi documentation
* fix compliation and tests
* upload/import password protected video on client
* add server error code
* Add video password to update resolver
* add custom message when sharing pw protected video
* improve confirm component
* Add new alert in component
* Add ability to watch protected video on client
* Cannot have password protected replay privacy
* Add migration
* Add tests
* update after review
* Update check params tests
* Add live videos test
* Add more filter test
* Update static file privacy test
* Update object storage tests
* Add test on feeds
* Add missing word
* Fix tests
* Fix tests on live videos
* add embed support on password protected videos
* fix style
* Correcting data leaks
* Unable to add password protected privacy on replay
* Updated code based on review comments
* fix validator and command
* Updated code based on review comments
---
.../+admin/overview/videos/video-admin.service.ts | 2 +-
.../my-account-two-factor-button.component.ts | 2 +-
.../+video-edit/shared/video-edit.component.html | 9 ++-
.../+video-edit/shared/video-edit.component.ts | 26 +++++++--
.../+videos/+video-edit/video-update.component.ts | 4 +-
.../+videos/+video-edit/video-update.resolver.ts | 20 ++++---
.../action-buttons/action-buttons.component.html | 6 +-
.../action-buttons/action-buttons.component.ts | 14 ++++-
.../shared/action-buttons/video-rate.component.ts | 5 +-
.../shared/comment/video-comment-add.component.ts | 10 +++-
.../shared/comment/video-comment.component.html | 2 +
.../shared/comment/video-comment.component.ts | 1 +
.../shared/comment/video-comments.component.html | 3 +
.../shared/comment/video-comments.component.ts | 5 +-
.../shared/information/video-alert.component.html | 4 ++
.../shared/information/video-alert.component.ts | 8 ++-
.../+video-watch/video-watch.component.html | 7 ++-
.../+videos/+video-watch/video-watch.component.ts | 64 ++++++++++++++++++----
client/src/app/core/confirm/confirm.service.ts | 12 +++-
client/src/app/modal/confirm.component.html | 6 +-
client/src/app/modal/confirm.component.ts | 9 ++-
.../app/shared/form-validators/video-validators.ts | 9 +++
.../app/shared/shared-main/shared-main.module.ts | 3 +
.../video-caption/video-caption.service.ts | 8 ++-
client/src/app/shared/shared-main/video/index.ts | 1 +
.../shared/shared-main/video/video-edit.model.ts | 8 ++-
.../shared-main/video/video-file-token.service.ts | 11 ++--
.../shared-main/video/video-password.service.ts | 29 ++++++++++
.../app/shared/shared-main/video/video.model.ts | 7 +++
.../app/shared/shared-main/video/video.service.ts | 53 +++++++++++-------
.../shared-share-modal/video-share.component.html | 4 ++
.../shared-share-modal/video-share.component.ts | 4 ++
.../shared-video-comment/video-comment.service.ts | 25 ++++++---
.../video-download.component.ts | 15 +++--
.../video-filters-header.component.html | 2 +-
.../video-miniature.component.html | 1 +
.../video-miniature.component.ts | 4 ++
.../videos-list.component.ts | 3 +-
...video-playlist-element-miniature.component.html | 3 +-
.../video-playlist-element-miniature.component.ts | 4 ++
.../shared/manager-options/hls-options-builder.ts | 17 ++++--
.../manager-options/webtorrent-options-builder.ts | 4 +-
.../shared/p2p-media-loader/segment-validator.ts | 40 ++++++++++----
.../player/shared/webtorrent/webtorrent-plugin.ts | 6 +-
client/src/assets/player/types/manager-options.ts | 4 +-
.../player/types/peertube-videojs-typings.ts | 4 +-
client/src/root-helpers/video.ts | 13 ++++-
client/src/standalone/videos/embed.html | 17 ++++++
client/src/standalone/videos/embed.scss | 43 ++++++++++++++-
client/src/standalone/videos/embed.ts | 44 +++++++++++++--
client/src/standalone/videos/shared/auth-http.ts | 10 ++--
client/src/standalone/videos/shared/player-html.ts | 52 ++++++++++++++++++
.../videos/shared/player-manager-options.ts | 12 +++-
.../src/standalone/videos/shared/video-fetcher.ts | 24 ++++----
client/src/types/index.ts | 1 +
client/src/types/server-error.model.ts | 11 ++++
56 files changed, 572 insertions(+), 143 deletions(-)
create mode 100644 client/src/app/shared/shared-main/video/video-password.service.ts
create mode 100644 client/src/types/server-error.model.ts
(limited to 'client')
diff --git a/client/src/app/+admin/overview/videos/video-admin.service.ts b/client/src/app/+admin/overview/videos/video-admin.service.ts
index 4b9357fb7..195b265a1 100644
--- a/client/src/app/+admin/overview/videos/video-admin.service.ts
+++ b/client/src/app/+admin/overview/videos/video-admin.service.ts
@@ -151,7 +151,7 @@ export class VideoAdminService {
}
if (filters.excludePublic) {
- privacyOneOf = [ VideoPrivacy.PRIVATE, VideoPrivacy.UNLISTED, VideoPrivacy.INTERNAL ]
+ privacyOneOf = [ VideoPrivacy.PRIVATE, VideoPrivacy.UNLISTED, VideoPrivacy.INTERNAL, VideoPrivacy.PASSWORD_PROTECTED ]
filters.excludePublic = undefined
}
diff --git a/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.ts b/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.ts
index 97ffb6013..393c3ad6b 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.ts
+++ b/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.ts
@@ -30,7 +30,7 @@ export class MyAccountTwoFactorButtonComponent implements OnInit {
async disableTwoFactor () {
const message = $localize`Are you sure you want to disable two factor authentication of your account?`
- const { confirmed, password } = await this.confirmService.confirmWithPassword(message, $localize`Disable two factor`)
+ const { confirmed, password } = await this.confirmService.confirmWithPassword({ message, title: $localize`Disable two factor` })
if (confirmed === false) return
this.twoFactorService.disableTwoFactor({ userId: this.user.id, currentPassword: password })
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.html b/client/src/app/+videos/+video-edit/shared/video-edit.component.html
index b607dabe9..97b713874 100644
--- a/client/src/app/+videos/+video-edit/shared/video-edit.component.html
+++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.html
@@ -120,7 +120,12 @@
-
@@ -92,6 +92,7 @@
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.ts b/client/src/app/+videos/+video-watch/video-watch.component.ts
index 19ad97d42..aba3ee086 100644
--- a/client/src/app/+videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/+videos/+video-watch/video-watch.component.ts
@@ -25,7 +25,7 @@ import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
import { LiveVideoService } from '@app/shared/shared-video-live'
import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist'
import { logger } from '@root-helpers/logger'
-import { isP2PEnabled, videoRequiresAuth } from '@root-helpers/video'
+import { isP2PEnabled, videoRequiresUserAuth, videoRequiresFileToken } from '@root-helpers/video'
import { timeToInt } from '@shared/core-utils'
import {
HTMLServerConfig,
@@ -68,6 +68,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
video: VideoDetails = null
videoCaptions: VideoCaption[] = []
liveVideo: LiveVideo
+ videoPassword: string
playlistPosition: number
playlist: VideoPlaylist = null
@@ -191,6 +192,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
return this.authService.isLoggedIn()
}
+ isUserOwner () {
+ return this.video.isLocal === true && this.video.account.name === this.user?.username
+ }
+
isVideoBlur (video: Video) {
return video.isVideoNSFWForUser(this.user, this.serverConfig)
}
@@ -243,8 +248,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
private loadVideo (options: {
videoId: string
forceAutoplay: boolean
+ videoPassword?: string
}) {
- const { videoId, forceAutoplay } = options
+ const { videoId, forceAutoplay, videoPassword } = options
if (this.isSameElement(this.video, videoId)) return
@@ -254,7 +260,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
const videoObs = this.hooks.wrapObsFun(
this.videoService.getVideo.bind(this.videoService),
- { videoId },
+ { videoId, videoPassword },
'video-watch',
'filter:api.video-watch.video.get.params',
'filter:api.video-watch.video.get.result'
@@ -269,16 +275,16 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
}),
switchMap(({ video, live }) => {
- if (!videoRequiresAuth(video)) return of({ video, live, videoFileToken: undefined })
+ if (!videoRequiresFileToken(video)) return of({ video, live, videoFileToken: undefined })
- return this.videoFileTokenService.getVideoFileToken(video.uuid)
+ return this.videoFileTokenService.getVideoFileToken({ videoUUID: video.uuid, videoPassword })
.pipe(map(({ token }) => ({ video, live, videoFileToken: token })))
})
)
forkJoin([
videoAndLiveObs,
- this.videoCaptionService.listCaptions(videoId),
+ this.videoCaptionService.listCaptions(videoId, videoPassword),
this.userService.getAnonymousOrLoggedUser()
]).subscribe({
next: ([ { video, live, videoFileToken }, captionsResult, loggedInOrAnonymousUser ]) => {
@@ -304,13 +310,25 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
live,
videoCaptions: captionsResult.data,
videoFileToken,
+ videoPassword,
loggedInOrAnonymousUser,
urlOptions,
forceAutoplay
- }).catch(err => this.handleGlobalError(err))
+ }).catch(err => {
+ this.handleGlobalError(err)
+ })
},
+ error: async err => {
+ if (err.body.code === ServerErrorCode.VIDEO_REQUIRES_PASSWORD || err.body.code === ServerErrorCode.INCORRECT_VIDEO_PASSWORD) {
+ const { confirmed, password } = await this.handleVideoPasswordError(err)
+
+ if (confirmed === false) return this.location.back()
- error: err => this.handleRequestError(err)
+ this.loadVideo({ ...options, videoPassword: password })
+ } else {
+ this.handleRequestError(err)
+ }
+ }
})
}
@@ -375,17 +393,35 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.notifier.error(errorMessage)
}
+ private handleVideoPasswordError (err: any) {
+ let isIncorrectPassword: boolean
+
+ if (err.body.code === ServerErrorCode.VIDEO_REQUIRES_PASSWORD) {
+ isIncorrectPassword = false
+ } else if (err.body.code === ServerErrorCode.INCORRECT_VIDEO_PASSWORD) {
+ this.videoPassword = undefined
+ isIncorrectPassword = true
+ }
+
+ return this.confirmService.confirmWithPassword({
+ message: $localize`You need a password to watch this video`,
+ title: $localize`This video is password protected`,
+ errorMessage: isIncorrectPassword ? $localize`Incorrect password, please enter a correct password` : ''
+ })
+ }
+
private async onVideoFetched (options: {
video: VideoDetails
live: LiveVideo
videoCaptions: VideoCaption[]
videoFileToken: string
+ videoPassword: string
urlOptions: URLOptions
loggedInOrAnonymousUser: User
forceAutoplay: boolean
}) {
- const { video, live, videoCaptions, urlOptions, videoFileToken, loggedInOrAnonymousUser, forceAutoplay } = options
+ const { video, live, videoCaptions, urlOptions, videoFileToken, videoPassword, loggedInOrAnonymousUser, forceAutoplay } = options
this.subscribeToLiveEventsIfNeeded(this.video, video)
@@ -393,6 +429,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.videoCaptions = videoCaptions
this.liveVideo = live
this.videoFileToken = videoFileToken
+ this.videoPassword = videoPassword
// Re init attributes
this.playerPlaceholderImgSrc = undefined
@@ -450,6 +487,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
videoCaptions: this.videoCaptions,
liveVideo: this.liveVideo,
videoFileToken: this.videoFileToken,
+ videoPassword: this.videoPassword,
urlOptions,
loggedInOrAnonymousUser,
forceAutoplay,
@@ -600,6 +638,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
videoCaptions: VideoCaption[]
videoFileToken: string
+ videoPassword: string
urlOptions: CustomizationOptions & { playerMode: PlayerMode }
@@ -607,7 +646,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
forceAutoplay: boolean
user?: AuthUser // Keep for plugins
}) {
- const { video, liveVideo, videoCaptions, videoFileToken, urlOptions, loggedInOrAnonymousUser, forceAutoplay } = params
+ const { video, liveVideo, videoCaptions, videoFileToken, videoPassword, urlOptions, loggedInOrAnonymousUser, forceAutoplay } = params
const getStartTime = () => {
const byUrl = urlOptions.startTime !== undefined
@@ -689,7 +728,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
serverUrl: environment.originServerUrl || window.location.origin,
videoFileToken: () => videoFileToken,
- requiresAuth: videoRequiresAuth(video),
+ requiresUserAuth: videoRequiresUserAuth(video, videoPassword),
+ requiresPassword: video.privacy.id === VideoPrivacy.PASSWORD_PROTECTED &&
+ !video.canAccessPasswordProtectedVideoWithoutPassword(this.user),
+ videoPassword: () => videoPassword,
videoCaptions: playerCaptions,
diff --git a/client/src/app/core/confirm/confirm.service.ts b/client/src/app/core/confirm/confirm.service.ts
index 89a25f0a5..abe163aae 100644
--- a/client/src/app/core/confirm/confirm.service.ts
+++ b/client/src/app/core/confirm/confirm.service.ts
@@ -4,6 +4,7 @@ import { Injectable } from '@angular/core'
type ConfirmOptions = {
title: string
message: string
+ errorMessage?: string
} & (
{
type: 'confirm'
@@ -12,6 +13,7 @@ type ConfirmOptions = {
{
type: 'confirm-password'
confirmButtonText?: string
+ isIncorrectPassword?: boolean
} |
{
type: 'confirm-expected-input'
@@ -32,8 +34,14 @@ export class ConfirmService {
return firstValueFrom(this.extractConfirmed(this.confirmResponse.asObservable()))
}
- confirmWithPassword (message: string, title = '', confirmButtonText?: string) {
- this.showConfirm.next({ type: 'confirm-password', title, message, confirmButtonText })
+ confirmWithPassword (options: {
+ message: string
+ title?: string
+ confirmButtonText?: string
+ errorMessage?: string
+ }) {
+ const { message, title = '', confirmButtonText, errorMessage } = options
+ this.showConfirm.next({ type: 'confirm-password', title, message, confirmButtonText, errorMessage })
const obs = this.confirmResponse.asObservable()
.pipe(map(({ confirmed, value }) => ({ confirmed, password: value })))
diff --git a/client/src/app/modal/confirm.component.html b/client/src/app/modal/confirm.component.html
index 6584db3e6..33696d0a5 100644
--- a/client/src/app/modal/confirm.component.html
+++ b/client/src/app/modal/confirm.component.html
@@ -12,10 +12,12 @@
-
+
-
+
+
+ {{ errorMessage }}
+
+ This video is password protected, please note that recipients will require the corresponding password to access the content.
+
+
diff --git a/client/src/app/shared/shared-share-modal/video-share.component.ts b/client/src/app/shared/shared-share-modal/video-share.component.ts
index 32f900f15..da4f2a4b4 100644
--- a/client/src/app/shared/shared-share-modal/video-share.component.ts
+++ b/client/src/app/shared/shared-share-modal/video-share.component.ts
@@ -243,6 +243,10 @@ export class VideoShareComponent {
return this.playlist.privacy.id === VideoPlaylistPrivacy.PRIVATE
}
+ isPasswordProtectedVideo () {
+ return this.video.privacy.id === VideoPrivacy.PASSWORD_PROTECTED
+ }
+
private getPlaylistOptions (baseUrl?: string) {
return {
baseUrl,
diff --git a/client/src/app/shared/shared-video-comment/video-comment.service.ts b/client/src/app/shared/shared-video-comment/video-comment.service.ts
index 8d2deedf7..3906652be 100644
--- a/client/src/app/shared/shared-video-comment/video-comment.service.ts
+++ b/client/src/app/shared/shared-video-comment/video-comment.service.ts
@@ -18,6 +18,7 @@ import {
import { environment } from '../../../environments/environment'
import { VideoCommentThreadTree } from './video-comment-thread-tree.model'
import { VideoComment } from './video-comment.model'
+import { VideoPasswordService } from '../shared-main'
@Injectable()
export class VideoCommentService {
@@ -31,22 +32,25 @@ export class VideoCommentService {
private restService: RestService
) {}
- addCommentThread (videoId: string, comment: VideoCommentCreate) {
+ addCommentThread (videoId: string, comment: VideoCommentCreate, videoPassword?: string) {
+ const headers = VideoPasswordService.buildVideoPasswordHeader(videoPassword)
const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comment-threads'
const normalizedComment = objectLineFeedToHtml(comment, 'text')
- return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment)
+ return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment, { headers })
.pipe(
map(data => this.extractVideoComment(data.comment)),
catchError(err => this.restExtractor.handleError(err))
)
}
- addCommentReply (videoId: string, inReplyToCommentId: number, comment: VideoCommentCreate) {
+ addCommentReply (options: { videoId: string, inReplyToCommentId: number, comment: VideoCommentCreate, videoPassword?: string }) {
+ const { videoId, inReplyToCommentId, comment, videoPassword } = options
+ const headers = VideoPasswordService.buildVideoPasswordHeader(videoPassword)
const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comments/' + inReplyToCommentId
const normalizedComment = objectLineFeedToHtml(comment, 'text')
- return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment)
+ return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment, { headers })
.pipe(
map(data => this.extractVideoComment(data.comment)),
catchError(err => this.restExtractor.handleError(err))
@@ -76,10 +80,13 @@ export class VideoCommentService {
getVideoCommentThreads (parameters: {
videoId: string
+ videoPassword: string
componentPagination: ComponentPaginationLight
sort: string
}): Observable> {
- const { videoId, componentPagination, sort } = parameters
+ const { videoId, videoPassword, componentPagination, sort } = parameters
+
+ const headers = VideoPasswordService.buildVideoPasswordHeader(videoPassword)
const pagination = this.restService.componentToRestPagination(componentPagination)
@@ -87,7 +94,7 @@ export class VideoCommentService {
params = this.restService.addRestGetParams(params, pagination, sort)
const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comment-threads'
- return this.authHttp.get>(url, { params })
+ return this.authHttp.get>(url, { params, headers })
.pipe(
map(result => this.extractVideoComments(result)),
catchError(err => this.restExtractor.handleError(err))
@@ -97,12 +104,14 @@ export class VideoCommentService {
getVideoThreadComments (parameters: {
videoId: string
threadId: number
+ videoPassword?: string
}): Observable {
- const { videoId, threadId } = parameters
+ const { videoId, threadId, videoPassword } = parameters
const url = `${VideoCommentService.BASE_VIDEO_URL + videoId}/comment-threads/${threadId}`
+ const headers = VideoPasswordService.buildVideoPasswordHeader(videoPassword)
return this.authHttp
- .get(url)
+ .get(url, { headers })
.pipe(
map(tree => this.extractVideoCommentTree(tree)),
catchError(err => this.restExtractor.handleError(err))
diff --git a/client/src/app/shared/shared-video-miniature/video-download.component.ts b/client/src/app/shared/shared-video-miniature/video-download.component.ts
index cac82d8d0..146ea7dfe 100644
--- a/client/src/app/shared/shared-video-miniature/video-download.component.ts
+++ b/client/src/app/shared/shared-video-miniature/video-download.component.ts
@@ -1,13 +1,13 @@
import { mapValues } from 'lodash-es'
import { firstValueFrom } from 'rxjs'
import { tap } from 'rxjs/operators'
-import { Component, ElementRef, Inject, LOCALE_ID, ViewChild } from '@angular/core'
+import { Component, ElementRef, Inject, Input, LOCALE_ID, ViewChild } from '@angular/core'
import { HooksService } from '@app/core'
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
import { logger } from '@root-helpers/logger'
-import { videoRequiresAuth } from '@root-helpers/video'
+import { videoRequiresFileToken } from '@root-helpers/video'
import { objectKeysTyped, pick } from '@shared/core-utils'
-import { VideoCaption, VideoFile, VideoPrivacy } from '@shared/models'
+import { VideoCaption, VideoFile } from '@shared/models'
import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoFileTokenService, VideoService } from '../shared-main'
type DownloadType = 'video' | 'subtitles'
@@ -21,6 +21,8 @@ type FileMetadata = { [key: string]: { label: string, value: string | number } }
export class VideoDownloadComponent {
@ViewChild('modal', { static: true }) modal: ElementRef
+ @Input() videoPassword: string
+
downloadType: 'direct' | 'torrent' = 'direct'
resolutionId: number | string = -1
@@ -89,8 +91,8 @@ export class VideoDownloadComponent {
this.subtitleLanguageId = this.videoCaptions[0].language.id
}
- if (videoRequiresAuth(this.video)) {
- this.videoFileTokenService.getVideoFileToken(this.video.uuid)
+ if (this.isConfidentialVideo()) {
+ this.videoFileTokenService.getVideoFileToken({ videoUUID: this.video.uuid, videoPassword: this.videoPassword })
.subscribe(({ token }) => this.videoFileToken = token)
}
@@ -201,7 +203,8 @@ export class VideoDownloadComponent {
}
isConfidentialVideo () {
- return this.video.privacy.id === VideoPrivacy.PRIVATE || this.video.privacy.id === VideoPrivacy.INTERNAL
+ return videoRequiresFileToken(this.video)
+
}
switchToType (type: DownloadType) {
diff --git a/client/src/app/shared/shared-video-miniature/video-filters-header.component.html b/client/src/app/shared/shared-video-miniature/video-filters-header.component.html
index 3d39c6fdc..3fbfaed28 100644
--- a/client/src/app/shared/shared-video-miniature/video-filters-header.component.html
+++ b/client/src/app/shared/shared-video-miniature/video-filters-header.component.html
@@ -125,7 +125,7 @@
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.html b/client/src/app/shared/shared-video-miniature/video-miniature.component.html
index 3f0180695..9e0a4f79b 100644
--- a/client/src/app/shared/shared-video-miniature/video-miniature.component.html
+++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.html
@@ -5,6 +5,7 @@
>
Unlisted
Private
+ Password protected
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts
index 2384b34d7..d453f37a1 100644
--- a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts
+++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts
@@ -171,6 +171,10 @@ export class VideoMiniatureComponent implements OnInit {
return this.video.privacy.id === VideoPrivacy.PRIVATE
}
+ isPasswordProtectedVideo () {
+ return this.video.privacy.id === VideoPrivacy.PASSWORD_PROTECTED
+ }
+
getStateLabel (video: Video) {
if (!video.state) return ''
diff --git a/client/src/app/shared/shared-video-miniature/videos-list.component.ts b/client/src/app/shared/shared-video-miniature/videos-list.component.ts
index 7b832263e..45df0be38 100644
--- a/client/src/app/shared/shared-video-miniature/videos-list.component.ts
+++ b/client/src/app/shared/shared-video-miniature/videos-list.component.ts
@@ -241,6 +241,7 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy {
}
reloadVideos () {
+ console.log('reload')
this.pagination.currentPage = 1
this.loadMoreVideos(true)
}
@@ -420,7 +421,7 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy {
if (reset) this.videos = []
this.videos = this.videos.concat(data)
-
+ console.log('subscribe')
if (this.groupByDate) this.buildGroupedDateLabels()
this.onDataSubject.next(data)
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html
index 75afa0709..882b14c5e 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html
+++ b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html
@@ -21,7 +21,8 @@
[attr.title]="playlistElement.video.name"
>{{ playlistElement.video.name }}
- Private
+ Private
+ Password protected
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts
index 552ea742b..b9a1d9623 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts
+++ b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts
@@ -60,6 +60,10 @@ export class VideoPlaylistElementMiniatureComponent implements OnInit {
return this.playlistElement.video.privacy.id === VideoPrivacy.PRIVATE
}
+ isVideoPasswordProtected () {
+ return this.playlistElement.video.privacy.id === VideoPrivacy.PASSWORD_PROTECTED
+ }
+
isUnavailable (e: VideoPlaylistElement) {
return e.type === VideoPlaylistElementType.UNAVAILABLE
}
diff --git a/client/src/assets/player/shared/manager-options/hls-options-builder.ts b/client/src/assets/player/shared/manager-options/hls-options-builder.ts
index 194991fa4..8091110bc 100644
--- a/client/src/assets/player/shared/manager-options/hls-options-builder.ts
+++ b/client/src/assets/player/shared/manager-options/hls-options-builder.ts
@@ -31,7 +31,7 @@ export class HLSOptionsBuilder {
const loader = new this.p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass() as P2PMediaLoader
const p2pMediaLoader: P2PMediaLoaderPluginOptions = {
- requiresAuth: commonOptions.requiresAuth,
+ requiresUserAuth: commonOptions.requiresUserAuth,
videoFileToken: commonOptions.videoFileToken,
redundancyUrlManager,
@@ -88,17 +88,24 @@ export class HLSOptionsBuilder {
httpFailedSegmentTimeout: 1000,
xhrSetup: (xhr, url) => {
- if (!this.options.common.requiresAuth) return
+ const { requiresUserAuth, requiresPassword } = this.options.common
+
+ if (!(requiresUserAuth || requiresPassword)) return
+
if (!isSameOrigin(this.options.common.serverUrl, url)) return
- xhr.setRequestHeader('Authorization', this.options.common.authorizationHeader())
+ if (requiresPassword) xhr.setRequestHeader('x-peertube-video-password', this.options.common.videoPassword())
+
+ else xhr.setRequestHeader('Authorization', this.options.common.authorizationHeader())
},
segmentValidator: segmentValidatorFactory({
segmentsSha256Url: this.options.p2pMediaLoader.segmentsSha256Url,
authorizationHeader: this.options.common.authorizationHeader,
- requiresAuth: this.options.common.requiresAuth,
- serverUrl: this.options.common.serverUrl
+ requiresUserAuth: this.options.common.requiresUserAuth,
+ serverUrl: this.options.common.serverUrl,
+ requiresPassword: this.options.common.requiresPassword,
+ videoPassword: this.options.common.videoPassword
}),
segmentUrlBuilder: segmentUrlBuilderFactory(redundancyUrlManager),
diff --git a/client/src/assets/player/shared/manager-options/webtorrent-options-builder.ts b/client/src/assets/player/shared/manager-options/webtorrent-options-builder.ts
index b5bdcd4e6..80eec02cf 100644
--- a/client/src/assets/player/shared/manager-options/webtorrent-options-builder.ts
+++ b/client/src/assets/player/shared/manager-options/webtorrent-options-builder.ts
@@ -26,10 +26,10 @@ export class WebTorrentOptionsBuilder {
videoFileToken: commonOptions.videoFileToken,
- requiresAuth: commonOptions.requiresAuth,
+ requiresUserAuth: commonOptions.requiresUserAuth,
buildWebSeedUrls: file => {
- if (!commonOptions.requiresAuth) return []
+ if (!commonOptions.requiresUserAuth && !commonOptions.requiresPassword) return []
return [ addQueryParams(file.fileUrl, { videoFileToken: commonOptions.videoFileToken() }) ]
},
diff --git a/client/src/assets/player/shared/p2p-media-loader/segment-validator.ts b/client/src/assets/player/shared/p2p-media-loader/segment-validator.ts
index 44a31bfb4..e86d3d159 100644
--- a/client/src/assets/player/shared/p2p-media-loader/segment-validator.ts
+++ b/client/src/assets/player/shared/p2p-media-loader/segment-validator.ts
@@ -13,11 +13,20 @@ function segmentValidatorFactory (options: {
serverUrl: string
segmentsSha256Url: string
authorizationHeader: () => string
- requiresAuth: boolean
+ requiresUserAuth: boolean
+ requiresPassword: boolean
+ videoPassword: () => string
}) {
- const { serverUrl, segmentsSha256Url, authorizationHeader, requiresAuth } = options
-
- let segmentsJSON = fetchSha256Segments({ serverUrl, segmentsSha256Url, authorizationHeader, requiresAuth })
+ const { serverUrl, segmentsSha256Url, authorizationHeader, requiresUserAuth, requiresPassword, videoPassword } = options
+
+ let segmentsJSON = fetchSha256Segments({
+ serverUrl,
+ segmentsSha256Url,
+ authorizationHeader,
+ requiresUserAuth,
+ requiresPassword,
+ videoPassword
+ })
const regex = /bytes=(\d+)-(\d+)/
return async function segmentValidator (segment: Segment, _method: string, _peerId: string, retry = 1) {
@@ -34,7 +43,14 @@ function segmentValidatorFactory (options: {
await wait(500)
- segmentsJSON = fetchSha256Segments({ serverUrl, segmentsSha256Url, authorizationHeader, requiresAuth })
+ segmentsJSON = fetchSha256Segments({
+ serverUrl,
+ segmentsSha256Url,
+ authorizationHeader,
+ requiresUserAuth,
+ requiresPassword,
+ videoPassword
+ })
await segmentValidator(segment, _method, _peerId, retry + 1)
return
@@ -78,13 +94,17 @@ function fetchSha256Segments (options: {
serverUrl: string
segmentsSha256Url: string
authorizationHeader: () => string
- requiresAuth: boolean
+ requiresUserAuth: boolean
+ requiresPassword: boolean
+ videoPassword: () => string
}): Promise {
- const { serverUrl, segmentsSha256Url, requiresAuth, authorizationHeader } = options
+ const { serverUrl, segmentsSha256Url, requiresUserAuth, authorizationHeader, requiresPassword, videoPassword } = options
- const headers = requiresAuth && isSameOrigin(serverUrl, segmentsSha256Url)
- ? { Authorization: authorizationHeader() }
- : {}
+ let headers: { [ id: string ]: string } = {}
+ if (isSameOrigin(serverUrl, segmentsSha256Url)) {
+ if (requiresPassword) headers = { 'x-peertube-video-password': videoPassword() }
+ else if (requiresUserAuth) headers = { Authorization: authorizationHeader() }
+ }
return fetch(segmentsSha256Url, { headers })
.then(res => res.json() as Promise)
diff --git a/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts b/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts
index 3dde44a60..e2e220c03 100644
--- a/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts
+++ b/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts
@@ -59,7 +59,7 @@ class WebTorrentPlugin extends Plugin {
private isAutoResolutionObservation = false
private playerRefusedP2P = false
- private requiresAuth: boolean
+ private requiresUserAuth: boolean
private videoFileToken: () => string
private torrentInfoInterval: any
@@ -86,7 +86,7 @@ class WebTorrentPlugin extends Plugin {
this.savePlayerSrcFunction = this.player.src
this.playerElement = options.playerElement
- this.requiresAuth = options.requiresAuth
+ this.requiresUserAuth = options.requiresUserAuth
this.videoFileToken = options.videoFileToken
this.buildWebSeedUrls = options.buildWebSeedUrls
@@ -546,7 +546,7 @@ class WebTorrentPlugin extends Plugin {
let httpUrl = this.currentVideoFile.fileUrl
- if (this.requiresAuth && this.videoFileToken) {
+ if (this.videoFileToken) {
httpUrl = addQueryParams(httpUrl, { videoFileToken: this.videoFileToken() })
}
diff --git a/client/src/assets/player/types/manager-options.ts b/client/src/assets/player/types/manager-options.ts
index c14fd7e99..1f3a0aa2e 100644
--- a/client/src/assets/player/types/manager-options.ts
+++ b/client/src/assets/player/types/manager-options.ts
@@ -83,8 +83,10 @@ export interface CommonOptions extends CustomizationOptions {
videoShortUUID: string
serverUrl: string
- requiresAuth: boolean
+ requiresUserAuth: boolean
videoFileToken: () => string
+ requiresPassword: boolean
+ videoPassword: () => string
errorNotifier: (message: string) => void
}
diff --git a/client/src/assets/player/types/peertube-videojs-typings.ts b/client/src/assets/player/types/peertube-videojs-typings.ts
index eadf56cfa..723c42c5d 100644
--- a/client/src/assets/player/types/peertube-videojs-typings.ts
+++ b/client/src/assets/player/types/peertube-videojs-typings.ts
@@ -155,7 +155,7 @@ type WebtorrentPluginOptions = {
playerRefusedP2P: boolean
- requiresAuth: boolean
+ requiresUserAuth: boolean
videoFileToken: () => string
buildWebSeedUrls: (file: VideoFile) => string[]
@@ -170,7 +170,7 @@ type P2PMediaLoaderPluginOptions = {
loader: P2PMediaLoader
- requiresAuth: boolean
+ requiresUserAuth: boolean
videoFileToken: () => string
}
diff --git a/client/src/root-helpers/video.ts b/client/src/root-helpers/video.ts
index 9022b908b..4a44615fb 100644
--- a/client/src/root-helpers/video.ts
+++ b/client/src/root-helpers/video.ts
@@ -41,14 +41,21 @@ function isP2PEnabled (video: Video, config: HTMLServerConfig, userP2PEnabled: b
return userP2PEnabled
}
-function videoRequiresAuth (video: Video) {
- return new Set([ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL ]).has(video.privacy.id)
+function videoRequiresUserAuth (video: Video, videoPassword?: string) {
+ return new Set([ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL ]).has(video.privacy.id) ||
+ (video.privacy.id === VideoPrivacy.PASSWORD_PROTECTED && !videoPassword)
+
+}
+
+function videoRequiresFileToken (video: Video, videoPassword?: string) {
+ return new Set([ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL, VideoPrivacy.PASSWORD_PROTECTED ]).has(video.privacy.id)
}
export {
buildVideoOrPlaylistEmbed,
isP2PEnabled,
- videoRequiresAuth
+ videoRequiresUserAuth,
+ videoRequiresFileToken
}
// ---------------------------------------------------------------------------
diff --git a/client/src/standalone/videos/embed.html b/client/src/standalone/videos/embed.html
index 32bf5f655..a74bb4cee 100644
--- a/client/src/standalone/videos/embed.html
+++ b/client/src/standalone/videos/embed.html
@@ -41,6 +41,23 @@
+
+
diff --git a/client/src/standalone/videos/embed.scss b/client/src/standalone/videos/embed.scss
index 3631ea7e6..d15887478 100644
--- a/client/src/standalone/videos/embed.scss
+++ b/client/src/standalone/videos/embed.scss
@@ -24,7 +24,7 @@ html,
body {
height: 100%;
margin: 0;
- background-color: #000;
+ background-color: #0f0f10;
}
#video-wrapper {
@@ -42,8 +42,10 @@ body {
}
}
-#error-block {
+#error-block,
+#video-password-block {
display: none;
+ user-select: none;
flex-direction: column;
align-content: center;
@@ -86,6 +88,43 @@ body {
text-align: center;
}
+#video-password-content {
+ @include margin(1rem, 0, 2rem);
+}
+
+#video-password-input,
+#video-password-submit {
+ line-height: 23px;
+ padding: 1rem;
+ margin: 1rem 0.5rem;
+ border: 0;
+ font-weight: 600;
+ border-radius: 3px!important;
+ font-size: 18px;
+ display: inline-block;
+}
+
+#video-password-submit {
+ color: #fff;
+ background-color: #f2690d;
+ cursor: pointer;
+}
+
+#video-password-submit:hover {
+ background-color: #f47825;
+}
+#video-password-error {
+ margin-top: 10px;
+ margin-bottom: 10px;
+ height: 2rem;
+ font-weight: bolder;
+}
+
+#video-password-block svg {
+ margin-left: auto;
+ margin-right: auto;
+}
+
@media screen and (max-width: 300px) {
#error-block {
font-size: 36px;
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts
index cc4274b99..cffda2cc7 100644
--- a/client/src/standalone/videos/embed.ts
+++ b/client/src/standalone/videos/embed.ts
@@ -3,10 +3,18 @@ import '../../assets/player/shared/dock/peertube-dock-component'
import '../../assets/player/shared/dock/peertube-dock-plugin'
import videojs from 'video.js'
import { peertubeTranslate } from '../../../../shared/core-utils/i18n'
-import { HTMLServerConfig, ResultList, VideoDetails, VideoPlaylist, VideoPlaylistElement, VideoState } from '../../../../shared/models'
+import {
+ HTMLServerConfig,
+ ResultList,
+ ServerErrorCode,
+ VideoDetails,
+ VideoPlaylist,
+ VideoPlaylistElement,
+ VideoState
+} from '../../../../shared/models'
import { PeertubePlayerManager } from '../../assets/player'
import { TranslationsManager } from '../../assets/player/translations-manager'
-import { getParamString, logger, videoRequiresAuth } from '../../root-helpers'
+import { getParamString, logger, videoRequiresFileToken } from '../../root-helpers'
import { PeerTubeEmbedApi } from './embed-api'
import {
AuthHTTP,
@@ -19,6 +27,7 @@ import {
VideoFetcher
} from './shared'
import { PlayerHTML } from './shared/player-html'
+import { PeerTubeServerError } from 'src/types'
export class PeerTubeEmbed {
player: videojs.Player
@@ -38,6 +47,8 @@ export class PeerTubeEmbed {
private readonly liveManager: LiveManager
private playlistTracker: PlaylistTracker
+ private videoPassword: string
+ private requiresPassword: boolean
constructor (videoWrapperId: string) {
logger.registerServerSending(window.location.origin)
@@ -50,6 +61,7 @@ export class PeerTubeEmbed {
this.playerHTML = new PlayerHTML(videoWrapperId)
this.playerManagerOptions = new PlayerManagerOptions(this.playerHTML, this.videoFetcher, this.peertubePlugin)
this.liveManager = new LiveManager(this.playerHTML)
+ this.requiresPassword = false
try {
this.config = JSON.parse((window as any)['PeerTubeServerConfig'])
@@ -176,11 +188,13 @@ export class PeerTubeEmbed {
const { uuid, autoplayFromPreviousVideo, forceAutoplay } = options
try {
- const { videoResponse, captionsPromise } = await this.videoFetcher.loadVideo(uuid)
+ const { videoResponse, captionsPromise } = await this.videoFetcher.loadVideo({ videoId: uuid, videoPassword: this.videoPassword })
return this.buildVideoPlayer({ videoResponse, captionsPromise, autoplayFromPreviousVideo, forceAutoplay })
} catch (err) {
- this.playerHTML.displayError(err.message, await this.translationsPromise)
+
+ if (await this.handlePasswordError(err)) this.loadVideoAndBuildPlayer({ ...options })
+ else this.playerHTML.displayError(err.message, await this.translationsPromise)
}
}
@@ -205,8 +219,8 @@ export class PeerTubeEmbed {
? await this.videoFetcher.loadLive(videoInfo)
: undefined
- const videoFileToken = videoRequiresAuth(videoInfo)
- ? await this.videoFetcher.loadVideoToken(videoInfo)
+ const videoFileToken = videoRequiresFileToken(videoInfo)
+ ? await this.videoFetcher.loadVideoToken(videoInfo, this.videoPassword)
: undefined
return { live, video: videoInfo, videoFileToken }
@@ -232,6 +246,8 @@ export class PeerTubeEmbed {
authorizationHeader: () => this.http.getHeaderTokenValue(),
videoFileToken: () => videoFileToken,
+ videoPassword: () => this.videoPassword,
+ requiresPassword: this.requiresPassword,
onVideoUpdate: (uuid: string) => this.loadVideoAndBuildPlayer({ uuid, autoplayFromPreviousVideo: true, forceAutoplay: false }),
@@ -263,6 +279,7 @@ export class PeerTubeEmbed {
this.initializeApi()
this.playerHTML.removePlaceholder()
+ if (this.videoPassword) this.playerHTML.removeVideoPasswordBlock()
if (this.isPlaylistEmbed()) {
await this.buildPlayerPlaylistUpnext()
@@ -401,6 +418,21 @@ export class PeerTubeEmbed {
(this.player.el() as HTMLElement).style.pointerEvents = 'none'
}
+ private async handlePasswordError (err: PeerTubeServerError) {
+ let incorrectPassword: boolean = null
+ if (err.serverCode === ServerErrorCode.VIDEO_REQUIRES_PASSWORD) incorrectPassword = false
+ else if (err.serverCode === ServerErrorCode.INCORRECT_VIDEO_PASSWORD) incorrectPassword = true
+
+ if (incorrectPassword === null) return false
+
+ this.requiresPassword = true
+ this.videoPassword = await this.playerHTML.askVideoPassword({
+ incorrectPassword,
+ translations: await this.translationsPromise
+ })
+ return true
+ }
+
}
PeerTubeEmbed.main()
diff --git a/client/src/standalone/videos/shared/auth-http.ts b/client/src/standalone/videos/shared/auth-http.ts
index 95e3b029e..c1e9f7750 100644
--- a/client/src/standalone/videos/shared/auth-http.ts
+++ b/client/src/standalone/videos/shared/auth-http.ts
@@ -18,10 +18,12 @@ export class AuthHTTP {
if (this.userOAuthTokens) this.setHeadersFromTokens()
}
- fetch (url: string, { optionalAuth, method }: { optionalAuth: boolean, method?: string }) {
- const refreshFetchOptions = optionalAuth
- ? { headers: this.headers }
- : {}
+ fetch (url: string, { optionalAuth, method }: { optionalAuth: boolean, method?: string }, videoPassword?: string) {
+ let refreshFetchOptions: { headers?: Headers } = {}
+
+ if (videoPassword) this.headers.set('x-peertube-video-password', videoPassword)
+
+ if (videoPassword || optionalAuth) refreshFetchOptions = { headers: this.headers }
return this.refreshFetch(url.toString(), { ...refreshFetchOptions, method })
}
diff --git a/client/src/standalone/videos/shared/player-html.ts b/client/src/standalone/videos/shared/player-html.ts
index d93678c10..a0846d9d7 100644
--- a/client/src/standalone/videos/shared/player-html.ts
+++ b/client/src/standalone/videos/shared/player-html.ts
@@ -55,6 +55,58 @@ export class PlayerHTML {
this.wrapperElement.style.display = 'none'
}
+ async askVideoPassword (options: { incorrectPassword: boolean, translations: Translations }): Promise {
+ const { incorrectPassword, translations } = options
+ return new Promise((resolve) => {
+
+ this.removePlaceholder()
+ this.wrapperElement.style.display = 'none'
+
+ const translatedTitle = peertubeTranslate('This video is password protected', translations)
+ const translatedMessage = peertubeTranslate('You need a password to watch this video.', translations)
+
+ document.title = translatedTitle
+
+ const videoPasswordBlock = document.getElementById('video-password-block')
+ videoPasswordBlock.style.display = 'flex'
+
+ const videoPasswordTitle = document.getElementById('video-password-title')
+ videoPasswordTitle.innerHTML = translatedTitle
+
+ const videoPasswordMessage = document.getElementById('video-password-content')
+ videoPasswordMessage.innerHTML = translatedMessage
+
+ if (incorrectPassword) {
+ const videoPasswordError = document.getElementById('video-password-error')
+ videoPasswordError.innerHTML = peertubeTranslate('Incorrect password, please enter a correct password', translations)
+ videoPasswordError.style.transform = 'scale(1.2)'
+
+ setTimeout(() => {
+ videoPasswordError.style.transform = 'scale(1)'
+ }, 500)
+ }
+
+ const videoPasswordSubmitButton = document.getElementById('video-password-submit')
+ videoPasswordSubmitButton.innerHTML = peertubeTranslate('Watch Video', translations)
+
+ const videoPasswordInput = document.getElementById('video-password-input') as HTMLInputElement
+ videoPasswordInput.placeholder = peertubeTranslate('Password', translations)
+
+ const videoPasswordForm = document.getElementById('video-password-form')
+ videoPasswordForm.addEventListener('submit', (event) => {
+ event.preventDefault()
+ const videoPassword = videoPasswordInput.value
+ resolve(videoPassword)
+ })
+ })
+ }
+
+ removeVideoPasswordBlock () {
+ const videoPasswordBlock = document.getElementById('video-password-block')
+ videoPasswordBlock.style.display = 'none'
+ this.wrapperElement.style.display = 'block'
+ }
+
buildPlaceholder (video: VideoDetails) {
const placeholder = this.getPlaceholderElement()
diff --git a/client/src/standalone/videos/shared/player-manager-options.ts b/client/src/standalone/videos/shared/player-manager-options.ts
index 43ae22a3b..587516410 100644
--- a/client/src/standalone/videos/shared/player-manager-options.ts
+++ b/client/src/standalone/videos/shared/player-manager-options.ts
@@ -18,7 +18,7 @@ import {
logger,
peertubeLocalStorage,
UserLocalStorageKeys,
- videoRequiresAuth
+ videoRequiresUserAuth
} from '../../../root-helpers'
import { PeerTubePlugin } from './peertube-plugin'
import { PlayerHTML } from './player-html'
@@ -162,6 +162,9 @@ export class PlayerManagerOptions {
authorizationHeader: () => string
videoFileToken: () => string
+ videoPassword: () => string
+ requiresPassword: boolean
+
serverConfig: HTMLServerConfig
autoplayFromPreviousVideo: boolean
@@ -178,6 +181,8 @@ export class PlayerManagerOptions {
captionsResponse,
autoplayFromPreviousVideo,
videoFileToken,
+ videoPassword,
+ requiresPassword,
translations,
forceAutoplay,
playlistTracker,
@@ -242,10 +247,13 @@ export class PlayerManagerOptions {
embedUrl: window.location.origin + video.embedPath,
embedTitle: video.name,
- requiresAuth: videoRequiresAuth(video),
+ requiresUserAuth: videoRequiresUserAuth(video),
authorizationHeader,
videoFileToken,
+ requiresPassword,
+ videoPassword,
+
errorNotifier: () => {
// Empty, we don't have a notifier in the embed
},
diff --git a/client/src/standalone/videos/shared/video-fetcher.ts b/client/src/standalone/videos/shared/video-fetcher.ts
index cf6d12831..76ba0a3ed 100644
--- a/client/src/standalone/videos/shared/video-fetcher.ts
+++ b/client/src/standalone/videos/shared/video-fetcher.ts
@@ -1,3 +1,4 @@
+import { PeerTubeServerError } from '../../../types'
import { HttpStatusCode, LiveVideo, VideoDetails, VideoToken } from '../../../../../shared/models'
import { logger } from '../../../root-helpers'
import { AuthHTTP } from './auth-http'
@@ -8,8 +9,8 @@ export class VideoFetcher {
}
- async loadVideo (videoId: string) {
- const videoPromise = this.loadVideoInfo(videoId)
+ async loadVideo ({ videoId, videoPassword }: { videoId: string, videoPassword?: string }) {
+ const videoPromise = this.loadVideoInfo({ videoId, videoPassword })
let videoResponse: Response
let isResponseOk: boolean
@@ -27,11 +28,14 @@ export class VideoFetcher {
if (videoResponse?.status === HttpStatusCode.NOT_FOUND_404) {
throw new Error('This video does not exist.')
}
-
+ if (videoResponse?.status === HttpStatusCode.FORBIDDEN_403) {
+ const res = await videoResponse.json()
+ throw new PeerTubeServerError(res.message, res.code)
+ }
throw new Error('We cannot fetch the video. Please try again later.')
}
- const captionsPromise = this.loadVideoCaptions(videoId)
+ const captionsPromise = this.loadVideoCaptions({ videoId, videoPassword })
return { captionsPromise, videoResponse }
}
@@ -41,8 +45,8 @@ export class VideoFetcher {
.then(res => res.json() as Promise)
}
- loadVideoToken (video: VideoDetails) {
- return this.http.fetch(this.getVideoTokenUrl(video.uuid), { optionalAuth: true, method: 'POST' })
+ loadVideoToken (video: VideoDetails, videoPassword?: string) {
+ return this.http.fetch(this.getVideoTokenUrl(video.uuid), { optionalAuth: true, method: 'POST' }, videoPassword)
.then(res => res.json() as Promise)
.then(token => token.files.token)
}
@@ -51,12 +55,12 @@ export class VideoFetcher {
return this.getVideoUrl(videoUUID) + '/views'
}
- private loadVideoInfo (videoId: string): Promise {
- return this.http.fetch(this.getVideoUrl(videoId), { optionalAuth: true })
+ private loadVideoInfo ({ videoId, videoPassword }: { videoId: string, videoPassword?: string }): Promise {
+ return this.http.fetch(this.getVideoUrl(videoId), { optionalAuth: true }, videoPassword)
}
- private loadVideoCaptions (videoId: string): Promise {
- return this.http.fetch(this.getVideoUrl(videoId) + '/captions', { optionalAuth: true })
+ private loadVideoCaptions ({ videoId, videoPassword }: { videoId: string, videoPassword?: string }): Promise {
+ return this.http.fetch(this.getVideoUrl(videoId) + '/captions', { optionalAuth: true }, videoPassword)
}
private getVideoUrl (id: string) {
diff --git a/client/src/types/index.ts b/client/src/types/index.ts
index 5508515fd..60564496c 100644
--- a/client/src/types/index.ts
+++ b/client/src/types/index.ts
@@ -1,4 +1,5 @@
export * from './client-script.model'
+export * from './server-error.model'
export * from './job-state-client.type'
export * from './job-type-client.type'
export * from './link.type'
diff --git a/client/src/types/server-error.model.ts b/client/src/types/server-error.model.ts
new file mode 100644
index 000000000..4a57287fe
--- /dev/null
+++ b/client/src/types/server-error.model.ts
@@ -0,0 +1,11 @@
+import { ServerErrorCode } from '@shared/models/index'
+
+export class PeerTubeServerError extends Error {
+ serverCode: ServerErrorCode
+
+ constructor (message: string, serverCode: ServerErrorCode) {
+ super(message)
+ this.name = 'CustomError'
+ this.serverCode = serverCode
+ }
+}
--
cgit v1.2.3