37264a8ad37292d5e500167b77c83cb81f88c622
[oweals/peertube.git] / client / src / app / core / auth / auth.service.ts
1 import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'
2 import { Injectable } from '@angular/core'
3 import { Router } from '@angular/router'
4 import { NotificationsService } from 'angular2-notifications'
5 import 'rxjs/add/observable/throw'
6 import 'rxjs/add/operator/do'
7 import 'rxjs/add/operator/map'
8 import 'rxjs/add/operator/mergeMap'
9 import { Observable } from 'rxjs/Observable'
10 import { ReplaySubject } from 'rxjs/ReplaySubject'
11 import { Subject } from 'rxjs/Subject'
12 import { OAuthClientLocal, User as UserServerModel, UserRefreshToken, UserRole, VideoChannel } from '../../../../../shared'
13 import { Account } from '../../../../../shared/models/accounts'
14 import { UserLogin } from '../../../../../shared/models/users/user-login.model'
15 import { environment } from '../../../environments/environment'
16 import { RestExtractor } from '../../shared/rest'
17 import { UserConstructorHash } from '../../shared/users/user.model'
18 import { AuthStatus } from './auth-status.model'
19 import { AuthUser } from './auth-user.model'
20
21 interface UserLoginWithUsername extends UserLogin {
22   access_token: string
23   refresh_token: string
24   token_type: string
25   username: string
26 }
27
28 interface UserLoginWithUserInformation extends UserLogin {
29   access_token: string
30   refresh_token: string
31   token_type: string
32   username: string
33   id: number
34   role: UserRole
35   displayNSFW: boolean
36   autoPlayVideo: boolean
37   email: string
38   videoQuota: number
39   account: Account
40   videoChannels: VideoChannel[]
41 }
42
43 @Injectable()
44 export class AuthService {
45   private static BASE_CLIENT_URL = environment.apiUrl + '/api/v1/oauth-clients/local'
46   private static BASE_TOKEN_URL = environment.apiUrl + '/api/v1/users/token'
47   private static BASE_USER_INFORMATION_URL = environment.apiUrl + '/api/v1/users/me'
48
49   loginChangedSource: Observable<AuthStatus>
50   userInformationLoaded = new ReplaySubject<boolean>(1)
51
52   private clientId: string
53   private clientSecret: string
54   private loginChanged: Subject<AuthStatus>
55   private user: AuthUser = null
56
57   constructor (
58     private http: HttpClient,
59     private notificationsService: NotificationsService,
60     private restExtractor: RestExtractor,
61     private router: Router
62    ) {
63     this.loginChanged = new Subject<AuthStatus>()
64     this.loginChangedSource = this.loginChanged.asObservable()
65
66     // Return null if there is nothing to load
67     this.user = AuthUser.load()
68   }
69
70   loadClientCredentials () {
71     // Fetch the client_id/client_secret
72     // FIXME: save in local storage?
73     this.http.get<OAuthClientLocal>(AuthService.BASE_CLIENT_URL)
74              .catch(res => this.restExtractor.handleError(res))
75              .subscribe(
76                res => {
77                  this.clientId = res.client_id
78                  this.clientSecret = res.client_secret
79                  console.log('Client credentials loaded.')
80                },
81
82                error => {
83                  let errorMessage = `Cannot retrieve OAuth Client credentials: ${error.text}. \n`
84                  errorMessage += 'Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.'
85
86                  // We put a bigger timeout
87                  // This is an important message
88                  this.notificationsService.error('Error', errorMessage, { timeOut: 7000 })
89                }
90              )
91   }
92
93   getRefreshToken () {
94     if (this.user === null) return null
95
96     return this.user.getRefreshToken()
97   }
98
99   getRequestHeaderValue () {
100     const accessToken = this.getAccessToken()
101
102     if (accessToken === null) return null
103
104     return `${this.getTokenType()} ${accessToken}`
105   }
106
107   getAccessToken () {
108     if (this.user === null) return null
109
110     return this.user.getAccessToken()
111   }
112
113   getTokenType () {
114     if (this.user === null) return null
115
116     return this.user.getTokenType()
117   }
118
119   getUser () {
120     return this.user
121   }
122
123   isLoggedIn () {
124     return !!this.getAccessToken()
125   }
126
127   login (username: string, password: string) {
128     // Form url encoded
129     const body = new HttpParams().set('client_id', this.clientId)
130                                  .set('client_secret', this.clientSecret)
131                                  .set('response_type', 'code')
132                                  .set('grant_type', 'password')
133                                  .set('scope', 'upload')
134                                  .set('username', username)
135                                  .set('password', password)
136
137     const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded')
138
139     return this.http.post<UserLogin>(AuthService.BASE_TOKEN_URL, body, { headers })
140                     .map(res => Object.assign(res, { username }))
141                     .flatMap(res => this.mergeUserInformation(res))
142                     .map(res => this.handleLogin(res))
143                     .catch(res => this.restExtractor.handleError(res))
144   }
145
146   logout () {
147     // TODO: make an HTTP request to revoke the tokens
148     this.user = null
149
150     AuthUser.flush()
151
152     this.setStatus(AuthStatus.LoggedOut)
153   }
154
155   refreshAccessToken () {
156     console.log('Refreshing token...')
157
158     const refreshToken = this.getRefreshToken()
159
160     // Form url encoded
161     const body = new HttpParams().set('refresh_token', refreshToken)
162                                  .set('client_id', this.clientId)
163                                  .set('client_secret', this.clientSecret)
164                                  .set('response_type', 'code')
165                                  .set('grant_type', 'refresh_token')
166
167     const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded')
168
169     return this.http.post<UserRefreshToken>(AuthService.BASE_TOKEN_URL, body, { headers })
170                     .map(res => this.handleRefreshToken(res))
171                     .catch(err => {
172                       console.error(err)
173                       console.log('Cannot refresh token -> logout...')
174                       this.logout()
175                       this.router.navigate(['/login'])
176
177                       return Observable.throw({
178                         error: 'You need to reconnect.'
179                       })
180                     })
181   }
182
183   refreshUserInformation () {
184     const obj = {
185       access_token: this.user.getAccessToken(),
186       refresh_token: null,
187       token_type: this.user.getTokenType(),
188       username: this.user.username
189     }
190
191     this.mergeUserInformation(obj)
192       .subscribe(
193         res => {
194           this.user.displayNSFW = res.displayNSFW
195           this.user.autoPlayVideo = res.autoPlayVideo
196           this.user.role = res.role
197           this.user.videoChannels = res.videoChannels
198           this.user.account = res.account
199
200           this.user.save()
201
202           this.userInformationLoaded.next(true)
203         }
204       )
205   }
206
207   private mergeUserInformation (obj: UserLoginWithUsername): Observable<UserLoginWithUserInformation> {
208     // User is not loaded yet, set manually auth header
209     const headers = new HttpHeaders().set('Authorization', `${obj.token_type} ${obj.access_token}`)
210
211     return this.http.get<UserServerModel>(AuthService.BASE_USER_INFORMATION_URL, { headers })
212                     .map(res => {
213                       const newProperties = {
214                         id: res.id,
215                         role: res.role,
216                         displayNSFW: res.displayNSFW,
217                         autoPlayVideo: res.autoPlayVideo,
218                         email: res.email,
219                         videoQuota: res.videoQuota,
220                         account: res.account,
221                         videoChannels: res.videoChannels
222                       }
223
224                       return Object.assign(obj, newProperties)
225                     }
226     )
227   }
228
229   private handleLogin (obj: UserLoginWithUserInformation) {
230     const hashUser: UserConstructorHash = {
231       id: obj.id,
232       username: obj.username,
233       role: obj.role,
234       email: obj.email,
235       displayNSFW: obj.displayNSFW,
236       autoPlayVideo: obj.autoPlayVideo,
237       videoQuota: obj.videoQuota,
238       videoChannels: obj.videoChannels,
239       account: obj.account
240     }
241     const hashTokens = {
242       accessToken: obj.access_token,
243       tokenType: obj.token_type,
244       refreshToken: obj.refresh_token
245     }
246
247     this.user = new AuthUser(hashUser, hashTokens)
248     this.user.save()
249
250     this.setStatus(AuthStatus.LoggedIn)
251     this.userInformationLoaded.next(true)
252   }
253
254   private handleRefreshToken (obj: UserRefreshToken) {
255     this.user.refreshTokens(obj.access_token, obj.refresh_token)
256     this.user.save()
257   }
258
259   private setStatus (status: AuthStatus) {
260     this.loginChanged.next(status)
261   }
262 }