Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/password protected videos #5836

Merged
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
f33bd40
Add server endpoints
wickloww May 23, 2023
c1faecd
Refactoring test suites
wickloww May 23, 2023
07088c5
Update server and add openapi documentation
wickloww May 26, 2023
5b64aa3
Merge branch 'develop' into feature/password-protected-videos
wickloww May 26, 2023
e0e2c03
fix compliation and tests
wickloww May 30, 2023
bc3d3a4
upload/import password protected video on client
wickloww May 31, 2023
c387669
add server error code
wickloww Jun 1, 2023
f3da896
Add video password to update resolver
wickloww Jun 2, 2023
d10b505
add custom message when sharing pw protected video
wickloww Jun 2, 2023
21c0d0e
improve confirm component
wickloww Jun 5, 2023
ed49c64
Add new alert in component
wickloww Jun 5, 2023
9148624
Add ability to watch protected video on client
wickloww Jun 6, 2023
ea5ded5
Cannot have password protected replay privacy
wickloww Jun 6, 2023
de4f766
Add migration
wickloww Jun 6, 2023
9e3d1ed
Merge branch 'Chocobozzz:develop' into feature/password-protected-videos
wickloww Jun 6, 2023
86632f5
Add tests
wickloww Jun 7, 2023
6227215
update after review
wickloww Jun 8, 2023
83348fb
Update check params tests
wickloww Jun 14, 2023
f3c85bf
Add live videos test
wickloww Jun 14, 2023
9ea195c
Add more filter test
wickloww Jun 14, 2023
db78624
Update static file privacy test
wickloww Jun 15, 2023
516d5bb
Update object storage tests
wickloww Jun 15, 2023
30cea27
Add test on feeds
wickloww Jun 15, 2023
2926bee
Merge branch 'Chocobozzz:develop' into feature/password-protected-videos
wickloww Jun 15, 2023
d162343
Add missing word
wickloww Jun 15, 2023
d55d8c3
Fix tests
wickloww Jun 15, 2023
153f298
Fix tests on live videos
wickloww Jun 19, 2023
9016d9e
add embed support on password protected videos
wickloww Jun 20, 2023
bbd465d
fix style
wickloww Jun 20, 2023
f85e787
Correcting data leaks
wickloww Jun 21, 2023
c9541ba
Unable to add password protected privacy on replay
wickloww Jun 21, 2023
6f31f66
Updated code based on review comments
wickloww Jun 23, 2023
9b53650
Merge branch 'develop' into feature/password-protected-videos
wickloww Jun 23, 2023
5d015d9
fix validator and command
wickloww Jun 23, 2023
3ee9794
Updated code based on review comments
wickloww Jun 27, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,12 @@
</div>
</div>

<div *ngIf="schedulePublicationEnabled" class="form-group">
<div *ngIf="passwordProtectionSelected" class="form-group">
<label i18n for="videoPassword">Password</label>
<my-input-text formControlName="videoPassword" inputId="videoPassword" [withCopy]="true" [formError]="formErrors['videoPassword']"></my-input-text>
</div>

<div *ngIf="schedulePublicationSelected" class="form-group">
<label i18n for="schedulePublicationAt">Schedule publication ({{ calendarTimezone }})</label>
<p-calendar
id="schedulePublicationAt" formControlName="schedulePublicationAt" [dateFormat]="calendarDateFormat"
Expand Down Expand Up @@ -287,7 +292,7 @@
<div class="form-group mx-4" *ngIf="isSaveReplayEnabled()">
<label i18n for="replayPrivacy">Privacy of the new replay</label>
<my-select-options
labelForId="replayPrivacy" [items]="videoPrivacies" [clearable]="false" formControlName="replayPrivacy"
labelForId="replayPrivacy" [items]="replayPrivacies" [clearable]="false" formControlName="replayPrivacy"
></my-select-options>
</div>

Expand Down
26 changes: 21 additions & 5 deletions client/src/app/+videos/+video-edit/shared/video-edit.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
VIDEO_LICENCE_VALIDATOR,
VIDEO_NAME_VALIDATOR,
VIDEO_ORIGINALLY_PUBLISHED_AT_VALIDATOR,
VIDEO_PASSWORD_VALIDATOR,
VIDEO_PRIVACY_VALIDATOR,
VIDEO_SCHEDULE_PUBLICATION_AT_VALIDATOR,
VIDEO_SUPPORT_VALIDATOR,
Expand Down Expand Up @@ -79,7 +80,8 @@ export class VideoEditComponent implements OnInit, OnDestroy {
// So that it can be accessed in the template
readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY

videoPrivacies: VideoConstant<VideoPrivacy>[] = []
videoPrivacies: VideoConstant<VideoPrivacy | typeof VideoEdit.SPECIAL_SCHEDULED_PRIVACY > [] = []
replayPrivacies: VideoConstant<VideoPrivacy> [] = []
videoCategories: VideoConstant<number>[] = []
videoLicences: VideoConstant<number>[] = []
videoLanguages: VideoLanguages[] = []
Expand All @@ -103,7 +105,8 @@ export class VideoEditComponent implements OnInit, OnDestroy {

pluginDataFormGroup: FormGroup

schedulePublicationEnabled = false
schedulePublicationSelected = false
passwordProtectionSelected = false

calendarLocale: any = {}
minScheduledDate = new Date()
Expand Down Expand Up @@ -148,6 +151,7 @@ export class VideoEditComponent implements OnInit, OnDestroy {
const obj: { [ id: string ]: BuildFormValidator } = {
name: VIDEO_NAME_VALIDATOR,
privacy: VIDEO_PRIVACY_VALIDATOR,
videoPassword: VIDEO_PASSWORD_VALIDATOR,
channelId: VIDEO_CHANNEL_VALIDATOR,
nsfw: null,
commentsEnabled: null,
Expand Down Expand Up @@ -222,7 +226,9 @@ export class VideoEditComponent implements OnInit, OnDestroy {

this.serverService.getVideoPrivacies()
.subscribe(privacies => {
this.videoPrivacies = this.videoService.explainedPrivacyLabels(privacies).videoPrivacies
const videoPrivacies = this.videoService.explainedPrivacyLabels(privacies).videoPrivacies
this.videoPrivacies = videoPrivacies
this.replayPrivacies = videoPrivacies.filter((privacy) => privacy.id !== VideoPrivacy.PASSWORD_PROTECTED)

// Can't schedule publication if private privacy is not available (could be deleted by a plugin)
const hasPrivatePrivacy = this.videoPrivacies.some(p => p.id === VideoPrivacy.PRIVATE)
Expand Down Expand Up @@ -410,13 +416,13 @@ export class VideoEditComponent implements OnInit, OnDestroy {
.subscribe(
newPrivacyId => {

this.schedulePublicationEnabled = newPrivacyId === this.SPECIAL_SCHEDULED_PRIVACY
this.schedulePublicationSelected = newPrivacyId === this.SPECIAL_SCHEDULED_PRIVACY

// Value changed
const scheduleControl = this.form.get('schedulePublicationAt')
const waitTranscodingControl = this.form.get('waitTranscoding')

if (this.schedulePublicationEnabled) {
if (this.schedulePublicationSelected) {
scheduleControl.setValidators([ Validators.required ])

waitTranscodingControl.disable()
Expand All @@ -437,6 +443,16 @@ export class VideoEditComponent implements OnInit, OnDestroy {

this.firstPatchDone = true

this.passwordProtectionSelected = newPrivacyId === VideoPrivacy.PASSWORD_PROTECTED
const videoPasswordControl = this.form.get('videoPassword')

if (this.passwordProtectionSelected) {
videoPasswordControl.setValidators([ Validators.required ])
} else {
videoPasswordControl.clearValidators()
}
videoPasswordControl.updateValueAndValidity()

}
)
}
Expand Down
4 changes: 2 additions & 2 deletions client/src/app/+videos/+video-edit/video-update.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
this.buildForm({})

const { videoData } = this.route.snapshot.data
const { video, videoChannels, videoCaptions, videoSource, liveVideo } = videoData
const { video, videoChannels, videoCaptions, videoSource, liveVideo, videoPassword } = videoData

this.videoDetails = video
this.videoEdit = new VideoEdit(this.videoDetails)
this.videoEdit = new VideoEdit(this.videoDetails, videoPassword)

this.userVideoChannels = videoChannels
this.videoCaptions = videoCaptions
Expand Down
20 changes: 13 additions & 7 deletions client/src/app/+videos/+video-edit/video-update.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,30 @@ import { Injectable } from '@angular/core'
import { ActivatedRouteSnapshot } from '@angular/router'
import { AuthService } from '@app/core'
import { listUserChannelsForSelect } from '@app/helpers'
import { VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main'
import { VideoCaptionService, VideoDetails, VideoService, VideoPasswordService } from '@app/shared/shared-main'
import { LiveVideoService } from '@app/shared/shared-video-live'
import { VideoPrivacy } from '@shared/models/videos'

@Injectable()
export class VideoUpdateResolver {
constructor (
private videoService: VideoService,
private liveVideoService: LiveVideoService,
private authService: AuthService,
private videoCaptionService: VideoCaptionService
private videoCaptionService: VideoCaptionService,
private videoPasswordService: VideoPasswordService
) {
}

resolve (route: ActivatedRouteSnapshot) {
const uuid: string = route.params['uuid']

return this.videoService.getVideo({ videoId: uuid })
.pipe(
switchMap(video => forkJoin(this.buildVideoObservables(video))),
map(([ video, videoSource, videoChannels, videoCaptions, liveVideo ]) =>
({ video, videoChannels, videoCaptions, videoSource, liveVideo }))
)
.pipe(
switchMap(video => forkJoin(this.buildVideoObservables(video))),
map(([ video, videoSource, videoChannels, videoCaptions, liveVideo, videoPassword ]) =>
({ video, videoChannels, videoCaptions, videoSource, liveVideo, videoPassword }))
)
}

private buildVideoObservables (video: VideoDetails) {
Expand All @@ -46,6 +48,10 @@ export class VideoUpdateResolver {

video.isLive
? this.liveVideoService.getVideoLive(video.id)
: of(undefined),

video.privacy.id === VideoPrivacy.PASSWORD_PROTECTED
? this.videoPasswordService.getVideoPasswords({ videoUUID: video.uuid })
: of(undefined)
]
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<div class="video-actions-rates">
<div class="video-actions justify-content-end">
<my-video-rate
[video]="video" [isUserLoggedIn]="isUserLoggedIn"
[video]="video" [videoPassword]="videoPassword" [isUserLoggedIn]="isUserLoggedIn"
(rateUpdated)="onRateUpdated($event)" (userRatingLoaded)="onRateUpdated($event)"
></my-video-rate>

Expand Down Expand Up @@ -43,7 +43,7 @@
<span class="icon-text d-none d-sm-inline" i18n>DOWNLOAD</span>
</button>

<my-video-download #videoDownloadModal></my-video-download>
<my-video-download #videoDownloadModal [videoPassword]="videoPassword"></my-video-download>
</ng-container>

<ng-container *ngIf="isUserLoggedIn">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export class ActionButtonsComponent implements OnInit, OnChanges {
@ViewChild('videoDownloadModal') videoDownloadModal: VideoDownloadComponent

@Input() video: VideoDetails
@Input() videoPassword: string
@Input() videoCaptions: VideoCaption[]
@Input() playlist: VideoPlaylist

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { UserVideoRateType } from '@shared/models'
})
export class VideoRateComponent implements OnInit, OnChanges, OnDestroy {
@Input() video: VideoDetails
@Input() videoPassword: string
@Input() isUserLoggedIn: boolean

@Output() userRatingLoaded = new EventEmitter<UserVideoRateType>()
Expand Down Expand Up @@ -103,13 +104,13 @@ export class VideoRateComponent implements OnInit, OnChanges, OnDestroy {
}

private setRating (nextRating: UserVideoRateType) {
const ratingMethods: { [id in UserVideoRateType]: (id: string) => Observable<any> } = {
const ratingMethods: { [id in UserVideoRateType]: (id: string, videoPassword: string) => Observable<any> } = {
like: this.videoService.setVideoLike,
dislike: this.videoService.setVideoDislike,
none: this.videoService.unsetVideoLike
}

ratingMethods[nextRating].call(this.videoService, this.video.uuid)
ratingMethods[nextRating].call(this.videoService, this.video.uuid, this.videoPassword)
.subscribe({
next: () => {
// Update the video like attribute
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { VideoCommentCreate } from '@shared/models'
export class VideoCommentAddComponent extends FormReactive implements OnChanges, OnInit {
@Input() user: User
@Input() video: Video
@Input() videoPassword: string
@Input() parentComment?: VideoComment
@Input() parentComments?: VideoComment[]
@Input() focusOnInit = false
Expand Down Expand Up @@ -176,12 +177,17 @@ export class VideoCommentAddComponent extends FormReactive implements OnChanges,

private addCommentReply (commentCreate: VideoCommentCreate) {
return this.videoCommentService
.addCommentReply(this.video.uuid, this.parentComment.id, commentCreate)
.addCommentReply({
videoId: this.video.uuid,
inReplyToCommentId: this.parentComment.id,
comment: commentCreate,
videoPassword: this.videoPassword
})
}

private addCommentThread (commentCreate: VideoCommentCreate) {
return this.videoCommentService
.addCommentThread(this.video.uuid, commentCreate)
.addCommentThread(this.video.uuid, commentCreate, this.videoPassword)
}

private initTextValue () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
*ngIf="!comment.isDeleted && inReplyToCommentId === comment.id"
[user]="user"
[video]="video"
[videoPassword]="videoPassword"
[parentComment]="comment"
[parentComments]="newParentComments"
[focusOnInit]="true"
Expand All @@ -75,6 +76,7 @@
<my-video-comment
[comment]="commentChild.comment"
[video]="video"
[videoPassword]="videoPassword"
[inReplyToCommentId]="inReplyToCommentId"
[commentTree]="commentChild"
[parentComments]="newParentComments"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export class VideoCommentComponent implements OnInit, OnChanges {
@ViewChild('commentReportModal') commentReportModal: CommentReportComponent

@Input() video: Video
@Input() videoPassword: string
@Input() comment: VideoComment
@Input() parentComments: VideoComment[] = []
@Input() commentTree: VideoCommentThreadTree
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ <h2 class="title-page">
<ng-template [ngIf]="video.commentsEnabled === true">
<my-video-comment-add
[video]="video"
[videoPassword]="videoPassword"
[user]="user"
(commentCreated)="onCommentThreadCreated($event)"
[textValue]="commentThreadRedraftValue"
Expand All @@ -34,6 +35,7 @@ <h2 class="title-page">
*ngIf="highlightedThread"
[comment]="highlightedThread"
[video]="video"
[videoPassword]="videoPassword"
[inReplyToCommentId]="inReplyToCommentId"
[commentTree]="threadComments[highlightedThread.id]"
[highlightedComment]="true"
Expand All @@ -53,6 +55,7 @@ <h2 class="title-page">
*ngIf="!highlightedThread || comment.id !== highlightedThread.id"
[comment]="comment"
[video]="video"
[videoPassword]="videoPassword"
[inReplyToCommentId]="inReplyToCommentId"
[commentTree]="threadComments[comment.id]"
[firstInThread]="i + 1 !== comments.length"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { PeerTubeProblemDocument, ServerErrorCode } from '@shared/models'
export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
@ViewChild('commentHighlightBlock') commentHighlightBlock: ElementRef
@Input() video: VideoDetails
@Input() videoPassword: string
@Input() user: User

@Output() timestampClicked = new EventEmitter<number>()
Expand Down Expand Up @@ -80,7 +81,8 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {

const params = {
videoId: this.video.uuid,
threadId: commentId
threadId: commentId,
videoPassword: this.videoPassword
}

const obs = this.hooks.wrapObsFun(
Expand Down Expand Up @@ -119,6 +121,7 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
loadMoreThreads () {
const params = {
videoId: this.video.uuid,
videoPassword: this.videoPassword,
componentPagination: this.componentPagination,
sort: this.sort
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,7 @@
<div class="blocked-label" i18n>This video is blocked.</div>
{{ video.blacklistedReason }}
</div>

<div i18n class="alert alert-warning" *ngIf="video?.canAccessVideoWithoutPassword(user)">
This video is password protected.
</div>
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { Component, Input } from '@angular/core'
import { AuthUser } from '@app/core'
import { VideoDetails } from '@app/shared/shared-main'
import { VideoState } from '@shared/models'
import { VideoPrivacy, VideoState } from '@shared/models'

@Component({
selector: 'my-video-alert',
templateUrl: './video-alert.component.html',
styleUrls: [ './video-alert.component.scss' ]
})
export class VideoAlertComponent {
@Input() user: AuthUser
@Input() video: VideoDetails
@Input() noPlaylistVideoFound: boolean

Expand Down Expand Up @@ -46,4 +48,8 @@ export class VideoAlertComponent {
isLiveEnded () {
return this.video?.state.id === VideoState.LIVE_ENDED
}

isVideoPasswordProtected () {
return this.video?.privacy.id === VideoPrivacy.PASSWORD_PROTECTED
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<my-plugin-placeholder pluginId="player-next"></my-plugin-placeholder>
</div>

<my-video-alert [video]="video" [noPlaylistVideoFound]="noPlaylistVideoFound"></my-video-alert>
<my-video-alert [video]="video" [user]="user" [noPlaylistVideoFound]="noPlaylistVideoFound"></my-video-alert>

<!-- Video information -->
<div *ngIf="video" class="margin-content video-bottom">
Expand Down Expand Up @@ -51,8 +51,8 @@ <h1 class="video-info-name">{{ video.name }}</h1>
</div>

<my-action-buttons
[video]="video" [isUserLoggedIn]="isUserLoggedIn()" [videoCaptions]="videoCaptions" [playlist]="playlist"
[currentTime]="getCurrentTime()" [currentPlaylistPosition]="getCurrentPlaylistPosition()"
[video]="video" [videoPassword]="videoPassword" [isUserLoggedIn]="isUserLoggedIn()" [videoCaptions]="videoCaptions"
[playlist]="playlist" [currentTime]="getCurrentTime()" [currentPlaylistPosition]="getCurrentPlaylistPosition()"
></my-action-buttons>
</div>
</div>
Expand Down Expand Up @@ -92,6 +92,7 @@ <h1 class="video-info-name">{{ video.name }}</h1>
<my-video-comments
class="border-top"
[video]="video"
[videoPassword]="videoPassword"
[user]="user"
(timestampClicked)="handleTimestampClicked($event)"
></my-video-comments>
Expand Down
Loading