From afff310e50f2fa8419bb4242470cbde46ab54463 Mon Sep 17 00:00:00 2001 From: Rigel Kent Date: Thu, 13 Aug 2020 15:07:23 +0200 Subject: allow private syndication feeds via a user feedToken --- .../edit-custom-config.component.html | 2 +- .../my-account-abuses-list.component.ts | 1 - .../my-account-applications.component.html | 35 +++++++++++++ .../my-account-applications.component.scss | 28 +++++++++++ .../my-account-applications.component.ts | 57 ++++++++++++++++++++++ .../app/+my-account/my-account-routing.module.ts | 10 ++++ client/src/app/+my-account/my-account.component.ts | 5 ++ client/src/app/+my-account/my-account.module.ts | 2 + .../video-user-subscriptions.component.ts | 27 +++++++++- client/src/app/core/auth/auth.service.ts | 45 +++++++++++++++++ .../shared/shared-icons/global-icon.component.ts | 3 +- .../app/shared/shared-main/video/video.service.ts | 15 +++++- .../abstract-video-list.html | 22 +++++++-- .../shared-video-miniature/abstract-video-list.ts | 5 +- client/src/assets/images/feather/codesandbox.svg | 1 + .../src/assets/player/peertube-player-manager.ts | 3 +- client/src/assets/player/utils.ts | 13 ----- client/src/root-helpers/utils.ts | 13 +++++ client/src/sass/include/_mixins.scss | 2 +- 19 files changed, 263 insertions(+), 26 deletions(-) create mode 100644 client/src/app/+my-account/my-account-applications/my-account-applications.component.html create mode 100644 client/src/app/+my-account/my-account-applications/my-account-applications.component.scss create mode 100644 client/src/app/+my-account/my-account-applications/my-account-applications.component.ts create mode 100644 client/src/assets/images/feather/codesandbox.svg (limited to 'client/src') diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html index 09539fa92..e73a9f8a8 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html @@ -243,7 +243,7 @@
APPEARANCE
-
+
Use plugins & themes for more involved changes, or add slight customizations.
diff --git a/client/src/app/+my-account/my-account-abuses/my-account-abuses-list.component.ts b/client/src/app/+my-account/my-account-abuses/my-account-abuses-list.component.ts index e5dd723ff..9316fc0dd 100644 --- a/client/src/app/+my-account/my-account-abuses/my-account-abuses-list.component.ts +++ b/client/src/app/+my-account/my-account-abuses/my-account-abuses-list.component.ts @@ -1,4 +1,3 @@ - import { Component } from '@angular/core' @Component({ diff --git a/client/src/app/+my-account/my-account-applications/my-account-applications.component.html b/client/src/app/+my-account/my-account-applications/my-account-applications.component.html new file mode 100644 index 000000000..62e2cb59b --- /dev/null +++ b/client/src/app/+my-account/my-account-applications/my-account-applications.component.html @@ -0,0 +1,35 @@ +

+ + Applications +

+ +
+
+

SUBSCRIPTION FEED

+
+ Used to retrieve the list of videos of the creators + you subscribed to from outside PeerTube +
+
+ +
+ +
+ + +
+ +
+ + +
+ +
+
+ +
+
+
+ +
+
diff --git a/client/src/app/+my-account/my-account-applications/my-account-applications.component.scss b/client/src/app/+my-account/my-account-applications/my-account-applications.component.scss new file mode 100644 index 000000000..704132c03 --- /dev/null +++ b/client/src/app/+my-account/my-account-applications/my-account-applications.component.scss @@ -0,0 +1,28 @@ +@import '_variables'; +@import '_mixins'; + +label { + font-weight: $font-regular; + font-size: 100%; +} + +.applications-title { + @include settings-big-title; +} + +.form-group { + max-width: 500px; +} + +input[type=submit] { + @include peertube-button; + @include orange-button; + + display: flex; + margin-left: auto; + + & + .form-error { + display: inline; + margin-left: 5px; + } +} diff --git a/client/src/app/+my-account/my-account-applications/my-account-applications.component.ts b/client/src/app/+my-account/my-account-applications/my-account-applications.component.ts new file mode 100644 index 000000000..c3f09dfe3 --- /dev/null +++ b/client/src/app/+my-account/my-account-applications/my-account-applications.component.ts @@ -0,0 +1,57 @@ + +import { Component, OnInit } from '@angular/core' +import { AuthService, Notifier, ConfirmService } from '@app/core' +import { VideoService } from '@app/shared/shared-main' +import { FeedFormat } from '@shared/models' +import { Subject, merge } from 'rxjs' +import { debounceTime } from 'rxjs/operators' + +@Component({ + selector: 'my-account-applications', + templateUrl: './my-account-applications.component.html', + styleUrls: [ './my-account-applications.component.scss' ] +}) +export class MyAccountApplicationsComponent implements OnInit { + feedUrl: string + feedToken: string + + private baseURL = window.location.protocol + '//' + window.location.host + private tokenStream = new Subject() + + constructor ( + private authService: AuthService, + private videoService: VideoService, + private notifier: Notifier, + private confirmService: ConfirmService + ) {} + + ngOnInit () { + this.feedUrl = this.baseURL + + merge( + this.tokenStream, + this.authService.userInformationLoaded + ).pipe(debounceTime(400)) + .subscribe( + _ => { + const user = this.authService.getUser() + this.videoService.getVideoSubscriptionFeedUrls(user.account.id) + .then(feeds => this.feedUrl = this.baseURL + feeds.find(f => f.format === FeedFormat.RSS).url) + .then(_ => this.authService.getScopedTokens().then(tokens => this.feedToken = tokens.feedToken)) + }, + + err => { + this.notifier.error(err.message) + } + ) + } + + async renewToken () { + const res = await this.confirmService.confirm('Renewing the token will disallow previously configured clients from retrieving the feed until they use the new token. Proceed?', 'Renew token') + if (res === false) return + + await this.authService.renewScopedTokens() + this.notifier.success('Token renewed. Update your client configuration accordingly.') + this.tokenStream.next() + } +} diff --git a/client/src/app/+my-account/my-account-routing.module.ts b/client/src/app/+my-account/my-account-routing.module.ts index 81380ec6e..226a4a7be 100644 --- a/client/src/app/+my-account/my-account-routing.module.ts +++ b/client/src/app/+my-account/my-account-routing.module.ts @@ -8,6 +8,7 @@ import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-acc import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component' import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' import { MyAccountComponent } from './my-account.component' +import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component' const myAccountRoutes: Routes = [ { @@ -117,6 +118,15 @@ const myAccountRoutes: Routes = [ title: $localize`My abuse reports` } } + }, + { + path: 'applications', + component: MyAccountApplicationsComponent, + data: { + meta: { + title: 'Applications' + } + } } ] } diff --git a/client/src/app/+my-account/my-account.component.ts b/client/src/app/+my-account/my-account.component.ts index d6e9d1c15..12966aebb 100644 --- a/client/src/app/+my-account/my-account.component.ts +++ b/client/src/app/+my-account/my-account.component.ts @@ -41,6 +41,11 @@ export class MyAccountComponent implements OnInit { label: $localize`Abuse reports`, routerLink: '/my-account/abuses', iconName: 'flag' + }, + { + label: $localize`Applications`, + routerLink: '/my-account/applications', + iconName: 'codesandbox' } ] } diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts index 9e3fbcf65..70bf58aae 100644 --- a/client/src/app/+my-account/my-account.module.ts +++ b/client/src/app/+my-account/my-account.module.ts @@ -21,6 +21,7 @@ import { MyAccountNotificationPreferencesComponent } from './my-account-settings import { MyAccountProfileComponent } from './my-account-settings/my-account-profile/my-account-profile.component' import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' import { MyAccountComponent } from './my-account.component' +import { VideoChangeOwnershipComponent } from './my-account-applications/my-account-applications.component' @NgModule({ imports: [ @@ -51,6 +52,7 @@ import { MyAccountComponent } from './my-account.component' MyAccountAbusesListComponent, MyAccountServerBlocklistComponent, MyAccountNotificationsComponent, + MyAccountNotificationPreferencesComponent, MyAccountNotificationPreferencesComponent ], diff --git a/client/src/app/+videos/video-list/video-user-subscriptions.component.ts b/client/src/app/+videos/video-list/video-user-subscriptions.component.ts index 6988c574b..10031d6cc 100644 --- a/client/src/app/+videos/video-list/video-user-subscriptions.component.ts +++ b/client/src/app/+videos/video-list/video-user-subscriptions.component.ts @@ -3,9 +3,12 @@ import { ActivatedRoute, Router } from '@angular/router' import { AuthService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core' import { HooksService } from '@app/core/plugins/hooks.service' import { immutableAssign } from '@app/helpers' +import { VideoService } from '@app/shared/shared-main' import { UserSubscriptionService } from '@app/shared/shared-user-subscription' import { AbstractVideoList, OwnerDisplayType } from '@app/shared/shared-video-miniature' -import { VideoSortField } from '@shared/models' +import { VideoSortField, FeedFormat } from '@shared/models' +import { copyToClipboard } from '../../../root-helpers/utils' +import { environment } from '../../../environments/environment' @Component({ selector: 'my-videos-user-subscriptions', @@ -28,11 +31,13 @@ export class VideoUserSubscriptionsComponent extends AbstractVideoList implement protected screenService: ScreenService, protected storageService: LocalStorageService, private userSubscription: UserSubscriptionService, - private hooks: HooksService + private hooks: HooksService, + private videoService: VideoService ) { super() this.titlePage = $localize`Videos from your subscriptions` + this.actions.push({ routerLink: '/my-library/subscriptions', label: $localize`Subscriptions`, @@ -42,6 +47,20 @@ export class VideoUserSubscriptionsComponent extends AbstractVideoList implement ngOnInit () { super.ngOnInit() + + const user = this.authService.getUser() + let feedUrl = environment.embedUrl + this.videoService.getVideoSubscriptionFeedUrls(user.account.id) + .then((feeds: any) => feedUrl = feedUrl + feeds.find((f: any) => f.format === FeedFormat.RSS).url) + this.actions.unshift({ + label: $localize`Feed`, + iconName: 'syndication', + justIcon: true, + click: () => { + copyToClipboard(feedUrl) + this.activateCopiedMessage() + } + }) } ngOnDestroy () { @@ -68,4 +87,8 @@ export class VideoUserSubscriptionsComponent extends AbstractVideoList implement generateSyndicationList () { // not implemented yet } + + activateCopiedMessage () { + this.notifier.success($localize`Feed URL copied`) + } } diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts index fd6062d3f..224f35f82 100644 --- a/client/src/app/core/auth/auth.service.ts +++ b/client/src/app/core/auth/auth.service.ts @@ -11,6 +11,7 @@ import { environment } from '../../../environments/environment' import { RestExtractor } from '../rest/rest-extractor.service' import { AuthStatus } from './auth-status.model' import { AuthUser } from './auth-user.model' +import { ScopedTokenType, ScopedToken } from '@shared/models/users/user-scoped-token' interface UserLoginWithUsername extends UserLogin { access_token: string @@ -26,6 +27,7 @@ export class AuthService { private static BASE_CLIENT_URL = environment.apiUrl + '/api/v1/oauth-clients/local' private static BASE_TOKEN_URL = environment.apiUrl + '/api/v1/users/token' private static BASE_REVOKE_TOKEN_URL = environment.apiUrl + '/api/v1/users/revoke-token' + private static BASE_SCOPED_TOKENS_URL = environment.apiUrl + '/api/v1/users/scoped-tokens' private static BASE_USER_INFORMATION_URL = environment.apiUrl + '/api/v1/users/me' private static LOCAL_STORAGE_OAUTH_CLIENT_KEYS = { CLIENT_ID: 'client_id', @@ -41,6 +43,7 @@ export class AuthService { private loginChanged: Subject private user: AuthUser = null private refreshingTokenObservable: Observable + private scopedTokens: ScopedToken constructor ( private http: HttpClient, @@ -244,6 +247,48 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular ) } + getScopedTokens (): Promise { + return new Promise((res, rej) => { + if (this.scopedTokens) return res(this.scopedTokens) + + const authHeaderValue = this.getRequestHeaderValue() + const headers = new HttpHeaders().set('Authorization', authHeaderValue) + + this.http.get(AuthService.BASE_SCOPED_TOKENS_URL, { headers }) + .subscribe( + scopedTokens => { + this.scopedTokens = scopedTokens + res(this.scopedTokens) + }, + + err => { + console.error(err) + rej(err) + } + ) + }) + } + + renewScopedTokens (): Promise { + return new Promise((res, rej) => { + const authHeaderValue = this.getRequestHeaderValue() + const headers = new HttpHeaders().set('Authorization', authHeaderValue) + + this.http.post(AuthService.BASE_SCOPED_TOKENS_URL, {}, { headers }) + .subscribe( + scopedTokens => { + this.scopedTokens = scopedTokens + res(this.scopedTokens) + }, + + err => { + console.error(err) + rej(err) + } + ) + }) + } + private mergeUserInformation (obj: UserLoginWithUsername): Observable { // User is not loaded yet, set manually auth header const headers = new HttpHeaders().set('Authorization', `${obj.token_type} ${obj.access_token}`) diff --git a/client/src/app/shared/shared-icons/global-icon.component.ts b/client/src/app/shared/shared-icons/global-icon.component.ts index f3c1fe59b..53a2aee9a 100644 --- a/client/src/app/shared/shared-icons/global-icon.component.ts +++ b/client/src/app/shared/shared-icons/global-icon.component.ts @@ -69,7 +69,8 @@ const icons = { 'columns': require('!!raw-loader?!../../../assets/images/feather/columns.svg').default, 'live': require('!!raw-loader?!../../../assets/images/feather/live.svg').default, 'repeat': require('!!raw-loader?!../../../assets/images/feather/repeat.svg').default, - 'message-circle': require('!!raw-loader?!../../../assets/images/feather/message-circle.svg').default + 'message-circle': require('!!raw-loader?!../../../assets/images/feather/message-circle.svg').default, + 'codesandbox': require('!!raw-loader?!../../../assets/images/feather/codesandbox.svg').default } export type GlobalIconName = keyof typeof icons 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 c8a3ec043..b81540e8d 100644 --- a/client/src/app/shared/shared-main/video/video.service.ts +++ b/client/src/app/shared/shared-main/video/video.service.ts @@ -2,7 +2,7 @@ import { Observable } from 'rxjs' import { catchError, map, switchMap } from 'rxjs/operators' import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http' import { Injectable } from '@angular/core' -import { ComponentPaginationLight, RestExtractor, RestService, ServerService, UserService } from '@app/core' +import { ComponentPaginationLight, RestExtractor, RestService, ServerService, UserService, AuthService } from '@app/core' import { objectToFormData } from '@app/helpers' import { FeedFormat, @@ -49,7 +49,8 @@ export class VideoService implements VideosProvider { private authHttp: HttpClient, private restExtractor: RestExtractor, private restService: RestService, - private serverService: ServerService + private serverService: ServerService, + private authService: AuthService ) {} getVideoViewUrl (uuid: string) { @@ -293,6 +294,16 @@ export class VideoService implements VideosProvider { return this.buildBaseFeedUrls(params) } + async getVideoSubscriptionFeedUrls (accountId: number) { + let params = this.restService.addRestGetParams(new HttpParams()) + params = params.set('accountId', accountId.toString()) + + const { feedToken } = await this.authService.getScopedTokens() + params = params.set('token', feedToken) + + return this.buildBaseFeedUrls(params) + } + getVideoFileMetadata (metadataUrl: string) { return this.authHttp .get(metadataUrl) diff --git a/client/src/app/shared/shared-video-miniature/abstract-video-list.html b/client/src/app/shared/shared-video-miniature/abstract-video-list.html index b1ac757db..18294513f 100644 --- a/client/src/app/shared/shared-video-miniature/abstract-video-list.html +++ b/client/src/app/shared/shared-video-miniature/abstract-video-list.html @@ -8,9 +8,25 @@
- - - + + + + + + + + + + + + + + + + + + +
diff --git a/client/src/app/shared/shared-video-miniature/abstract-video-list.ts b/client/src/app/shared/shared-video-miniature/abstract-video-list.ts index 2219ced30..c55e85afe 100644 --- a/client/src/app/shared/shared-video-miniature/abstract-video-list.ts +++ b/client/src/app/shared/shared-video-miniature/abstract-video-list.ts @@ -70,9 +70,12 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor } actions: { - routerLink: string iconName: GlobalIconName label: string + justIcon?: boolean + routerLink?: string + click?: Function + clipboard?: string }[] = [] onDataSubject = new Subject() diff --git a/client/src/assets/images/feather/codesandbox.svg b/client/src/assets/images/feather/codesandbox.svg new file mode 100644 index 000000000..49848f520 --- /dev/null +++ b/client/src/assets/images/feather/codesandbox.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts index da23c59a7..9407cf123 100644 --- a/client/src/assets/player/peertube-player-manager.ts +++ b/client/src/assets/player/peertube-player-manager.ts @@ -35,7 +35,8 @@ import { VideoJSPluginOptions } from './peertube-videojs-typings' import { TranslationsManager } from './translations-manager' -import { buildVideoOrPlaylistEmbed, buildVideoLink, copyToClipboard, getRtcConfig, isSafari, isIOS } from './utils' +import { buildVideoOrPlaylistEmbed, buildVideoLink, getRtcConfig, isSafari, isIOS } from './utils' +import { copyToClipboard } from '../../root-helpers/utils' // Change 'Playback Rate' to 'Speed' (smaller for our settings menu) (videojs.getComponent('PlaybackRateMenuButton') as any).prototype.controlText_ = 'Speed' diff --git a/client/src/assets/player/utils.ts b/client/src/assets/player/utils.ts index ce7a7fe6c..280f721bd 100644 --- a/client/src/assets/player/utils.ts +++ b/client/src/assets/player/utils.ts @@ -176,18 +176,6 @@ function buildVideoOrPlaylistEmbed (embedUrl: string) { '' } -function copyToClipboard (text: string) { - const el = document.createElement('textarea') - el.value = text - el.setAttribute('readonly', '') - el.style.position = 'absolute' - el.style.left = '-9999px' - document.body.appendChild(el) - el.select() - document.execCommand('copy') - document.body.removeChild(el) -} - function videoFileMaxByResolution (files: VideoFile[]) { let max = files[0] @@ -236,7 +224,6 @@ export { buildVideoOrPlaylistEmbed, videoFileMaxByResolution, videoFileMinByResolution, - copyToClipboard, isMobile, bytes, isIOS, diff --git a/client/src/root-helpers/utils.ts b/client/src/root-helpers/utils.ts index de4e08bf5..e32187ddb 100644 --- a/client/src/root-helpers/utils.ts +++ b/client/src/root-helpers/utils.ts @@ -9,6 +9,18 @@ function objectToUrlEncoded (obj: any) { return str.join('&') } +function copyToClipboard (text: string) { + const el = document.createElement('textarea') + el.value = text + el.setAttribute('readonly', '') + el.style.position = 'absolute' + el.style.left = '-9999px' + document.body.appendChild(el) + el.select() + document.execCommand('copy') + document.body.removeChild(el) +} + // Thanks: https://github.com/uupaa/dynamic-import-polyfill function importModule (path: string) { return new Promise((resolve, reject) => { @@ -51,6 +63,7 @@ function wait (ms: number) { } export { + copyToClipboard, importModule, objectToUrlEncoded, wait diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss index e6491b492..4d70110fe 100644 --- a/client/src/sass/include/_mixins.scss +++ b/client/src/sass/include/_mixins.scss @@ -225,7 +225,7 @@ line-height: $button-height; border-radius: 3px; text-align: center; - padding: 0 17px 0 13px; + padding: 0 13px 0 13px; cursor: pointer; } -- cgit v1.2.3