Commit f18bddc7 authored by Maxime REYROLLE's avatar Maxime REYROLLE

Merge branch '1-authentication' into 'develop'

Resolve "Authentication"

Closes #1

See merge request !41
parents a3ed308d c24e6c6f
Pipeline #1298 passed with stage
in 16 minutes and 7 seconds
......@@ -23,7 +23,8 @@
"@angular/router": "^5.0.0",
"core-js": "^2.4.1",
"rxjs": "^5.5.2",
"zone.js": "^0.8.14"
"zone.js": "^0.8.14",
"angular2-jwt": "^0.2.3"
},
"devDependencies": {
"@angular/cli": "1.5.0",
......
......@@ -2,14 +2,17 @@ import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {HttpModule} from '@angular/http';
import {HttpClientModule} from '@angular/common/http';
import {AppRoutingModule} from './app-routing.module';
import {AppComponent} from './app.component';
import {UsersModule} from './users/users.module';
import {LocalGovernmentsModule} from './local-governments/local-governments.module';
import {MemberService} from './local-governments/members/shared/member.service';
import {ConnectorsModule} from './connectors/connectors.module';
import {SubscriptionsModule} from './subscriptions/subscriptions.module';
import {AuthenticationModule} from './authentication/authentication.module';
import {AuthenticationService} from './authentication/shared/authentication.service';
import {AuthenticationGuard} from './authentication/guard/authentication.guard';
@NgModule({
declarations: [
......@@ -19,13 +22,18 @@ import {SubscriptionsModule} from './subscriptions/subscriptions.module';
BrowserModule,
FormsModule,
HttpModule,
HttpClientModule,
AppRoutingModule,
UsersModule,
LocalGovernmentsModule,
ConnectorsModule,
SubscriptionsModule
SubscriptionsModule,
AuthenticationModule
],
providers: [
AuthenticationService,
AuthenticationGuard,
],
providers: [MemberService],
bootstrap: [AppComponent]
})
export class AppModule {
......
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import {LoginComponent} from './login/login.component';
const routes: Routes = [
{
path: 'login',
component: LoginComponent
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AuthenticationRoutingModule { }
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {Http, RequestOptions} from '@angular/http';
import {AuthConfig, AuthHttp} from 'angular2-jwt';
import {AuthenticationRoutingModule} from './authentication-routing.module';
import {LoginComponent} from './login/login.component';
export function authHttpServiceFactory(http: Http, options: RequestOptions) {
return new AuthHttp(new AuthConfig(), http, options);
}
@NgModule({
imports: [
CommonModule,
AuthenticationRoutingModule
],
declarations: [LoginComponent],
providers: [
{
provide: AuthHttp,
useFactory: authHttpServiceFactory,
deps: [Http, RequestOptions]
}
]
})
export class AuthenticationModule {
}
import {TestBed, async, inject} from '@angular/core/testing';
import {RouterTestingModule} from '@angular/router/testing';
import {Router, RouterStateSnapshot} from '@angular/router';
import {AuthenticationGuard} from './authentication.guard';
import {AuthenticationService} from '../shared/authentication.service';
import {AuthenticationServiceMock} from '../shared/authentication.service.mock';
describe('AuthenticationGuard', () => {
let mockSnapshot: RouterStateSnapshot;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
AuthenticationGuard,
{provide: AuthenticationService, useClass: AuthenticationServiceMock},
],
imports: [RouterTestingModule]
});
mockSnapshot = jasmine.createSpyObj<RouterStateSnapshot>('RouterStateSnapshot', ['toString']);
});
it('should ...', inject([AuthenticationGuard], (guard: AuthenticationGuard) => {
expect(guard).toBeTruthy();
}));
it('should activate the page', inject([AuthenticationGuard, Router], (guard: AuthenticationGuard, router: Router) => {
spyOn(router, 'navigate');
expect(guard.canActivate(null, null)).toBeTruthy();
expect(router.navigate).not.toHaveBeenCalled();
}));
it('should not activate the page and redirect the user',
inject([AuthenticationGuard, Router, AuthenticationService],
(guard: AuthenticationGuard, router: Router, mockAuth: AuthenticationService) => {
mockSnapshot.url = '/dashboard';
spyOn(router, 'navigate');
spyOn(mockAuth, 'isAuthenticated').and.returnValue(false);
expect(guard.canActivate(null, mockSnapshot)).toBeFalsy();
expect(router.navigate).toHaveBeenCalled();
}));
});
import {Injectable} from '@angular/core';
import {CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router} from '@angular/router';
import {Observable} from 'rxjs/Observable';
import {AuthenticationService} from '../shared/authentication.service';
@Injectable()
export class AuthenticationGuard implements CanActivate {
constructor(private authenticationService: AuthenticationService, private router: Router) {
}
canActivate(next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
if (!this.authenticationService.isAuthenticated()) {
this.router.navigate(['/login'], {queryParams: {redirectUrl: state.url}});
return false;
}
return true;
}
}
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {ActivatedRoute, Router} from '@angular/router';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import {LoginComponent} from './login.component';
import {AuthenticationService} from '../shared/authentication.service';
import {AuthenticationServiceMock} from '../shared/authentication.service.mock';
describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
const mock = {
snapshot: {
queryParams: Observable.of({
redirectUrl: ''
})
}
};
const router = {
navigateByUrl: jasmine.createSpy('navigateByUrl')
};
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [LoginComponent],
providers: [
{provide: AuthenticationService, useClass: AuthenticationServiceMock},
{provide: Router, useValue: router},
{provide: ActivatedRoute, useValue: mock}
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import {Component, OnInit} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {environment} from '../../../environments/environment';
import {AuthenticationService} from '../shared/authentication.service';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
redirectUrl: string;
constructor(private authenticationService: AuthenticationService, private route: ActivatedRoute, private router: Router) {
// get redirect url from route parameters or default to /
this.redirectUrl = this.route.snapshot.queryParams['redirectUrl'] || '/';
}
ngOnInit() {
this.authenticationService.authenticate()
.subscribe(result => {
// login successful so redirect the user
this.router.navigateByUrl(this.redirectUrl);
},
error => {
location.href = environment.loginUrl + '?redirect_url=' + this.router.url;
});
}
}
import {Observable} from 'rxjs/Observable';
export class AuthenticationServiceMock {
authenticate(): Observable<boolean> {
return Observable.of(true);
}
isAuthenticated(): boolean {
return true;
}
}
import {TestBed, inject} from '@angular/core/testing';
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
import {encodeTestToken} from 'angular2-jwt/angular2-jwt-test-helpers';
import {AuthenticationService} from './authentication.service';
describe('AuthenticationService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
AuthenticationService,
],
imports: [
HttpClientTestingModule,
],
});
});
it('should be created', inject([AuthenticationService], (service: AuthenticationService) => {
expect(service).toBeTruthy();
}));
describe('authenticate()', () => {
it('should authenticate a user', inject([AuthenticationService, HttpTestingController],
(authenticationService: AuthenticationService, httpMock: HttpTestingController) => {
const expectedBody = {
token: encodeTestToken({'sub': 2, 'name': 'mreyrolle', 'exp': 9999999999}),
user: {
id: 2,
name: 'mreyrolle',
mail: 'maxime.reyrolle@example.com',
firstname: 'Maxime',
lastname: 'REYROLLE',
superadmin: false
}
};
// An HTTP GET request is made in the authenticate() method, expect that the authenticate() method return true.
authenticationService.authenticate().subscribe(data => {
expect(data).toBeTruthy();
});
// The request is pending, and no response has been sent. Expect that the request happened.
const req = httpMock.expectOne('https://cake.test.adullact.org/api/v1/users/token.json');
// If no request with that URL was made, or if multiple requests match, expectOne() would throw.
// However this test makes only one request to this URL, so it will match and return a mock request.
// The mock request can be used to deliver a response or make assertions against the request.
// In this case, the test asserts that the request is a GET.
expect(req.request.method).toEqual('GET');
// Fulfill the request by transmitting a response.
req.flush(expectedBody);
// Assert that there are no outstanding requests.
httpMock.verify();
}));
it('should not authenticate a user', inject([AuthenticationService, HttpTestingController],
(authenticationService: AuthenticationService, httpMock: HttpTestingController) => {
const expectedBody = {};
authenticationService.authenticate().subscribe(data => {
expect(data).toBeFalsy();
});
const req = httpMock.expectOne('https://cake.test.adullact.org/api/v1/users/token.json');
expect(req.request.method).toEqual('GET');
req.flush(expectedBody);
httpMock.verify();
}));
});
describe('logout()', () => {
it('should remove token from localStorage when logging out a user', inject([AuthenticationService],
(authenticationService: AuthenticationService) => {
spyOn(localStorage, 'removeItem').and.callThrough();
authenticationService.logout();
expect(localStorage.removeItem).toHaveBeenCalledWith('token');
expect(localStorage.removeItem).toHaveBeenCalledWith('user');
}));
});
describe('isAuthenticated()', () => {
it('should return false when no token', inject([AuthenticationService],
(authenticationService: AuthenticationService) => {
expect(authenticationService.isAuthenticated()).toBeFalsy();
}));
it('should return false when token is expired', inject([AuthenticationService],
(authenticationService: AuthenticationService) => {
const expiredToken = encodeTestToken({'sub': 1, 'name': 'my_user', 'exp': 0});
localStorage.setItem('token', expiredToken);
expect(authenticationService.isAuthenticated()).toBeFalsy();
}));
it('should return true when token is not expired', inject([AuthenticationService],
(authenticationService: AuthenticationService) => {
const validToken = encodeTestToken({'sub': 1, 'name': 'my_user', 'exp': 9999999999});
localStorage.setItem('token', validToken);
expect(authenticationService.isAuthenticated()).toBeTruthy();
}));
});
});
import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {Observable} from 'rxjs/Observable';
import {environment} from '../../../environments/environment';
import {tokenNotExpired} from 'angular2-jwt';
import {User} from '../../users/shared/user.model';
@Injectable()
export class AuthenticationService {
private url = environment.apiUrl + '/users/token.json';
constructor(private http: HttpClient) {
}
authenticate(): Observable<boolean> {
return this.http.get<{ token: string, user: User }>(this.url, {withCredentials: true})
.map(response => {
// successful login if there is a jwt in the response
const token = response.token;
const user = response.user;
if (token) {
// store user and token in local storage to keep user logged in
localStorage.setItem('token', token);
localStorage.setItem('user', JSON.stringify(user));
return true;
}
return false;
}
);
}
logout() {
localStorage.removeItem('token');
localStorage.removeItem('user');
}
isAuthenticated(): boolean {
return tokenNotExpired();
}
}
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import {NgModule} from '@angular/core';
import {Routes, RouterModule} from '@angular/router';
import {ConnectorListComponent} from './connector-list/connector-list.component';
import {ConnectorFormComponent} from './connector-form/connector-form.component';
import {ConnectorComponent} from './connector/connector.component';
import {AuthenticationGuard} from '../authentication/guard/authentication.guard';
const routes: Routes = [
{
path: 'connectors',
children: [
canActivate: [AuthenticationGuard],
children: [
{
path: '',
component: ConnectorListComponent,
......@@ -42,4 +44,5 @@ const routes: Routes = [
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ConnectorsRoutingModule { }
export class ConnectorsRoutingModule {
}
import {TestBed, inject, fakeAsync, tick} from '@angular/core/testing';
import {BaseRequestOptions, Http, RequestMethod, ResponseOptions, Response} from '@angular/http';
import {MockBackend} from '@angular/http/testing';
import {AuthHttp} from 'angular2-jwt';
import {RoleService} from './role.service';
......@@ -30,6 +31,7 @@ describe('RoleService', () => {
},
MockBackend,
BaseRequestOptions,
{provide: AuthHttp, useExisting: Http, deps: [Http]},
]
});
});
......
import {Injectable} from '@angular/core';
import {Http, RequestOptions, Headers} from '@angular/http';
import {RequestOptions, Headers} from '@angular/http';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import {environment} from '../../../../../environments/environment';
import {AuthHttp} from 'angular2-jwt';
import {Role} from './role.model';
......@@ -10,21 +11,21 @@ import {Role} from './role.model';
export class RoleService {
private url = environment.apiUrl + '/connectors';
constructor(private http: Http) {
constructor(private authHttp: AuthHttp) {
}
getRoles(connector_id, service_id): Observable<Role[]> {
return this.http.get(this.url + '/' + connector_id + '/services/' + service_id + '/roles.json')
return this.authHttp.get(this.url + '/' + connector_id + '/services/' + service_id + '/roles.json')
.map(response => response.json());
}
getRole(connector_id, service_id, role_id): Observable<Role> {
return this.http.get(this.url + '/' + connector_id + '/services/' + service_id + '/roles/' + role_id + '.json')
return this.authHttp.get(this.url + '/' + connector_id + '/services/' + service_id + '/roles/' + role_id + '.json')
.map(response => response.json());
}
deleteRole(connector_id, service_id, role_id): Observable<Response> {
return this.http.delete(this.url + '/' + connector_id + '/services/' + service_id + '/roles/' + role_id + '.json')
return this.authHttp.delete(this.url + '/' + connector_id + '/services/' + service_id + '/roles/' + role_id + '.json')
.map(response => response.json());
}
......@@ -32,7 +33,7 @@ export class RoleService {
const headers = new Headers({'Content-Type': 'application/json'});
const options = new RequestOptions({headers: headers});
const body = JSON.stringify(role);
return this.http.post(this.url + '/' + connector_id + '/services/' + service_id + '/roles.json', body, options)
return this.authHttp.post(this.url + '/' + connector_id + '/services/' + service_id + '/roles.json', body, options)
.map(response => response.json());
}
......@@ -41,7 +42,7 @@ export class RoleService {
const options = new RequestOptions({headers: headers});
const body = JSON.stringify(role);
return this.http.put(this.url + '/' + connector_id + '/services/' + role.service_id + '/roles/' + role.id + '.json', body, options)
return this.authHttp.put(this.url + '/' + connector_id + '/services/' + role.service_id + '/roles/' + role.id + '.json', body, options)
.map(response => response.json());
}
}
import {TestBed, inject, fakeAsync, tick} from '@angular/core/testing';
import {BaseRequestOptions, Http, RequestMethod, ResponseOptions, Response} from '@angular/http';
import {MockBackend} from '@angular/http/testing';
import {AuthHttp} from 'angular2-jwt';
import {ServiceService} from './service.service';
......@@ -41,6 +42,7 @@ describe('ServiceService', () => {
},
MockBackend,
BaseRequestOptions,
{provide: AuthHttp, useExisting: Http, deps: [Http]},
]
});
});
......
import {Injectable} from '@angular/core';
import {Http, RequestOptions, Headers} from '@angular/http';
import {RequestOptions, Headers} from '@angular/http';
import {Observable} from 'rxjs/Observable';
import {environment} from '../../../../environments/environment';
import {AuthHttp} from 'angular2-jwt';
import {Service} from './service.model';
......@@ -9,16 +10,16 @@ import {Service} from './service.model';
export class ServiceService {
private url = environment.apiUrl + '/connectors';
constructor(private http: Http) {
constructor(private authHttp: AuthHttp) {
}
getServices(connector_id): Observable<{ services: Service[] }> {
return this.http.get(this.url + '/' + connector_id + '/services.json')
return this.authHttp.get(this.url + '/' + connector_id + '/services.json')
.map(response => response.json());
}
getService(connector_id, service_id): Observable<Service> {
return this.http.get(this.url + '/' + connector_id + '/services/' + service_id + '.json')
return this.authHttp.get(this.url + '/' + connector_id + '/services/' + service_id + '.json')
.map(response => response.json());
}
......@@ -26,7 +27,7 @@ export class ServiceService {
const headers = new Headers({'Content-Type': 'application/json'});
const options = new RequestOptions({headers: headers});
const body = JSON.stringify(service);
return this.http.post(this.url + '/' + connector_id + '/services.json', body, options)
return this.authHttp.post(this.url + '/' + connector_id + '/services.json', body, options)
.map(response => response.json());
}
......@@ -35,12 +36,12 @@ export class ServiceService {
const options = new RequestOptions({headers: headers});
const body = JSON.stringify(service);
return this.http.put(this.url + '/' + service.connector_id + '/services/' + service.id + '.json', body, options)
return this.authHttp.put(this.url + '/' + service.connector_id + '/services/' + service.id + '.json', body, options)
.map(response => response.json());
}
deleteService(connector_id, service_id): Observable<Response> {
return this.http.delete(this.url + '/' + connector_id + '/services/' + service_id + '.json')
return this.authHttp.delete(this.url + '/' + connector_id + '/services/' + service_id + '.json')
.map(response => response.json());
}
}
import {TestBed, inject, fakeAsync, tick} from '@angular/core/testing';
import {BaseRequestOptions, Http, RequestMethod, ResponseOptions, Response} from '@angular/http';
import {MockBackend} from '@angular/http/testing';
import {AuthHttp} from 'angular2-jwt';
import {ConnectorService} from './connector.service';
......@@ -32,6 +33,7 @@ describe('ConnectorService', () => {
},
MockBackend,
BaseRequestOptions,
{provide: AuthHttp, useExisting: Http, deps: [Http]},
]
});
});
......
import {Injectable} from '@angular/core';
import {Http, RequestOptions, Headers} from '@angular/http';
import {RequestOptions, Headers} from '@angular/http';
import {Observable} from 'rxjs/Observable';
import {environment} from '../../../environments/environment';
import {AuthHttp} from 'angular2-jwt';
import {Connector} from './connector.model';
......@@ -9,11 +10,11 @@ import {Connector} from './connector.model';
export class ConnectorService {
private url = environment.apiUrl + '/connectors';
constructor(private http: Http) {
constructor(private authHttp: AuthHttp) {
}
getConnectors(): Observable<{ connectors: Connector[] }> {
return this.http.get(this.url + '.json')
return this.authHttp.get(this.url + '.json')
.map(response => response.json());
}
......@@ -22,7 +23,7 @@ export class ConnectorService {
const options = new RequestOptions({headers: headers});
const body = JSON.stringify(connector);
return this.http.post(this.url + '.json', body, options)
return this.authHttp.post(this.url + '.json', body, options)
.map(response => response.json());
}
......@@ -31,17 +32,17 @@ export class ConnectorService {
const options = new RequestOptions({headers: headers});
const body = JSON.stringify(connector);
return this.http.put(this.url + '/' + connector.id + '.json', body, options)
return this.authHttp.put(this.url + '/' + connector.id + '.json', body, options)
.map(response => response.json());
}
getConnector(id): Observable<Connector> {
return this.http.get(this.url + '/' + id + '.json')
return this.authHttp.get(this.url + '/' + id + '.json')
.map(response => response.json());
}
deleteConnector(id): Observable<Response> {
return this.http.delete(this.url + '/' + id + '.json')
return this.authHttp.delete(this.url + '/' + id + '.json')
.map(response => response.json());
}
}
......@@ -4,11 +4,13 @@ import {Routes, RouterModule} from '@angular/router';
import {LocalGovernmentListComponent} from './local-government-list/local-government-list.component';
import {LocalGovernmentComponent} from './local-government/local-government.component';
import {LocalGovernmentFormComponent} from './local-government-form/local-government-form.component';
import {AuthenticationGuard} from '../authentication/guard/authentication.guard';
const routes: Routes = [
{
path: 'local_governments',
children: [
canActivate: [AuthenticationGuard],
children: [
{
path: '',
component: LocalGovernmentListComponent,
......