diff options
author | Wicklow <123956049+wickloww@users.noreply.github.com> | 2023-06-29 07:48:55 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-06-29 09:48:55 +0200 |
commit | 40346ead2b0b7afa475aef057d3673b6c7574b7a (patch) | |
tree | 24ffdc23c3a9d987334842e0d400b5bd44500cf7 /client/src/app/shared | |
parent | ae22c59f14d0d553f60b281948b6c232c2aca178 (diff) | |
download | PeerTube-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')
19 files changed, 148 insertions, 46 deletions
diff --git a/client/src/app/shared/form-validators/video-validators.ts b/client/src/app/shared/form-validators/video-validators.ts index a4bda8f16..090a76e43 100644 --- a/client/src/app/shared/form-validators/video-validators.ts +++ b/client/src/app/shared/form-validators/video-validators.ts | |||
@@ -26,6 +26,15 @@ export const VIDEO_PRIVACY_VALIDATOR: BuildFormValidator = { | |||
26 | } | 26 | } |
27 | } | 27 | } |
28 | 28 | ||
29 | export const VIDEO_PASSWORD_VALIDATOR: BuildFormValidator = { | ||
30 | VALIDATORS: [ Validators.minLength(2), Validators.maxLength(100) ], // Required is set dynamically | ||
31 | MESSAGES: { | ||
32 | minLength: $localize`A password should be at least 2 characters long.`, | ||
33 | maxLength: $localize`A password should be shorter than 100 characters long.`, | ||
34 | required: $localize`A password is required for password protected video.` | ||
35 | } | ||
36 | } | ||
37 | |||
29 | export const VIDEO_CATEGORY_VALIDATOR: BuildFormValidator = { | 38 | export const VIDEO_CATEGORY_VALIDATOR: BuildFormValidator = { |
30 | VALIDATORS: [ ], | 39 | VALIDATORS: [ ], |
31 | MESSAGES: {} | 40 | MESSAGES: {} |
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' | |||
4 | import { Injectable } from '@angular/core' | 4 | import { Injectable } from '@angular/core' |
5 | import { RestExtractor, ServerService } from '@app/core' | 5 | import { RestExtractor, ServerService } from '@app/core' |
6 | import { objectToFormData, sortBy } from '@app/helpers' | 6 | import { objectToFormData, sortBy } from '@app/helpers' |
7 | import { VideoService } from '@app/shared/shared-main/video' | 7 | import { VideoPasswordService, VideoService } from '@app/shared/shared-main/video' |
8 | import { peertubeTranslate } from '@shared/core-utils/i18n' | 8 | import { peertubeTranslate } from '@shared/core-utils/i18n' |
9 | import { ResultList, VideoCaption } from '@shared/models' | 9 | import { ResultList, VideoCaption } from '@shared/models' |
10 | import { environment } from '../../../../environments/environment' | 10 | import { 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' | |||
5 | export * from './video-file-token.service' | 5 | export * from './video-file-token.service' |
6 | export * from './video-import.service' | 6 | export * from './video-import.service' |
7 | export * from './video-ownership.service' | 7 | export * from './video-ownership.service' |
8 | export * from './video-password.service' | ||
8 | export * from './video.model' | 9 | export * from './video.model' |
9 | export * from './video.resolver' | 10 | export * from './video.resolver' |
10 | export * from './video.service' | 11 | export * 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 @@ | |||
1 | import { getAbsoluteAPIUrl } from '@app/helpers' | 1 | import { getAbsoluteAPIUrl } from '@app/helpers' |
2 | import { VideoPrivacy, VideoScheduleUpdate, VideoUpdate } from '@shared/models' | 2 | import { VideoPassword, VideoPrivacy, VideoScheduleUpdate, VideoUpdate } from '@shared/models' |
3 | import { VideoDetails } from './video-details.model' | 3 | import { VideoDetails } from './video-details.model' |
4 | import { objectKeysTyped } from '@shared/core-utils' | 4 | import { 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' | |||
4 | import { RestExtractor } from '@app/core' | 4 | import { RestExtractor } from '@app/core' |
5 | import { VideoToken } from '@shared/models' | 5 | import { VideoToken } from '@shared/models' |
6 | import { VideoService } from './video.service' | 6 | import { VideoService } from './video.service' |
7 | import { VideoPasswordService } from './video-password.service' | ||
7 | 8 | ||
8 | @Injectable() | 9 | @Injectable() |
9 | export class VideoFileTokenService { | 10 | export 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 @@ | |||
1 | import { ResultList, VideoPassword } from '@shared/models' | ||
2 | import { Injectable } from '@angular/core' | ||
3 | import { catchError, switchMap } from 'rxjs' | ||
4 | import { HttpClient, HttpHeaders } from '@angular/common/http' | ||
5 | import { RestExtractor } from '@app/core' | ||
6 | import { VideoService } from './video.service' | ||
7 | |||
8 | @Injectable() | ||
9 | export 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' | |||
33 | import { VideoDetails } from './video-details.model' | 33 | import { VideoDetails } from './video-details.model' |
34 | import { VideoEdit } from './video-edit.model' | 34 | import { VideoEdit } from './video-edit.model' |
35 | import { Video } from './video.model' | 35 | import { Video } from './video.model' |
36 | import { VideoPasswordService } from './video-password.service' | ||
36 | 37 | ||
37 | export type CommonVideoParams = { | 38 | export 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 | } |
diff --git a/client/src/app/shared/shared-share-modal/video-share.component.html b/client/src/app/shared/shared-share-modal/video-share.component.html index 5650fa948..9f1455561 100644 --- a/client/src/app/shared/shared-share-modal/video-share.component.html +++ b/client/src/app/shared/shared-share-modal/video-share.component.html | |||
@@ -107,6 +107,10 @@ | |||
107 | </a> | 107 | </a> |
108 | </div> | 108 | </div> |
109 | 109 | ||
110 | <div i18n *ngIf="isPasswordProtectedVideo()" class="alert-private alert alert-warning"> | ||
111 | This video is password protected, please note that recipients will require the corresponding password to access the content. | ||
112 | </div> | ||
113 | |||
110 | <div ngbNav #nav="ngbNav" class="nav-tabs" [(activeId)]="activeVideoId"> | 114 | <div ngbNav #nav="ngbNav" class="nav-tabs" [(activeId)]="activeVideoId"> |
111 | 115 | ||
112 | <ng-container ngbNavItem="url"> | 116 | <ng-container ngbNavItem="url"> |
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 { | |||
243 | return this.playlist.privacy.id === VideoPlaylistPrivacy.PRIVATE | 243 | return this.playlist.privacy.id === VideoPlaylistPrivacy.PRIVATE |
244 | } | 244 | } |
245 | 245 | ||
246 | isPasswordProtectedVideo () { | ||
247 | return this.video.privacy.id === VideoPrivacy.PASSWORD_PROTECTED | ||
248 | } | ||
249 | |||
246 | private getPlaylistOptions (baseUrl?: string) { | 250 | private getPlaylistOptions (baseUrl?: string) { |
247 | return { | 251 | return { |
248 | baseUrl, | 252 | 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 { | |||
18 | import { environment } from '../../../environments/environment' | 18 | import { environment } from '../../../environments/environment' |
19 | import { VideoCommentThreadTree } from './video-comment-thread-tree.model' | 19 | import { VideoCommentThreadTree } from './video-comment-thread-tree.model' |
20 | import { VideoComment } from './video-comment.model' | 20 | import { VideoComment } from './video-comment.model' |
21 | import { VideoPasswordService } from '../shared-main' | ||
21 | 22 | ||
22 | @Injectable() | 23 | @Injectable() |
23 | export class VideoCommentService { | 24 | export class VideoCommentService { |
@@ -31,22 +32,25 @@ export class VideoCommentService { | |||
31 | private restService: RestService | 32 | private restService: RestService |
32 | ) {} | 33 | ) {} |
33 | 34 | ||
34 | addCommentThread (videoId: string, comment: VideoCommentCreate) { | 35 | addCommentThread (videoId: string, comment: VideoCommentCreate, videoPassword?: string) { |
36 | const headers = VideoPasswordService.buildVideoPasswordHeader(videoPassword) | ||
35 | const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comment-threads' | 37 | const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comment-threads' |
36 | const normalizedComment = objectLineFeedToHtml(comment, 'text') | 38 | const normalizedComment = objectLineFeedToHtml(comment, 'text') |
37 | 39 | ||
38 | return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment) | 40 | return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment, { headers }) |
39 | .pipe( | 41 | .pipe( |
40 | map(data => this.extractVideoComment(data.comment)), | 42 | map(data => this.extractVideoComment(data.comment)), |
41 | catchError(err => this.restExtractor.handleError(err)) | 43 | catchError(err => this.restExtractor.handleError(err)) |
42 | ) | 44 | ) |
43 | } | 45 | } |
44 | 46 | ||
45 | addCommentReply (videoId: string, inReplyToCommentId: number, comment: VideoCommentCreate) { | 47 | addCommentReply (options: { videoId: string, inReplyToCommentId: number, comment: VideoCommentCreate, videoPassword?: string }) { |
48 | const { videoId, inReplyToCommentId, comment, videoPassword } = options | ||
49 | const headers = VideoPasswordService.buildVideoPasswordHeader(videoPassword) | ||
46 | const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comments/' + inReplyToCommentId | 50 | const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comments/' + inReplyToCommentId |
47 | const normalizedComment = objectLineFeedToHtml(comment, 'text') | 51 | const normalizedComment = objectLineFeedToHtml(comment, 'text') |
48 | 52 | ||
49 | return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment) | 53 | return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment, { headers }) |
50 | .pipe( | 54 | .pipe( |
51 | map(data => this.extractVideoComment(data.comment)), | 55 | map(data => this.extractVideoComment(data.comment)), |
52 | catchError(err => this.restExtractor.handleError(err)) | 56 | catchError(err => this.restExtractor.handleError(err)) |
@@ -76,10 +80,13 @@ export class VideoCommentService { | |||
76 | 80 | ||
77 | getVideoCommentThreads (parameters: { | 81 | getVideoCommentThreads (parameters: { |
78 | videoId: string | 82 | videoId: string |
83 | videoPassword: string | ||
79 | componentPagination: ComponentPaginationLight | 84 | componentPagination: ComponentPaginationLight |
80 | sort: string | 85 | sort: string |
81 | }): Observable<ThreadsResultList<VideoComment>> { | 86 | }): Observable<ThreadsResultList<VideoComment>> { |
82 | const { videoId, componentPagination, sort } = parameters | 87 | const { videoId, videoPassword, componentPagination, sort } = parameters |
88 | |||
89 | const headers = VideoPasswordService.buildVideoPasswordHeader(videoPassword) | ||
83 | 90 | ||
84 | const pagination = this.restService.componentToRestPagination(componentPagination) | 91 | const pagination = this.restService.componentToRestPagination(componentPagination) |
85 | 92 | ||
@@ -87,7 +94,7 @@ export class VideoCommentService { | |||
87 | params = this.restService.addRestGetParams(params, pagination, sort) | 94 | params = this.restService.addRestGetParams(params, pagination, sort) |
88 | 95 | ||
89 | const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comment-threads' | 96 | const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comment-threads' |
90 | return this.authHttp.get<ThreadsResultList<VideoComment>>(url, { params }) | 97 | return this.authHttp.get<ThreadsResultList<VideoComment>>(url, { params, headers }) |
91 | .pipe( | 98 | .pipe( |
92 | map(result => this.extractVideoComments(result)), | 99 | map(result => this.extractVideoComments(result)), |
93 | catchError(err => this.restExtractor.handleError(err)) | 100 | catchError(err => this.restExtractor.handleError(err)) |
@@ -97,12 +104,14 @@ export class VideoCommentService { | |||
97 | getVideoThreadComments (parameters: { | 104 | getVideoThreadComments (parameters: { |
98 | videoId: string | 105 | videoId: string |
99 | threadId: number | 106 | threadId: number |
107 | videoPassword?: string | ||
100 | }): Observable<VideoCommentThreadTree> { | 108 | }): Observable<VideoCommentThreadTree> { |
101 | const { videoId, threadId } = parameters | 109 | const { videoId, threadId, videoPassword } = parameters |
102 | const url = `${VideoCommentService.BASE_VIDEO_URL + videoId}/comment-threads/${threadId}` | 110 | const url = `${VideoCommentService.BASE_VIDEO_URL + videoId}/comment-threads/${threadId}` |
111 | const headers = VideoPasswordService.buildVideoPasswordHeader(videoPassword) | ||
103 | 112 | ||
104 | return this.authHttp | 113 | return this.authHttp |
105 | .get<VideoCommentThreadTreeServerModel>(url) | 114 | .get<VideoCommentThreadTreeServerModel>(url, { headers }) |
106 | .pipe( | 115 | .pipe( |
107 | map(tree => this.extractVideoCommentTree(tree)), | 116 | map(tree => this.extractVideoCommentTree(tree)), |
108 | catchError(err => this.restExtractor.handleError(err)) | 117 | 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 @@ | |||
1 | import { mapValues } from 'lodash-es' | 1 | import { mapValues } from 'lodash-es' |
2 | import { firstValueFrom } from 'rxjs' | 2 | import { firstValueFrom } from 'rxjs' |
3 | import { tap } from 'rxjs/operators' | 3 | import { tap } from 'rxjs/operators' |
4 | import { Component, ElementRef, Inject, LOCALE_ID, ViewChild } from '@angular/core' | 4 | import { Component, ElementRef, Inject, Input, LOCALE_ID, ViewChild } from '@angular/core' |
5 | import { HooksService } from '@app/core' | 5 | import { HooksService } from '@app/core' |
6 | import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' | 6 | import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' |
7 | import { logger } from '@root-helpers/logger' | 7 | import { logger } from '@root-helpers/logger' |
8 | import { videoRequiresAuth } from '@root-helpers/video' | 8 | import { videoRequiresFileToken } from '@root-helpers/video' |
9 | import { objectKeysTyped, pick } from '@shared/core-utils' | 9 | import { objectKeysTyped, pick } from '@shared/core-utils' |
10 | import { VideoCaption, VideoFile, VideoPrivacy } from '@shared/models' | 10 | import { VideoCaption, VideoFile } from '@shared/models' |
11 | import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoFileTokenService, VideoService } from '../shared-main' | 11 | import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoFileTokenService, VideoService } from '../shared-main' |
12 | 12 | ||
13 | type DownloadType = 'video' | 'subtitles' | 13 | type DownloadType = 'video' | 'subtitles' |
@@ -21,6 +21,8 @@ type FileMetadata = { [key: string]: { label: string, value: string | number } } | |||
21 | export class VideoDownloadComponent { | 21 | export class VideoDownloadComponent { |
22 | @ViewChild('modal', { static: true }) modal: ElementRef | 22 | @ViewChild('modal', { static: true }) modal: ElementRef |
23 | 23 | ||
24 | @Input() videoPassword: string | ||
25 | |||
24 | downloadType: 'direct' | 'torrent' = 'direct' | 26 | downloadType: 'direct' | 'torrent' = 'direct' |
25 | 27 | ||
26 | resolutionId: number | string = -1 | 28 | resolutionId: number | string = -1 |
@@ -89,8 +91,8 @@ export class VideoDownloadComponent { | |||
89 | this.subtitleLanguageId = this.videoCaptions[0].language.id | 91 | this.subtitleLanguageId = this.videoCaptions[0].language.id |
90 | } | 92 | } |
91 | 93 | ||
92 | if (videoRequiresAuth(this.video)) { | 94 | if (this.isConfidentialVideo()) { |
93 | this.videoFileTokenService.getVideoFileToken(this.video.uuid) | 95 | this.videoFileTokenService.getVideoFileToken({ videoUUID: this.video.uuid, videoPassword: this.videoPassword }) |
94 | .subscribe(({ token }) => this.videoFileToken = token) | 96 | .subscribe(({ token }) => this.videoFileToken = token) |
95 | } | 97 | } |
96 | 98 | ||
@@ -201,7 +203,8 @@ export class VideoDownloadComponent { | |||
201 | } | 203 | } |
202 | 204 | ||
203 | isConfidentialVideo () { | 205 | isConfidentialVideo () { |
204 | return this.video.privacy.id === VideoPrivacy.PRIVATE || this.video.privacy.id === VideoPrivacy.INTERNAL | 206 | return videoRequiresFileToken(this.video) |
207 | |||
205 | } | 208 | } |
206 | 209 | ||
207 | switchToType (type: DownloadType) { | 210 | 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 @@ | |||
125 | <my-peertube-checkbox | 125 | <my-peertube-checkbox |
126 | formControlName="allVideos" | 126 | formControlName="allVideos" |
127 | inputName="allVideos" | 127 | inputName="allVideos" |
128 | i18n-labelText labelText="Display all videos (private, unlisted or not yet published)" | 128 | i18n-labelText labelText="Display all videos (private, unlisted, password protected or not yet published)" |
129 | ></my-peertube-checkbox> | 129 | ></my-peertube-checkbox> |
130 | </div> | 130 | </div> |
131 | </div> | 131 | </div> |
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 @@ | |||
5 | > | 5 | > |
6 | <ng-container ngProjectAs="label-warning" *ngIf="displayOptions.privacyLabel && isUnlistedVideo()" i18n>Unlisted</ng-container> | 6 | <ng-container ngProjectAs="label-warning" *ngIf="displayOptions.privacyLabel && isUnlistedVideo()" i18n>Unlisted</ng-container> |
7 | <ng-container ngProjectAs="label-danger" *ngIf="displayOptions.privacyLabel && isPrivateVideo()" i18n>Private</ng-container> | 7 | <ng-container ngProjectAs="label-danger" *ngIf="displayOptions.privacyLabel && isPrivateVideo()" i18n>Private</ng-container> |
8 | <ng-container ngProjectAs="label-danger" *ngIf="displayOptions.privacyLabel && isPasswordProtectedVideo()" i18n>Password protected</ng-container> | ||
8 | </my-video-thumbnail> | 9 | </my-video-thumbnail> |
9 | 10 | ||
10 | <div class="video-bottom"> | 11 | <div class="video-bottom"> |
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 { | |||
171 | return this.video.privacy.id === VideoPrivacy.PRIVATE | 171 | return this.video.privacy.id === VideoPrivacy.PRIVATE |
172 | } | 172 | } |
173 | 173 | ||
174 | isPasswordProtectedVideo () { | ||
175 | return this.video.privacy.id === VideoPrivacy.PASSWORD_PROTECTED | ||
176 | } | ||
177 | |||
174 | getStateLabel (video: Video) { | 178 | getStateLabel (video: Video) { |
175 | if (!video.state) return '' | 179 | if (!video.state) return '' |
176 | 180 | ||
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 { | |||
241 | } | 241 | } |
242 | 242 | ||
243 | reloadVideos () { | 243 | reloadVideos () { |
244 | console.log('reload') | ||
244 | this.pagination.currentPage = 1 | 245 | this.pagination.currentPage = 1 |
245 | this.loadMoreVideos(true) | 246 | this.loadMoreVideos(true) |
246 | } | 247 | } |
@@ -420,7 +421,7 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { | |||
420 | 421 | ||
421 | if (reset) this.videos = [] | 422 | if (reset) this.videos = [] |
422 | this.videos = this.videos.concat(data) | 423 | this.videos = this.videos.concat(data) |
423 | 424 | console.log('subscribe') | |
424 | if (this.groupByDate) this.buildGroupedDateLabels() | 425 | if (this.groupByDate) this.buildGroupedDateLabels() |
425 | 426 | ||
426 | this.onDataSubject.next(data) | 427 | 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 @@ | |||
21 | [attr.title]="playlistElement.video.name" | 21 | [attr.title]="playlistElement.video.name" |
22 | >{{ playlistElement.video.name }}</a> | 22 | >{{ playlistElement.video.name }}</a> |
23 | 23 | ||
24 | <span *ngIf="isVideoPrivate()" class="pt-badge badge-yellow">Private</span> | 24 | <span i18n *ngIf="isVideoPrivate()" class="pt-badge badge-yellow">Private</span> |
25 | <span i18n *ngIf="isVideoPasswordProtected()" class="pt-badge badge-yellow">Password protected</span> | ||
25 | </div> | 26 | </div> |
26 | 27 | ||
27 | <span class="video-miniature-created-at-views"> | 28 | <span class="video-miniature-created-at-views"> |
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 { | |||
60 | return this.playlistElement.video.privacy.id === VideoPrivacy.PRIVATE | 60 | return this.playlistElement.video.privacy.id === VideoPrivacy.PRIVATE |
61 | } | 61 | } |
62 | 62 | ||
63 | isVideoPasswordProtected () { | ||
64 | return this.playlistElement.video.privacy.id === VideoPrivacy.PASSWORD_PROTECTED | ||
65 | } | ||
66 | |||
63 | isUnavailable (e: VideoPlaylistElement) { | 67 | isUnavailable (e: VideoPlaylistElement) { |
64 | return e.type === VideoPlaylistElementType.UNAVAILABLE | 68 | return e.type === VideoPlaylistElementType.UNAVAILABLE |
65 | } | 69 | } |