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/shared-main | |
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/shared-main')
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' | |||
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 | } |