X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=client%2Fsrc%2Fapp%2Fcore%2Fauth%2Fauth.service.ts;h=4de28e51e9d95ee53c969b882e53065a43e617ca;hb=3545e72c686ff1725bbdfd8d16d693e2f4aa75a3;hp=e887dde1ff5be7c023144ff277140e1c88d2b72c;hpb=fada8d75550dc7365f7e18ee1569b9406251d660;p=github%2FChocobozzz%2FPeerTube.git diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts index e887dde1f..4de28e51e 100644 --- a/client/src/app/core/auth/auth.service.ts +++ b/client/src/app/core/auth/auth.service.ts @@ -1,22 +1,14 @@ -import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http' +import { Hotkey, HotkeysService } from 'angular2-hotkeys' +import { Observable, ReplaySubject, Subject, throwError as observableThrowError } from 'rxjs' +import { catchError, map, mergeMap, share, tap } from 'rxjs/operators' +import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '@angular/common/http' import { Injectable } from '@angular/core' import { Router } from '@angular/router' - -import { NotificationsService } from 'angular2-notifications' -import 'rxjs/add/observable/throw' -import 'rxjs/add/operator/do' -import 'rxjs/add/operator/map' -import 'rxjs/add/operator/mergeMap' -import { Observable } from 'rxjs/Observable' -import { ReplaySubject } from 'rxjs/ReplaySubject' -import { Subject } from 'rxjs/Subject' -import { OAuthClientLocal, User as UserServerModel, UserRefreshToken, UserRole, VideoChannel } from '../../../../../shared' -import { Account } from '../../../../../shared/models/accounts' -import { UserLogin } from '../../../../../shared/models/users/user-login.model' -// Do not use the barrel (dependency loop) -import { RestExtractor } from '../../shared/rest' -import { UserConstructorHash } from '../../shared/users/user.model' - +import { Notifier } from '@app/core/notification/notifier.service' +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' import { AuthStatus } from './auth-status.model' import { AuthUser } from './auth-user.model' @@ -27,68 +19,92 @@ interface UserLoginWithUsername extends UserLogin { username: string } -interface UserLoginWithUserInformation extends UserLogin { - access_token: string - refresh_token: string - token_type: string - username: string - id: number - role: UserRole - displayNSFW: boolean - email: string - videoQuota: number - account: Account - videoChannels: VideoChannel[] -} +type UserLoginWithUserInformation = UserLoginWithUsername & User @Injectable() export class AuthService { - private static BASE_CLIENT_URL = API_URL + '/api/v1/oauth-clients/local' - private static BASE_TOKEN_URL = API_URL + '/api/v1/users/token' - private static BASE_USER_INFORMATION_URL = API_URL + '/api/v1/users/me' + 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_USER_INFORMATION_URL = environment.apiUrl + '/api/v1/users/me' + private static LOCAL_STORAGE_OAUTH_CLIENT_KEYS = { + CLIENT_ID: 'client_id', + CLIENT_SECRET: 'client_secret' + } loginChangedSource: Observable userInformationLoaded = new ReplaySubject(1) + tokensRefreshed = new ReplaySubject(1) + hotkeys: Hotkey[] - private clientId: string - private clientSecret: string + private clientId: string = peertubeLocalStorage.getItem(AuthService.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_ID) + private clientSecret: string = peertubeLocalStorage.getItem(AuthService.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_SECRET) private loginChanged: Subject private user: AuthUser = null + private refreshingTokenObservable: Observable constructor ( private http: HttpClient, - private notificationsService: NotificationsService, + private notifier: Notifier, + private hotkeysService: HotkeysService, private restExtractor: RestExtractor, private router: Router - ) { + ) { this.loginChanged = new Subject() this.loginChangedSource = this.loginChanged.asObservable() - // Return null if there is nothing to load - this.user = AuthUser.load() + // Set HotKeys + this.hotkeys = [ + new Hotkey('m s', (event: KeyboardEvent): boolean => { + this.router.navigate([ '/videos/subscriptions' ]) + return false + }, undefined, $localize`Go to my subscriptions`), + new Hotkey('m v', (event: KeyboardEvent): boolean => { + this.router.navigate([ '/my-library/videos' ]) + return false + }, undefined, $localize`Go to my videos`), + new Hotkey('m i', (event: KeyboardEvent): boolean => { + this.router.navigate([ '/my-library/video-imports' ]) + return false + }, undefined, $localize`Go to my imports`), + new Hotkey('m c', (event: KeyboardEvent): boolean => { + this.router.navigate([ '/my-library/video-channels' ]) + return false + }, undefined, $localize`Go to my channels`) + ] + } + + buildAuthUser (userInfo: Partial, tokens: OAuthUserTokens) { + this.user = new AuthUser(userInfo, tokens) } loadClientCredentials () { // Fetch the client_id/client_secret - // FIXME: save in local storage? this.http.get(AuthService.BASE_CLIENT_URL) - .catch(res => this.restExtractor.handleError(res)) - .subscribe( - res => { - this.clientId = res.client_id - this.clientSecret = res.client_secret - console.log('Client credentials loaded.') - }, - - error => { - let errorMessage = `Cannot retrieve OAuth Client credentials: ${error.text}. \n` - errorMessage += 'Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.' - - // We put a bigger timeout - // This is an important message - this.notificationsService.error('Error', errorMessage, { timeOut: 7000 }) - } - ) + .pipe(catchError(res => this.restExtractor.handleError(res))) + .subscribe({ + next: res => { + this.clientId = res.client_id + this.clientSecret = res.client_secret + + peertubeLocalStorage.setItem(AuthService.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_ID, this.clientId) + peertubeLocalStorage.setItem(AuthService.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_SECRET, this.clientSecret) + + logger.info('Client credentials loaded.') + }, + + error: err => { + let errorMessage = err.message + + if (err.status === HttpStatusCode.FORBIDDEN_403) { + errorMessage = $localize`Cannot retrieve OAuth Client credentials: ${err.message}. +Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.` + } + + // We put a bigger timeout: this is an important message + this.notifier.error(errorMessage, $localize`Error`, 7000) + } + }) } getRefreshToken () { @@ -125,36 +141,65 @@ export class AuthService { return !!this.getAccessToken() } - login (username: string, password: string) { + login (options: { + username: string + password: string + otpToken?: string + token?: string + }) { + const { username, password, token, otpToken } = options + // Form url encoded - const body = new HttpParams().set('client_id', this.clientId) - .set('client_secret', this.clientSecret) - .set('response_type', 'code') - .set('grant_type', 'password') - .set('scope', 'upload') - .set('username', username) - .set('password', password) + const body = { + client_id: this.clientId, + client_secret: this.clientSecret, + response_type: 'code', + grant_type: 'password', + scope: 'upload', + username, + password + } - const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded') + if (token) Object.assign(body, { externalAuthToken: token }) - return this.http.post(AuthService.BASE_TOKEN_URL, body, { headers }) - .map(res => Object.assign(res, { username })) - .flatMap(res => this.mergeUserInformation(res)) - .map(res => this.handleLogin(res)) - .catch(res => this.restExtractor.handleError(res)) + let headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded') + if (otpToken) headers = headers.set('x-peertube-otp', otpToken) + + return this.http.post(AuthService.BASE_TOKEN_URL, objectToUrlEncoded(body), { headers }) + .pipe( + map(res => Object.assign(res, { username })), + mergeMap(res => this.mergeUserInformation(res)), + map(res => this.handleLogin(res)), + catchError(res => this.restExtractor.handleError(res)) + ) } logout () { - // TODO: make an HTTP request to revoke the tokens - this.user = null + const authHeaderValue = this.getRequestHeaderValue() + const headers = new HttpHeaders().set('Authorization', authHeaderValue) - AuthUser.flush() + this.http.post<{ redirectUrl?: string }>(AuthService.BASE_REVOKE_TOKEN_URL, {}, { headers }) + .subscribe({ + next: res => { + if (res.redirectUrl) { + window.location.href = res.redirectUrl + } + }, + + error: err => logger.error(err) + }) + + this.user = null this.setStatus(AuthStatus.LoggedOut) + + this.hotkeysService.remove(this.hotkeys) } refreshAccessToken () { - console.log('Refreshing token...') + if (this.refreshingTokenObservable) return this.refreshingTokenObservable + + logger.info('Refreshing token...') const refreshToken = this.getRefreshToken() @@ -167,22 +212,32 @@ export class AuthService { const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded') - return this.http.post(AuthService.BASE_TOKEN_URL, body, { headers }) - .map(res => this.handleRefreshToken(res)) - .catch(err => { - console.error(err) - console.log('Cannot refresh token -> logout...') - this.logout() - this.router.navigate(['/login']) - - return Observable.throw({ - error: 'You need to reconnect.' - }) - }) + this.refreshingTokenObservable = this.http.post(AuthService.BASE_TOKEN_URL, body, { headers }) + .pipe( + map(res => this.handleRefreshToken(res)), + tap(() => { + this.refreshingTokenObservable = null + }), + catchError(err => { + this.refreshingTokenObservable = null + + logger.error(err) + logger.info('Cannot refresh token -> logout...') + this.logout() + this.router.navigate([ '/login' ]) + + return observableThrowError(() => ({ + error: $localize`You need to reconnect.` + })) + }), + share() + ) + + return this.refreshingTokenObservable } refreshUserInformation () { - const obj = { + const obj: UserLoginWithUsername = { access_token: this.user.getAccessToken(), refresh_token: null, token_type: this.user.getTokenType(), @@ -190,18 +245,21 @@ export class AuthService { } this.mergeUserInformation(obj) - .subscribe( - res => { - this.user.displayNSFW = res.displayNSFW - this.user.role = res.role - this.user.videoChannels = res.videoChannels - this.user.account = res.account - - this.user.save() - - this.userInformationLoaded.next(true) - } - ) + .subscribe({ + next: res => { + this.user.patch(res) + + this.userInformationLoaded.next(true) + } + }) + } + + isOTPMissingError (err: HttpErrorResponse) { + if (err.status !== HttpStatusCode.UNAUTHORIZED_401) return false + + if (err.headers.get('x-peertube-otp') !== 'required; app') return false + + return true } private mergeUserInformation (obj: UserLoginWithUsername): Observable { @@ -209,49 +267,27 @@ export class AuthService { const headers = new HttpHeaders().set('Authorization', `${obj.token_type} ${obj.access_token}`) return this.http.get(AuthService.BASE_USER_INFORMATION_URL, { headers }) - .map(res => { - const newProperties = { - id: res.id, - role: res.role, - displayNSFW: res.displayNSFW, - email: res.email, - videoQuota: res.videoQuota, - account: res.account, - videoChannels: res.videoChannels - } - - return Object.assign(obj, newProperties) - } - ) + .pipe(map(res => Object.assign(obj, res))) } private handleLogin (obj: UserLoginWithUserInformation) { - const hashUser: UserConstructorHash = { - id: obj.id, - username: obj.username, - role: obj.role, - email: obj.email, - displayNSFW: obj.displayNSFW, - videoQuota: obj.videoQuota, - videoChannels: obj.videoChannels, - account: obj.account - } const hashTokens = { accessToken: obj.access_token, tokenType: obj.token_type, refreshToken: obj.refresh_token } - this.user = new AuthUser(hashUser, hashTokens) - this.user.save() + this.user = new AuthUser(obj, hashTokens) this.setStatus(AuthStatus.LoggedIn) this.userInformationLoaded.next(true) + + this.hotkeysService.add(this.hotkeys) } private handleRefreshToken (obj: UserRefreshToken) { this.user.refreshTokens(obj.access_token, obj.refresh_token) - this.user.save() + this.tokensRefreshed.next() } private setStatus (status: AuthStatus) {