From 823daf740c336234164f56c6ce707f5bdc917318 Mon Sep 17 00:00:00 2001 From: Matthew Ross Date: Wed, 20 Jul 2016 20:21:15 -0400 Subject: [PATCH] New Http wrapper, updated components, minor API change The new Http wrapper automatically handles updates to the JWT during server communication and redirects to login on unauthenticated calls. --- src/api/controllers/Auth.php | 12 ++- src/api/controllers/BaseController.php | 5 +- src/app/login/login.component.ts | 10 +- src/app/main.ts | 3 +- .../user-settings/user-settings.component.ts | 1 - .../user-settings/user-settings.service.ts | 22 +--- src/app/shared/auth-http.provider.ts | 102 ++++++++++++++++++ src/app/shared/auth/auth.service.ts | 58 ++-------- src/app/shared/index.ts | 1 + 9 files changed, 135 insertions(+), 79 deletions(-) create mode 100644 src/app/shared/auth-http.provider.ts diff --git a/src/api/controllers/Auth.php b/src/api/controllers/Auth.php index 3ba83ff..2ab9c3b 100644 --- a/src/api/controllers/Auth.php +++ b/src/api/controllers/Auth.php @@ -74,7 +74,7 @@ class Auth extends BaseController { return $response->withStatus(401); } - $jwt = self::createJwt($payload->uid); + $jwt = self::createJwt($payload->uid, (int) $payload->mul); $user->active_token = $jwt; $user->save(); @@ -117,7 +117,7 @@ class Auth extends BaseController { return $this->jsonResponse($response, 401); } - $jwt = self::createJwt($user->id); + $jwt = self::createJwt($user->id, ($data->remember ? 200 : 1)); $user = new User($this->container, $user->id); $user->active_token = $jwt; @@ -199,10 +199,12 @@ class Auth extends BaseController { return $payload; } - private static function createJwt($userId) { + private static function createJwt($userId, $mult = 1) { + // If 'remember me' feature is desired, set the multiplier higher return JWT::encode(array( - 'exp' => time() + (60 * 30), // 30 minutes - 'uid' => $userId + 'exp' => time() + (60 * 30) * $mult, // 30 minutes * $mult + 'uid' => $userId, + 'mul' => $mult ), Auth::getJwtKey()); } diff --git a/src/api/controllers/BaseController.php b/src/api/controllers/BaseController.php index 69de655..865866d 100644 --- a/src/api/controllers/BaseController.php +++ b/src/api/controllers/BaseController.php @@ -50,10 +50,7 @@ abstract class BaseController { return 403; } - $token = new stdClass(); - $token->jwt = (string) $response->getBody(); - - $this->apiJson->addData($token); + $this->apiJson->addData((string) $response->getBody()); return $status; } diff --git a/src/app/login/login.component.ts b/src/app/login/login.component.ts index 16a13a3..a46598d 100644 --- a/src/app/login/login.component.ts +++ b/src/app/login/login.component.ts @@ -15,8 +15,8 @@ import { }) export class Login { version: string; - username: string; - password: string; + username: string = ''; + password: string = ''; remember: boolean; isSubmitted: boolean = false; @@ -26,6 +26,12 @@ export class Login { } login(): void { + if (this.username === '' || this.password === '') { + this.notes.add(new Notification('error', + 'Username and password are required.')); + return; + } + this.isSubmitted = true; this.authService.login(this.username, this.password, this.remember) diff --git a/src/app/main.ts b/src/app/main.ts index 49bff6c..65aac47 100644 --- a/src/app/main.ts +++ b/src/app/main.ts @@ -5,7 +5,7 @@ import { enableProdMode } from '@angular/core'; import { AppComponent } from './app.component'; import { APP_ROUTER_PROVIDERS } from './app.routes'; -import { NotificationsService } from './shared/index'; +import { NotificationsService, API_HTTP_PROVIDERS } from './shared/index'; import { Constants } from './shared/constants'; // enableProdMode(); @@ -16,6 +16,7 @@ bootstrap(AppComponent, disableDeprecatedForms(), provideForms(), APP_ROUTER_PROVIDERS, + API_HTTP_PROVIDERS, Constants, NotificationsService ] diff --git a/src/app/settings/user-settings/user-settings.component.ts b/src/app/settings/user-settings/user-settings.component.ts index e44593b..2873125 100644 --- a/src/app/settings/user-settings/user-settings.component.ts +++ b/src/app/settings/user-settings/user-settings.component.ts @@ -43,7 +43,6 @@ export class UserSettings { .changePassword(this.changePassword.current, this.changePassword.newPass) .subscribe((response: ApiResponse) => { - console.log(response); response.alerts.forEach(msg => { this.notes.add(new Notification(msg.type, msg.text)); diff --git a/src/app/settings/user-settings/user-settings.service.ts b/src/app/settings/user-settings/user-settings.service.ts index 22dbbff..dd04c0c 100644 --- a/src/app/settings/user-settings/user-settings.service.ts +++ b/src/app/settings/user-settings/user-settings.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@angular/core'; import { Http, Response, Headers } from '@angular/http'; +import { Router } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/observable/of'; @@ -20,11 +21,10 @@ interface UpdateUser extends User { @Injectable() export class UserSettingsService { activeUser: User = null; - jwtKey: string; - constructor(auth: AuthService, constants: Constants, private http: Http) { + constructor(auth: AuthService, constants: Constants, + private http: Http, private router: Router) { this.activeUser = auth.activeUser; - this.jwtKey = constants.TOKEN; } changePassword(oldPass: string, newPass: string): Observable { @@ -33,30 +33,16 @@ export class UserSettingsService { updateUser.old_password = oldPass; let json = JSON.stringify(updateUser); - let headers = this.getHeaders(); - return this.http.post('api/users/' + this.activeUser.id, json, - { headers: headers }) + return this.http.post('api/users/' + this.activeUser.id, json) .map(res => { let response: ApiResponse = res.json(); - return response; }) .catch((res, caught) => { let response: ApiResponse = res.json(); - return Observable.of(response); }); } - - private getHeaders(): Headers { - let token = localStorage.getItem(this.jwtKey); - let headers = new Headers(); - - headers.append('Content-Type', 'application/json'); - headers.append('Authorization', token); - - return headers; - } } diff --git a/src/app/shared/auth-http.provider.ts b/src/app/shared/auth-http.provider.ts new file mode 100644 index 0000000..86adbfb --- /dev/null +++ b/src/app/shared/auth-http.provider.ts @@ -0,0 +1,102 @@ +import { provide } from '@angular/core'; +import { + Http, + Request, + RequestOptionsArgs, + Response, + XHRBackend, + RequestOptions, + ConnectionBackend, + Headers +} from '@angular/http'; +import { Router } from '@angular/router'; + +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/observable/throw'; + +import { ApiResponse } from './index'; + +export const API_HTTP_PROVIDERS = [ + provide(Http, { + useFactory: (xhrBackend: XHRBackend, requestOptions: RequestOptions, + router: Router) => new ApiHttp(xhrBackend, requestOptions, router), + deps: [XHRBackend, RequestOptions, Router] + }) +]; + +export class ApiHttp extends Http { + private JWT_KEY = 'taskboard.jwt'; + + constructor(backend: ConnectionBackend, + defaultOptions: RequestOptions, + private router: Router) { + super(backend, defaultOptions); + } + + request(url: string | Request, + options?: RequestOptionsArgs): Observable { + return this.intercept(super.request(url, options)); + } + + get(url: string, options?: RequestOptionsArgs): Observable { + return this.intercept(super.get(url, options)); + } + + post(url: string, body: string, + options?: RequestOptionsArgs): Observable { + return this.intercept(super.post(url, body, + this.getRequestOptionArgs(options))); + } + + put(url: string, body: string, + options?: RequestOptionsArgs): Observable { + return this.intercept(super.put(url, body, + this.getRequestOptionArgs(options))); + } + + delete(url: string, options?: RequestOptionsArgs): Observable { + return this.intercept(super.delete(url, options)); + } + + getRequestOptionArgs(options?: RequestOptionsArgs) : RequestOptionsArgs { + if (options == null) { + options = new RequestOptions(); + } + + if (options.headers == null) { + options.headers = new Headers(); + } + + options.headers.append('Content-Type', 'application/json'); + + let jwt = localStorage.getItem(this.JWT_KEY); + if (jwt) { + options.headers.append('Authorization', jwt); + } + + return options; + } + + intercept(observable: Observable): Observable { + return observable + .map((res: Response) => { + let response: ApiResponse = res.json(); + console.log(response); + + localStorage.setItem(this.JWT_KEY, response.data[0]); + + return res; + }) + .catch((err, source) => { + console.log(err); + + if (err.status === 401 && err.url.indexOf('login') === -1) { + this.router.navigate(['']); + localStorage.removeItem(this.JWT_KEY); + } + + return Observable.throw(err); + }); + } +} + diff --git a/src/app/shared/auth/auth.service.ts b/src/app/shared/auth/auth.service.ts index 657303e..a0a9864 100644 --- a/src/app/shared/auth/auth.service.ts +++ b/src/app/shared/auth/auth.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { Http, Response, Headers } from '@angular/http'; +import { Http, Response } from '@angular/http'; import { Router } from '@angular/router'; import { Observable } from 'rxjs/Observable'; @@ -12,92 +12,54 @@ import { Constants } from '../constants'; @Injectable() export class AuthService { activeUser: User = null; - jwtKey: string; constructor(constants: Constants, private http: Http, private router: Router) { - this.jwtKey = constants.TOKEN; } authenticate(): Observable { - let headers = this.getHeaders(); - - return this.http.post('api/authenticate', null, { headers: headers }) + return this.http.post('api/authenticate', null) .map(res => { - this.checkStatus(res); + let response: ApiResponse = res.json(); + this.activeUser = response.data[1]; return this.activeUser !== null; }) .catch((res, caught) => { - this.checkStatus(res); - return Observable.of(false); }); } login(username: string, password: string, remember: boolean): Observable { - // TODO Add remember flag to API let json = JSON.stringify({ username: username, - password: password + password: password, + remember: remember }); return this.http.post('api/login', json) .map(res => { let response: ApiResponse = res.json(); - this.checkStatus(res); + this.activeUser = response.data[1]; return response; }) .catch((res, caught) => { let response: ApiResponse = res.json(); - this.checkStatus(res); + this.activeUser = null; return Observable.of(response); }); } logout(): Observable { - let headers = this.getHeaders(); - - this.clearData(); - - return this.http.post('api/logout', null, { headers: headers }) + return this.http.post('api/logout', null) .map(res => { let response: ApiResponse = res.json(); + return response; }); } - - private clearData(): void { - this.activeUser = null; - localStorage.removeItem(this.jwtKey); - - this.router.navigate(['']); - } - - private checkStatus(response: Response): void { - if (response.status === 200) { - let apiResponse: ApiResponse = response.json(); - - this.activeUser = apiResponse.data[1]; - localStorage.setItem(this.jwtKey, apiResponse.data[0]) - } - - if (response.status === 401) { - this.clearData(); - } - } - - private getHeaders(): Headers { - let token = localStorage.getItem(this.jwtKey); - let headers = new Headers(); - - headers.append('Content-Type', 'application/json'); - headers.append('Authorization', token); - - return headers; - } } diff --git a/src/app/shared/index.ts b/src/app/shared/index.ts index 149f079..d991f65 100644 --- a/src/app/shared/index.ts +++ b/src/app/shared/index.ts @@ -3,4 +3,5 @@ export * from './auth/index'; export * from './models/index'; export * from './notifications/index'; export * from './constants'; +export * from './auth-http.provider';