X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=client%2Fsrc%2Fapp%2Fcore%2Fauth%2Fauth.service.ts;h=6fe601d8d24634275bfb0e555e3560d5aad07f3d;hb=98bd5e2256bfdeba6d5ab07f0421acfde1a0de26;hp=00a4216ef202b8f066ac4d5bcfcc57e289ff3e5b;hpb=af5e743b01f20f24d0c25e786d57f557b21f3a24;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 00a4216ef..6fe601d8d 100644 --- a/client/src/app/core/auth/auth.service.ts +++ b/client/src/app/core/auth/auth.service.ts @@ -1,243 +1,299 @@ -import { Injectable } from '@angular/core'; -import { Headers, Http, Response, URLSearchParams } from '@angular/http'; -import { Router } from '@angular/router'; -import { Observable } from 'rxjs/Observable'; -import { Subject } from 'rxjs/Subject'; -import 'rxjs/add/operator/map'; -import 'rxjs/add/operator/mergeMap'; -import 'rxjs/add/observable/throw'; - -import { NotificationsService } from 'angular2-notifications'; - -import { AuthStatus } from './auth-status.model'; -import { AuthUser } from './auth-user.model'; -// Do not use the barrel (dependency loop) -import { RestExtractor } from '../../shared/rest'; +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 { 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 { RedirectService } from '../routing' +import { AuthStatus } from './auth-status.model' +import { AuthUser } from './auth-user.model' + +interface UserLoginWithUsername extends UserLogin { + access_token: string + refresh_token: string + token_type: string + username: string +} + +type UserLoginWithUserInformation = UserLoginWithUsername & User @Injectable() export class AuthService { - private static BASE_CLIENT_URL = '/api/v1/clients/local'; - private static BASE_TOKEN_URL = '/api/v1/users/token'; - private static BASE_USER_INFORMATIONS_URL = '/api/v1/users/me'; - - loginChangedSource: Observable; - - private clientId: string; - private clientSecret: string; - private loginChanged: Subject; - private user: AuthUser = null; + 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' + } - constructor( - private http: Http, - private notificationsService: NotificationsService, + loginChangedSource: Observable + userInformationLoaded = new ReplaySubject(1) + tokensRefreshed = new ReplaySubject(1) + hotkeys: Hotkey[] + + 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 redirectService: RedirectService, + private http: HttpClient, + private notifier: Notifier, + private hotkeysService: HotkeysService, private restExtractor: RestExtractor, private router: Router - ) { - this.loginChanged = new Subject(); - this.loginChangedSource = this.loginChanged.asObservable(); + ) { + this.loginChanged = new Subject() + this.loginChangedSource = this.loginChanged.asObservable() + + // 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) - .map(this.restExtractor.extractDataGet) - .catch((res) => this.restExtractor.handleError(res)) - .subscribe( - result => { - this.clientId = result.client_id; - this.clientSecret = result.client_secret; - console.log('Client credentials loaded.'); - }, + this.http.get(AuthService.BASE_CLIENT_URL) + .pipe(catchError(res => this.restExtractor.handleError(res))) + .subscribe({ + next: res => { + this.clientId = res.client_id + this.clientSecret = res.client_secret - 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.'; + peertubeLocalStorage.setItem(AuthService.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_ID, this.clientId) + peertubeLocalStorage.setItem(AuthService.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_SECRET, this.clientSecret) - // We put a bigger timeout - // This is an important message - this.notificationsService.error('Error', errorMessage, { timeOut: 7000 }); - } - ); + logger.info('Client credentials loaded.') + }, - // Return null if there is nothing to load - this.user = AuthUser.load(); - } + error: err => { + let errorMessage = err.message - getRefreshToken() { - if (this.user === null) return null; + 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.` + } - return this.user.getRefreshToken(); + // We put a bigger timeout: this is an important message + this.notifier.error(errorMessage, $localize`Error`, 7000) + } + }) } - getRequestHeaderValue() { - return `${this.getTokenType()} ${this.getAccessToken()}`; + getRefreshToken () { + if (this.user === null) return null + + return this.user.getRefreshToken() } - getAccessToken() { - if (this.user === null) return null; + getRequestHeaderValue () { + const accessToken = this.getAccessToken() - return this.user.getAccessToken(); + if (accessToken === null) return null + + return `${this.getTokenType()} ${accessToken}` } - getTokenType() { - if (this.user === null) return null; + getAccessToken () { + if (this.user === null) return null - return this.user.getTokenType(); + return this.user.getAccessToken() } - getUser(): AuthUser { - return this.user; - } + getTokenType () { + if (this.user === null) return null - isAdmin() { - if (this.user === null) return false; + return this.user.getTokenType() + } - return this.user.isAdmin(); + getUser () { + return this.user } - isLoggedIn() { - if (this.getAccessToken()) { - return true; - } else { - return false; - } + isLoggedIn () { + return !!this.getAccessToken() } - login(username: string, password: string) { - let body = new URLSearchParams(); - body.set('client_id', this.clientId); - body.set('client_secret', this.clientSecret); - body.set('response_type', 'code'); - body.set('grant_type', 'password'); - body.set('scope', 'upload'); - body.set('username', username); - body.set('password', password); + login (options: { + username: string + password: string + otpToken?: string + token?: string + }) { + const { username, password, token, otpToken } = options + + // Form url encoded + const body = { + client_id: this.clientId, + client_secret: this.clientSecret, + response_type: 'code', + grant_type: 'password', + scope: 'upload', + username, + password + } - let headers = new Headers(); - headers.append('Content-Type', 'application/x-www-form-urlencoded'); + if (token) Object.assign(body, { externalAuthToken: token }) - let options = { - headers: headers - }; + 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, body.toString(), options) - .map(this.restExtractor.extractDataGet) - .map(res => { - res.username = username; - return res; - }) - .flatMap(res => this.mergeUserInformations(res)) - .map(res => this.handleLogin(res)) - .catch((res) => this.restExtractor.handleError(res)); + 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; + logout () { + const authHeaderValue = this.getRequestHeaderValue() + const headers = new HttpHeaders().set('Authorization', authHeaderValue) + + 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) + }) - AuthUser.flush(); + this.user = null - this.setStatus(AuthStatus.LoggedOut); + this.setStatus(AuthStatus.LoggedOut) + + this.hotkeysService.remove(this.hotkeys) } - refreshAccessToken() { - console.log('Refreshing token...'); + refreshAccessToken () { + if (this.refreshingTokenObservable) return this.refreshingTokenObservable + + logger.info('Refreshing token...') - const refreshToken = this.getRefreshToken(); + const refreshToken = this.getRefreshToken() - let body = new URLSearchParams(); - body.set('refresh_token', refreshToken); - body.set('client_id', this.clientId); - body.set('client_secret', this.clientSecret); - body.set('response_type', 'code'); - body.set('grant_type', 'refresh_token'); + // Form url encoded + const body = new HttpParams().set('refresh_token', refreshToken) + .set('client_id', this.clientId) + .set('client_secret', this.clientSecret) + .set('response_type', 'code') + .set('grant_type', 'refresh_token') - let headers = new Headers(); - headers.append('Content-Type', 'application/x-www-form-urlencoded'); + const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded') - let options = { - headers: headers - }; + 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 - return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options) - .map(this.restExtractor.extractDataGet) - .map(res => this.handleRefreshToken(res)) - .catch((res: Response) => { - // The refresh token is invalid? - if (res.status === 400 && res.json() && res.json().error === 'invalid_grant') { - console.error('Cannot refresh token -> logout...'); - this.logout(); - this.router.navigate(['/login']); + logger.error(err) + logger.info('Cannot refresh token -> logout...') + this.logout() - return Observable.throw({ - json: () => '', - text: () => 'You need to reconnect.' - }); - } + this.redirectService.redirectToLogin() - return this.restExtractor.handleError(res); - }); + return observableThrowError(() => ({ + error: $localize`You need to reconnect.` + })) + }), + share() + ) + + return this.refreshingTokenObservable } - refreshUserInformations() { - const obj = { - access_token: this.user.getAccessToken() - }; + refreshUserInformation () { + const obj: UserLoginWithUsername = { + access_token: this.user.getAccessToken(), + refresh_token: null, + token_type: this.user.getTokenType(), + username: this.user.username + } - this.mergeUserInformations(obj) - .subscribe( - res => { - this.user.displayNSFW = res.displayNSFW; - this.user.role = res.role; + this.mergeUserInformation(obj) + .subscribe({ + next: res => { + this.user.patch(res) - this.user.save(); + this.userInformationLoaded.next(true) } - ); + }) } - private mergeUserInformations(obj: { access_token: string }) { - // Do not call authHttp here to avoid circular dependencies headaches + isOTPMissingError (err: HttpErrorResponse) { + if (err.status !== HttpStatusCode.UNAUTHORIZED_401) return false - const headers = new Headers(); - headers.set('Authorization', `Bearer ${obj.access_token}`); + if (err.headers.get('x-peertube-otp') !== 'required; app') return false + + return true + } - return this.http.get(AuthService.BASE_USER_INFORMATIONS_URL, { headers }) - .map(res => res.json()) - .map(res => { - const newProperties = { - id: res.id, - role: res.role, - displayNSFW: res.displayNSFW - }; + 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}`) - return Object.assign(obj, newProperties); - } - ); + return this.http.get(AuthService.BASE_USER_INFORMATION_URL, { headers }) + .pipe(map(res => Object.assign(obj, res))) } - private handleLogin (obj: any) { - const id = obj.id; - const username = obj.username; - const role = obj.role; - const displayNSFW = obj.displayNSFW; + private handleLogin (obj: UserLoginWithUserInformation) { const hashTokens = { - access_token: obj.access_token, - token_type: obj.token_type, - refresh_token: obj.refresh_token - }; + accessToken: obj.access_token, + tokenType: obj.token_type, + refreshToken: obj.refresh_token + } - this.user = new AuthUser({ id, username, role, displayNSFW }, hashTokens); - this.user.save(); + this.user = new AuthUser(obj, hashTokens) - this.setStatus(AuthStatus.LoggedIn); - } + this.setStatus(AuthStatus.LoggedIn) + this.userInformationLoaded.next(true) - private handleRefreshToken (obj: any) { - this.user.refreshTokens(obj.access_token, obj.refresh_token); - this.user.save(); + this.hotkeysService.add(this.hotkeys) } - private setStatus(status: AuthStatus) { - this.loginChanged.next(status); + private handleRefreshToken (obj: UserRefreshToken) { + this.user.refreshTokens(obj.access_token, obj.refresh_token) + this.tokensRefreshed.next() } + private setStatus (status: AuthStatus) { + this.loginChanged.next(status) + } }