aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/shared/shared-main
diff options
context:
space:
mode:
authorWicklow <123956049+wickloww@users.noreply.github.com>2023-06-29 07:48:55 +0000
committerGitHub <noreply@github.com>2023-06-29 09:48:55 +0200
commit40346ead2b0b7afa475aef057d3673b6c7574b7a (patch)
tree24ffdc23c3a9d987334842e0d400b5bd44500cf7 /client/src/app/shared/shared-main
parentae22c59f14d0d553f60b281948b6c232c2aca178 (diff)
downloadPeerTube-40346ead2b0b7afa475aef057d3673b6c7574b7a.tar.gz
PeerTube-40346ead2b0b7afa475aef057d3673b6c7574b7a.tar.zst
PeerTube-40346ead2b0b7afa475aef057d3673b6c7574b7a.zip
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
Diffstat (limited to 'client/src/app/shared/shared-main')
-rw-r--r--client/src/app/shared/shared-main/shared-main.module.ts3
-rw-r--r--client/src/app/shared/shared-main/video-caption/video-caption.service.ts8
-rw-r--r--client/src/app/shared/shared-main/video/index.ts1
-rw-r--r--client/src/app/shared/shared-main/video/video-edit.model.ts8
-rw-r--r--client/src/app/shared/shared-main/video/video-file-token.service.ts11
-rw-r--r--client/src/app/shared/shared-main/video/video-password.service.ts29
-rw-r--r--client/src/app/shared/shared-main/video/video.model.ts7
-rw-r--r--client/src/app/shared/shared-main/video/video.service.ts53
8 files changed, 91 insertions, 29 deletions
diff --git a/client/src/app/shared/shared-main/shared-main.module.ts b/client/src/app/shared/shared-main/shared-main.module.ts
index d3ec31d6e..480277450 100644
--- a/client/src/app/shared/shared-main/shared-main.module.ts
+++ b/client/src/app/shared/shared-main/shared-main.module.ts
@@ -52,6 +52,7 @@ import {
52 VideoFileTokenService, 52 VideoFileTokenService,
53 VideoImportService, 53 VideoImportService,
54 VideoOwnershipService, 54 VideoOwnershipService,
55 VideoPasswordService,
55 VideoResolver, 56 VideoResolver,
56 VideoService 57 VideoService
57} from './video' 58} from './video'
@@ -210,6 +211,8 @@ import { VideoChannelService } from './video-channel'
210 211
211 VideoChannelService, 212 VideoChannelService,
212 213
214 VideoPasswordService,
215
213 CustomPageService, 216 CustomPageService,
214 217
215 ActorRedirectGuard 218 ActorRedirectGuard
diff --git a/client/src/app/shared/shared-main/video-caption/video-caption.service.ts b/client/src/app/shared/shared-main/video-caption/video-caption.service.ts
index 0f3afd116..21f31a717 100644
--- a/client/src/app/shared/shared-main/video-caption/video-caption.service.ts
+++ b/client/src/app/shared/shared-main/video-caption/video-caption.service.ts
@@ -4,7 +4,7 @@ import { HttpClient } from '@angular/common/http'
4import { Injectable } from '@angular/core' 4import { Injectable } from '@angular/core'
5import { RestExtractor, ServerService } from '@app/core' 5import { RestExtractor, ServerService } from '@app/core'
6import { objectToFormData, sortBy } from '@app/helpers' 6import { objectToFormData, sortBy } from '@app/helpers'
7import { VideoService } from '@app/shared/shared-main/video' 7import { VideoPasswordService, VideoService } from '@app/shared/shared-main/video'
8import { peertubeTranslate } from '@shared/core-utils/i18n' 8import { peertubeTranslate } from '@shared/core-utils/i18n'
9import { ResultList, VideoCaption } from '@shared/models' 9import { ResultList, VideoCaption } from '@shared/models'
10import { environment } from '../../../../environments/environment' 10import { environment } from '../../../../environments/environment'
@@ -18,8 +18,10 @@ export class VideoCaptionService {
18 private restExtractor: RestExtractor 18 private restExtractor: RestExtractor
19 ) {} 19 ) {}
20 20
21 listCaptions (videoId: string): Observable<ResultList<VideoCaption>> { 21 listCaptions (videoId: string, videoPassword?: string): Observable<ResultList<VideoCaption>> {
22 return this.authHttp.get<ResultList<VideoCaption>>(`${VideoService.BASE_VIDEO_URL}/${videoId}/captions`) 22 const headers = VideoPasswordService.buildVideoPasswordHeader(videoPassword)
23
24 return this.authHttp.get<ResultList<VideoCaption>>(`${VideoService.BASE_VIDEO_URL}/${videoId}/captions`, { headers })
23 .pipe( 25 .pipe(
24 switchMap(captionsResult => { 26 switchMap(captionsResult => {
25 return this.serverService.getServerLocale() 27 return this.serverService.getServerLocale()
diff --git a/client/src/app/shared/shared-main/video/index.ts b/client/src/app/shared/shared-main/video/index.ts
index a2e47883e..07d40b117 100644
--- a/client/src/app/shared/shared-main/video/index.ts
+++ b/client/src/app/shared/shared-main/video/index.ts
@@ -5,6 +5,7 @@ export * from './video-edit.model'
5export * from './video-file-token.service' 5export * from './video-file-token.service'
6export * from './video-import.service' 6export * from './video-import.service'
7export * from './video-ownership.service' 7export * from './video-ownership.service'
8export * from './video-password.service'
8export * from './video.model' 9export * from './video.model'
9export * from './video.resolver' 10export * from './video.resolver'
10export * from './video.service' 11export * from './video.service'
diff --git a/client/src/app/shared/shared-main/video/video-edit.model.ts b/client/src/app/shared/shared-main/video/video-edit.model.ts
index 47eee80d8..1b8b67ee2 100644
--- a/client/src/app/shared/shared-main/video/video-edit.model.ts
+++ b/client/src/app/shared/shared-main/video/video-edit.model.ts
@@ -1,5 +1,5 @@
1import { getAbsoluteAPIUrl } from '@app/helpers' 1import { getAbsoluteAPIUrl } from '@app/helpers'
2import { VideoPrivacy, VideoScheduleUpdate, VideoUpdate } from '@shared/models' 2import { VideoPassword, VideoPrivacy, VideoScheduleUpdate, VideoUpdate } from '@shared/models'
3import { VideoDetails } from './video-details.model' 3import { VideoDetails } from './video-details.model'
4import { objectKeysTyped } from '@shared/core-utils' 4import { objectKeysTyped } from '@shared/core-utils'
5 5
@@ -18,6 +18,7 @@ export class VideoEdit implements VideoUpdate {
18 waitTranscoding: boolean 18 waitTranscoding: boolean
19 channelId: number 19 channelId: number
20 privacy: VideoPrivacy 20 privacy: VideoPrivacy
21 videoPassword?: string
21 support: string 22 support: string
22 thumbnailfile?: any 23 thumbnailfile?: any
23 previewfile?: any 24 previewfile?: any
@@ -32,7 +33,7 @@ export class VideoEdit implements VideoUpdate {
32 33
33 pluginData?: any 34 pluginData?: any
34 35
35 constructor (video?: VideoDetails) { 36 constructor (video?: VideoDetails, videoPassword?: VideoPassword) {
36 if (!video) return 37 if (!video) return
37 38
38 this.id = video.id 39 this.id = video.id
@@ -63,6 +64,8 @@ export class VideoEdit implements VideoUpdate {
63 : null 64 : null
64 65
65 this.pluginData = video.pluginData 66 this.pluginData = video.pluginData
67
68 if (videoPassword) this.videoPassword = videoPassword.password
66 } 69 }
67 70
68 patch (values: { [ id: string ]: any }) { 71 patch (values: { [ id: string ]: any }) {
@@ -112,6 +115,7 @@ export class VideoEdit implements VideoUpdate {
112 waitTranscoding: this.waitTranscoding, 115 waitTranscoding: this.waitTranscoding,
113 channelId: this.channelId, 116 channelId: this.channelId,
114 privacy: this.privacy, 117 privacy: this.privacy,
118 videoPassword: this.videoPassword,
115 originallyPublishedAt: this.originallyPublishedAt 119 originallyPublishedAt: this.originallyPublishedAt
116 } 120 }
117 121
diff --git a/client/src/app/shared/shared-main/video/video-file-token.service.ts b/client/src/app/shared/shared-main/video/video-file-token.service.ts
index 791607249..9bca5b9ec 100644
--- a/client/src/app/shared/shared-main/video/video-file-token.service.ts
+++ b/client/src/app/shared/shared-main/video/video-file-token.service.ts
@@ -4,6 +4,7 @@ import { Injectable } from '@angular/core'
4import { RestExtractor } from '@app/core' 4import { RestExtractor } from '@app/core'
5import { VideoToken } from '@shared/models' 5import { VideoToken } from '@shared/models'
6import { VideoService } from './video.service' 6import { VideoService } from './video.service'
7import { VideoPasswordService } from './video-password.service'
7 8
8@Injectable() 9@Injectable()
9export class VideoFileTokenService { 10export class VideoFileTokenService {
@@ -15,16 +16,18 @@ export class VideoFileTokenService {
15 private restExtractor: RestExtractor 16 private restExtractor: RestExtractor
16 ) {} 17 ) {}
17 18
18 getVideoFileToken (videoUUID: string) { 19 getVideoFileToken ({ videoUUID, videoPassword }: { videoUUID: string, videoPassword?: string }) {
19 const existing = this.store.get(videoUUID) 20 const existing = this.store.get(videoUUID)
20 if (existing) return of(existing) 21 if (existing) return of(existing)
21 22
22 return this.createVideoFileToken(videoUUID) 23 return this.createVideoFileToken(videoUUID, videoPassword)
23 .pipe(tap(result => this.store.set(videoUUID, { token: result.token, expires: new Date(result.expires) }))) 24 .pipe(tap(result => this.store.set(videoUUID, { token: result.token, expires: new Date(result.expires) })))
24 } 25 }
25 26
26 private createVideoFileToken (videoUUID: string) { 27 private createVideoFileToken (videoUUID: string, videoPassword?: string) {
27 return this.authHttp.post<VideoToken>(`${VideoService.BASE_VIDEO_URL}/${videoUUID}/token`, {}) 28 const headers = VideoPasswordService.buildVideoPasswordHeader(videoPassword)
29
30 return this.authHttp.post<VideoToken>(`${VideoService.BASE_VIDEO_URL}/${videoUUID}/token`, {}, { headers })
28 .pipe( 31 .pipe(
29 map(({ files }) => files), 32 map(({ files }) => files),
30 catchError(err => this.restExtractor.handleError(err)) 33 catchError(err => this.restExtractor.handleError(err))
diff --git a/client/src/app/shared/shared-main/video/video-password.service.ts b/client/src/app/shared/shared-main/video/video-password.service.ts
new file mode 100644
index 000000000..d5b0406f8
--- /dev/null
+++ b/client/src/app/shared/shared-main/video/video-password.service.ts
@@ -0,0 +1,29 @@
1import { ResultList, VideoPassword } from '@shared/models'
2import { Injectable } from '@angular/core'
3import { catchError, switchMap } from 'rxjs'
4import { HttpClient, HttpHeaders } from '@angular/common/http'
5import { RestExtractor } from '@app/core'
6import { VideoService } from './video.service'
7
8@Injectable()
9export class VideoPasswordService {
10
11 constructor (
12 private authHttp: HttpClient,
13 private restExtractor: RestExtractor
14 ) {}
15
16 static buildVideoPasswordHeader (videoPassword: string) {
17 return videoPassword
18 ? new HttpHeaders().set('x-peertube-video-password', videoPassword)
19 : undefined
20 }
21
22 getVideoPasswords (options: { videoUUID: string }) {
23 return this.authHttp.get<ResultList<VideoPassword>>(`${VideoService.BASE_VIDEO_URL}/${options.videoUUID}/passwords`)
24 .pipe(
25 switchMap(res => res.data),
26 catchError(err => this.restExtractor.handleError(err))
27 )
28 }
29}
diff --git a/client/src/app/shared/shared-main/video/video.model.ts b/client/src/app/shared/shared-main/video/video.model.ts
index 6fdffb394..24c00c3d5 100644
--- a/client/src/app/shared/shared-main/video/video.model.ts
+++ b/client/src/app/shared/shared-main/video/video.model.ts
@@ -281,6 +281,13 @@ export class Video implements VideoServerModel {
281 return user && this.isLocal === false && user.hasRight(UserRight.MANAGE_VIDEOS_REDUNDANCIES) 281 return user && this.isLocal === false && user.hasRight(UserRight.MANAGE_VIDEOS_REDUNDANCIES)
282 } 282 }
283 283
284 canAccessPasswordProtectedVideoWithoutPassword (user: AuthUser) {
285 return this.privacy.id === VideoPrivacy.PASSWORD_PROTECTED &&
286 user &&
287 this.isLocal === true &&
288 (this.account.name === user.username || user.hasRight(UserRight.SEE_ALL_VIDEOS))
289 }
290
284 getExactNumberOfViews () { 291 getExactNumberOfViews () {
285 if (this.isLive) { 292 if (this.isLive) {
286 return Video.viewersICU({ viewers: this.viewers }, $localize`${this.viewers} viewer(s)`) 293 return Video.viewersICU({ viewers: this.viewers }, $localize`${this.viewers} viewer(s)`)
diff --git a/client/src/app/shared/shared-main/video/video.service.ts b/client/src/app/shared/shared-main/video/video.service.ts
index 78a49567f..d67a2e192 100644
--- a/client/src/app/shared/shared-main/video/video.service.ts
+++ b/client/src/app/shared/shared-main/video/video.service.ts
@@ -33,6 +33,7 @@ import { VideoChannel, VideoChannelService } from '../video-channel'
33import { VideoDetails } from './video-details.model' 33import { VideoDetails } from './video-details.model'
34import { VideoEdit } from './video-edit.model' 34import { VideoEdit } from './video-edit.model'
35import { Video } from './video.model' 35import { Video } from './video.model'
36import { VideoPasswordService } from './video-password.service'
36 37
37export type CommonVideoParams = { 38export type CommonVideoParams = {
38 videoPagination?: ComponentPaginationLight 39 videoPagination?: ComponentPaginationLight
@@ -69,16 +70,17 @@ export class VideoService {
69 return `${VideoService.BASE_VIDEO_URL}/${uuid}/views` 70 return `${VideoService.BASE_VIDEO_URL}/${uuid}/views`
70 } 71 }
71 72
72 getVideo (options: { videoId: string }): Observable<VideoDetails> { 73 getVideo (options: { videoId: string, videoPassword?: string }): Observable<VideoDetails> {
73 return this.serverService.getServerLocale() 74 const headers = VideoPasswordService.buildVideoPasswordHeader(options.videoPassword)
74 .pipe( 75
75 switchMap(translations => { 76 return this.serverService.getServerLocale().pipe(
76 return this.authHttp.get<VideoDetailsServerModel>(`${VideoService.BASE_VIDEO_URL}/${options.videoId}`) 77 switchMap(translations => {
77 .pipe(map(videoHash => ({ videoHash, translations }))) 78 return this.authHttp.get<VideoDetailsServerModel>(`${VideoService.BASE_VIDEO_URL}/${options.videoId}`, { headers })
78 }), 79 .pipe(map(videoHash => ({ videoHash, translations })))
79 map(({ videoHash, translations }) => new VideoDetails(videoHash, translations)), 80 }),
80 catchError(err => this.restExtractor.handleError(err)) 81 map(({ videoHash, translations }) => new VideoDetails(videoHash, translations)),
81 ) 82 catchError(err => this.restExtractor.handleError(err))
83 )
82 } 84 }
83 85
84 updateVideo (video: VideoEdit) { 86 updateVideo (video: VideoEdit) {
@@ -99,6 +101,9 @@ export class VideoService {
99 description, 101 description,
100 channelId: video.channelId, 102 channelId: video.channelId,
101 privacy: video.privacy, 103 privacy: video.privacy,
104 videoPasswords: video.privacy === VideoPrivacy.PASSWORD_PROTECTED
105 ? [ video.videoPassword ]
106 : undefined,
102 tags: video.tags, 107 tags: video.tags,
103 nsfw: video.nsfw, 108 nsfw: video.nsfw,
104 waitTranscoding: video.waitTranscoding, 109 waitTranscoding: video.waitTranscoding,
@@ -353,16 +358,16 @@ export class VideoService {
353 ) 358 )
354 } 359 }
355 360
356 setVideoLike (id: string) { 361 setVideoLike (id: string, videoPassword: string) {
357 return this.setVideoRate(id, 'like') 362 return this.setVideoRate(id, 'like', videoPassword)
358 } 363 }
359 364
360 setVideoDislike (id: string) { 365 setVideoDislike (id: string, videoPassword: string) {
361 return this.setVideoRate(id, 'dislike') 366 return this.setVideoRate(id, 'dislike', videoPassword)
362 } 367 }
363 368
364 unsetVideoLike (id: string) { 369 unsetVideoLike (id: string, videoPassword: string) {
365 return this.setVideoRate(id, 'none') 370 return this.setVideoRate(id, 'none', videoPassword)
366 } 371 }
367 372
368 getUserVideoRating (id: string) { 373 getUserVideoRating (id: string) {
@@ -394,7 +399,8 @@ export class VideoService {
394 [VideoPrivacy.PRIVATE]: $localize`Only I can see this video`, 399 [VideoPrivacy.PRIVATE]: $localize`Only I can see this video`,
395 [VideoPrivacy.UNLISTED]: $localize`Only shareable via a private link`, 400 [VideoPrivacy.UNLISTED]: $localize`Only shareable via a private link`,
396 [VideoPrivacy.PUBLIC]: $localize`Anyone can see this video`, 401 [VideoPrivacy.PUBLIC]: $localize`Anyone can see this video`,
397 [VideoPrivacy.INTERNAL]: $localize`Only users of this instance can see this video` 402 [VideoPrivacy.INTERNAL]: $localize`Only users of this instance can see this video`,
403 [VideoPrivacy.PASSWORD_PROTECTED]: $localize`Only users with the appropriate password can see this video`
398 } 404 }
399 405
400 const videoPrivacies = serverPrivacies.map(p => { 406 const videoPrivacies = serverPrivacies.map(p => {
@@ -412,7 +418,13 @@ export class VideoService {
412 } 418 }
413 419
414 getHighestAvailablePrivacy (serverPrivacies: VideoConstant<VideoPrivacy>[]) { 420 getHighestAvailablePrivacy (serverPrivacies: VideoConstant<VideoPrivacy>[]) {
415 const order = [ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL, VideoPrivacy.UNLISTED, VideoPrivacy.PUBLIC ] 421 // We do not add a password as this requires additional configuration.
422 const order = [
423 VideoPrivacy.PRIVATE,
424 VideoPrivacy.INTERNAL,
425 VideoPrivacy.UNLISTED,
426 VideoPrivacy.PUBLIC
427 ]
416 428
417 for (const privacy of order) { 429 for (const privacy of order) {
418 if (serverPrivacies.find(p => p.id === privacy)) { 430 if (serverPrivacies.find(p => p.id === privacy)) {
@@ -499,14 +511,15 @@ export class VideoService {
499 } 511 }
500 } 512 }
501 513
502 private setVideoRate (id: string, rateType: UserVideoRateType) { 514 private setVideoRate (id: string, rateType: UserVideoRateType, videoPassword?: string) {
503 const url = `${VideoService.BASE_VIDEO_URL}/${id}/rate` 515 const url = `${VideoService.BASE_VIDEO_URL}/${id}/rate`
504 const body: UserVideoRateUpdate = { 516 const body: UserVideoRateUpdate = {
505 rating: rateType 517 rating: rateType
506 } 518 }
519 const headers = VideoPasswordService.buildVideoPasswordHeader(videoPassword)
507 520
508 return this.authHttp 521 return this.authHttp
509 .put(url, body) 522 .put(url, body, { headers })
510 .pipe(catchError(err => this.restExtractor.handleError(err))) 523 .pipe(catchError(err => this.restExtractor.handleError(err)))
511 } 524 }
512} 525}