diff --git a/app/Http/Controllers/Auth/AuthController.php b/app/Http/Controllers/Auth/AuthController.php index 1ed001d..44e5566 100644 --- a/app/Http/Controllers/Auth/AuthController.php +++ b/app/Http/Controllers/Auth/AuthController.php @@ -25,7 +25,6 @@ public function login(LoginRequest $request) try { - // attempt to verify the credentials and create a token for the user if(!$token = \JWTAuth::attempt($credentials)) { return response()->json(['error' => \Lang::get('auth.failed')], 401); @@ -33,7 +32,19 @@ public function login(LoginRequest $request) } catch(JWTException $e) { - // something went wrong whilst attempting to encode the token + return response()->json(['error' => \Lang::get('auth.could_not_create_token')], 500); + } + + return response()->json(['token' => $token]); + } + public function refresh() + { + try + { + $token = \JWTAuth::refresh(); + } + catch(JWTException $e) + { return response()->json(['error' => \Lang::get('auth.could_not_create_token')], 500); } diff --git a/frontend/src/app/_commons/auth/auth-guard.ts b/frontend/src/app/_commons/auth/auth-guard.ts index 91b8d55..007432d 100644 --- a/frontend/src/app/_commons/auth/auth-guard.ts +++ b/frontend/src/app/_commons/auth/auth-guard.ts @@ -5,20 +5,17 @@ import { Observable } from 'rxjs/Observable'; @Injectable() export class AuthGuard implements CanActivate { - constructor(private router: Router, private authService: AuthService) { + constructor(private router: Router) { } canActivate(): Observable { - let check = this.authService.check(); - check.subscribe( - (result) => { - if (!result) { - // TODO: Add "login required" message ;) - this.router.navigate(['/']); - } - } - ); + let isLoggedIn = AuthService.isLoggedIn(); - return check; + if (!isLoggedIn){ + // TODO: Add "login required" message ;) + this.router.navigate(['/']); + } + + return Observable.of(isLoggedIn); } } diff --git a/frontend/src/app/_commons/auth/auth.service.ts b/frontend/src/app/_commons/auth/auth.service.ts index 44d69ad..a596e11 100755 --- a/frontend/src/app/_commons/auth/auth.service.ts +++ b/frontend/src/app/_commons/auth/auth.service.ts @@ -1,55 +1,74 @@ -import 'rxjs/add/observable/of'; import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs/Observable'; -import { Router } from '@angular/router'; -import { LoginModel } from 'app/homepage/login'; import { CookieService } from 'angular2-cookie/core'; import { Response } from '@angular/http'; -import { tokenNotExpired, AuthHttp } from 'angular2-jwt'; -import 'rxjs/add/operator/toPromise'; +import { tokenNotExpired, JwtHelper } from 'angular2-jwt'; +import { Observable } from 'rxjs/Observable'; +import * as moment from 'moment'; +import 'rxjs/add/operator/do'; +import { LoginModel } from 'app/homepage/login'; +import { AuthHttp } from './http'; @Injectable() export class AuthService { - public KNOWN_USER = 'known_user'; - private TOKEN = 'id_token'; - - private loggedIn = false; - private storage: Storage; - - constructor(private router: Router, private cookies: CookieService, private http: AuthHttp) { - this.storage = localStorage; - this.loggedIn = tokenNotExpired(); - } - - public login(model: LoginModel): Promise { - let request = this.http.post('/api/authorize', model); - request.subscribe( - (response) => { - this.storage.setItem(this.TOKEN, response.json().token); - this.loggedIn = tokenNotExpired(); - this.cookies.put(this.KNOWN_USER, 'true'); - this.router.navigate(['/reception']); - }, (error) => error - ); + private static TOKEN = 'id_token'; + private static KNOWN_USER = 'known_user'; + private static refreshTimeoutId: any; - return request.toPromise(); + constructor(private cookies: CookieService, private http: AuthHttp) { + this.scheduleTokenRefreshing(); } - public logout() { - this.http.post('/api/logout', {}).subscribe( - () => { - this.loggedIn = false; - this.storage.removeItem(this.TOKEN); - this.router.navigate(['/']); - } - ); + public static clear() { + clearTimeout(AuthService.refreshTimeoutId); + localStorage.removeItem(AuthService.TOKEN); + } + + public login(model: LoginModel): Observable { + return this.http.post('/api/authorize', model) + .do((response) => this.saveToken(response)); + } + + public logout(): Observable { + return this.http.post('/api/logout', {}) + .do(() => AuthService.clear()); } - public check() { - return Observable.of(this.loggedIn); + public static isLoggedIn(): boolean { + try { + return tokenNotExpired(); + } catch (e) { + return false; + } } public isKnownUser(): boolean { - return this.cookies.get(this.KNOWN_USER) === 'true'; + return this.cookies.get(AuthService.KNOWN_USER) === 'true'; + } + + private saveToken(response: Response) { + localStorage.setItem(AuthService.TOKEN, response.json().token); + this.cookies.put(AuthService.KNOWN_USER, 'true'); + this.scheduleTokenRefreshing(); + } + + private scheduleTokenRefreshing() { + if (!AuthService.isLoggedIn()) { + AuthService.clear(); + return; + } + + let token = localStorage.getItem(AuthService.TOKEN); + let timeout = AuthService.getTokenTimeout(token); + + AuthService.refreshTimeoutId = setTimeout( + () => this.http.post('/api/refresh-token', {}).subscribe((response) => this.saveToken(response)), timeout + ); + } + + private static getTokenTimeout(token: string): number { + let expiry = moment(new JwtHelper().getTokenExpirationDate(token)); + let now = moment(); + // Subtract 1 minute to be sure token is still valid + return moment.duration(expiry.diff(now)).subtract(1, 'minute').asMilliseconds(); } } diff --git a/frontend/src/app/_commons/auth/http.ts b/frontend/src/app/_commons/auth/http.ts new file mode 100644 index 0000000..495f465 --- /dev/null +++ b/frontend/src/app/_commons/auth/http.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@angular/core'; +import { Http, RequestOptions, Request, RequestOptionsArgs, Response } from '@angular/http'; +import { AuthHttp as BasicAuthHttp, AuthConfig } from 'angular2-jwt'; +import { Observable } from 'rxjs'; +import 'rxjs/add/operator/do'; +import { AuthService } from './auth.service'; + +@Injectable() +export class AuthHttp extends BasicAuthHttp { + constructor(options: AuthConfig, http: Http, defOpts?: RequestOptions) { + super(options, http, defOpts); + } + + request(url: string | Request, options?: RequestOptionsArgs): Observable { + return super.request(url, options) + .catch((response: Response) => { + if ([401, 403].indexOf(response.status) !== -1) { + // TODO: Add notification about invalid response - logout + AuthService.clear(); + } + if (response.status === 500) { + console.error(response); + } + return Observable.of(response); + }); + } +} diff --git a/frontend/src/app/_commons/auth/index.ts b/frontend/src/app/_commons/auth/index.ts index 7f8160a..351e1de 100644 --- a/frontend/src/app/_commons/auth/index.ts +++ b/frontend/src/app/_commons/auth/index.ts @@ -1,2 +1,3 @@ export * from './auth-guard'; export * from './auth.service'; +export * from './http'; diff --git a/frontend/src/app/_commons/commons.module.ts b/frontend/src/app/_commons/commons.module.ts index 614023f..76c985c 100644 --- a/frontend/src/app/_commons/commons.module.ts +++ b/frontend/src/app/_commons/commons.module.ts @@ -2,10 +2,12 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { ScheduleModule } from 'primeng/components/schedule/schedule'; +import { Http } from '@angular/http'; +import { AuthConfig } from 'angular2-jwt'; import { FormField } from './form-field'; import { CalendarComponent } from './calendar'; import { AddItemButtonComponent } from './add-item-button'; -import { AuthService, AuthGuard } from './auth'; +import { AuthHttp, AuthService, AuthGuard } from './auth'; import { LabelledFormField } from './labelled-form-field'; @NgModule( @@ -17,7 +19,20 @@ import { LabelledFormField } from './labelled-form-field'; BrowserModule, FormsModule, ScheduleModule ], providers: [ - AuthService, AuthGuard + AuthService, AuthGuard, { + provide: AuthHttp, + useFactory: (http) => { + return new AuthHttp( + new AuthConfig( + { + globalHeaders: [{ 'Content-Type': 'application/json' }, { 'Accept': 'application/json' }], + noJwtError: true + } + ), http + ); + }, + deps: [Http] + } ], exports: [ FormField, LabelledFormField, CalendarComponent, AddItemButtonComponent diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 9ac2e63..e494a5c 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -1,4 +1,5 @@ import { Component } from '@angular/core'; +import { Router } from '@angular/router'; import { AuthService } from 'app/_commons/auth'; /* @@ -13,14 +14,16 @@ import { AuthService } from 'app/_commons/auth'; } ) export class App { - constructor(private authService: AuthService) { + constructor(private router: Router, private authService: AuthService) { } - logout() { - this.authService.logout(); + public logout() { + this.authService.logout().subscribe(() => { + this.router.navigate(['/']); + }); } - isLoggedIn() { - return this.authService.check(); + public isLoggedIn() { + return AuthService.isLoggedIn(); } } diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 7d65858..714eea8 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -1,9 +1,9 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; -import { HttpModule, XSRFStrategy, CookieXSRFStrategy, Http } from '@angular/http'; +import { HttpModule, XSRFStrategy, CookieXSRFStrategy } from '@angular/http'; import { RouterModule } from '@angular/router'; -import { AUTH_PROVIDERS, AuthHttp, AuthConfig } from 'angular2-jwt'; +import { AUTH_PROVIDERS } from 'angular2-jwt'; import { CookieService } from 'angular2-cookie/services/cookies.service'; import { ENV_PROVIDERS } from './environment'; import { ROUTES } from './app.routes'; @@ -11,26 +11,13 @@ import { CommonsModule } from './_commons/commons.module'; import { App } from './app.component'; import { APP_RESOLVER_PROVIDERS } from './app.resolver'; import { NoContent } from './no-content'; -import { Homepage } from './homepage/homepage.component'; +import { Homepage, HomepageGuard } from './homepage'; // Application wide providers const APP_PROVIDERS = [ - ...APP_RESOLVER_PROVIDERS, CookieService, ...AUTH_PROVIDERS, { + ...APP_RESOLVER_PROVIDERS, CookieService, HomepageGuard, ...AUTH_PROVIDERS, { provide: XSRFStrategy, useValue: new CookieXSRFStrategy('XSRF-TOKEN', 'X-XSRF-TOKEN') - }, { - provide: AuthHttp, - useFactory: (http) => { - return new AuthHttp( - new AuthConfig( - { - globalHeaders: [{ 'Content-Type': 'application/json' }, { 'Accept': 'application/json' }], - noJwtError: true - } - ), http - ); - }, - deps: [Http] } ]; diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index a5a7a93..e2f1813 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -1,12 +1,14 @@ import { Routes } from '@angular/router'; -import { NoContent } from './no-content'; -import { Homepage } from './homepage/homepage.component'; import { AuthGuard } from 'app/_commons/auth'; +import { NoContent } from './no-content'; +import { Homepage, HomepageGuard } from './homepage'; export const ROUTES: Routes = [ { path: '', - component: Homepage + pathMatch: 'full', + component: Homepage, + canActivate: [HomepageGuard] }, { path: 'reception', diff --git a/frontend/src/app/app.template.html b/frontend/src/app/app.template.html index d0ada4f..8b7a9ec 100755 --- a/frontend/src/app/app.template.html +++ b/frontend/src/app/app.template.html @@ -1,11 +1,11 @@ -