From 3545e72c686ff1725bbdfd8d16d693e2f4aa75a3 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 12 Oct 2022 16:09:02 +0200 Subject: Put private videos under a specific subdirectory --- .../+videos/+video-watch/video-watch.component.ts | 53 +++++++++++++++------- client/src/app/core/auth/auth-user.model.ts | 18 ++++---- client/src/app/core/auth/auth.service.ts | 4 +- .../app/core/users/user-local-storage.service.ts | 14 +++--- client/src/app/helpers/utils/url.ts | 5 +- .../app/shared/shared-main/shared-main.module.ts | 11 ++++- client/src/app/shared/shared-main/video/index.ts | 1 + .../shared-main/video/video-file-token.service.ts | 33 ++++++++++++++ .../video-download.component.html | 5 +- .../video-download.component.ts | 19 ++++++-- 10 files changed, 117 insertions(+), 46 deletions(-) create mode 100644 client/src/app/shared/shared-main/video/video-file-token.service.ts (limited to 'client/src/app') diff --git a/client/src/app/+videos/+video-watch/video-watch.component.ts b/client/src/app/+videos/+video-watch/video-watch.component.ts index 9ae6f9f12..b3818c8de 100644 --- a/client/src/app/+videos/+video-watch/video-watch.component.ts +++ b/client/src/app/+videos/+video-watch/video-watch.component.ts @@ -20,12 +20,12 @@ import { } from '@app/core' import { HooksService } from '@app/core/plugins/hooks.service' import { isXPercentInViewport, scrollToTop } from '@app/helpers' -import { Video, VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main' +import { Video, VideoCaptionService, VideoDetails, VideoFileTokenService, VideoService } from '@app/shared/shared-main' import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' import { LiveVideoService } from '@app/shared/shared-video-live' import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' import { logger } from '@root-helpers/logger' -import { isP2PEnabled } from '@root-helpers/video' +import { isP2PEnabled, videoRequiresAuth } from '@root-helpers/video' import { timeToInt } from '@shared/core-utils' import { HTMLServerConfig, @@ -78,6 +78,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy { private nextVideoUUID = '' private nextVideoTitle = '' + private videoFileToken: string + private currentTime: number private paramsSub: Subscription @@ -110,6 +112,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { private pluginService: PluginService, private peertubeSocket: PeerTubeSocket, private screenService: ScreenService, + private videoFileTokenService: VideoFileTokenService, private location: PlatformLocation, @Inject(LOCALE_ID) private localeId: string ) { } @@ -252,12 +255,19 @@ export class VideoWatchComponent implements OnInit, OnDestroy { 'filter:api.video-watch.video.get.result' ) - const videoAndLiveObs: Observable<{ video: VideoDetails, live?: LiveVideo }> = videoObs.pipe( + const videoAndLiveObs: Observable<{ video: VideoDetails, live?: LiveVideo, videoFileToken?: string }> = videoObs.pipe( switchMap(video => { - if (!video.isLive) return of({ video }) + if (!video.isLive) return of({ video, live: undefined }) return this.liveVideoService.getVideoLive(video.uuid) .pipe(map(live => ({ live, video }))) + }), + + switchMap(({ video, live }) => { + if (!videoRequiresAuth(video)) return of({ video, live, videoFileToken: undefined }) + + return this.videoFileTokenService.getVideoFileToken(video.uuid) + .pipe(map(({ token }) => ({ video, live, videoFileToken: token }))) }) ) @@ -266,7 +276,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { this.videoCaptionService.listCaptions(videoId), this.userService.getAnonymousOrLoggedUser() ]).subscribe({ - next: ([ { video, live }, captionsResult, loggedInOrAnonymousUser ]) => { + next: ([ { video, live, videoFileToken }, captionsResult, loggedInOrAnonymousUser ]) => { const queryParams = this.route.snapshot.queryParams const urlOptions = { @@ -283,7 +293,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { peertubeLink: false } - this.onVideoFetched({ video, live, videoCaptions: captionsResult.data, loggedInOrAnonymousUser, urlOptions }) + this.onVideoFetched({ video, live, videoCaptions: captionsResult.data, videoFileToken, loggedInOrAnonymousUser, urlOptions }) .catch(err => this.handleGlobalError(err)) }, @@ -356,16 +366,19 @@ export class VideoWatchComponent implements OnInit, OnDestroy { video: VideoDetails live: LiveVideo videoCaptions: VideoCaption[] + videoFileToken: string + urlOptions: URLOptions loggedInOrAnonymousUser: User }) { - const { video, live, videoCaptions, urlOptions, loggedInOrAnonymousUser } = options + const { video, live, videoCaptions, urlOptions, videoFileToken, loggedInOrAnonymousUser } = options this.subscribeToLiveEventsIfNeeded(this.video, video) this.video = video this.videoCaptions = videoCaptions this.liveVideo = live + this.videoFileToken = videoFileToken // Re init attributes this.playerPlaceholderImgSrc = undefined @@ -414,6 +427,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { video: this.video, videoCaptions: this.videoCaptions, liveVideo: this.liveVideo, + videoFileToken: this.videoFileToken, urlOptions, loggedInOrAnonymousUser, user: this.user @@ -561,11 +575,15 @@ export class VideoWatchComponent implements OnInit, OnDestroy { video: VideoDetails liveVideo: LiveVideo videoCaptions: VideoCaption[] + + videoFileToken: string + urlOptions: CustomizationOptions & { playerMode: PlayerMode } + loggedInOrAnonymousUser: User user?: AuthUser // Keep for plugins }) { - const { video, liveVideo, videoCaptions, urlOptions, loggedInOrAnonymousUser } = params + const { video, liveVideo, videoCaptions, videoFileToken, urlOptions, loggedInOrAnonymousUser } = params const getStartTime = () => { const byUrl = urlOptions.startTime !== undefined @@ -623,13 +641,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy { theaterButton: true, captions: videoCaptions.length !== 0, - videoViewUrl: video.privacy.id !== VideoPrivacy.PRIVATE - ? this.videoService.getVideoViewUrl(video.uuid) - : null, - authorizationHeader: this.authService.getRequestHeaderValue(), - - metricsUrl: environment.apiUrl + '/api/v1/metrics/playback', - embedUrl: video.embedUrl, embedTitle: video.name, instanceName: this.serverConfig.instance.name, @@ -639,7 +650,17 @@ export class VideoWatchComponent implements OnInit, OnDestroy { language: this.localeId, - serverUrl: environment.apiUrl, + metricsUrl: environment.apiUrl + '/api/v1/metrics/playback', + + videoViewUrl: video.privacy.id !== VideoPrivacy.PRIVATE + ? this.videoService.getVideoViewUrl(video.uuid) + : null, + authorizationHeader: () => this.authService.getRequestHeaderValue(), + + serverUrl: environment.originServerUrl, + + videoFileToken: () => videoFileToken, + requiresAuth: videoRequiresAuth(video), videoCaptions: playerCaptions, diff --git a/client/src/app/core/auth/auth-user.model.ts b/client/src/app/core/auth/auth-user.model.ts index cd9665e37..a12325421 100644 --- a/client/src/app/core/auth/auth-user.model.ts +++ b/client/src/app/core/auth/auth-user.model.ts @@ -1,7 +1,7 @@ import { Observable, of } from 'rxjs' import { map } from 'rxjs/operators' import { User } from '@app/core/users/user.model' -import { UserTokens } from '@root-helpers/users' +import { OAuthUserTokens } from '@root-helpers/users' import { hasUserRight } from '@shared/core-utils/users' import { MyUser as ServerMyUserModel, @@ -13,33 +13,33 @@ import { } from '@shared/models' export class AuthUser extends User implements ServerMyUserModel { - tokens: UserTokens + oauthTokens: OAuthUserTokens specialPlaylists: MyUserSpecialPlaylist[] canSeeVideosLink = true - constructor (userHash: Partial, hashTokens: Partial) { + constructor (userHash: Partial, hashTokens: Partial) { super(userHash) - this.tokens = new UserTokens(hashTokens) + this.oauthTokens = new OAuthUserTokens(hashTokens) this.specialPlaylists = userHash.specialPlaylists } getAccessToken () { - return this.tokens.accessToken + return this.oauthTokens.accessToken } getRefreshToken () { - return this.tokens.refreshToken + return this.oauthTokens.refreshToken } getTokenType () { - return this.tokens.tokenType + return this.oauthTokens.tokenType } refreshTokens (accessToken: string, refreshToken: string) { - this.tokens.accessToken = accessToken - this.tokens.refreshToken = refreshToken + this.oauthTokens.accessToken = accessToken + this.oauthTokens.refreshToken = refreshToken } hasRight (right: UserRight) { diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts index 7f4fae4aa..4de28e51e 100644 --- a/client/src/app/core/auth/auth.service.ts +++ b/client/src/app/core/auth/auth.service.ts @@ -5,7 +5,7 @@ import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '@angular import { Injectable } from '@angular/core' import { Router } from '@angular/router' import { Notifier } from '@app/core/notification/notifier.service' -import { logger, objectToUrlEncoded, peertubeLocalStorage, UserTokens } from '@root-helpers/index' +import { logger, OAuthUserTokens, objectToUrlEncoded, peertubeLocalStorage } from '@root-helpers/index' import { HttpStatusCode, MyUser as UserServerModel, OAuthClientLocal, User, UserLogin, UserRefreshToken } from '@shared/models' import { environment } from '../../../environments/environment' import { RestExtractor } from '../rest/rest-extractor.service' @@ -74,7 +74,7 @@ export class AuthService { ] } - buildAuthUser (userInfo: Partial, tokens: UserTokens) { + buildAuthUser (userInfo: Partial, tokens: OAuthUserTokens) { this.user = new AuthUser(userInfo, tokens) } diff --git a/client/src/app/core/users/user-local-storage.service.ts b/client/src/app/core/users/user-local-storage.service.ts index fff649eef..f1588bdd2 100644 --- a/client/src/app/core/users/user-local-storage.service.ts +++ b/client/src/app/core/users/user-local-storage.service.ts @@ -4,7 +4,7 @@ import { Injectable } from '@angular/core' import { AuthService, AuthStatus } from '@app/core/auth' import { getBoolOrDefault } from '@root-helpers/local-storage-utils' import { logger } from '@root-helpers/logger' -import { UserLocalStorageKeys, UserTokens } from '@root-helpers/users' +import { UserLocalStorageKeys, OAuthUserTokens } from '@root-helpers/users' import { UserRole, UserUpdateMe } from '@shared/models' import { NSFWPolicyType } from '@shared/models/videos' import { ServerService } from '../server' @@ -24,7 +24,7 @@ export class UserLocalStorageService { this.setLoggedInUser(user) this.setUserInfo(user) - this.setTokens(user.tokens) + this.setTokens(user.oauthTokens) } }) @@ -43,7 +43,7 @@ export class UserLocalStorageService { next: () => { const user = this.authService.getUser() - this.setTokens(user.tokens) + this.setTokens(user.oauthTokens) } }) } @@ -174,14 +174,14 @@ export class UserLocalStorageService { // --------------------------------------------------------------------------- getTokens () { - return UserTokens.getUserTokens(this.localStorageService) + return OAuthUserTokens.getUserTokens(this.localStorageService) } - setTokens (tokens: UserTokens) { - UserTokens.saveToLocalStorage(this.localStorageService, tokens) + setTokens (tokens: OAuthUserTokens) { + OAuthUserTokens.saveToLocalStorage(this.localStorageService, tokens) } flushTokens () { - UserTokens.flushLocalStorage(this.localStorageService) + OAuthUserTokens.flushLocalStorage(this.localStorageService) } } diff --git a/client/src/app/helpers/utils/url.ts b/client/src/app/helpers/utils/url.ts index 08c27e3c1..9e7dc3e6f 100644 --- a/client/src/app/helpers/utils/url.ts +++ b/client/src/app/helpers/utils/url.ts @@ -54,8 +54,9 @@ function objectToFormData (obj: any, form?: FormData, namespace?: string) { } export { - objectToFormData, getAbsoluteAPIUrl, getAPIHost, - getAbsoluteEmbedUrl + getAbsoluteEmbedUrl, + + objectToFormData } 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 04b223cc5..c1523bc50 100644 --- a/client/src/app/shared/shared-main/shared-main.module.ts +++ b/client/src/app/shared/shared-main/shared-main.module.ts @@ -44,7 +44,15 @@ import { import { PluginPlaceholderComponent, PluginSelectorDirective } from './plugins' import { ActorRedirectGuard } from './router' import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users' -import { EmbedComponent, RedundancyService, VideoImportService, VideoOwnershipService, VideoResolver, VideoService } from './video' +import { + EmbedComponent, + RedundancyService, + VideoFileTokenService, + VideoImportService, + VideoOwnershipService, + VideoResolver, + VideoService +} from './video' import { VideoCaptionService } from './video-caption' import { VideoChannelService } from './video-channel' @@ -185,6 +193,7 @@ import { VideoChannelService } from './video-channel' VideoImportService, VideoOwnershipService, VideoService, + VideoFileTokenService, VideoResolver, VideoCaptionService, diff --git a/client/src/app/shared/shared-main/video/index.ts b/client/src/app/shared/shared-main/video/index.ts index 361601456..a2e47883e 100644 --- a/client/src/app/shared/shared-main/video/index.ts +++ b/client/src/app/shared/shared-main/video/index.ts @@ -2,6 +2,7 @@ export * from './embed.component' export * from './redundancy.service' export * from './video-details.model' export * from './video-edit.model' +export * from './video-file-token.service' export * from './video-import.service' export * from './video-ownership.service' export * from './video.model' 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 new file mode 100644 index 000000000..791607249 --- /dev/null +++ b/client/src/app/shared/shared-main/video/video-file-token.service.ts @@ -0,0 +1,33 @@ +import { catchError, map, of, tap } from 'rxjs' +import { HttpClient } from '@angular/common/http' +import { Injectable } from '@angular/core' +import { RestExtractor } from '@app/core' +import { VideoToken } from '@shared/models' +import { VideoService } from './video.service' + +@Injectable() +export class VideoFileTokenService { + + private readonly store = new Map() + + constructor ( + private authHttp: HttpClient, + private restExtractor: RestExtractor + ) {} + + getVideoFileToken (videoUUID: string) { + const existing = this.store.get(videoUUID) + if (existing) return of(existing) + + return this.createVideoFileToken(videoUUID) + .pipe(tap(result => this.store.set(videoUUID, { token: result.token, expires: new Date(result.expires) }))) + } + + private createVideoFileToken (videoUUID: string) { + return this.authHttp.post(`${VideoService.BASE_VIDEO_URL}/${videoUUID}/token`, {}) + .pipe( + map(({ files }) => files), + catchError(err => this.restExtractor.handleError(err)) + ) + } +} diff --git a/client/src/app/shared/shared-video-miniature/video-download.component.html b/client/src/app/shared/shared-video-miniature/video-download.component.html index 1c7458b4b..1f622933d 100644 --- a/client/src/app/shared/shared-video-miniature/video-download.component.html +++ b/client/src/app/shared/shared-video-miniature/video-download.component.html @@ -48,10 +48,7 @@ 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 47482caaa..667cb107f 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 @@ -2,11 +2,12 @@ import { mapValues, pick } from 'lodash-es' import { firstValueFrom } from 'rxjs' import { tap } from 'rxjs/operators' import { Component, ElementRef, Inject, LOCALE_ID, ViewChild } from '@angular/core' -import { AuthService, HooksService, Notifier } from '@app/core' +import { HooksService } from '@app/core' import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' import { logger } from '@root-helpers/logger' +import { videoRequiresAuth } from '@root-helpers/video' import { VideoCaption, VideoFile, VideoPrivacy } from '@shared/models' -import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoService } from '../shared-main' +import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoFileTokenService, VideoService } from '../shared-main' type DownloadType = 'video' | 'subtitles' type FileMetadata = { [key: string]: { label: string, value: string }} @@ -32,6 +33,8 @@ export class VideoDownloadComponent { type: DownloadType = 'video' + videoFileToken: string + private activeModal: NgbModalRef private bytesPipe: BytesPipe @@ -42,10 +45,9 @@ export class VideoDownloadComponent { constructor ( @Inject(LOCALE_ID) private localeId: string, - private notifier: Notifier, private modalService: NgbModal, private videoService: VideoService, - private auth: AuthService, + private videoFileTokenService: VideoFileTokenService, private hooks: HooksService ) { this.bytesPipe = new BytesPipe() @@ -71,6 +73,8 @@ export class VideoDownloadComponent { } show (video: VideoDetails, videoCaptions?: VideoCaption[]) { + this.videoFileToken = undefined + this.video = video this.videoCaptions = videoCaptions @@ -84,6 +88,11 @@ export class VideoDownloadComponent { this.subtitleLanguageId = this.videoCaptions[0].language.id } + if (videoRequiresAuth(this.video)) { + this.videoFileTokenService.getVideoFileToken(this.video.uuid) + .subscribe(({ token }) => this.videoFileToken = token) + } + this.activeModal.shown.subscribe(() => { this.hooks.runAction('action:modal.video-download.shown', 'common') }) @@ -155,7 +164,7 @@ export class VideoDownloadComponent { if (!file) return '' const suffix = this.isConfidentialVideo() - ? '?access_token=' + this.auth.getAccessToken() + ? '?videoFileToken=' + this.videoFileToken : '' switch (this.downloadType) { -- cgit v1.2.3