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