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