mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-13 04:53:06 +00:00
Merge remote-tracking branch 'dspace/master' into w2p-50632_Replace-mock-registry-service-with-implementation
Conflicts: src/app/app-routing.module.ts src/app/core/cache/response-cache.models.ts
This commit is contained in:
@@ -23,9 +23,6 @@ git clone https://github.com/DSpace/dspace-angular.git
|
|||||||
# change directory to our repo
|
# change directory to our repo
|
||||||
cd dspace-angular
|
cd dspace-angular
|
||||||
|
|
||||||
# install the global dependencies
|
|
||||||
yarn run global
|
|
||||||
|
|
||||||
# install the local dependencies
|
# install the local dependencies
|
||||||
yarn install
|
yarn install
|
||||||
|
|
||||||
|
@@ -99,8 +99,10 @@
|
|||||||
"font-awesome": "4.7.0",
|
"font-awesome": "4.7.0",
|
||||||
"http-server": "0.11.1",
|
"http-server": "0.11.1",
|
||||||
"https": "1.0.0",
|
"https": "1.0.0",
|
||||||
|
"js-cookie": "2.2.0",
|
||||||
"js.clone": "0.0.3",
|
"js.clone": "0.0.3",
|
||||||
"jsonschema": "1.2.2",
|
"jsonschema": "1.2.2",
|
||||||
|
"jwt-decode": "^2.2.0",
|
||||||
"methods": "1.1.2",
|
"methods": "1.1.2",
|
||||||
"morgan": "1.9.0",
|
"morgan": "1.9.0",
|
||||||
"ngx-pagination": "3.0.3",
|
"ngx-pagination": "3.0.3",
|
||||||
@@ -124,6 +126,7 @@
|
|||||||
"@types/express-serve-static-core": "4.11.1",
|
"@types/express-serve-static-core": "4.11.1",
|
||||||
"@types/hammerjs": "2.0.35",
|
"@types/hammerjs": "2.0.35",
|
||||||
"@types/jasmine": "^2.8.6",
|
"@types/jasmine": "^2.8.6",
|
||||||
|
"@types/js-cookie": "2.1.0",
|
||||||
"@types/memory-cache": "0.2.0",
|
"@types/memory-cache": "0.2.0",
|
||||||
"@types/mime": "2.0.0",
|
"@types/mime": "2.0.0",
|
||||||
"@types/node": "^9.4.6",
|
"@types/node": "^9.4.6",
|
||||||
|
@@ -46,7 +46,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"home": "Home"
|
"home": "Home",
|
||||||
|
"login": "Log In",
|
||||||
|
"logout": "Log Out"
|
||||||
},
|
},
|
||||||
"pagination": {
|
"pagination": {
|
||||||
"results-per-page": "Results Per Page",
|
"results-per-page": "Results Per Page",
|
||||||
@@ -203,5 +205,31 @@
|
|||||||
"item": "Error fetching item",
|
"item": "Error fetching item",
|
||||||
"objects": "Error fetching objects",
|
"objects": "Error fetching objects",
|
||||||
"search-results": "Error fetching search results"
|
"search-results": "Error fetching search results"
|
||||||
|
},
|
||||||
|
"login": {
|
||||||
|
"title": "Login",
|
||||||
|
"form": {
|
||||||
|
"header": "Please log in to DSpace",
|
||||||
|
"email": "Email address",
|
||||||
|
"forgot-password": "Have you forgotten your password?",
|
||||||
|
"new-user": "New user? Click here to register.",
|
||||||
|
"password": "Password",
|
||||||
|
"submit": "Log in"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"logout": {
|
||||||
|
"title": "Logout",
|
||||||
|
"form": {
|
||||||
|
"header": "Log out from DSpace",
|
||||||
|
"submit": "Log out"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"messages": {
|
||||||
|
"expired": "Your session has expired. Please log in again."
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"invalid-user": "Invalid email or password."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
13
src/app/+login-page/login-page-routing.module.ts
Normal file
13
src/app/+login-page/login-page-routing.module.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { LoginPageComponent } from './login-page.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forChild([
|
||||||
|
{ path: '', component: LoginPageComponent, data: { title: 'login.title' } }
|
||||||
|
])
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class LoginPageRoutingModule { }
|
9
src/app/+login-page/login-page.component.html
Normal file
9
src/app/+login-page/login-page.component.html
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<div class="container w-100 h-100">
|
||||||
|
<div class="text-center mt-5 row justify-content-center">
|
||||||
|
<div>
|
||||||
|
<img class="mb-4 login-logo" src="assets/images/dspace-logo.png">
|
||||||
|
<h1 class="h3 mb-0 font-weight-normal">{{"login.form.header" | translate}}</h1>
|
||||||
|
<ds-log-in></ds-log-in>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
6
src/app/+login-page/login-page.component.scss
Normal file
6
src/app/+login-page/login-page.component.scss
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
@import '../../styles/variables.scss';
|
||||||
|
|
||||||
|
.login-logo {
|
||||||
|
height: $login-logo-height;
|
||||||
|
width: $login-logo-width;
|
||||||
|
}
|
47
src/app/+login-page/login-page.component.spec.ts
Normal file
47
src/app/+login-page/login-page.component.spec.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import 'rxjs/add/observable/of';
|
||||||
|
|
||||||
|
import { LoginPageComponent } from './login-page.component';
|
||||||
|
|
||||||
|
describe('LoginPageComponent', () => {
|
||||||
|
let comp: LoginPageComponent;
|
||||||
|
let fixture: ComponentFixture<LoginPageComponent>;
|
||||||
|
|
||||||
|
const store: Store<LoginPageComponent> = jasmine.createSpyObj('store', {
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
dispatch: {},
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
|
select: Observable.of(true)
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
TranslateModule.forRoot()
|
||||||
|
],
|
||||||
|
declarations: [LoginPageComponent],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: Store, useValue: store
|
||||||
|
}
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(LoginPageComponent);
|
||||||
|
comp = fixture.componentInstance; // SearchPageComponent test instance
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create instance', () => {
|
||||||
|
expect(comp).toBeDefined()
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
21
src/app/+login-page/login-page.component.ts
Normal file
21
src/app/+login-page/login-page.component.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Component, OnDestroy } from '@angular/core';
|
||||||
|
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
|
||||||
|
import { AppState } from '../app.reducer';
|
||||||
|
import { ResetAuthenticationMessagesAction } from '../core/auth/auth.actions';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-login-page',
|
||||||
|
styleUrls: ['./login-page.component.scss'],
|
||||||
|
templateUrl: './login-page.component.html'
|
||||||
|
})
|
||||||
|
export class LoginPageComponent implements OnDestroy {
|
||||||
|
|
||||||
|
constructor(private store: Store<AppState>) {}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
// Clear all authentication messages when leaving login page
|
||||||
|
this.store.dispatch(new ResetAuthenticationMessagesAction());
|
||||||
|
}
|
||||||
|
}
|
19
src/app/+login-page/login-page.module.ts
Normal file
19
src/app/+login-page/login-page.module.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { SharedModule } from '../shared/shared.module';
|
||||||
|
import { LoginPageComponent } from './login-page.component';
|
||||||
|
import { LoginPageRoutingModule } from './login-page-routing.module';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
LoginPageRoutingModule,
|
||||||
|
CommonModule,
|
||||||
|
SharedModule,
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
LoginPageComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class LoginPageModule {
|
||||||
|
|
||||||
|
}
|
19
src/app/+logout-page/logout-page-routing.module.ts
Normal file
19
src/app/+logout-page/logout-page-routing.module.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { LogoutPageComponent } from './logout-page.component';
|
||||||
|
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forChild([
|
||||||
|
{
|
||||||
|
canActivate: [AuthenticatedGuard],
|
||||||
|
path: '',
|
||||||
|
component: LogoutPageComponent,
|
||||||
|
data: { title: 'logout.title' }
|
||||||
|
}
|
||||||
|
])
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class LogoutPageRoutingModule { }
|
9
src/app/+logout-page/logout-page.component.html
Normal file
9
src/app/+logout-page/logout-page.component.html
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<div class="container w-100 h-100">
|
||||||
|
<div class="text-center mt-5 row justify-content-md-center">
|
||||||
|
<div>
|
||||||
|
<img class="mb-4 login-logo" src="assets/images/dspace-logo.png">
|
||||||
|
<h1 class="h3 mb-0 font-weight-normal">{{"logout.form.header" | translate}}</h1>
|
||||||
|
<ds-log-out></ds-log-out>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
1
src/app/+logout-page/logout-page.component.scss
Normal file
1
src/app/+logout-page/logout-page.component.scss
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@import '../+login-page/login-page.component.scss';
|
31
src/app/+logout-page/logout-page.component.spec.ts
Normal file
31
src/app/+logout-page/logout-page.component.spec.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { LogoutPageComponent } from './logout-page.component';
|
||||||
|
|
||||||
|
describe('LogoutPageComponent', () => {
|
||||||
|
let comp: LogoutPageComponent;
|
||||||
|
let fixture: ComponentFixture<LogoutPageComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
TranslateModule.forRoot()
|
||||||
|
],
|
||||||
|
declarations: [LogoutPageComponent],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(LogoutPageComponent);
|
||||||
|
comp = fixture.componentInstance; // SearchPageComponent test instance
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create instance', () => {
|
||||||
|
expect(comp).toBeDefined()
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
10
src/app/+logout-page/logout-page.component.ts
Normal file
10
src/app/+logout-page/logout-page.component.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-logout-page',
|
||||||
|
styleUrls: ['./logout-page.component.scss'],
|
||||||
|
templateUrl: './logout-page.component.html'
|
||||||
|
})
|
||||||
|
export class LogoutPageComponent {
|
||||||
|
|
||||||
|
}
|
19
src/app/+logout-page/logout-page.module.ts
Normal file
19
src/app/+logout-page/logout-page.module.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { SharedModule } from '../shared/shared.module';
|
||||||
|
import { LogoutPageComponent } from './logout-page.component';
|
||||||
|
import { LogoutPageRoutingModule } from './logout-page-routing.module';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
LogoutPageRoutingModule,
|
||||||
|
CommonModule,
|
||||||
|
SharedModule,
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
LogoutPageComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class LogoutPageModule {
|
||||||
|
|
||||||
|
}
|
@@ -14,7 +14,7 @@ import {
|
|||||||
import { hasValue, isEmpty, isNotEmpty, } from '../../../shared/empty.util';
|
import { hasValue, isEmpty, isNotEmpty, } from '../../../shared/empty.util';
|
||||||
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
|
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
|
||||||
import { SearchService } from '../../search-service/search.service';
|
import { SearchService } from '../../search-service/search.service';
|
||||||
import { RouteService } from '../../../shared/route.service';
|
import { RouteService } from '../../../shared/services/route.service';
|
||||||
import ObjectExpression from 'rollup/dist/typings/ast/nodes/ObjectExpression';
|
import ObjectExpression from 'rollup/dist/typings/ast/nodes/ObjectExpression';
|
||||||
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
||||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { isNotEmpty } from '../shared/empty.util';
|
import { isNotEmpty } from '../shared/empty.util';
|
||||||
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||||
import 'core-js/fn/object/entries';
|
import 'core-js/library/fn/object/entries';
|
||||||
|
|
||||||
export enum ViewMode {
|
export enum ViewMode {
|
||||||
List = 'list',
|
List = 'list',
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="search-page row">
|
<div class="search-page row">
|
||||||
<ds-search-sidebar *ngIf="!(isMobileView$ | async)" class="col-3 sidebar-md-sticky"
|
<ds-search-sidebar *ngIf="!(isXsOrSm$ | async)" class="col-3 sidebar-md-sticky"
|
||||||
id="search-sidebar"
|
id="search-sidebar"
|
||||||
[resultCount]="(resultsRD$ | async)?.pageInfo?.totalElements"></ds-search-sidebar>
|
[resultCount]="(resultsRD$ | async)?.pageInfo?.totalElements"></ds-search-sidebar>
|
||||||
<div class="col-12 col-md-9">
|
<div class="col-12 col-md-9">
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
<div id="search-body"
|
<div id="search-body"
|
||||||
class="row-offcanvas row-offcanvas-left"
|
class="row-offcanvas row-offcanvas-left"
|
||||||
[@pushInOut]="(isSidebarCollapsed() | async) ? 'collapsed' : 'expanded'">
|
[@pushInOut]="(isSidebarCollapsed() | async) ? 'collapsed' : 'expanded'">
|
||||||
<ds-search-sidebar *ngIf="(isMobileView$ | async)" class="col-12"
|
<ds-search-sidebar *ngIf="(isXsOrSm$ | async)" class="col-12"
|
||||||
id="search-sidebar-sm"
|
id="search-sidebar-sm"
|
||||||
[resultCount]="(resultsRD$ | async)?.pageInfo?.totalElements"
|
[resultCount]="(resultsRD$ | async)?.pageInfo?.totalElements"
|
||||||
(toggleSidebar)="closeSidebar()"
|
(toggleSidebar)="closeSidebar()"
|
||||||
|
@@ -6,6 +6,7 @@ import { Store } from '@ngrx/store';
|
|||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { cold, hot } from 'jasmine-marbles';
|
import { cold, hot } from 'jasmine-marbles';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import 'rxjs/add/observable/of';
|
||||||
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
|
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
|
||||||
import { CommunityDataService } from '../core/data/community-data.service';
|
import { CommunityDataService } from '../core/data/community-data.service';
|
||||||
import { HostWindowService } from '../shared/host-window.service';
|
import { HostWindowService } from '../shared/host-window.service';
|
||||||
@@ -77,7 +78,8 @@ describe('SearchPageComponent', () => {
|
|||||||
provide: HostWindowService, useValue: jasmine.createSpyObj('hostWindowService',
|
provide: HostWindowService, useValue: jasmine.createSpyObj('hostWindowService',
|
||||||
{
|
{
|
||||||
isXs: Observable.of(true),
|
isXs: Observable.of(true),
|
||||||
isSm: Observable.of(false)
|
isSm: Observable.of(false),
|
||||||
|
isXsOrSm: Observable.of(true)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@@ -34,7 +34,7 @@ export class SearchPageComponent implements OnInit {
|
|||||||
searchOptions$: Observable<PaginatedSearchOptions>;
|
searchOptions$: Observable<PaginatedSearchOptions>;
|
||||||
sortConfig: SortOptions;
|
sortConfig: SortOptions;
|
||||||
scopeListRD$: Observable<RemoteData<PaginatedList<Community>>>;
|
scopeListRD$: Observable<RemoteData<PaginatedList<Community>>>;
|
||||||
isMobileView$: Observable<boolean>;
|
isXsOrSm$: Observable<boolean>;
|
||||||
pageSize;
|
pageSize;
|
||||||
pageSizeOptions;
|
pageSizeOptions;
|
||||||
defaults = {
|
defaults = {
|
||||||
@@ -52,11 +52,7 @@ export class SearchPageComponent implements OnInit {
|
|||||||
private sidebarService: SearchSidebarService,
|
private sidebarService: SearchSidebarService,
|
||||||
private windowService: HostWindowService,
|
private windowService: HostWindowService,
|
||||||
private filterService: SearchFilterService) {
|
private filterService: SearchFilterService) {
|
||||||
this.isMobileView$ = Observable.combineLatest(
|
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
||||||
this.windowService.isXs(),
|
|
||||||
this.windowService.isSm(),
|
|
||||||
((isXs, isSm) => isXs || isSm)
|
|
||||||
);
|
|
||||||
this.scopeListRD$ = communityService.findAll();
|
this.scopeListRD$ = communityService.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -7,7 +7,7 @@ import { Component } from '@angular/core';
|
|||||||
import { SearchService } from './search.service';
|
import { SearchService } from './search.service';
|
||||||
import { ItemDataService } from './../../core/data/item-data.service';
|
import { ItemDataService } from './../../core/data/item-data.service';
|
||||||
import { ViewMode } from '../../+search-page/search-options.model';
|
import { ViewMode } from '../../+search-page/search-options.model';
|
||||||
import { RouteService } from '../../shared/route.service';
|
import { RouteService } from '../../shared/services/route.service';
|
||||||
import { GLOBAL_CONFIG } from '../../../config';
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
|
||||||
import { ActivatedRoute, Router, UrlTree } from '@angular/router';
|
import { ActivatedRoute, Router, UrlTree } from '@angular/router';
|
||||||
|
@@ -23,6 +23,7 @@ import { RequestService } from '../../core/data/request.service';
|
|||||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||||
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
||||||
|
import { configureRequest } from '../../core/shared/operators';
|
||||||
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
||||||
import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
|
import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
@@ -78,7 +79,7 @@ export class SearchService implements OnDestroy {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
tap((request: RestRequest) => this.requestService.configure(request)),
|
configureRequest(this.requestService)
|
||||||
);
|
);
|
||||||
const requestEntryObs = requestObs.pipe(
|
const requestEntryObs = requestObs.pipe(
|
||||||
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
||||||
@@ -153,7 +154,7 @@ export class SearchService implements OnDestroy {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
tap((request: RestRequest) => this.requestService.configure(request)),
|
configureRequest(this.requestService)
|
||||||
);
|
);
|
||||||
|
|
||||||
const requestEntryObs = requestObs.pipe(
|
const requestEntryObs = requestObs.pipe(
|
||||||
@@ -188,7 +189,7 @@ export class SearchService implements OnDestroy {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
tap((request: RestRequest) => this.requestService.configure(request)),
|
configureRequest(this.requestService)
|
||||||
);
|
);
|
||||||
|
|
||||||
const requestEntryObs = requestObs.pipe(
|
const requestEntryObs = requestObs.pipe(
|
||||||
|
@@ -3,6 +3,7 @@ import { SearchSidebarService } from './search-sidebar.service';
|
|||||||
import { AppState } from '../../app.reducer';
|
import { AppState } from '../../app.reducer';
|
||||||
import { async, inject, TestBed } from '@angular/core/testing';
|
import { async, inject, TestBed } from '@angular/core/testing';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import 'rxjs/add/observable/of';
|
||||||
import { SearchSidebarCollapseAction, SearchSidebarExpandAction } from './search-sidebar.actions';
|
import { SearchSidebarCollapseAction, SearchSidebarExpandAction } from './search-sidebar.actions';
|
||||||
import { HostWindowService } from '../../shared/host-window.service';
|
import { HostWindowService } from '../../shared/host-window.service';
|
||||||
|
|
||||||
@@ -17,7 +18,8 @@ describe('SearchSidebarService', () => {
|
|||||||
const windowService = jasmine.createSpyObj('hostWindowService',
|
const windowService = jasmine.createSpyObj('hostWindowService',
|
||||||
{
|
{
|
||||||
isXs: Observable.of(true),
|
isXs: Observable.of(true),
|
||||||
isSm: Observable.of(false)
|
isSm: Observable.of(false),
|
||||||
|
isXsOrSm: Observable.of(true)
|
||||||
});
|
});
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
|
@@ -11,22 +11,17 @@ const sidebarCollapsedSelector = createSelector(sidebarStateSelector, (sidebar:
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SearchSidebarService {
|
export class SearchSidebarService {
|
||||||
private isMobileView: Observable<boolean>;
|
private isXsOrSm$: Observable<boolean>;
|
||||||
private isCollapsdeInStored: Observable<boolean>;
|
private isCollapsdeInStored: Observable<boolean>;
|
||||||
|
|
||||||
constructor(private store: Store<AppState>, private windowService: HostWindowService) {
|
constructor(private store: Store<AppState>, private windowService: HostWindowService) {
|
||||||
this.isMobileView =
|
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
||||||
Observable.combineLatest(
|
|
||||||
this.windowService.isXs(),
|
|
||||||
this.windowService.isSm(),
|
|
||||||
((isXs, isSm) => isXs || isSm)
|
|
||||||
);
|
|
||||||
this.isCollapsdeInStored = this.store.select(sidebarCollapsedSelector);
|
this.isCollapsdeInStored = this.store.select(sidebarCollapsedSelector);
|
||||||
}
|
}
|
||||||
|
|
||||||
get isCollapsed(): Observable<boolean> {
|
get isCollapsed(): Observable<boolean> {
|
||||||
return Observable.combineLatest(
|
return Observable.combineLatest(
|
||||||
this.isMobileView,
|
this.isXsOrSm$,
|
||||||
this.isCollapsdeInStored,
|
this.isCollapsdeInStored,
|
||||||
(mobile, store) => mobile ? store : true);
|
(mobile, store) => mobile ? store : true);
|
||||||
}
|
}
|
||||||
|
@@ -13,6 +13,8 @@ import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
|
|||||||
{ path: 'items', loadChildren: './+item-page/item-page.module#ItemPageModule' },
|
{ path: 'items', loadChildren: './+item-page/item-page.module#ItemPageModule' },
|
||||||
{ path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' },
|
{ path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' },
|
||||||
{ path: 'admin', loadChildren: './+admin/admin.module#AdminModule' },
|
{ path: 'admin', loadChildren: './+admin/admin.module#AdminModule' },
|
||||||
|
{ path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' },
|
||||||
|
{ path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' },
|
||||||
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
|
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
|
@@ -26,12 +26,14 @@ import { HostWindowResizeAction } from './shared/host-window.actions';
|
|||||||
import { MetadataService } from './core/metadata/metadata.service';
|
import { MetadataService } from './core/metadata/metadata.service';
|
||||||
|
|
||||||
import { GLOBAL_CONFIG, ENV_CONFIG } from '../config';
|
import { GLOBAL_CONFIG, ENV_CONFIG } from '../config';
|
||||||
import { NativeWindowRef, NativeWindowService } from './shared/window.service';
|
import { NativeWindowRef, NativeWindowService } from './shared/services/window.service';
|
||||||
|
|
||||||
import { MockTranslateLoader } from './shared/mocks/mock-translate-loader';
|
import { MockTranslateLoader } from './shared/mocks/mock-translate-loader';
|
||||||
import { MockMetadataService } from './shared/mocks/mock-metadata-service';
|
import { MockMetadataService } from './shared/mocks/mock-metadata-service';
|
||||||
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
|
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
|
||||||
import { AngularticsMock } from './shared/mocks/mock-angulartics.service';
|
import { AngularticsMock } from './shared/mocks/mock-angulartics.service';
|
||||||
|
import { AuthServiceMock } from './shared/mocks/mock-auth.service';
|
||||||
|
import { AuthService } from './core/auth/auth.service';
|
||||||
|
|
||||||
let comp: AppComponent;
|
let comp: AppComponent;
|
||||||
let fixture: ComponentFixture<AppComponent>;
|
let fixture: ComponentFixture<AppComponent>;
|
||||||
@@ -59,6 +61,7 @@ describe('App component', () => {
|
|||||||
{ provide: NativeWindowService, useValue: new NativeWindowRef() },
|
{ provide: NativeWindowService, useValue: new NativeWindowRef() },
|
||||||
{ provide: MetadataService, useValue: new MockMetadataService() },
|
{ provide: MetadataService, useValue: new MockMetadataService() },
|
||||||
{ provide: Angulartics2GoogleAnalytics, useValue: new AngularticsMock() },
|
{ provide: Angulartics2GoogleAnalytics, useValue: new AngularticsMock() },
|
||||||
|
{ provide: AuthService, useValue: new AuthServiceMock() },
|
||||||
AppComponent
|
AppComponent
|
||||||
],
|
],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
@@ -9,7 +9,9 @@ import { GLOBAL_CONFIG, GlobalConfig } from '../config';
|
|||||||
import { MetadataService } from './core/metadata/metadata.service';
|
import { MetadataService } from './core/metadata/metadata.service';
|
||||||
import { HostWindowResizeAction } from './shared/host-window.actions';
|
import { HostWindowResizeAction } from './shared/host-window.actions';
|
||||||
import { HostWindowState } from './shared/host-window.reducer';
|
import { HostWindowState } from './shared/host-window.reducer';
|
||||||
import { NativeWindowRef, NativeWindowService } from './shared/window.service';
|
import { NativeWindowRef, NativeWindowService } from './shared/services/window.service';
|
||||||
|
import { isAuthenticated } from './core/auth/selectors';
|
||||||
|
import { AuthService } from './core/auth/auth.service';
|
||||||
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
|
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -27,7 +29,8 @@ export class AppComponent implements OnInit {
|
|||||||
private translate: TranslateService,
|
private translate: TranslateService,
|
||||||
private store: Store<HostWindowState>,
|
private store: Store<HostWindowState>,
|
||||||
private metadata: MetadataService,
|
private metadata: MetadataService,
|
||||||
private angulartics2GoogleAnalytics: Angulartics2GoogleAnalytics
|
private angulartics2GoogleAnalytics: Angulartics2GoogleAnalytics,
|
||||||
|
private authService: AuthService
|
||||||
) {
|
) {
|
||||||
// this language will be used as a fallback when a translation isn't found in the current language
|
// this language will be used as a fallback when a translation isn't found in the current language
|
||||||
translate.setDefaultLang('en');
|
translate.setDefaultLang('en');
|
||||||
@@ -46,6 +49,13 @@ export class AppComponent implements OnInit {
|
|||||||
const color: string = this.config.production ? 'red' : 'green';
|
const color: string = this.config.production ? 'red' : 'green';
|
||||||
console.info(`Environment: %c${env}`, `color: ${color}; font-weight: bold;`);
|
console.info(`Environment: %c${env}`, `color: ${color}; font-weight: bold;`);
|
||||||
this.dispatchWindowSize(this._window.nativeWindow.innerWidth, this._window.nativeWindow.innerHeight);
|
this.dispatchWindowSize(this._window.nativeWindow.innerWidth, this._window.nativeWindow.innerHeight);
|
||||||
|
|
||||||
|
// Whether is not authenticathed try to retrieve a possible stored auth token
|
||||||
|
this.store.select(isAuthenticated)
|
||||||
|
.take(1)
|
||||||
|
.filter((authenticated) => !authenticated)
|
||||||
|
.subscribe((authenticated) => this.authService.checkAuthenticationToken());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('window:resize', ['$event'])
|
@HostListener('window:resize', ['$event'])
|
||||||
|
@@ -30,6 +30,7 @@ import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
|
|||||||
import { DSpaceRouterStateSerializer } from './shared/ngrx/dspace-router-state-serializer';
|
import { DSpaceRouterStateSerializer } from './shared/ngrx/dspace-router-state-serializer';
|
||||||
import { NotificationsBoardComponent } from './shared/notifications/notifications-board/notifications-board.component';
|
import { NotificationsBoardComponent } from './shared/notifications/notifications-board/notifications-board.component';
|
||||||
import { NotificationComponent } from './shared/notifications/notification/notification.component';
|
import { NotificationComponent } from './shared/notifications/notification/notification.component';
|
||||||
|
import { SharedModule } from './shared/shared.module';
|
||||||
|
|
||||||
export function getConfig() {
|
export function getConfig() {
|
||||||
return ENV_CONFIG;
|
return ENV_CONFIG;
|
||||||
@@ -53,6 +54,7 @@ if (!ENV_CONFIG.production) {
|
|||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
SharedModule,
|
||||||
HttpClientModule,
|
HttpClientModule,
|
||||||
AppRoutingModule,
|
AppRoutingModule,
|
||||||
CoreModule.forRoot(),
|
CoreModule.forRoot(),
|
||||||
|
@@ -33,3 +33,5 @@ export const appReducers: ActionReducerMap<AppState> = {
|
|||||||
searchFilter: filterReducer,
|
searchFilter: filterReducer,
|
||||||
truncatable: truncatableReducer
|
truncatable: truncatableReducer
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const routerStateSelector = (state: AppState) => state.router;
|
||||||
|
23
src/app/core/auth/auth-object-factory.ts
Normal file
23
src/app/core/auth/auth-object-factory.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { AuthType } from './auth-type';
|
||||||
|
import { GenericConstructor } from '../shared/generic-constructor';
|
||||||
|
import { NormalizedAuthStatus } from './models/normalized-auth-status.model';
|
||||||
|
import { NormalizedDSpaceObject } from '../cache/models/normalized-dspace-object.model';
|
||||||
|
import { NormalizedEpersonModel } from '../eperson/models/NormalizedEperson.model';
|
||||||
|
|
||||||
|
export class AuthObjectFactory {
|
||||||
|
public static getConstructor(type): GenericConstructor<NormalizedDSpaceObject> {
|
||||||
|
switch (type) {
|
||||||
|
case AuthType.Eperson: {
|
||||||
|
return NormalizedEpersonModel
|
||||||
|
}
|
||||||
|
|
||||||
|
case AuthType.Status: {
|
||||||
|
return NormalizedAuthStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
65
src/app/core/auth/auth-request.service.ts
Normal file
65
src/app/core/auth/auth-request.service.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { Inject, Injectable } from '@angular/core';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
|
import { RequestService } from '../data/request.service';
|
||||||
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { AuthGetRequest, AuthPostRequest, PostRequest, RestRequest } from '../data/request.models';
|
||||||
|
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
||||||
|
import { AuthStatusResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models';
|
||||||
|
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthRequestService {
|
||||||
|
protected linkName = 'authn';
|
||||||
|
protected browseEndpoint = '';
|
||||||
|
|
||||||
|
constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected responseCache: ResponseCacheService,
|
||||||
|
protected requestService: RequestService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fetchRequest(request: RestRequest): Observable<any> {
|
||||||
|
const [successResponse, errorResponse] = this.responseCache.get(request.href)
|
||||||
|
.map((entry: ResponseCacheEntry) => entry.response)
|
||||||
|
// TODO to review when https://github.com/DSpace/dspace-angular/issues/217 will be fixed
|
||||||
|
.do(() => this.responseCache.remove(request.href))
|
||||||
|
.partition((response: RestResponse) => response.isSuccessful);
|
||||||
|
return Observable.merge(
|
||||||
|
errorResponse.flatMap((response: ErrorResponse) =>
|
||||||
|
Observable.throw(new Error(response.errorMessage))),
|
||||||
|
successResponse
|
||||||
|
.filter((response: AuthStatusResponse) => isNotEmpty(response))
|
||||||
|
.map((response: AuthStatusResponse) => response.response)
|
||||||
|
.distinctUntilChanged());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getEndpointByMethod(endpoint: string, method: string): string {
|
||||||
|
return isNotEmpty(method) ? `${endpoint}/${method}` : `${endpoint}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public postToEndpoint(method: string, body: any, options?: HttpOptions): Observable<any> {
|
||||||
|
return this.halService.getEndpoint(this.linkName)
|
||||||
|
.filter((href: string) => isNotEmpty(href))
|
||||||
|
.map((endpointURL) => this.getEndpointByMethod(endpointURL, method))
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.map((endpointURL: string) => new AuthPostRequest(this.requestService.generateRequestId(), endpointURL, body, options))
|
||||||
|
.do((request: PostRequest) => this.requestService.configure(request, true))
|
||||||
|
.flatMap((request: PostRequest) => this.fetchRequest(request))
|
||||||
|
.distinctUntilChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getRequest(method: string, options?: HttpOptions): Observable<any> {
|
||||||
|
return this.halService.getEndpoint(this.linkName)
|
||||||
|
.filter((href: string) => isNotEmpty(href))
|
||||||
|
.map((endpointURL) => this.getEndpointByMethod(endpointURL, method))
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.map((endpointURL: string) => new AuthGetRequest(this.requestService.generateRequestId(), endpointURL, options))
|
||||||
|
.do((request: PostRequest) => this.requestService.configure(request, true))
|
||||||
|
.flatMap((request: PostRequest) => this.fetchRequest(request))
|
||||||
|
.distinctUntilChanged();
|
||||||
|
}
|
||||||
|
}
|
117
src/app/core/auth/auth-response-parsing.service.spec.ts
Normal file
117
src/app/core/auth/auth-response-parsing.service.spec.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { AuthStatusResponse } from '../cache/response-cache.models';
|
||||||
|
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
|
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { CoreState } from '../core.reducers';
|
||||||
|
import { AuthStatus } from './models/auth-status.model';
|
||||||
|
import { AuthResponseParsingService } from './auth-response-parsing.service';
|
||||||
|
import { AuthGetRequest, AuthPostRequest } from '../data/request.models';
|
||||||
|
|
||||||
|
describe('ConfigResponseParsingService', () => {
|
||||||
|
let service: AuthResponseParsingService;
|
||||||
|
|
||||||
|
const EnvConfig = {} as GlobalConfig;
|
||||||
|
const store = {} as Store<CoreState>;
|
||||||
|
const objectCacheService = new ObjectCacheService(store);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = new AuthResponseParsingService(EnvConfig, objectCacheService);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parse', () => {
|
||||||
|
const validRequest = new AuthPostRequest(
|
||||||
|
'69f375b5-19f4-4453-8c7a-7dc5c55aafbb',
|
||||||
|
'https://rest.api/dspace-spring-rest/api/authn/login',
|
||||||
|
'password=test&user=myself@testshib.org');
|
||||||
|
|
||||||
|
const validRequest2 = new AuthGetRequest(
|
||||||
|
'69f375b5-19f4-4453-8c7a-7dc5c55aafbb',
|
||||||
|
'https://rest.api/dspace-spring-rest/api/authn/status');
|
||||||
|
|
||||||
|
const validResponse = {
|
||||||
|
payload: {
|
||||||
|
authenticated: true,
|
||||||
|
id: null,
|
||||||
|
okay: true,
|
||||||
|
token: {
|
||||||
|
accessToken: 'eyJhbGciOiJIUzI1NiJ9.eyJlaWQiOiI0ZGM3MGFiNS1jZDczLTQ5MmYtYjAwNy0zMTc5ZDJkOTI5NmIiLCJzZyI6W10sImV4cCI6MTUyNjMxODMyMn0.ASmvcbJFBfzhN7D5ncloWnaVZr5dLtgTuOgHaCKiimc',
|
||||||
|
expires: 1526318322000
|
||||||
|
},
|
||||||
|
} as AuthStatus,
|
||||||
|
statusCode: '200'
|
||||||
|
};
|
||||||
|
|
||||||
|
const validResponse1 = {
|
||||||
|
payload: {},
|
||||||
|
statusCode: '404'
|
||||||
|
};
|
||||||
|
|
||||||
|
const validResponse2 = {
|
||||||
|
payload: {
|
||||||
|
authenticated: true,
|
||||||
|
id: null,
|
||||||
|
okay: true,
|
||||||
|
type: 'status',
|
||||||
|
_embedded: {
|
||||||
|
eperson: {
|
||||||
|
canLogIn: true,
|
||||||
|
email: 'myself@testshib.org',
|
||||||
|
groups: [],
|
||||||
|
handle: null,
|
||||||
|
id: '4dc70ab5-cd73-492f-b007-3179d2d9296b',
|
||||||
|
lastActive: '2018-05-14T17:03:31.277+0000',
|
||||||
|
metadata: [
|
||||||
|
{
|
||||||
|
key: 'eperson.firstname',
|
||||||
|
language: null,
|
||||||
|
value: 'User'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'eperson.lastname',
|
||||||
|
language: null,
|
||||||
|
value: 'Test'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'eperson.language',
|
||||||
|
language: null,
|
||||||
|
value: 'en'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
name: 'User Test',
|
||||||
|
netid: 'myself@testshib.org',
|
||||||
|
requireCertificate: false,
|
||||||
|
selfRegistered: false,
|
||||||
|
type: 'eperson',
|
||||||
|
uuid: '4dc70ab5-cd73-492f-b007-3179d2d9296b',
|
||||||
|
_links: {
|
||||||
|
self: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/eperson/epersons/4dc70ab5-cd73-492f-b007-3179d2d9296b'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_links: {
|
||||||
|
eperson: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/eperson/epersons/4dc70ab5-cd73-492f-b007-3179d2d9296b',
|
||||||
|
self: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/authn/status'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
statusCode: '200'
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should return a AuthStatusResponse if data contains a valid AuthStatus object as payload', () => {
|
||||||
|
const response = service.parse(validRequest, validResponse);
|
||||||
|
expect(response.constructor).toBe(AuthStatusResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a AuthStatusResponse if data contains a valid endpoint response', () => {
|
||||||
|
const response = service.parse(validRequest2, validResponse2);
|
||||||
|
expect(response.constructor).toBe(AuthStatusResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a AuthStatusResponse if data contains an empty 404 endpoint response', () => {
|
||||||
|
const response = service.parse(validRequest, validResponse1);
|
||||||
|
expect(response.constructor).toBe(AuthStatusResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
36
src/app/core/auth/auth-response-parsing.service.ts
Normal file
36
src/app/core/auth/auth-response-parsing.service.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Inject, Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { AuthObjectFactory } from './auth-object-factory';
|
||||||
|
import { BaseResponseParsingService } from '../data/base-response-parsing.service';
|
||||||
|
import { AuthStatusResponse, RestResponse } from '../cache/response-cache.models';
|
||||||
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { ResponseParsingService } from '../data/parsing.service';
|
||||||
|
import { RestRequest } from '../data/request.models';
|
||||||
|
import { AuthType } from './auth-type';
|
||||||
|
import { AuthStatus } from './models/auth-status.model';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
|
||||||
|
|
||||||
|
protected objectFactory = AuthObjectFactory;
|
||||||
|
protected toCache = false;
|
||||||
|
|
||||||
|
constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
||||||
|
protected objectCache: ObjectCacheService,) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||||
|
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === '200' || data.statusCode === 'OK')) {
|
||||||
|
const response = this.process<AuthStatus, AuthType>(data.payload, request.href);
|
||||||
|
return new AuthStatusResponse(response[Object.keys(response)[0]][0], data.statusCode);
|
||||||
|
} else {
|
||||||
|
return new AuthStatusResponse(data.payload as AuthStatus, data.statusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
4
src/app/core/auth/auth-type.ts
Normal file
4
src/app/core/auth/auth-type.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export enum AuthType {
|
||||||
|
Eperson = 'eperson',
|
||||||
|
Status = 'status'
|
||||||
|
}
|
346
src/app/core/auth/auth.actions.ts
Normal file
346
src/app/core/auth/auth.actions.ts
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
// import @ngrx
|
||||||
|
import { Action } from '@ngrx/store';
|
||||||
|
|
||||||
|
// import type function
|
||||||
|
import { type } from '../../shared/ngrx/type';
|
||||||
|
|
||||||
|
// import models
|
||||||
|
import { Eperson } from '../eperson/models/eperson.model';
|
||||||
|
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||||
|
|
||||||
|
export const AuthActionTypes = {
|
||||||
|
AUTHENTICATE: type('dspace/auth/AUTHENTICATE'),
|
||||||
|
AUTHENTICATE_ERROR: type('dspace/auth/AUTHENTICATE_ERROR'),
|
||||||
|
AUTHENTICATE_SUCCESS: type('dspace/auth/AUTHENTICATE_SUCCESS'),
|
||||||
|
AUTHENTICATED: type('dspace/auth/AUTHENTICATED'),
|
||||||
|
AUTHENTICATED_ERROR: type('dspace/auth/AUTHENTICATED_ERROR'),
|
||||||
|
AUTHENTICATED_SUCCESS: type('dspace/auth/AUTHENTICATED_SUCCESS'),
|
||||||
|
CHECK_AUTHENTICATION_TOKEN: type('dspace/auth/CHECK_AUTHENTICATION_TOKEN'),
|
||||||
|
CHECK_AUTHENTICATION_TOKEN_ERROR: type('dspace/auth/CHECK_AUTHENTICATION_TOKEN_ERROR'),
|
||||||
|
REDIRECT_TOKEN_EXPIRED: type('dspace/auth/REDIRECT_TOKEN_EXPIRED'),
|
||||||
|
REDIRECT_AUTHENTICATION_REQUIRED: type('dspace/auth/REDIRECT_AUTHENTICATION_REQUIRED'),
|
||||||
|
REFRESH_TOKEN: type('dspace/auth/REFRESH_TOKEN'),
|
||||||
|
REFRESH_TOKEN_SUCCESS: type('dspace/auth/REFRESH_TOKEN_SUCCESS'),
|
||||||
|
REFRESH_TOKEN_ERROR: type('dspace/auth/REFRESH_TOKEN_ERROR'),
|
||||||
|
ADD_MESSAGE: type('dspace/auth/ADD_MESSAGE'),
|
||||||
|
RESET_MESSAGES: type('dspace/auth/RESET_MESSAGES'),
|
||||||
|
LOG_OUT: type('dspace/auth/LOG_OUT'),
|
||||||
|
LOG_OUT_ERROR: type('dspace/auth/LOG_OUT_ERROR'),
|
||||||
|
LOG_OUT_SUCCESS: type('dspace/auth/LOG_OUT_SUCCESS'),
|
||||||
|
REGISTRATION: type('dspace/auth/REGISTRATION'),
|
||||||
|
REGISTRATION_ERROR: type('dspace/auth/REGISTRATION_ERROR'),
|
||||||
|
REGISTRATION_SUCCESS: type('dspace/auth/REGISTRATION_SUCCESS'),
|
||||||
|
SET_REDIRECT_URL: type('dspace/auth/SET_REDIRECT_URL'),
|
||||||
|
};
|
||||||
|
|
||||||
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticate.
|
||||||
|
* @class AuthenticateAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class AuthenticateAction implements Action {
|
||||||
|
public type: string = AuthActionTypes.AUTHENTICATE;
|
||||||
|
payload: {
|
||||||
|
email: string;
|
||||||
|
password: string
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(email: string, password: string) {
|
||||||
|
this.payload = { email, password };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if user is authenticated.
|
||||||
|
* @class AuthenticatedAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class AuthenticatedAction implements Action {
|
||||||
|
public type: string = AuthActionTypes.AUTHENTICATED;
|
||||||
|
payload: AuthTokenInfo;
|
||||||
|
|
||||||
|
constructor(token: AuthTokenInfo) {
|
||||||
|
this.payload = token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticated check success.
|
||||||
|
* @class AuthenticatedSuccessAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class AuthenticatedSuccessAction implements Action {
|
||||||
|
public type: string = AuthActionTypes.AUTHENTICATED_SUCCESS;
|
||||||
|
payload: {
|
||||||
|
authenticated: boolean;
|
||||||
|
authToken: AuthTokenInfo;
|
||||||
|
user: Eperson
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(authenticated: boolean, authToken: AuthTokenInfo, user: Eperson) {
|
||||||
|
this.payload = { authenticated, authToken, user };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticated check error.
|
||||||
|
* @class AuthenticatedErrorAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class AuthenticatedErrorAction implements Action {
|
||||||
|
public type: string = AuthActionTypes.AUTHENTICATED_ERROR;
|
||||||
|
payload: Error;
|
||||||
|
|
||||||
|
constructor(payload: Error) {
|
||||||
|
this.payload = payload ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authentication error.
|
||||||
|
* @class AuthenticationErrorAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class AuthenticationErrorAction implements Action {
|
||||||
|
public type: string = AuthActionTypes.AUTHENTICATE_ERROR;
|
||||||
|
payload: Error;
|
||||||
|
|
||||||
|
constructor(payload: Error) {
|
||||||
|
this.payload = payload ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authentication success.
|
||||||
|
* @class AuthenticationSuccessAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class AuthenticationSuccessAction implements Action {
|
||||||
|
public type: string = AuthActionTypes.AUTHENTICATE_SUCCESS;
|
||||||
|
payload: AuthTokenInfo;
|
||||||
|
|
||||||
|
constructor(token: AuthTokenInfo) {
|
||||||
|
this.payload = token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if token is already present upon initial load.
|
||||||
|
* @class CheckAuthenticationTokenAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class CheckAuthenticationTokenAction implements Action {
|
||||||
|
public type: string = AuthActionTypes.CHECK_AUTHENTICATION_TOKEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check Authentication Token Error.
|
||||||
|
* @class CheckAuthenticationTokenErrorAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class CheckAuthenticationTokenErrorAction implements Action {
|
||||||
|
public type: string = AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign out.
|
||||||
|
* @class LogOutAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class LogOutAction implements Action {
|
||||||
|
public type: string = AuthActionTypes.LOG_OUT;
|
||||||
|
constructor(public payload?: any) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign out error.
|
||||||
|
* @class LogOutErrorAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class LogOutErrorAction implements Action {
|
||||||
|
public type: string = AuthActionTypes.LOG_OUT_ERROR;
|
||||||
|
payload: Error;
|
||||||
|
|
||||||
|
constructor(payload: Error) {
|
||||||
|
this.payload = payload ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign out success.
|
||||||
|
* @class LogOutSuccessAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class LogOutSuccessAction implements Action {
|
||||||
|
public type: string = AuthActionTypes.LOG_OUT_SUCCESS;
|
||||||
|
constructor(public payload?: any) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirect to login page when authentication is required.
|
||||||
|
* @class RedirectWhenAuthenticationIsRequiredAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class RedirectWhenAuthenticationIsRequiredAction implements Action {
|
||||||
|
public type: string = AuthActionTypes.REDIRECT_AUTHENTICATION_REQUIRED;
|
||||||
|
payload: string;
|
||||||
|
|
||||||
|
constructor(message: string) {
|
||||||
|
this.payload = message ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirect to login page when token is expired.
|
||||||
|
* @class RedirectWhenTokenExpiredAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class RedirectWhenTokenExpiredAction implements Action {
|
||||||
|
public type: string = AuthActionTypes.REDIRECT_TOKEN_EXPIRED;
|
||||||
|
payload: string;
|
||||||
|
|
||||||
|
constructor(message: string) {
|
||||||
|
this.payload = message ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh authentication token.
|
||||||
|
* @class RefreshTokenAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class RefreshTokenAction implements Action {
|
||||||
|
public type: string = AuthActionTypes.REFRESH_TOKEN;
|
||||||
|
payload: AuthTokenInfo;
|
||||||
|
|
||||||
|
constructor(token: AuthTokenInfo) {
|
||||||
|
this.payload = token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh authentication token success.
|
||||||
|
* @class RefreshTokenSuccessAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class RefreshTokenSuccessAction implements Action {
|
||||||
|
public type: string = AuthActionTypes.REFRESH_TOKEN_SUCCESS;
|
||||||
|
payload: AuthTokenInfo;
|
||||||
|
|
||||||
|
constructor(token: AuthTokenInfo) {
|
||||||
|
this.payload = token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh authentication token error.
|
||||||
|
* @class RefreshTokenErrorAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class RefreshTokenErrorAction implements Action {
|
||||||
|
public type: string = AuthActionTypes.REFRESH_TOKEN_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign up.
|
||||||
|
* @class RegistrationAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class RegistrationAction implements Action {
|
||||||
|
public type: string = AuthActionTypes.REGISTRATION;
|
||||||
|
payload: Eperson;
|
||||||
|
|
||||||
|
constructor(user: Eperson) {
|
||||||
|
this.payload = user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign up error.
|
||||||
|
* @class RegistrationErrorAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class RegistrationErrorAction implements Action {
|
||||||
|
public type: string = AuthActionTypes.REGISTRATION_ERROR;
|
||||||
|
payload: Error;
|
||||||
|
|
||||||
|
constructor(payload: Error) {
|
||||||
|
this.payload = payload ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign up success.
|
||||||
|
* @class RegistrationSuccessAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class RegistrationSuccessAction implements Action {
|
||||||
|
public type: string = AuthActionTypes.REGISTRATION_SUCCESS;
|
||||||
|
payload: Eperson;
|
||||||
|
|
||||||
|
constructor(user: Eperson) {
|
||||||
|
this.payload = user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add uthentication message.
|
||||||
|
* @class AddAuthenticationMessageAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class AddAuthenticationMessageAction implements Action {
|
||||||
|
public type: string = AuthActionTypes.ADD_MESSAGE;
|
||||||
|
payload: string;
|
||||||
|
|
||||||
|
constructor(message: string) {
|
||||||
|
this.payload = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset error.
|
||||||
|
* @class ResetAuthenticationMessagesAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class ResetAuthenticationMessagesAction implements Action {
|
||||||
|
public type: string = AuthActionTypes.RESET_MESSAGES;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the redirect url.
|
||||||
|
* @class SetRedirectUrlAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class SetRedirectUrlAction implements Action {
|
||||||
|
public type: string = AuthActionTypes.SET_REDIRECT_URL;
|
||||||
|
payload: string;
|
||||||
|
|
||||||
|
constructor(url: string) {
|
||||||
|
this.payload = url ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tslint:enable:max-classes-per-file */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actions type.
|
||||||
|
* @type {AuthActions}
|
||||||
|
*/
|
||||||
|
export type AuthActions
|
||||||
|
= AuthenticateAction
|
||||||
|
| AuthenticatedAction
|
||||||
|
| AuthenticatedErrorAction
|
||||||
|
| AuthenticatedSuccessAction
|
||||||
|
| AuthenticationErrorAction
|
||||||
|
| AuthenticationSuccessAction
|
||||||
|
| CheckAuthenticationTokenAction
|
||||||
|
| CheckAuthenticationTokenErrorAction
|
||||||
|
| RedirectWhenAuthenticationIsRequiredAction
|
||||||
|
| RedirectWhenTokenExpiredAction
|
||||||
|
| RegistrationAction
|
||||||
|
| RegistrationErrorAction
|
||||||
|
| RegistrationSuccessAction
|
||||||
|
| AddAuthenticationMessageAction
|
||||||
|
| ResetAuthenticationMessagesAction;
|
204
src/app/core/auth/auth.effects.spec.ts
Normal file
204
src/app/core/auth/auth.effects.spec.ts
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { provideMockActions } from '@ngrx/effects/testing';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { cold, hot } from 'jasmine-marbles';
|
||||||
|
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import 'rxjs/add/observable/of'
|
||||||
|
|
||||||
|
import { AuthEffects } from './auth.effects';
|
||||||
|
import {
|
||||||
|
AuthActionTypes,
|
||||||
|
AuthenticatedAction,
|
||||||
|
AuthenticatedErrorAction,
|
||||||
|
AuthenticatedSuccessAction,
|
||||||
|
AuthenticationErrorAction,
|
||||||
|
AuthenticationSuccessAction,
|
||||||
|
CheckAuthenticationTokenErrorAction,
|
||||||
|
LogOutErrorAction,
|
||||||
|
LogOutSuccessAction,
|
||||||
|
RefreshTokenErrorAction,
|
||||||
|
RefreshTokenSuccessAction
|
||||||
|
} from './auth.actions';
|
||||||
|
import { AuthServiceStub } from '../../shared/testing/auth-service-stub';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { TruncatablesState } from '../../shared/truncatable/truncatable.reducer';
|
||||||
|
|
||||||
|
import { EpersonMock } from '../../shared/testing/eperson-mock';
|
||||||
|
|
||||||
|
describe('AuthEffects', () => {
|
||||||
|
let authEffects: AuthEffects;
|
||||||
|
let actions: Observable<any>;
|
||||||
|
|
||||||
|
const authServiceStub = new AuthServiceStub();
|
||||||
|
const store: Store<TruncatablesState> = jasmine.createSpyObj('store', {
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
dispatch: {},
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
|
select: Observable.of(true)
|
||||||
|
});
|
||||||
|
const token = authServiceStub.getToken();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
AuthEffects,
|
||||||
|
{provide: AuthService, useValue: authServiceStub},
|
||||||
|
{provide: Store, useValue: store},
|
||||||
|
provideMockActions(() => actions),
|
||||||
|
// other providers
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
authEffects = TestBed.get(AuthEffects);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('authenticate$', () => {
|
||||||
|
describe('when credentials are correct', () => {
|
||||||
|
it('should return a AUTHENTICATE_SUCCESS action in response to a AUTHENTICATE action', () => {
|
||||||
|
actions = hot('--a-', {
|
||||||
|
a: {
|
||||||
|
type: AuthActionTypes.AUTHENTICATE,
|
||||||
|
payload: {email: 'user', password: 'password'}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const expected = cold('--b-', {b: new AuthenticationSuccessAction(token)});
|
||||||
|
|
||||||
|
expect(authEffects.authenticate$).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when credentials are wrong', () => {
|
||||||
|
it('should return a AUTHENTICATE_ERROR action in response to a AUTHENTICATE action', () => {
|
||||||
|
spyOn((authEffects as any).authService, 'authenticate').and.returnValue(Observable.throw(new Error('Message Error test')));
|
||||||
|
|
||||||
|
actions = hot('--a-', {
|
||||||
|
a: {
|
||||||
|
type: AuthActionTypes.AUTHENTICATE,
|
||||||
|
payload: {email: 'user', password: 'wrongpassword'}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const expected = cold('--b-', {b: new AuthenticationErrorAction(new Error('Message Error test'))});
|
||||||
|
|
||||||
|
expect(authEffects.authenticate$).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('authenticateSuccess$', () => {
|
||||||
|
|
||||||
|
it('should return a AUTHENTICATED action in response to a AUTHENTICATE_SUCCESS action', () => {
|
||||||
|
actions = hot('--a-', {a: {type: AuthActionTypes.AUTHENTICATE_SUCCESS, payload: token}});
|
||||||
|
|
||||||
|
const expected = cold('--b-', {b: new AuthenticatedAction(token)});
|
||||||
|
|
||||||
|
expect(authEffects.authenticateSuccess$).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('authenticated$', () => {
|
||||||
|
|
||||||
|
describe('when token is valid', () => {
|
||||||
|
it('should return a AUTHENTICATED_SUCCESS action in response to a AUTHENTICATED action', () => {
|
||||||
|
actions = hot('--a-', {a: {type: AuthActionTypes.AUTHENTICATED, payload: token}});
|
||||||
|
|
||||||
|
const expected = cold('--b-', {b: new AuthenticatedSuccessAction(true, token, EpersonMock)});
|
||||||
|
|
||||||
|
expect(authEffects.authenticated$).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when token is not valid', () => {
|
||||||
|
it('should return a AUTHENTICATED_ERROR action in response to a AUTHENTICATED action', () => {
|
||||||
|
spyOn((authEffects as any).authService, 'authenticatedUser').and.returnValue(Observable.throw(new Error('Message Error test')));
|
||||||
|
|
||||||
|
actions = hot('--a-', {a: {type: AuthActionTypes.AUTHENTICATED, payload: token}});
|
||||||
|
|
||||||
|
const expected = cold('--b-', {b: new AuthenticatedErrorAction(new Error('Message Error test'))});
|
||||||
|
|
||||||
|
expect(authEffects.authenticated$).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('checkToken$', () => {
|
||||||
|
|
||||||
|
describe('when check token succeeded', () => {
|
||||||
|
it('should return a AUTHENTICATED action in response to a CHECK_AUTHENTICATION_TOKEN action', () => {
|
||||||
|
|
||||||
|
actions = hot('--a-', {a: {type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN}});
|
||||||
|
|
||||||
|
const expected = cold('--b-', {b: new AuthenticatedAction(token)});
|
||||||
|
|
||||||
|
expect(authEffects.checkToken$).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when check token failed', () => {
|
||||||
|
it('should return a CHECK_AUTHENTICATION_TOKEN_ERROR action in response to a CHECK_AUTHENTICATION_TOKEN action', () => {
|
||||||
|
spyOn((authEffects as any).authService, 'hasValidAuthenticationToken').and.returnValue(Observable.throw(''));
|
||||||
|
|
||||||
|
actions = hot('--a-', {a: {type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN, payload: token}});
|
||||||
|
|
||||||
|
const expected = cold('--b-', {b: new CheckAuthenticationTokenErrorAction()});
|
||||||
|
|
||||||
|
expect(authEffects.checkToken$).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('refreshToken$', () => {
|
||||||
|
|
||||||
|
describe('when refresh token succeeded', () => {
|
||||||
|
it('should return a REFRESH_TOKEN_SUCCESS action in response to a REFRESH_TOKEN action', () => {
|
||||||
|
|
||||||
|
actions = hot('--a-', {a: {type: AuthActionTypes.REFRESH_TOKEN}});
|
||||||
|
|
||||||
|
const expected = cold('--b-', {b: new RefreshTokenSuccessAction(token)});
|
||||||
|
|
||||||
|
expect(authEffects.refreshToken$).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when refresh token failed', () => {
|
||||||
|
it('should return a REFRESH_TOKEN_ERROR action in response to a REFRESH_TOKEN action', () => {
|
||||||
|
spyOn((authEffects as any).authService, 'refreshAuthenticationToken').and.returnValue(Observable.throw(''));
|
||||||
|
|
||||||
|
actions = hot('--a-', {a: {type: AuthActionTypes.REFRESH_TOKEN, payload: token}});
|
||||||
|
|
||||||
|
const expected = cold('--b-', {b: new RefreshTokenErrorAction()});
|
||||||
|
|
||||||
|
expect(authEffects.refreshToken$).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('logOut$', () => {
|
||||||
|
|
||||||
|
describe('when refresh token succeeded', () => {
|
||||||
|
it('should return a LOG_OUT_SUCCESS action in response to a LOG_OUT action', () => {
|
||||||
|
|
||||||
|
actions = hot('--a-', {a: {type: AuthActionTypes.LOG_OUT}});
|
||||||
|
|
||||||
|
const expected = cold('--b-', {b: new LogOutSuccessAction()});
|
||||||
|
|
||||||
|
expect(authEffects.logOut$).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when refresh token failed', () => {
|
||||||
|
it('should return a REFRESH_TOKEN_ERROR action in response to a LOG_OUT action', () => {
|
||||||
|
spyOn((authEffects as any).authService, 'logout').and.returnValue(Observable.throw(new Error('Message Error test')));
|
||||||
|
|
||||||
|
actions = hot('--a-', {a: {type: AuthActionTypes.LOG_OUT, payload: token}});
|
||||||
|
|
||||||
|
const expected = cold('--b-', {b: new LogOutErrorAction(new Error('Message Error test'))});
|
||||||
|
|
||||||
|
expect(authEffects.logOut$).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
163
src/app/core/auth/auth.effects.ts
Normal file
163
src/app/core/auth/auth.effects.ts
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
// import @ngrx
|
||||||
|
import { Actions, Effect } from '@ngrx/effects';
|
||||||
|
import { Action, Store } from '@ngrx/store';
|
||||||
|
|
||||||
|
// import rxjs
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
|
||||||
|
// import services
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
// import actions
|
||||||
|
import {
|
||||||
|
AuthActionTypes,
|
||||||
|
AuthenticateAction,
|
||||||
|
AuthenticatedAction,
|
||||||
|
AuthenticatedErrorAction,
|
||||||
|
AuthenticatedSuccessAction,
|
||||||
|
AuthenticationErrorAction,
|
||||||
|
AuthenticationSuccessAction,
|
||||||
|
CheckAuthenticationTokenErrorAction,
|
||||||
|
LogOutErrorAction,
|
||||||
|
LogOutSuccessAction,
|
||||||
|
RefreshTokenAction,
|
||||||
|
RefreshTokenErrorAction,
|
||||||
|
RefreshTokenSuccessAction,
|
||||||
|
RegistrationAction,
|
||||||
|
RegistrationErrorAction,
|
||||||
|
RegistrationSuccessAction
|
||||||
|
} from './auth.actions';
|
||||||
|
import { Eperson } from '../eperson/models/eperson.model';
|
||||||
|
import { AuthStatus } from './models/auth-status.model';
|
||||||
|
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||||
|
import { AppState } from '../../app.reducer';
|
||||||
|
import { isAuthenticated } from './selectors';
|
||||||
|
import { StoreActionTypes } from '../../store.actions';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthEffects {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticate user.
|
||||||
|
* @method authenticate
|
||||||
|
*/
|
||||||
|
@Effect()
|
||||||
|
public authenticate$: Observable<Action> = this.actions$
|
||||||
|
.ofType(AuthActionTypes.AUTHENTICATE)
|
||||||
|
.switchMap((action: AuthenticateAction) => {
|
||||||
|
return this.authService.authenticate(action.payload.email, action.payload.password)
|
||||||
|
.first()
|
||||||
|
.map((response: AuthStatus) => new AuthenticationSuccessAction(response.token))
|
||||||
|
.catch((error) => Observable.of(new AuthenticationErrorAction(error)));
|
||||||
|
});
|
||||||
|
|
||||||
|
@Effect()
|
||||||
|
public authenticateSuccess$: Observable<Action> = this.actions$
|
||||||
|
.ofType(AuthActionTypes.AUTHENTICATE_SUCCESS)
|
||||||
|
.do((action: AuthenticationSuccessAction) => this.authService.storeToken(action.payload))
|
||||||
|
.map((action: AuthenticationSuccessAction) => new AuthenticatedAction(action.payload));
|
||||||
|
|
||||||
|
@Effect()
|
||||||
|
public authenticated$: Observable<Action> = this.actions$
|
||||||
|
.ofType(AuthActionTypes.AUTHENTICATED)
|
||||||
|
.switchMap((action: AuthenticatedAction) => {
|
||||||
|
return this.authService.authenticatedUser(action.payload)
|
||||||
|
.map((user: Eperson) => new AuthenticatedSuccessAction((user !== null), action.payload, user))
|
||||||
|
.catch((error) => Observable.of(new AuthenticatedErrorAction(error)));
|
||||||
|
});
|
||||||
|
|
||||||
|
// It means "reacts to this action but don't send another"
|
||||||
|
@Effect({dispatch: false})
|
||||||
|
public authenticatedError$: Observable<Action> = this.actions$
|
||||||
|
.ofType(AuthActionTypes.AUTHENTICATED_ERROR)
|
||||||
|
.do((action: LogOutSuccessAction) => this.authService.removeToken());
|
||||||
|
|
||||||
|
@Effect()
|
||||||
|
public checkToken$: Observable<Action> = this.actions$
|
||||||
|
.ofType(AuthActionTypes.CHECK_AUTHENTICATION_TOKEN)
|
||||||
|
.switchMap(() => {
|
||||||
|
return this.authService.hasValidAuthenticationToken()
|
||||||
|
.map((token: AuthTokenInfo) => new AuthenticatedAction(token))
|
||||||
|
.catch((error) => Observable.of(new CheckAuthenticationTokenErrorAction()));
|
||||||
|
});
|
||||||
|
|
||||||
|
@Effect()
|
||||||
|
public createUser$: Observable<Action> = this.actions$
|
||||||
|
.ofType(AuthActionTypes.REGISTRATION)
|
||||||
|
.debounceTime(500) // to remove when functionality is implemented
|
||||||
|
.switchMap((action: RegistrationAction) => {
|
||||||
|
return this.authService.create(action.payload)
|
||||||
|
.map((user: Eperson) => new RegistrationSuccessAction(user))
|
||||||
|
.catch((error) => Observable.of(new RegistrationErrorAction(error)));
|
||||||
|
});
|
||||||
|
|
||||||
|
@Effect()
|
||||||
|
public refreshToken$: Observable<Action> = this.actions$
|
||||||
|
.ofType(AuthActionTypes.REFRESH_TOKEN)
|
||||||
|
.switchMap((action: RefreshTokenAction) => {
|
||||||
|
return this.authService.refreshAuthenticationToken(action.payload)
|
||||||
|
.map((token: AuthTokenInfo) => new RefreshTokenSuccessAction(token))
|
||||||
|
.catch((error) => Observable.of(new RefreshTokenErrorAction()));
|
||||||
|
});
|
||||||
|
|
||||||
|
// It means "reacts to this action but don't send another"
|
||||||
|
@Effect({dispatch: false})
|
||||||
|
public refreshTokenSuccess$: Observable<Action> = this.actions$
|
||||||
|
.ofType(AuthActionTypes.REFRESH_TOKEN_SUCCESS)
|
||||||
|
.do((action: RefreshTokenSuccessAction) => this.authService.replaceToken(action.payload));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the store is rehydrated in the browser,
|
||||||
|
* clear a possible invalid token or authentication errors
|
||||||
|
*/
|
||||||
|
@Effect({dispatch: false})
|
||||||
|
public clearInvalidTokenOnRehydrate$: Observable<any> = this.actions$
|
||||||
|
.ofType(StoreActionTypes.REHYDRATE)
|
||||||
|
.switchMap(() => {
|
||||||
|
return this.store.select(isAuthenticated)
|
||||||
|
.take(1)
|
||||||
|
.filter((authenticated) => !authenticated)
|
||||||
|
.do(() => this.authService.removeToken())
|
||||||
|
.do(() => this.authService.resetAuthenticationError());
|
||||||
|
});
|
||||||
|
|
||||||
|
@Effect()
|
||||||
|
public logOut$: Observable<Action> = this.actions$
|
||||||
|
.ofType(AuthActionTypes.LOG_OUT)
|
||||||
|
.switchMap(() => {
|
||||||
|
return this.authService.logout()
|
||||||
|
.map((value) => new LogOutSuccessAction())
|
||||||
|
.catch((error) => Observable.of(new LogOutErrorAction(error)));
|
||||||
|
});
|
||||||
|
|
||||||
|
@Effect({dispatch: false})
|
||||||
|
public logOutSuccess$: Observable<Action> = this.actions$
|
||||||
|
.ofType(AuthActionTypes.LOG_OUT_SUCCESS)
|
||||||
|
.do(() => this.authService.removeToken())
|
||||||
|
.do(() => this.authService.clearRedirectUrl())
|
||||||
|
.do(() => this.authService.refreshAfterLogout());
|
||||||
|
|
||||||
|
@Effect({dispatch: false})
|
||||||
|
public redirectToLogin$: Observable<Action> = this.actions$
|
||||||
|
.ofType(AuthActionTypes.REDIRECT_AUTHENTICATION_REQUIRED)
|
||||||
|
.do(() => this.authService.removeToken())
|
||||||
|
.do(() => this.authService.redirectToLogin());
|
||||||
|
|
||||||
|
@Effect({dispatch: false})
|
||||||
|
public redirectToLoginTokenExpired$: Observable<Action> = this.actions$
|
||||||
|
.ofType(AuthActionTypes.REDIRECT_TOKEN_EXPIRED)
|
||||||
|
.do(() => this.authService.removeToken())
|
||||||
|
.do(() => this.authService.redirectToLoginWhenTokenExpired());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
* @param {Actions} actions$
|
||||||
|
* @param {AuthService} authService
|
||||||
|
* @param {Store} store
|
||||||
|
*/
|
||||||
|
constructor(private actions$: Actions,
|
||||||
|
private authService: AuthService,
|
||||||
|
private store: Store<AppState>) {
|
||||||
|
}
|
||||||
|
}
|
98
src/app/core/auth/auth.interceptor.spec.ts
Normal file
98
src/app/core/auth/auth.interceptor.spec.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { HttpClientTestingModule, HttpTestingController, } from '@angular/common/http/testing';
|
||||||
|
import { HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
|
||||||
|
import { AuthInterceptor } from './auth.interceptor';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { DSpaceRESTv2Service } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||||
|
import { RestRequestMethod } from '../data/request.models';
|
||||||
|
import { RouterStub } from '../../shared/testing/router-stub';
|
||||||
|
import { TruncatablesState } from '../../shared/truncatable/truncatable.reducer';
|
||||||
|
import { AuthServiceStub } from '../../shared/testing/auth-service-stub';
|
||||||
|
|
||||||
|
describe(`AuthInterceptor`, () => {
|
||||||
|
let service: DSpaceRESTv2Service;
|
||||||
|
let httpMock: HttpTestingController;
|
||||||
|
|
||||||
|
const authServiceStub = new AuthServiceStub();
|
||||||
|
const store: Store<TruncatablesState> = jasmine.createSpyObj('store', {
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
dispatch: {},
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
|
select: Observable.of(true)
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [HttpClientTestingModule],
|
||||||
|
providers: [
|
||||||
|
DSpaceRESTv2Service,
|
||||||
|
{provide: AuthService, useValue: authServiceStub},
|
||||||
|
{provide: Router, useClass: RouterStub},
|
||||||
|
{
|
||||||
|
provide: HTTP_INTERCEPTORS,
|
||||||
|
useClass: AuthInterceptor,
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
{provide: Store, useValue: store},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
service = TestBed.get(DSpaceRESTv2Service);
|
||||||
|
httpMock = TestBed.get(HttpTestingController);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when has a valid token', () => {
|
||||||
|
|
||||||
|
it('should not add an Authorization header when we’re sending a HTTP request to \'authn\' endpoint', () => {
|
||||||
|
service.request(RestRequestMethod.Post, 'dspace-spring-rest/api/authn/login', 'password=password&user=user').subscribe((response) => {
|
||||||
|
expect(response).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
const httpRequest = httpMock.expectOne(`dspace-spring-rest/api/authn/login`);
|
||||||
|
|
||||||
|
const token = httpRequest.request.headers.get('authorization');
|
||||||
|
expect(token).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add an Authorization header when we’re sending a HTTP request to \'authn\' endpoint', () => {
|
||||||
|
service.request(RestRequestMethod.Post, 'dspace-spring-rest/api/submission/workspaceitems', 'test').subscribe((response) => {
|
||||||
|
expect(response).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
const httpRequest = httpMock.expectOne(`dspace-spring-rest/api/submission/workspaceitems`);
|
||||||
|
|
||||||
|
expect(httpRequest.request.headers.has('authorization'));
|
||||||
|
const token = httpRequest.request.headers.get('authorization');
|
||||||
|
expect(token).toBe('Bearer token_test');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when has an expired token', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
authServiceStub.setTokenAsExpired();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
authServiceStub.setTokenAsNotExpired();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should redirect to login', () => {
|
||||||
|
|
||||||
|
service.request(RestRequestMethod.Post, 'dspace-spring-rest/api/submission/workspaceitems', 'password=password&user=user').subscribe((response) => {
|
||||||
|
expect(response).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
service.request(RestRequestMethod.Post, 'dspace-spring-rest/api/submission/workspaceitems', 'password=password&user=user');
|
||||||
|
|
||||||
|
httpMock.expectNone('dspace-spring-rest/api/submission/workspaceitems');
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
});
|
153
src/app/core/auth/auth.interceptor.ts
Normal file
153
src/app/core/auth/auth.interceptor.ts
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import { Injectable, Injector } from '@angular/core';
|
||||||
|
import {
|
||||||
|
HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse,
|
||||||
|
HttpErrorResponse, HttpResponseBase
|
||||||
|
} from '@angular/common/http';
|
||||||
|
|
||||||
|
import { Observable } from 'rxjs/Rx';
|
||||||
|
import 'rxjs/add/observable/throw'
|
||||||
|
import 'rxjs/add/operator/catch';
|
||||||
|
|
||||||
|
import { find } from 'lodash';
|
||||||
|
|
||||||
|
import { AppState } from '../../app.reducer';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { AuthStatus } from './models/auth-status.model';
|
||||||
|
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||||
|
import { isNotEmpty, isUndefined } from '../../shared/empty.util';
|
||||||
|
import { RedirectWhenTokenExpiredAction, RefreshTokenAction } from './auth.actions';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthInterceptor implements HttpInterceptor {
|
||||||
|
|
||||||
|
// Intercetor is called twice per request,
|
||||||
|
// so to prevent RefreshTokenAction is dispatched twice
|
||||||
|
// we're creating a refresh token request list
|
||||||
|
protected refreshTokenRequestUrls = [];
|
||||||
|
|
||||||
|
constructor(private inj: Injector, private router: Router, private store: Store<AppState>) { }
|
||||||
|
|
||||||
|
private isUnauthorized(response: HttpResponseBase): boolean {
|
||||||
|
// invalid_token The access token provided is expired, revoked, malformed, or invalid for other reasons
|
||||||
|
return response.status === 401;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isSuccess(response: HttpResponseBase): boolean {
|
||||||
|
return response.status === 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isAuthRequest(http: HttpRequest<any> | HttpResponseBase): boolean {
|
||||||
|
return http && http.url
|
||||||
|
&& (http.url.endsWith('/authn/login')
|
||||||
|
|| http.url.endsWith('/authn/logout')
|
||||||
|
|| http.url.endsWith('/authn/status'));
|
||||||
|
}
|
||||||
|
|
||||||
|
private isLoginResponse(http: HttpRequest<any> | HttpResponseBase): boolean {
|
||||||
|
return http.url && http.url.endsWith('/authn/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
private isLogoutResponse(http: HttpRequest<any> | HttpResponseBase): boolean {
|
||||||
|
return http.url && http.url.endsWith('/authn/logout');
|
||||||
|
}
|
||||||
|
|
||||||
|
private makeAuthStatusObject(authenticated:boolean, accessToken?: string, error?: string): AuthStatus {
|
||||||
|
const authStatus = new AuthStatus();
|
||||||
|
authStatus.id = null;
|
||||||
|
authStatus.okay = true;
|
||||||
|
if (authenticated) {
|
||||||
|
authStatus.authenticated = true;
|
||||||
|
authStatus.token = new AuthTokenInfo(accessToken);
|
||||||
|
} else {
|
||||||
|
authStatus.authenticated = false;
|
||||||
|
authStatus.error = isNotEmpty(error) ? ((typeof error === 'string') ? JSON.parse(error) : error) : null;
|
||||||
|
}
|
||||||
|
return authStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||||
|
|
||||||
|
const authService = this.inj.get(AuthService);
|
||||||
|
|
||||||
|
const token = authService.getToken();
|
||||||
|
let newReq;
|
||||||
|
|
||||||
|
if (authService.isTokenExpired()) {
|
||||||
|
authService.setRedirectUrl(this.router.url);
|
||||||
|
// The access token is expired
|
||||||
|
// Redirect to the login route
|
||||||
|
this.store.dispatch(new RedirectWhenTokenExpiredAction('auth.messages.expired'));
|
||||||
|
return Observable.of(null);
|
||||||
|
} else if (!this.isAuthRequest(req) && isNotEmpty(token)) {
|
||||||
|
// Intercept a request that is not to the authentication endpoint
|
||||||
|
authService.isTokenExpiring()
|
||||||
|
.filter((isExpiring) => isExpiring)
|
||||||
|
.subscribe(() => {
|
||||||
|
// If the current request url is already in the refresh token request list, skip it
|
||||||
|
if (isUndefined(find(this.refreshTokenRequestUrls, req.url))) {
|
||||||
|
// When a token is about to expire, refresh it
|
||||||
|
this.store.dispatch(new RefreshTokenAction(token));
|
||||||
|
this.refreshTokenRequestUrls.push(req.url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Get the auth header from the service.
|
||||||
|
const Authorization = authService.buildAuthHeader(token);
|
||||||
|
// Clone the request to add the new header.
|
||||||
|
newReq = req.clone({headers: req.headers.set('authorization', Authorization)});
|
||||||
|
} else {
|
||||||
|
newReq = req;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass on the new request instead of the original request.
|
||||||
|
return next.handle(newReq)
|
||||||
|
.map((response) => {
|
||||||
|
// Intercept a Login/Logout response
|
||||||
|
if (response instanceof HttpResponse && this.isSuccess(response) && (this.isLoginResponse(response) || this.isLogoutResponse(response))) {
|
||||||
|
// It's a success Login/Logout response
|
||||||
|
let authRes: HttpResponse<any>;
|
||||||
|
if (this.isLoginResponse(response)) {
|
||||||
|
// login successfully
|
||||||
|
const newToken = response.headers.get('authorization');
|
||||||
|
authRes = response.clone({body: this.makeAuthStatusObject(true, newToken)});
|
||||||
|
|
||||||
|
// clean eventually refresh Requests list
|
||||||
|
this.refreshTokenRequestUrls = [];
|
||||||
|
} else {
|
||||||
|
// logout successfully
|
||||||
|
authRes = response.clone({body: this.makeAuthStatusObject(false)});
|
||||||
|
}
|
||||||
|
return authRes;
|
||||||
|
} else {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error, caught) => {
|
||||||
|
// Intercept an error response
|
||||||
|
if (error instanceof HttpErrorResponse) {
|
||||||
|
// Checks if is a response from a request to an authentication endpoint
|
||||||
|
if (this.isAuthRequest(error)) {
|
||||||
|
// clean eventually refresh Requests list
|
||||||
|
this.refreshTokenRequestUrls = [];
|
||||||
|
// Create a new HttpResponse and return it, so it can be handle properly by AuthService.
|
||||||
|
const authResponse = new HttpResponse({
|
||||||
|
body: this.makeAuthStatusObject(false, null, error.error),
|
||||||
|
headers: error.headers,
|
||||||
|
status: error.status,
|
||||||
|
statusText: error.statusText,
|
||||||
|
url: error.url
|
||||||
|
});
|
||||||
|
return Observable.of(authResponse);
|
||||||
|
} else if (this.isUnauthorized(error)) {
|
||||||
|
// The access token provided is expired, revoked, malformed, or invalid for other reasons
|
||||||
|
// Redirect to the login route
|
||||||
|
this.store.dispatch(new RedirectWhenTokenExpiredAction('auth.messages.expired'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Return error response as is.
|
||||||
|
return Observable.throw(error);
|
||||||
|
}) as any;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
411
src/app/core/auth/auth.reducer.spec.ts
Normal file
411
src/app/core/auth/auth.reducer.spec.ts
Normal file
@@ -0,0 +1,411 @@
|
|||||||
|
import { authReducer, AuthState } from './auth.reducer';
|
||||||
|
import {
|
||||||
|
AddAuthenticationMessageAction,
|
||||||
|
AuthenticateAction,
|
||||||
|
AuthenticatedAction,
|
||||||
|
AuthenticatedErrorAction,
|
||||||
|
AuthenticatedSuccessAction,
|
||||||
|
AuthenticationErrorAction,
|
||||||
|
AuthenticationSuccessAction,
|
||||||
|
CheckAuthenticationTokenAction,
|
||||||
|
CheckAuthenticationTokenErrorAction,
|
||||||
|
LogOutAction,
|
||||||
|
LogOutErrorAction,
|
||||||
|
LogOutSuccessAction,
|
||||||
|
RedirectWhenAuthenticationIsRequiredAction,
|
||||||
|
RedirectWhenTokenExpiredAction,
|
||||||
|
RefreshTokenAction,
|
||||||
|
RefreshTokenErrorAction,
|
||||||
|
RefreshTokenSuccessAction,
|
||||||
|
ResetAuthenticationMessagesAction,
|
||||||
|
SetRedirectUrlAction
|
||||||
|
} from './auth.actions';
|
||||||
|
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||||
|
import { EpersonMock } from '../../shared/testing/eperson-mock';
|
||||||
|
|
||||||
|
describe('authReducer', () => {
|
||||||
|
|
||||||
|
let initialState: AuthState;
|
||||||
|
let state: AuthState;
|
||||||
|
const mockTokenInfo = new AuthTokenInfo('test_token');
|
||||||
|
const mockError = new Error('Test error message');
|
||||||
|
|
||||||
|
it('should properly set the state, in response to a AUTHENTICATE action', () => {
|
||||||
|
initialState = {
|
||||||
|
authenticated: false,
|
||||||
|
loaded: false,
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
const action = new AuthenticateAction('user', 'password');
|
||||||
|
const newState = authReducer(initialState, action);
|
||||||
|
state = {
|
||||||
|
authenticated: false,
|
||||||
|
loaded: false,
|
||||||
|
error: undefined,
|
||||||
|
loading: true,
|
||||||
|
info: undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly set the state, in response to a AUTHENTICATE_SUCCESS action', () => {
|
||||||
|
initialState = {
|
||||||
|
authenticated: false,
|
||||||
|
loaded: false,
|
||||||
|
error: undefined,
|
||||||
|
loading: true,
|
||||||
|
info: undefined
|
||||||
|
};
|
||||||
|
const action = new AuthenticationSuccessAction(mockTokenInfo);
|
||||||
|
const newState = authReducer(initialState, action);
|
||||||
|
|
||||||
|
expect(newState).toEqual(initialState);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly set the state, in response to a AUTHENTICATE_ERROR action', () => {
|
||||||
|
initialState = {
|
||||||
|
authenticated: false,
|
||||||
|
loaded: false,
|
||||||
|
error: undefined,
|
||||||
|
loading: true,
|
||||||
|
info: undefined
|
||||||
|
};
|
||||||
|
const action = new AuthenticationErrorAction(mockError);
|
||||||
|
const newState = authReducer(initialState, action);
|
||||||
|
state = {
|
||||||
|
authenticated: false,
|
||||||
|
loaded: false,
|
||||||
|
loading: false,
|
||||||
|
info: undefined,
|
||||||
|
authToken: undefined,
|
||||||
|
error: 'Test error message'
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly set the state, in response to a AUTHENTICATED action', () => {
|
||||||
|
initialState = {
|
||||||
|
authenticated: false,
|
||||||
|
loaded: false,
|
||||||
|
error: undefined,
|
||||||
|
loading: true,
|
||||||
|
info: undefined
|
||||||
|
};
|
||||||
|
const action = new AuthenticatedAction(mockTokenInfo);
|
||||||
|
const newState = authReducer(initialState, action);
|
||||||
|
|
||||||
|
expect(newState).toEqual(initialState);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly set the state, in response to a AUTHENTICATED_SUCCESS action', () => {
|
||||||
|
initialState = {
|
||||||
|
authenticated: false,
|
||||||
|
loaded: false,
|
||||||
|
error: undefined,
|
||||||
|
loading: true,
|
||||||
|
info: undefined
|
||||||
|
};
|
||||||
|
const action = new AuthenticatedSuccessAction(true, mockTokenInfo, EpersonMock);
|
||||||
|
const newState = authReducer(initialState, action);
|
||||||
|
state = {
|
||||||
|
authenticated: true,
|
||||||
|
authToken: mockTokenInfo,
|
||||||
|
loaded: true,
|
||||||
|
error: undefined,
|
||||||
|
loading: false,
|
||||||
|
info: undefined,
|
||||||
|
user: EpersonMock
|
||||||
|
};
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly set the state, in response to a AUTHENTICATED_ERROR action', () => {
|
||||||
|
initialState = {
|
||||||
|
authenticated: false,
|
||||||
|
loaded: false,
|
||||||
|
error: undefined,
|
||||||
|
loading: true,
|
||||||
|
info: undefined
|
||||||
|
};
|
||||||
|
const action = new AuthenticatedErrorAction(mockError);
|
||||||
|
const newState = authReducer(initialState, action);
|
||||||
|
state = {
|
||||||
|
authenticated: false,
|
||||||
|
authToken: undefined,
|
||||||
|
error: 'Test error message',
|
||||||
|
loaded: true,
|
||||||
|
loading: false,
|
||||||
|
info: undefined
|
||||||
|
};
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly set the state, in response to a CHECK_AUTHENTICATION_TOKEN action', () => {
|
||||||
|
initialState = {
|
||||||
|
authenticated: false,
|
||||||
|
loaded: false,
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
const action = new CheckAuthenticationTokenAction();
|
||||||
|
const newState = authReducer(initialState, action);
|
||||||
|
state = {
|
||||||
|
authenticated: false,
|
||||||
|
loaded: false,
|
||||||
|
loading: true,
|
||||||
|
};
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly set the state, in response to a CHECK_AUTHENTICATION_TOKEN_ERROR action', () => {
|
||||||
|
initialState = {
|
||||||
|
authenticated: false,
|
||||||
|
loaded: false,
|
||||||
|
loading: true,
|
||||||
|
};
|
||||||
|
const action = new CheckAuthenticationTokenErrorAction();
|
||||||
|
const newState = authReducer(initialState, action);
|
||||||
|
state = {
|
||||||
|
authenticated: false,
|
||||||
|
loaded: false,
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly set the state, in response to a LOG_OUT action', () => {
|
||||||
|
initialState = {
|
||||||
|
authenticated: true,
|
||||||
|
authToken: mockTokenInfo,
|
||||||
|
loaded: true,
|
||||||
|
error: undefined,
|
||||||
|
loading: false,
|
||||||
|
info: undefined,
|
||||||
|
user: EpersonMock
|
||||||
|
};
|
||||||
|
|
||||||
|
const action = new LogOutAction();
|
||||||
|
const newState = authReducer(initialState, action);
|
||||||
|
|
||||||
|
expect(newState).toEqual(initialState);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly set the state, in response to a LOG_OUT_SUCCESS action', () => {
|
||||||
|
initialState = {
|
||||||
|
authenticated: true,
|
||||||
|
authToken: mockTokenInfo,
|
||||||
|
loaded: true,
|
||||||
|
error: undefined,
|
||||||
|
loading: false,
|
||||||
|
info: undefined,
|
||||||
|
user: EpersonMock
|
||||||
|
};
|
||||||
|
|
||||||
|
const action = new LogOutSuccessAction();
|
||||||
|
const newState = authReducer(initialState, action);
|
||||||
|
state = {
|
||||||
|
authenticated: false,
|
||||||
|
authToken: undefined,
|
||||||
|
error: undefined,
|
||||||
|
loaded: false,
|
||||||
|
loading: false,
|
||||||
|
info: undefined,
|
||||||
|
refreshing: false,
|
||||||
|
user: undefined
|
||||||
|
};
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly set the state, in response to a LOG_OUT_ERROR action', () => {
|
||||||
|
initialState = {
|
||||||
|
authenticated: true,
|
||||||
|
authToken: mockTokenInfo,
|
||||||
|
loaded: true,
|
||||||
|
error: undefined,
|
||||||
|
loading: false,
|
||||||
|
info: undefined,
|
||||||
|
user: EpersonMock
|
||||||
|
};
|
||||||
|
|
||||||
|
const action = new LogOutErrorAction(mockError);
|
||||||
|
const newState = authReducer(initialState, action);
|
||||||
|
state = {
|
||||||
|
authenticated: true,
|
||||||
|
authToken: mockTokenInfo,
|
||||||
|
loaded: true,
|
||||||
|
error: 'Test error message',
|
||||||
|
loading: false,
|
||||||
|
info: undefined,
|
||||||
|
user: EpersonMock
|
||||||
|
};
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly set the state, in response to a REFRESH_TOKEN action', () => {
|
||||||
|
initialState = {
|
||||||
|
authenticated: true,
|
||||||
|
authToken: mockTokenInfo,
|
||||||
|
loaded: true,
|
||||||
|
error: undefined,
|
||||||
|
loading: false,
|
||||||
|
info: undefined,
|
||||||
|
user: EpersonMock
|
||||||
|
};
|
||||||
|
const newTokenInfo = new AuthTokenInfo('Refreshed token');
|
||||||
|
const action = new RefreshTokenAction(newTokenInfo);
|
||||||
|
const newState = authReducer(initialState, action);
|
||||||
|
state = {
|
||||||
|
authenticated: true,
|
||||||
|
authToken: mockTokenInfo,
|
||||||
|
loaded: true,
|
||||||
|
error: undefined,
|
||||||
|
loading: false,
|
||||||
|
info: undefined,
|
||||||
|
user: EpersonMock,
|
||||||
|
refreshing: true
|
||||||
|
};
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly set the state, in response to a REFRESH_TOKEN_SUCCESS action', () => {
|
||||||
|
initialState = {
|
||||||
|
authenticated: true,
|
||||||
|
authToken: mockTokenInfo,
|
||||||
|
loaded: true,
|
||||||
|
error: undefined,
|
||||||
|
loading: false,
|
||||||
|
info: undefined,
|
||||||
|
user: EpersonMock,
|
||||||
|
refreshing: true
|
||||||
|
};
|
||||||
|
const newTokenInfo = new AuthTokenInfo('Refreshed token');
|
||||||
|
const action = new RefreshTokenSuccessAction(newTokenInfo);
|
||||||
|
const newState = authReducer(initialState, action);
|
||||||
|
state = {
|
||||||
|
authenticated: true,
|
||||||
|
authToken: newTokenInfo,
|
||||||
|
loaded: true,
|
||||||
|
error: undefined,
|
||||||
|
loading: false,
|
||||||
|
info: undefined,
|
||||||
|
user: EpersonMock,
|
||||||
|
refreshing: false
|
||||||
|
};
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly set the state, in response to a REFRESH_TOKEN_ERROR action', () => {
|
||||||
|
initialState = {
|
||||||
|
authenticated: true,
|
||||||
|
authToken: mockTokenInfo,
|
||||||
|
loaded: true,
|
||||||
|
error: undefined,
|
||||||
|
loading: false,
|
||||||
|
info: undefined,
|
||||||
|
user: EpersonMock,
|
||||||
|
refreshing: true
|
||||||
|
};
|
||||||
|
const action = new RefreshTokenErrorAction();
|
||||||
|
const newState = authReducer(initialState, action);
|
||||||
|
state = {
|
||||||
|
authenticated: false,
|
||||||
|
authToken: undefined,
|
||||||
|
error: undefined,
|
||||||
|
loaded: false,
|
||||||
|
loading: false,
|
||||||
|
info: undefined,
|
||||||
|
refreshing: false,
|
||||||
|
user: undefined
|
||||||
|
};
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
initialState = {
|
||||||
|
authenticated: true,
|
||||||
|
authToken: mockTokenInfo,
|
||||||
|
loaded: true,
|
||||||
|
error: undefined,
|
||||||
|
loading: false,
|
||||||
|
info: undefined,
|
||||||
|
user: EpersonMock
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
authenticated: false,
|
||||||
|
authToken: undefined,
|
||||||
|
loaded: false,
|
||||||
|
loading: false,
|
||||||
|
error: undefined,
|
||||||
|
info: 'Message',
|
||||||
|
user: undefined
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly set the state, in response to a REDIRECT_AUTHENTICATION_REQUIRED action', () => {
|
||||||
|
const action = new RedirectWhenAuthenticationIsRequiredAction('Message');
|
||||||
|
const newState = authReducer(initialState, action);
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly set the state, in response to a REDIRECT_TOKEN_EXPIRED action', () => {
|
||||||
|
const action = new RedirectWhenTokenExpiredAction('Message');
|
||||||
|
const newState = authReducer(initialState, action);
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly set the state, in response to a ADD_MESSAGE action', () => {
|
||||||
|
initialState = {
|
||||||
|
authenticated: false,
|
||||||
|
loaded: false,
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
const action = new AddAuthenticationMessageAction('Message');
|
||||||
|
const newState = authReducer(initialState, action);
|
||||||
|
state = {
|
||||||
|
authenticated: false,
|
||||||
|
loaded: false,
|
||||||
|
loading: false,
|
||||||
|
info: 'Message'
|
||||||
|
};
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly set the state, in response to a RESET_MESSAGES action', () => {
|
||||||
|
initialState = {
|
||||||
|
authenticated: false,
|
||||||
|
loaded: false,
|
||||||
|
loading: false,
|
||||||
|
error: 'Error',
|
||||||
|
info: 'Message'
|
||||||
|
};
|
||||||
|
const action = new ResetAuthenticationMessagesAction();
|
||||||
|
const newState = authReducer(initialState, action);
|
||||||
|
state = {
|
||||||
|
authenticated: false,
|
||||||
|
loaded: false,
|
||||||
|
loading: false,
|
||||||
|
error: undefined,
|
||||||
|
info: undefined
|
||||||
|
};
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly set the state, in response to a SET_REDIRECT_URL action', () => {
|
||||||
|
initialState = {
|
||||||
|
authenticated: false,
|
||||||
|
loaded: false,
|
||||||
|
loading: false
|
||||||
|
};
|
||||||
|
const action = new SetRedirectUrlAction('redirect.url');
|
||||||
|
const newState = authReducer(initialState, action);
|
||||||
|
state = {
|
||||||
|
authenticated: false,
|
||||||
|
loaded: false,
|
||||||
|
loading: false,
|
||||||
|
redirectUrl: 'redirect.url'
|
||||||
|
};
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
});
|
198
src/app/core/auth/auth.reducer.ts
Normal file
198
src/app/core/auth/auth.reducer.ts
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
// import actions
|
||||||
|
import {
|
||||||
|
AddAuthenticationMessageAction,
|
||||||
|
AuthActions,
|
||||||
|
AuthActionTypes,
|
||||||
|
AuthenticatedSuccessAction,
|
||||||
|
AuthenticationErrorAction,
|
||||||
|
LogOutErrorAction,
|
||||||
|
RedirectWhenAuthenticationIsRequiredAction,
|
||||||
|
RedirectWhenTokenExpiredAction,
|
||||||
|
RefreshTokenSuccessAction,
|
||||||
|
SetRedirectUrlAction
|
||||||
|
} from './auth.actions';
|
||||||
|
// import models
|
||||||
|
import { Eperson } from '../eperson/models/eperson.model';
|
||||||
|
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The auth state.
|
||||||
|
* @interface State
|
||||||
|
*/
|
||||||
|
export interface AuthState {
|
||||||
|
|
||||||
|
// boolean if user is authenticated
|
||||||
|
authenticated: boolean;
|
||||||
|
|
||||||
|
// the authentication token
|
||||||
|
authToken?: AuthTokenInfo;
|
||||||
|
|
||||||
|
// error message
|
||||||
|
error?: string;
|
||||||
|
|
||||||
|
// true if we have attempted existing auth session
|
||||||
|
loaded: boolean;
|
||||||
|
|
||||||
|
// true when loading
|
||||||
|
loading: boolean;
|
||||||
|
|
||||||
|
// info message
|
||||||
|
info?: string;
|
||||||
|
|
||||||
|
// redirect url after login
|
||||||
|
redirectUrl?: string;
|
||||||
|
|
||||||
|
// true when refreshing token
|
||||||
|
refreshing?: boolean;
|
||||||
|
|
||||||
|
// the authenticated user
|
||||||
|
user?: Eperson;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The initial state.
|
||||||
|
*/
|
||||||
|
const initialState: AuthState = {
|
||||||
|
authenticated: false,
|
||||||
|
loaded: false,
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The reducer function.
|
||||||
|
* @function reducer
|
||||||
|
* @param {State} state Current state
|
||||||
|
* @param {AuthActions} action Incoming action
|
||||||
|
*/
|
||||||
|
export function authReducer(state: any = initialState, action: AuthActions): AuthState {
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case AuthActionTypes.AUTHENTICATE:
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
error: undefined,
|
||||||
|
loading: true,
|
||||||
|
info: undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
case AuthActionTypes.AUTHENTICATED:
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
loading: true
|
||||||
|
});
|
||||||
|
|
||||||
|
case AuthActionTypes.AUTHENTICATED_ERROR:
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
authenticated: false,
|
||||||
|
authToken: undefined,
|
||||||
|
error: (action as AuthenticationErrorAction).payload.message,
|
||||||
|
loaded: true,
|
||||||
|
loading: false
|
||||||
|
});
|
||||||
|
|
||||||
|
case AuthActionTypes.AUTHENTICATED_SUCCESS:
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
authenticated: true,
|
||||||
|
authToken: (action as AuthenticatedSuccessAction).payload.authToken,
|
||||||
|
loaded: true,
|
||||||
|
error: undefined,
|
||||||
|
loading: false,
|
||||||
|
info: undefined,
|
||||||
|
user: (action as AuthenticatedSuccessAction).payload.user
|
||||||
|
});
|
||||||
|
|
||||||
|
case AuthActionTypes.AUTHENTICATE_ERROR:
|
||||||
|
case AuthActionTypes.REGISTRATION_ERROR:
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
authenticated: false,
|
||||||
|
authToken: undefined,
|
||||||
|
error: (action as AuthenticationErrorAction).payload.message,
|
||||||
|
loading: false
|
||||||
|
});
|
||||||
|
|
||||||
|
case AuthActionTypes.AUTHENTICATED:
|
||||||
|
case AuthActionTypes.AUTHENTICATE_SUCCESS:
|
||||||
|
case AuthActionTypes.LOG_OUT:
|
||||||
|
return state;
|
||||||
|
|
||||||
|
case AuthActionTypes.CHECK_AUTHENTICATION_TOKEN:
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
loading: true
|
||||||
|
});
|
||||||
|
|
||||||
|
case AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_ERROR:
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
loading: false
|
||||||
|
});
|
||||||
|
|
||||||
|
case AuthActionTypes.LOG_OUT_ERROR:
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
authenticated: true,
|
||||||
|
error: (action as LogOutErrorAction).payload.message
|
||||||
|
});
|
||||||
|
|
||||||
|
case AuthActionTypes.LOG_OUT_SUCCESS:
|
||||||
|
case AuthActionTypes.REFRESH_TOKEN_ERROR:
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
authenticated: false,
|
||||||
|
authToken: undefined,
|
||||||
|
error: undefined,
|
||||||
|
loaded: false,
|
||||||
|
loading: false,
|
||||||
|
info: undefined,
|
||||||
|
refreshing: false,
|
||||||
|
user: undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
case AuthActionTypes.REDIRECT_AUTHENTICATION_REQUIRED:
|
||||||
|
case AuthActionTypes.REDIRECT_TOKEN_EXPIRED:
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
authenticated: false,
|
||||||
|
authToken: undefined,
|
||||||
|
loaded: false,
|
||||||
|
loading: false,
|
||||||
|
info: (action as RedirectWhenTokenExpiredAction as RedirectWhenAuthenticationIsRequiredAction).payload,
|
||||||
|
user: undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
case AuthActionTypes.REGISTRATION:
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
authenticated: false,
|
||||||
|
authToken: undefined,
|
||||||
|
error: undefined,
|
||||||
|
loading: true,
|
||||||
|
info: undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
case AuthActionTypes.REGISTRATION_SUCCESS:
|
||||||
|
return state;
|
||||||
|
|
||||||
|
case AuthActionTypes.REFRESH_TOKEN:
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
refreshing: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
case AuthActionTypes.REFRESH_TOKEN_SUCCESS:
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
authToken: (action as RefreshTokenSuccessAction).payload,
|
||||||
|
refreshing: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
case AuthActionTypes.ADD_MESSAGE:
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
info: (action as AddAuthenticationMessageAction).payload,
|
||||||
|
});
|
||||||
|
|
||||||
|
case AuthActionTypes.RESET_MESSAGES:
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
error: undefined,
|
||||||
|
info: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
case AuthActionTypes.SET_REDIRECT_URL:
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
redirectUrl: (action as SetRedirectUrlAction).payload,
|
||||||
|
});
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
221
src/app/core/auth/auth.service.spec.ts
Normal file
221
src/app/core/auth/auth.service.spec.ts
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
import { async, inject, TestBed } from '@angular/core/testing';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
|
||||||
|
import { Store, StoreModule } from '@ngrx/store';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
||||||
|
import 'rxjs/add/observable/of';
|
||||||
|
|
||||||
|
import { authReducer, AuthState } from './auth.reducer';
|
||||||
|
import { NativeWindowRef, NativeWindowService } from '../../shared/services/window.service';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { RouterStub } from '../../shared/testing/router-stub';
|
||||||
|
import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
|
||||||
|
|
||||||
|
import { CookieService } from '../../shared/services/cookie.service';
|
||||||
|
import { AuthRequestServiceStub } from '../../shared/testing/auth-request-service-stub';
|
||||||
|
import { AuthRequestService } from './auth-request.service';
|
||||||
|
import { AuthStatus } from './models/auth-status.model';
|
||||||
|
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||||
|
import { Eperson } from '../eperson/models/eperson.model';
|
||||||
|
import { EpersonMock } from '../../shared/testing/eperson-mock';
|
||||||
|
import { AppState } from '../../app.reducer';
|
||||||
|
import { ClientCookieService } from '../../shared/services/client-cookie.service';
|
||||||
|
|
||||||
|
describe('AuthService test', () => {
|
||||||
|
|
||||||
|
const mockStore: Store<AuthState> = jasmine.createSpyObj('store', {
|
||||||
|
dispatch: {},
|
||||||
|
select: Observable.of(true)
|
||||||
|
});
|
||||||
|
let authService: AuthService;
|
||||||
|
const authRequest = new AuthRequestServiceStub();
|
||||||
|
const window = new NativeWindowRef();
|
||||||
|
const routerStub = new RouterStub();
|
||||||
|
const routeStub = new ActivatedRouteStub();
|
||||||
|
let storage: CookieService;
|
||||||
|
const token: AuthTokenInfo = new AuthTokenInfo('test_token');
|
||||||
|
token.expires = Date.now() + (1000 * 60 * 60);
|
||||||
|
let authenticatedState = {
|
||||||
|
authenticated: true,
|
||||||
|
loaded: true,
|
||||||
|
loading: false,
|
||||||
|
authToken: token,
|
||||||
|
user: EpersonMock
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
StoreModule.forRoot({authReducer}),
|
||||||
|
],
|
||||||
|
declarations: [],
|
||||||
|
providers: [
|
||||||
|
{provide: AuthRequestService, useValue: authRequest},
|
||||||
|
{provide: NativeWindowService, useValue: window},
|
||||||
|
{provide: REQUEST, useValue: {}},
|
||||||
|
{provide: Router, useValue: routerStub},
|
||||||
|
{provide: ActivatedRoute, useValue: routeStub},
|
||||||
|
{provide: Store, useValue: mockStore},
|
||||||
|
CookieService,
|
||||||
|
AuthService
|
||||||
|
],
|
||||||
|
});
|
||||||
|
authService = TestBed.get(AuthService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the authentication status object when user credentials are correct', () => {
|
||||||
|
authService.authenticate('user', 'password').subscribe((status: AuthStatus) => {
|
||||||
|
expect(status).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error when user credentials are wrong', () => {
|
||||||
|
expect(authService.authenticate.bind(null, 'user', 'passwordwrong')).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the authenticated user object when user token is valid', () => {
|
||||||
|
authService.authenticatedUser(new AuthTokenInfo('test_token')).subscribe((user: Eperson) => {
|
||||||
|
expect(user).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error when user credentials when user token is not valid', () => {
|
||||||
|
expect(authService.authenticatedUser.bind(null, new AuthTokenInfo('test_token_expired'))).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a valid refreshed token', () => {
|
||||||
|
authService.refreshAuthenticationToken(new AuthTokenInfo('test_token')).subscribe((tokenState: AuthTokenInfo) => {
|
||||||
|
expect(tokenState).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error when is not possible to refresh token', () => {
|
||||||
|
expect(authService.refreshAuthenticationToken.bind(null, new AuthTokenInfo('test_token_expired'))).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when logout succeeded', () => {
|
||||||
|
authService.logout().subscribe((status: boolean) => {
|
||||||
|
expect(status).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error when logout is not succeeded', () => {
|
||||||
|
expect(authService.logout.bind(null)).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('', () => {
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
StoreModule.forRoot({authReducer})
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{provide: AuthRequestService, useValue: authRequest},
|
||||||
|
{provide: REQUEST, useValue: {}},
|
||||||
|
{provide: Router, useValue: routerStub},
|
||||||
|
CookieService
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(inject([CookieService, AuthRequestService, Store, Router], (cookieService: CookieService, authReqService: AuthRequestService, store: Store<AppState>, router: Router) => {
|
||||||
|
store
|
||||||
|
.subscribe((state) => {
|
||||||
|
(state as any).core = Object.create({});
|
||||||
|
(state as any).core.auth = authenticatedState;
|
||||||
|
});
|
||||||
|
authService = new AuthService({}, window, authReqService, router, cookieService, store);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should return true when user is logged in', () => {
|
||||||
|
authService.isAuthenticated().subscribe((status: boolean) => {
|
||||||
|
expect(status).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return token object when it is valid', () => {
|
||||||
|
authService.hasValidAuthenticationToken().subscribe((tokenState: AuthTokenInfo) => {
|
||||||
|
expect(tokenState).toBe(token);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a token object', () => {
|
||||||
|
const result = authService.getToken();
|
||||||
|
expect(result).toBe(token);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when token is not expired', () => {
|
||||||
|
const result = authService.isTokenExpired();
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('', () => {
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
StoreModule.forRoot({authReducer})
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{provide: AuthRequestService, useValue: authRequest},
|
||||||
|
{provide: REQUEST, useValue: {}},
|
||||||
|
{provide: Router, useValue: routerStub},
|
||||||
|
ClientCookieService,
|
||||||
|
CookieService
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(inject([ClientCookieService, AuthRequestService, Store, Router], (cookieService: ClientCookieService, authReqService: AuthRequestService, store: Store<AppState>, router: Router) => {
|
||||||
|
const expiredToken: AuthTokenInfo = new AuthTokenInfo('test_token');
|
||||||
|
expiredToken.expires = Date.now() - (1000 * 60 * 60);
|
||||||
|
authenticatedState = {
|
||||||
|
authenticated: true,
|
||||||
|
loaded: true,
|
||||||
|
loading: false,
|
||||||
|
authToken: expiredToken,
|
||||||
|
user: EpersonMock
|
||||||
|
};
|
||||||
|
store
|
||||||
|
.subscribe((state) => {
|
||||||
|
(state as any).core = Object.create({});
|
||||||
|
(state as any).core.auth = authenticatedState;
|
||||||
|
});
|
||||||
|
authService = new AuthService({}, window, authReqService, router, cookieService, store);
|
||||||
|
storage = (authService as any).storage;
|
||||||
|
spyOn(storage, 'get');
|
||||||
|
spyOn(storage, 'remove');
|
||||||
|
spyOn(storage, 'set');
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should throw false when token is not valid', () => {
|
||||||
|
expect(authService.hasValidAuthenticationToken.bind(null)).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when token is expired', () => {
|
||||||
|
const result = authService.isTokenExpired();
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should save token into storage', () => {
|
||||||
|
authService.storeToken(token);
|
||||||
|
expect(storage.set).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove token from storage', () => {
|
||||||
|
authService.removeToken();
|
||||||
|
expect(storage.remove).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
389
src/app/core/auth/auth.service.ts
Normal file
389
src/app/core/auth/auth.service.ts
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
import { Inject, Injectable } from '@angular/core';
|
||||||
|
import { PRIMARY_OUTLET, Router, UrlSegmentGroup, UrlTree } from '@angular/router';
|
||||||
|
import { HttpHeaders } from '@angular/common/http';
|
||||||
|
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
||||||
|
|
||||||
|
import { RouterReducerState } from '@ngrx/router-store';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { CookieAttributes } from 'js-cookie';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { map, withLatestFrom } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { Eperson } from '../eperson/models/eperson.model';
|
||||||
|
import { AuthRequestService } from './auth-request.service';
|
||||||
|
|
||||||
|
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||||
|
import { AuthStatus } from './models/auth-status.model';
|
||||||
|
import { AuthTokenInfo, TOKENITEM } from './models/auth-token-info.model';
|
||||||
|
import { isEmpty, isNotEmpty, isNotNull, isNotUndefined } from '../../shared/empty.util';
|
||||||
|
import { CookieService } from '../../shared/services/cookie.service';
|
||||||
|
import { getAuthenticationToken, getRedirectUrl, isAuthenticated, isTokenRefreshing } from './selectors';
|
||||||
|
import { AppState, routerStateSelector } from '../../app.reducer';
|
||||||
|
import { ResetAuthenticationMessagesAction, SetRedirectUrlAction } from './auth.actions';
|
||||||
|
import { NativeWindowRef, NativeWindowService } from '../../shared/services/window.service';
|
||||||
|
import { Base64EncodeUrl } from '../../shared/utils/encode-decode.util';
|
||||||
|
|
||||||
|
export const LOGIN_ROUTE = '/login';
|
||||||
|
export const LOGOUT_ROUTE = '/logout';
|
||||||
|
|
||||||
|
export const REDIRECT_COOKIE = 'dsRedirectUrl';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The auth service.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AuthService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if authenticated
|
||||||
|
* @type boolean
|
||||||
|
*/
|
||||||
|
protected _authenticated: boolean;
|
||||||
|
|
||||||
|
constructor(@Inject(REQUEST) protected req: any,
|
||||||
|
@Inject(NativeWindowService) protected _window: NativeWindowRef,
|
||||||
|
protected authRequestService: AuthRequestService,
|
||||||
|
protected router: Router,
|
||||||
|
protected storage: CookieService,
|
||||||
|
protected store: Store<AppState>) {
|
||||||
|
this.store.select(isAuthenticated)
|
||||||
|
.startWith(false)
|
||||||
|
.subscribe((authenticated: boolean) => this._authenticated = authenticated);
|
||||||
|
|
||||||
|
// If current route is different from the one setted in authentication guard
|
||||||
|
// and is not the login route, clear redirect url and messages
|
||||||
|
const routeUrl$ = this.store.select(routerStateSelector)
|
||||||
|
.filter((routerState: RouterReducerState) => isNotUndefined(routerState) && isNotUndefined(routerState.state))
|
||||||
|
.filter((routerState: RouterReducerState) => !this.isLoginRoute(routerState.state.url))
|
||||||
|
.map((routerState: RouterReducerState) => routerState.state.url);
|
||||||
|
const redirectUrl$ = this.store.select(getRedirectUrl).distinctUntilChanged();
|
||||||
|
routeUrl$.pipe(
|
||||||
|
withLatestFrom(redirectUrl$),
|
||||||
|
map(([routeUrl, redirectUrl]) => [routeUrl, redirectUrl])
|
||||||
|
).filter(([routeUrl, redirectUrl]) => isNotEmpty(redirectUrl) && (routeUrl !== redirectUrl))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.clearRedirectUrl();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if is a login page route
|
||||||
|
*
|
||||||
|
* @param {string} url
|
||||||
|
* @returns {Boolean}.
|
||||||
|
*/
|
||||||
|
protected isLoginRoute(url: string) {
|
||||||
|
const urlTree: UrlTree = this.router.parseUrl(url);
|
||||||
|
const g: UrlSegmentGroup = urlTree.root.children[PRIMARY_OUTLET];
|
||||||
|
const segment = '/' + g.toString();
|
||||||
|
return segment === LOGIN_ROUTE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticate the user
|
||||||
|
*
|
||||||
|
* @param {string} user The user name
|
||||||
|
* @param {string} password The user's password
|
||||||
|
* @returns {Observable<User>} The authenticated user observable.
|
||||||
|
*/
|
||||||
|
public authenticate(user: string, password: string): Observable<AuthStatus> {
|
||||||
|
// Attempt authenticating the user using the supplied credentials.
|
||||||
|
const body = (`password=${Base64EncodeUrl(password)}&user=${Base64EncodeUrl(user)}`);
|
||||||
|
const options: HttpOptions = Object.create({});
|
||||||
|
let headers = new HttpHeaders();
|
||||||
|
headers = headers.append('Content-Type', 'application/x-www-form-urlencoded');
|
||||||
|
options.headers = headers;
|
||||||
|
return this.authRequestService.postToEndpoint('login', body, options)
|
||||||
|
.map((status: AuthStatus) => {
|
||||||
|
if (status.authenticated) {
|
||||||
|
return status;
|
||||||
|
} else {
|
||||||
|
throw(new Error('Invalid email or password'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the user is authenticated
|
||||||
|
* @returns {Observable<boolean>}
|
||||||
|
*/
|
||||||
|
public isAuthenticated(): Observable<boolean> {
|
||||||
|
return this.store.select(isAuthenticated);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the authenticated user
|
||||||
|
* @returns {User}
|
||||||
|
*/
|
||||||
|
public authenticatedUser(token: AuthTokenInfo): Observable<Eperson> {
|
||||||
|
// Determine if the user has an existing auth session on the server
|
||||||
|
const options: HttpOptions = Object.create({});
|
||||||
|
let headers = new HttpHeaders();
|
||||||
|
headers = headers.append('Accept', 'application/json');
|
||||||
|
headers = headers.append('Authorization', `Bearer ${token.accessToken}`);
|
||||||
|
options.headers = headers;
|
||||||
|
return this.authRequestService.getRequest('status', options)
|
||||||
|
.map((status: AuthStatus) => {
|
||||||
|
if (status.authenticated) {
|
||||||
|
return status.eperson[0];
|
||||||
|
} else {
|
||||||
|
throw(new Error('Not authenticated'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if token is present into browser storage and is valid. (NB Check is done only on SSR)
|
||||||
|
*/
|
||||||
|
public checkAuthenticationToken() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if token is present into storage and is not expired
|
||||||
|
*/
|
||||||
|
public hasValidAuthenticationToken(): Observable<AuthTokenInfo> {
|
||||||
|
return this.store.select(getAuthenticationToken)
|
||||||
|
.take(1)
|
||||||
|
.map((authTokenInfo: AuthTokenInfo) => {
|
||||||
|
let token: AuthTokenInfo;
|
||||||
|
// Retrieve authentication token info and check if is valid
|
||||||
|
token = isNotEmpty(authTokenInfo) ? authTokenInfo : this.storage.get(TOKENITEM);
|
||||||
|
if (isNotEmpty(token) && token.hasOwnProperty('accessToken') && isNotEmpty(token.accessToken) && !this.isTokenExpired(token)) {
|
||||||
|
return token;
|
||||||
|
} else {
|
||||||
|
throw false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if token is present into storage
|
||||||
|
*/
|
||||||
|
public refreshAuthenticationToken(token: AuthTokenInfo): Observable<AuthTokenInfo> {
|
||||||
|
const options: HttpOptions = Object.create({});
|
||||||
|
let headers = new HttpHeaders();
|
||||||
|
headers = headers.append('Accept', 'application/json');
|
||||||
|
headers = headers.append('Authorization', `Bearer ${token.accessToken}`);
|
||||||
|
options.headers = headers;
|
||||||
|
return this.authRequestService.postToEndpoint('login', {}, options)
|
||||||
|
.map((status: AuthStatus) => {
|
||||||
|
if (status.authenticated) {
|
||||||
|
return status.token;
|
||||||
|
} else {
|
||||||
|
throw(new Error('Not authenticated'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear authentication errors
|
||||||
|
*/
|
||||||
|
public resetAuthenticationError(): void {
|
||||||
|
this.store.dispatch(new ResetAuthenticationMessagesAction());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new user
|
||||||
|
* @returns {User}
|
||||||
|
*/
|
||||||
|
public create(user: Eperson): Observable<Eperson> {
|
||||||
|
// Normally you would do an HTTP request to POST the user
|
||||||
|
// details and then return the new user object
|
||||||
|
// but, let's just return the new user for this example.
|
||||||
|
// this._authenticated = true;
|
||||||
|
return Observable.of(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* End session
|
||||||
|
* @returns {Observable<boolean>}
|
||||||
|
*/
|
||||||
|
public logout(): Observable<boolean> {
|
||||||
|
// Send a request that sign end the session
|
||||||
|
let headers = new HttpHeaders();
|
||||||
|
headers = headers.append('Content-Type', 'application/x-www-form-urlencoded');
|
||||||
|
const options: HttpOptions = Object.create({headers, responseType: 'text'});
|
||||||
|
return this.authRequestService.getRequest('logout', options)
|
||||||
|
.map((status: AuthStatus) => {
|
||||||
|
if (!status.authenticated) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw(new Error('auth.errors.invalid-user'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve authentication token info and make authorization header
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
public buildAuthHeader(token?: AuthTokenInfo): string {
|
||||||
|
if (isEmpty(token)) {
|
||||||
|
token = this.getToken();
|
||||||
|
}
|
||||||
|
return (this._authenticated && isNotNull(token)) ? `Bearer ${token.accessToken}` : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get authentication token info
|
||||||
|
* @returns {AuthTokenInfo}
|
||||||
|
*/
|
||||||
|
public getToken(): AuthTokenInfo {
|
||||||
|
let token: AuthTokenInfo;
|
||||||
|
this.store.select(getAuthenticationToken)
|
||||||
|
.subscribe((authTokenInfo: AuthTokenInfo) => {
|
||||||
|
// Retrieve authentication token info and check if is valid
|
||||||
|
token = authTokenInfo || null;
|
||||||
|
});
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a token is next to be expired
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
public isTokenExpiring(): Observable<boolean> {
|
||||||
|
return this.store.select(isTokenRefreshing)
|
||||||
|
.first()
|
||||||
|
.map((isRefreshing: boolean) => {
|
||||||
|
if (this.isTokenExpired() || isRefreshing) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
const token = this.getToken();
|
||||||
|
return token.expires - (60 * 5 * 1000) < Date.now();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a token is expired
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
public isTokenExpired(token?: AuthTokenInfo): boolean {
|
||||||
|
token = token || this.getToken();
|
||||||
|
return token && token.expires < Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save authentication token info
|
||||||
|
*
|
||||||
|
* @param {AuthTokenInfo} token The token to save
|
||||||
|
* @returns {AuthTokenInfo}
|
||||||
|
*/
|
||||||
|
public storeToken(token: AuthTokenInfo) {
|
||||||
|
// Add 1 day to the current date
|
||||||
|
const expireDate = Date.now() + (1000 * 60 * 60 * 24);
|
||||||
|
|
||||||
|
// Set the cookie expire date
|
||||||
|
const expires = new Date(expireDate);
|
||||||
|
const options: CookieAttributes = {expires: expires};
|
||||||
|
|
||||||
|
// Save cookie with the token
|
||||||
|
return this.storage.set(TOKENITEM, token, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove authentication token info
|
||||||
|
*/
|
||||||
|
public removeToken() {
|
||||||
|
return this.storage.remove(TOKENITEM);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace authentication token info with a new one
|
||||||
|
*/
|
||||||
|
public replaceToken(token: AuthTokenInfo) {
|
||||||
|
this.removeToken();
|
||||||
|
return this.storeToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirect to the login route
|
||||||
|
*/
|
||||||
|
public redirectToLogin() {
|
||||||
|
this.router.navigate([LOGIN_ROUTE]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirect to the login route when token has expired
|
||||||
|
*/
|
||||||
|
public redirectToLoginWhenTokenExpired() {
|
||||||
|
const redirectUrl = LOGIN_ROUTE + '?expired=true';
|
||||||
|
if (this._window.nativeWindow.location) {
|
||||||
|
// Hard redirect to login page, so that all state is definitely lost
|
||||||
|
this._window.nativeWindow.location.href = redirectUrl;
|
||||||
|
} else {
|
||||||
|
this.router.navigateByUrl(redirectUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirect to the route navigated before the login
|
||||||
|
*/
|
||||||
|
public redirectToPreviousUrl() {
|
||||||
|
this.getRedirectUrl()
|
||||||
|
.first()
|
||||||
|
.subscribe((redirectUrl) => {
|
||||||
|
if (isNotEmpty(redirectUrl)) {
|
||||||
|
this.clearRedirectUrl();
|
||||||
|
|
||||||
|
// override the route reuse strategy
|
||||||
|
this.router.routeReuseStrategy.shouldReuseRoute = () => {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
this.router.navigated = false;
|
||||||
|
const url = decodeURIComponent(redirectUrl);
|
||||||
|
this.router.navigateByUrl(url);
|
||||||
|
} else {
|
||||||
|
this.router.navigate(['/']);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh route navigated
|
||||||
|
*/
|
||||||
|
public refreshAfterLogout() {
|
||||||
|
this.router.navigate(['/home']);
|
||||||
|
// Hard redirect to home page, so that all state is definitely lost
|
||||||
|
this._window.nativeWindow.location.href = '/home';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get redirect url
|
||||||
|
*/
|
||||||
|
getRedirectUrl(): Observable<string> {
|
||||||
|
const redirectUrl = this.storage.get(REDIRECT_COOKIE);
|
||||||
|
if (isNotEmpty(redirectUrl)) {
|
||||||
|
return Observable.of(redirectUrl);
|
||||||
|
} else {
|
||||||
|
return this.store.select(getRedirectUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set redirect url
|
||||||
|
*/
|
||||||
|
setRedirectUrl(url: string) {
|
||||||
|
// Add 1 hour to the current date
|
||||||
|
const expireDate = Date.now() + (1000 * 60 * 60);
|
||||||
|
|
||||||
|
// Set the cookie expire date
|
||||||
|
const expires = new Date(expireDate);
|
||||||
|
const options: CookieAttributes = {expires: expires};
|
||||||
|
this.storage.set(REDIRECT_COOKIE, url, options);
|
||||||
|
this.store.dispatch(new SetRedirectUrlAction(isNotUndefined(url) ? url : ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear redirect url
|
||||||
|
*/
|
||||||
|
clearRedirectUrl() {
|
||||||
|
this.store.dispatch(new SetRedirectUrlAction(''));
|
||||||
|
this.storage.remove(REDIRECT_COOKIE);
|
||||||
|
}
|
||||||
|
}
|
70
src/app/core/auth/authenticated.guard.ts
Normal file
70
src/app/core/auth/authenticated.guard.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, CanActivate, CanLoad, Route, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
|
||||||
|
// reducers
|
||||||
|
import { CoreState } from '../core.reducers';
|
||||||
|
import { isAuthenticated, isAuthenticationLoading } from './selectors';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { RedirectWhenAuthenticationIsRequiredAction } from './auth.actions';
|
||||||
|
import { isEmpty } from '../../shared/empty.util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent unauthorized activating and loading of routes
|
||||||
|
* @class AuthenticatedGuard
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AuthenticatedGuard implements CanActivate, CanLoad {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
constructor(private authService: AuthService, private router: Router, private store: Store<CoreState>) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when user is authenticated
|
||||||
|
* @method canActivate
|
||||||
|
*/
|
||||||
|
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
|
||||||
|
const url = state.url;
|
||||||
|
return this.handleAuth(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when user is authenticated
|
||||||
|
* @method canActivateChild
|
||||||
|
*/
|
||||||
|
canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
|
||||||
|
return this.canActivate(route, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when user is authenticated
|
||||||
|
* @method canLoad
|
||||||
|
*/
|
||||||
|
canLoad(route: Route): Observable<boolean> {
|
||||||
|
const url = `/${route.path}`;
|
||||||
|
|
||||||
|
return this.handleAuth(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleAuth(url: string): Observable<boolean> {
|
||||||
|
// get observable
|
||||||
|
const observable = this.store.select(isAuthenticated);
|
||||||
|
|
||||||
|
// redirect to sign in page if user is not authenticated
|
||||||
|
observable
|
||||||
|
// .filter(() => isEmpty(this.router.routerState.snapshot.url) || this.router.routerState.snapshot.url === url)
|
||||||
|
.take(1)
|
||||||
|
.subscribe((authenticated) => {
|
||||||
|
if (!authenticated) {
|
||||||
|
this.authService.setRedirectUrl(url);
|
||||||
|
this.store.dispatch(new RedirectWhenAuthenticationIsRequiredAction('Login required'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return observable;
|
||||||
|
}
|
||||||
|
}
|
7
src/app/core/auth/models/auth-error.model.ts
Normal file
7
src/app/core/auth/models/auth-error.model.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export interface AuthError {
|
||||||
|
error: string,
|
||||||
|
message: string,
|
||||||
|
path: string,
|
||||||
|
status: number
|
||||||
|
timestamp: number
|
||||||
|
}
|
21
src/app/core/auth/models/auth-status.model.ts
Normal file
21
src/app/core/auth/models/auth-status.model.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { AuthError } from './auth-error.model';
|
||||||
|
import { AuthTokenInfo } from './auth-token-info.model';
|
||||||
|
import { DSpaceObject } from '../../shared/dspace-object.model';
|
||||||
|
import { Eperson } from '../../eperson/models/eperson.model';
|
||||||
|
|
||||||
|
export class AuthStatus {
|
||||||
|
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
okay: boolean;
|
||||||
|
|
||||||
|
authenticated: boolean;
|
||||||
|
|
||||||
|
error?: AuthError;
|
||||||
|
|
||||||
|
eperson: Eperson[];
|
||||||
|
|
||||||
|
token?: AuthTokenInfo;
|
||||||
|
|
||||||
|
self: string;
|
||||||
|
}
|
19
src/app/core/auth/models/auth-token-info.model.ts
Normal file
19
src/app/core/auth/models/auth-token-info.model.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { default as decode } from 'jwt-decode';
|
||||||
|
|
||||||
|
export const TOKENITEM = 'dsAuthInfo';
|
||||||
|
|
||||||
|
export class AuthTokenInfo {
|
||||||
|
public accessToken: string;
|
||||||
|
public expires: number;
|
||||||
|
|
||||||
|
constructor(token: string) {
|
||||||
|
this.accessToken = token.replace('Bearer ', '');
|
||||||
|
try {
|
||||||
|
const tokenClaims = decode(this.accessToken);
|
||||||
|
// exp claim is in seconds, convert it se to milliseconds
|
||||||
|
this.expires = tokenClaims.exp * 1000;
|
||||||
|
} catch (err) {
|
||||||
|
this.expires = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
src/app/core/auth/models/normalized-auth-status.model.ts
Normal file
26
src/app/core/auth/models/normalized-auth-status.model.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { AuthStatus } from './auth-status.model';
|
||||||
|
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
|
||||||
|
import { mapsTo } from '../../cache/builders/build-decorators';
|
||||||
|
import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model';
|
||||||
|
import { Eperson } from '../../eperson/models/eperson.model';
|
||||||
|
|
||||||
|
@mapsTo(AuthStatus)
|
||||||
|
@inheritSerialization(NormalizedDSpaceObject)
|
||||||
|
export class NormalizedAuthStatus extends NormalizedDSpaceObject {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if REST API is up and running, should never return false
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
okay: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the token is valid, false if there was no token or the token wasn't valid
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
authenticated: boolean;
|
||||||
|
|
||||||
|
@autoserializeAs(Eperson)
|
||||||
|
eperson: Eperson[];
|
||||||
|
|
||||||
|
}
|
204
src/app/core/auth/selectors.ts
Normal file
204
src/app/core/auth/selectors.ts
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import { createSelector } from '@ngrx/store';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every reducer module's default export is the reducer function itself. In
|
||||||
|
* addition, each module should export a type or interface that describes
|
||||||
|
* the state of the reducer plus any selector functions. The `* as`
|
||||||
|
* notation packages up all of the exports into a single object.
|
||||||
|
*/
|
||||||
|
import { AuthState } from './auth.reducer';
|
||||||
|
import { AppState } from '../../app.reducer';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the user state.
|
||||||
|
* @function getUserState
|
||||||
|
* @param {AppState} state Top level state.
|
||||||
|
* @return {AuthState}
|
||||||
|
*/
|
||||||
|
export const getAuthState = (state: any) => state.core.auth;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the user is authenticated.
|
||||||
|
* @function _isAuthenticated
|
||||||
|
* @param {State} state
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
const _isAuthenticated = (state: AuthState) => state.authenticated;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the authenticated has loaded.
|
||||||
|
* @function _isAuthenticatedLoaded
|
||||||
|
* @param {State} state
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
const _isAuthenticatedLoaded = (state: AuthState) => state.loaded;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the users state
|
||||||
|
* @function _getAuthenticatedUser
|
||||||
|
* @param {State} state
|
||||||
|
* @returns {User}
|
||||||
|
*/
|
||||||
|
const _getAuthenticatedUser = (state: AuthState) => state.user;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the authentication error.
|
||||||
|
* @function _getAuthenticationError
|
||||||
|
* @param {State} state
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
const _getAuthenticationError = (state: AuthState) => state.error;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the authentication info message.
|
||||||
|
* @function _getAuthenticationInfo
|
||||||
|
* @param {State} state
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
const _getAuthenticationInfo = (state: AuthState) => state.info;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if request is in progress.
|
||||||
|
* @function _isLoading
|
||||||
|
* @param {State} state
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
const _isLoading = (state: AuthState) => state.loading;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if a refresh token request is in progress.
|
||||||
|
* @function _isRefreshing
|
||||||
|
* @param {State} state
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
const _isRefreshing = (state: AuthState) => state.refreshing;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the authentication token.
|
||||||
|
* @function _getAuthenticationToken
|
||||||
|
* @param {State} state
|
||||||
|
* @returns {AuthToken}
|
||||||
|
*/
|
||||||
|
const _getAuthenticationToken = (state: AuthState) => state.authToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the sign out error.
|
||||||
|
* @function _getLogOutError
|
||||||
|
* @param {State} state
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
const _getLogOutError = (state: AuthState) => state.error;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the sign up error.
|
||||||
|
* @function _getRegistrationError
|
||||||
|
* @param {State} state
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
const _getRegistrationError = (state: AuthState) => state.error;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the redirect url.
|
||||||
|
* @function _getRedirectUrl
|
||||||
|
* @param {State} state
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
const _getRedirectUrl = (state: AuthState) => state.redirectUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the authenticated user
|
||||||
|
* @function getAuthenticatedUser
|
||||||
|
* @param {AuthState} state
|
||||||
|
* @param {any} props
|
||||||
|
* @return {User}
|
||||||
|
*/
|
||||||
|
export const getAuthenticatedUser = createSelector(getAuthState, _getAuthenticatedUser);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the authentication error.
|
||||||
|
* @function getAuthenticationError
|
||||||
|
* @param {AuthState} state
|
||||||
|
* @param {any} props
|
||||||
|
* @return {Error}
|
||||||
|
*/
|
||||||
|
export const getAuthenticationError = createSelector(getAuthState, _getAuthenticationError);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the authentication info message.
|
||||||
|
* @function getAuthenticationInfo
|
||||||
|
* @param {AuthState} state
|
||||||
|
* @param {any} props
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
export const getAuthenticationInfo = createSelector(getAuthState, _getAuthenticationInfo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the user is authenticated
|
||||||
|
* @function isAuthenticated
|
||||||
|
* @param {AuthState} state
|
||||||
|
* @param {any} props
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
export const isAuthenticated = createSelector(getAuthState, _isAuthenticated);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the user is authenticated
|
||||||
|
* @function isAuthenticated
|
||||||
|
* @param {AuthState} state
|
||||||
|
* @param {any} props
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
export const isAuthenticatedLoaded = createSelector(getAuthState, _isAuthenticatedLoaded);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the authentication request is loading.
|
||||||
|
* @function isAuthenticationLoading
|
||||||
|
* @param {AuthState} state
|
||||||
|
* @param {any} props
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
export const isAuthenticationLoading = createSelector(getAuthState, _isLoading);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the refresh token request is loading.
|
||||||
|
* @function isTokenRefreshing
|
||||||
|
* @param {AuthState} state
|
||||||
|
* @param {any} props
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
export const isTokenRefreshing = createSelector(getAuthState, _isRefreshing);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the authentication token.
|
||||||
|
* @function getAuthenticationToken
|
||||||
|
* @param {State} state
|
||||||
|
* @returns {AuthToken}
|
||||||
|
*/
|
||||||
|
export const getAuthenticationToken = createSelector(getAuthState, _getAuthenticationToken);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the log out error.
|
||||||
|
* @function getLogOutError
|
||||||
|
* @param {AuthState} state
|
||||||
|
* @param {any} props
|
||||||
|
* @return {Error}
|
||||||
|
*/
|
||||||
|
export const getLogOutError = createSelector(getAuthState, _getLogOutError);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the registration error.
|
||||||
|
* @function getRegistrationError
|
||||||
|
* @param {AuthState} state
|
||||||
|
* @param {any} props
|
||||||
|
* @return {Error}
|
||||||
|
*/
|
||||||
|
export const getRegistrationError = createSelector(getAuthState, _getRegistrationError);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the redirect url.
|
||||||
|
* @function getRedirectUrl
|
||||||
|
* @param {AuthState} state
|
||||||
|
* @param {any} props
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
export const getRedirectUrl = createSelector(getAuthState, _getRedirectUrl);
|
74
src/app/core/auth/server-auth.service.ts
Normal file
74
src/app/core/auth/server-auth.service.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { HttpHeaders } from '@angular/common/http';
|
||||||
|
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||||
|
import { AuthStatus } from './models/auth-status.model';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||||
|
import { CheckAuthenticationTokenAction } from './auth.actions';
|
||||||
|
import { Eperson } from '../eperson/models/eperson.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The auth service.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ServerAuthService extends AuthService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the authenticated user
|
||||||
|
* @returns {User}
|
||||||
|
*/
|
||||||
|
public authenticatedUser(token: AuthTokenInfo): Observable<Eperson> {
|
||||||
|
// Determine if the user has an existing auth session on the server
|
||||||
|
const options: HttpOptions = Object.create({});
|
||||||
|
let headers = new HttpHeaders();
|
||||||
|
|
||||||
|
headers = headers.append('Accept', 'application/json');
|
||||||
|
headers = headers.append('Authorization', `Bearer ${token.accessToken}`);
|
||||||
|
// NB this is used to pass server client IP check.
|
||||||
|
const clientIp = this.req.get('x-forwarded-for') || this.req.connection.remoteAddress;
|
||||||
|
headers = headers.append('X-Forwarded-For', clientIp);
|
||||||
|
|
||||||
|
options.headers = headers;
|
||||||
|
return this.authRequestService.getRequest('status', options)
|
||||||
|
.map((status: AuthStatus) => {
|
||||||
|
if (status.authenticated) {
|
||||||
|
return status.eperson[0];
|
||||||
|
} else {
|
||||||
|
throw(new Error('Not authenticated'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if token is present into browser storage and is valid. (NB Check is done only on SSR)
|
||||||
|
*/
|
||||||
|
public checkAuthenticationToken() {
|
||||||
|
this.store.dispatch(new CheckAuthenticationTokenAction())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirect to the route navigated before the login
|
||||||
|
*/
|
||||||
|
public redirectToPreviousUrl() {
|
||||||
|
this.getRedirectUrl()
|
||||||
|
.first()
|
||||||
|
.subscribe((redirectUrl) => {
|
||||||
|
if (isNotEmpty(redirectUrl)) {
|
||||||
|
// override the route reuse strategy
|
||||||
|
this.router.routeReuseStrategy.shouldReuseRoute = () => {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
this.router.navigated = false;
|
||||||
|
const url = decodeURIComponent(redirectUrl);
|
||||||
|
this.router.navigateByUrl(url);
|
||||||
|
} else {
|
||||||
|
this.router.navigate(['/']);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1,24 +1,28 @@
|
|||||||
|
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
||||||
|
import { TestScheduler } from 'rxjs/Rx';
|
||||||
|
import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service';
|
||||||
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
||||||
import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service';
|
import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service';
|
||||||
import { BrowseService } from './browse.service';
|
|
||||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
|
||||||
import { RequestService } from '../data/request.service';
|
|
||||||
import { hot, cold, getTestScheduler } from 'jasmine-marbles';
|
|
||||||
import { BrowseDefinition } from '../shared/browse-definition.model';
|
|
||||||
import { BrowseEndpointRequest } from '../data/request.models';
|
|
||||||
import { TestScheduler } from 'rxjs/Rx';
|
|
||||||
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
|
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
|
import { BrowseEndpointRequest, BrowseEntriesRequest } from '../data/request.models';
|
||||||
|
import { RequestService } from '../data/request.service';
|
||||||
|
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||||
|
import { BrowseService } from './browse.service';
|
||||||
|
|
||||||
describe('BrowseService', () => {
|
describe('BrowseService', () => {
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
let service: BrowseService;
|
let service: BrowseService;
|
||||||
let responseCache: ResponseCacheService;
|
let responseCache: ResponseCacheService;
|
||||||
let requestService: RequestService;
|
let requestService: RequestService;
|
||||||
|
let rdbService: RemoteDataBuildService;
|
||||||
|
|
||||||
const browsesEndpointURL = 'https://rest.api/browses';
|
const browsesEndpointURL = 'https://rest.api/browses';
|
||||||
const halService: any = new HALEndpointServiceStub(browsesEndpointURL);
|
const halService: any = new HALEndpointServiceStub(browsesEndpointURL);
|
||||||
const browseDefinitions = [
|
const browseDefinitions = [
|
||||||
Object.assign(new BrowseDefinition(), {
|
Object.assign(new BrowseDefinition(), {
|
||||||
|
id: 'date',
|
||||||
metadataBrowse: false,
|
metadataBrowse: false,
|
||||||
sortOptions: [
|
sortOptions: [
|
||||||
{
|
{
|
||||||
@@ -45,6 +49,7 @@ describe('BrowseService', () => {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
Object.assign(new BrowseDefinition(), {
|
Object.assign(new BrowseDefinition(), {
|
||||||
|
id: 'author',
|
||||||
metadataBrowse: true,
|
metadataBrowse: true,
|
||||||
sortOptions: [
|
sortOptions: [
|
||||||
{
|
{
|
||||||
@@ -80,7 +85,7 @@ describe('BrowseService', () => {
|
|||||||
b: {
|
b: {
|
||||||
response: {
|
response: {
|
||||||
isSuccessful,
|
isSuccessful,
|
||||||
browseDefinitions,
|
payload: browseDefinitions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
@@ -91,7 +96,8 @@ describe('BrowseService', () => {
|
|||||||
return new BrowseService(
|
return new BrowseService(
|
||||||
responseCache,
|
responseCache,
|
||||||
requestService,
|
requestService,
|
||||||
halService
|
halService,
|
||||||
|
rdbService
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,15 +105,99 @@ describe('BrowseService', () => {
|
|||||||
scheduler = getTestScheduler();
|
scheduler = getTestScheduler();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getBrowseURLFor', () => {
|
describe('getBrowseDefinitions', () => {
|
||||||
|
|
||||||
describe('if getEndpoint fires', () => {
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
responseCache = initMockResponseCacheService(true);
|
responseCache = initMockResponseCacheService(true);
|
||||||
requestService = getMockRequestService();
|
requestService = getMockRequestService();
|
||||||
|
rdbService = getMockRemoteDataBuildService();
|
||||||
service = initTestService();
|
service = initTestService();
|
||||||
spyOn(halService, 'getEndpoint').and
|
spyOn(halService, 'getEndpoint').and
|
||||||
.returnValue(hot('--a-', { a: browsesEndpointURL }));
|
.returnValue(hot('--a-', { a: browsesEndpointURL }));
|
||||||
|
spyOn(rdbService, 'toRemoteDataObservable').and.callThrough();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should configure a new BrowseEndpointRequest', () => {
|
||||||
|
const expected = new BrowseEndpointRequest(requestService.generateRequestId(), browsesEndpointURL);
|
||||||
|
|
||||||
|
scheduler.schedule(() => service.getBrowseDefinitions().subscribe());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call RemoteDataBuildService to create the RemoteData Observable', () => {
|
||||||
|
service.getBrowseDefinitions();
|
||||||
|
|
||||||
|
expect(rdbService.toRemoteDataObservable).toHaveBeenCalled();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a RemoteData object containing the correct BrowseDefinition[]', () => {
|
||||||
|
const expected = cold('--a-', { a: {
|
||||||
|
payload: browseDefinitions
|
||||||
|
}});
|
||||||
|
|
||||||
|
expect(service.getBrowseDefinitions()).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getBrowseEntriesFor', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
responseCache = initMockResponseCacheService(true);
|
||||||
|
requestService = getMockRequestService();
|
||||||
|
rdbService = getMockRemoteDataBuildService();
|
||||||
|
service = initTestService();
|
||||||
|
spyOn(service, 'getBrowseDefinitions').and
|
||||||
|
.returnValue(hot('--a-', { a: {
|
||||||
|
payload: browseDefinitions
|
||||||
|
}}));
|
||||||
|
spyOn(rdbService, 'toRemoteDataObservable').and.callThrough();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when called with a valid browse definition id', () => {
|
||||||
|
it('should configure a new BrowseEntriesRequest', () => {
|
||||||
|
const expected = new BrowseEntriesRequest(requestService.generateRequestId(), browseDefinitions[1]._links.entries);
|
||||||
|
|
||||||
|
scheduler.schedule(() => service.getBrowseEntriesFor(browseDefinitions[1].id).subscribe());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call RemoteDataBuildService to create the RemoteData Observable', () => {
|
||||||
|
service.getBrowseEntriesFor(browseDefinitions[1].id);
|
||||||
|
|
||||||
|
expect(rdbService.toRemoteDataObservable).toHaveBeenCalled();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when called with an invalid browse definition id', () => {
|
||||||
|
it('should throw an Error', () => {
|
||||||
|
|
||||||
|
const definitionID = 'invalidID';
|
||||||
|
const expected = cold('--#-', undefined, new Error(`No metadata browse definition could be found for id '${definitionID}'`))
|
||||||
|
|
||||||
|
expect(service.getBrowseEntriesFor(definitionID)).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getBrowseURLFor', () => {
|
||||||
|
|
||||||
|
describe('if getBrowseDefinitions fires', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
responseCache = initMockResponseCacheService(true);
|
||||||
|
requestService = getMockRequestService();
|
||||||
|
rdbService = getMockRemoteDataBuildService();
|
||||||
|
service = initTestService();
|
||||||
|
spyOn(service, 'getBrowseDefinitions').and
|
||||||
|
.returnValue(hot('--a-', { a: {
|
||||||
|
payload: browseDefinitions
|
||||||
|
}}));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the URL for the given metadatumKey and linkPath', () => {
|
it('should return the URL for the given metadatumKey and linkPath', () => {
|
||||||
@@ -152,26 +242,15 @@ describe('BrowseService', () => {
|
|||||||
expect(result).toBeObservable(expected);
|
expect(result).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should configure a new BrowseEndpointRequest', () => {
|
|
||||||
const metadatumKey = 'dc.date.issued';
|
|
||||||
const linkPath = 'items';
|
|
||||||
const expected = new BrowseEndpointRequest(requestService.generateRequestId(), browsesEndpointURL);
|
|
||||||
|
|
||||||
scheduler.schedule(() => service.getBrowseURLFor(metadatumKey, linkPath).subscribe());
|
|
||||||
scheduler.flush();
|
|
||||||
|
|
||||||
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
describe('if getBrowseDefinitions doesn\'t fire', () => {
|
||||||
|
|
||||||
describe('if getEndpoint doesn\'t fire', () => {
|
|
||||||
it('should return undefined', () => {
|
it('should return undefined', () => {
|
||||||
responseCache = initMockResponseCacheService(true);
|
responseCache = initMockResponseCacheService(true);
|
||||||
requestService = getMockRequestService();
|
requestService = getMockRequestService();
|
||||||
|
rdbService = getMockRemoteDataBuildService();
|
||||||
service = initTestService();
|
service = initTestService();
|
||||||
spyOn(halService, 'getEndpoint').and
|
spyOn(service, 'getBrowseDefinitions').and
|
||||||
.returnValue(hot('----'));
|
.returnValue(hot('----'));
|
||||||
|
|
||||||
const metadatumKey = 'dc.date.issued';
|
const metadatumKey = 'dc.date.issued';
|
||||||
@@ -182,22 +261,5 @@ describe('BrowseService', () => {
|
|||||||
expect(result).toBeObservable(expected);
|
expect(result).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('if the browses endpoint can\'t be retrieved', () => {
|
|
||||||
it('should throw an error', () => {
|
|
||||||
responseCache = initMockResponseCacheService(false);
|
|
||||||
requestService = getMockRequestService();
|
|
||||||
service = initTestService();
|
|
||||||
spyOn(halService, 'getEndpoint').and
|
|
||||||
.returnValue(hot('--a-', { a: browsesEndpointURL }));
|
|
||||||
|
|
||||||
const metadatumKey = 'dc.date.issued';
|
|
||||||
const linkPath = 'items';
|
|
||||||
|
|
||||||
const result = service.getBrowseURLFor(metadatumKey, linkPath);
|
|
||||||
const expected = cold('c-#-', { c: undefined }, new Error(`Couldn't retrieve the browses endpoint`));
|
|
||||||
expect(result).toBeObservable(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,15 +1,34 @@
|
|||||||
import { Inject, Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { GLOBAL_CONFIG } from '../../../config';
|
import { distinctUntilChanged, map, startWith } from 'rxjs/operators';
|
||||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
import {
|
||||||
import { isEmpty, isNotEmpty } from '../../shared/empty.util';
|
ensureArrayHasValue,
|
||||||
import { BrowseSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models';
|
hasValueOperator,
|
||||||
|
isEmpty,
|
||||||
|
isNotEmpty,
|
||||||
|
isNotEmptyOperator
|
||||||
|
} from '../../shared/empty.util';
|
||||||
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { SortOptions } from '../cache/models/sort-options.model';
|
||||||
|
import { GenericSuccessResponse } from '../cache/response-cache.models';
|
||||||
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
||||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
import { BrowseEndpointRequest, RestRequest } from '../data/request.models';
|
import { PaginatedList } from '../data/paginated-list';
|
||||||
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
import { BrowseEndpointRequest, BrowseEntriesRequest, RestRequest } from '../data/request.models';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { BrowseDefinition } from '../shared/browse-definition.model';
|
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||||
|
import { BrowseEntry } from '../shared/browse-entry.model';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import {
|
||||||
|
configureRequest,
|
||||||
|
filterSuccessfulResponses,
|
||||||
|
getRemoteDataPayload,
|
||||||
|
getRequestFromSelflink,
|
||||||
|
getResponseFromSelflink
|
||||||
|
} from '../shared/operators';
|
||||||
|
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BrowseService {
|
export class BrowseService {
|
||||||
@@ -31,42 +50,106 @@ export class BrowseService {
|
|||||||
constructor(
|
constructor(
|
||||||
protected responseCache: ResponseCacheService,
|
protected responseCache: ResponseCacheService,
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected halService: HALEndpointService) {
|
protected halService: HALEndpointService,
|
||||||
|
private rdb: RemoteDataBuildService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
getBrowseDefinitions(): Observable<RemoteData<BrowseDefinition[]>> {
|
||||||
|
const request$ = this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
|
isNotEmptyOperator(),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
map((endpointURL: string) => new BrowseEndpointRequest(this.requestService.generateRequestId(), endpointURL)),
|
||||||
|
configureRequest(this.requestService)
|
||||||
|
);
|
||||||
|
|
||||||
|
const href$ = request$.pipe(map((request: RestRequest) => request.href));
|
||||||
|
const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService));
|
||||||
|
const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache));
|
||||||
|
const payload$ = responseCache$.pipe(
|
||||||
|
filterSuccessfulResponses(),
|
||||||
|
map((entry: ResponseCacheEntry) => entry.response),
|
||||||
|
map((response: GenericSuccessResponse<BrowseDefinition[]>) => response.payload),
|
||||||
|
ensureArrayHasValue(),
|
||||||
|
distinctUntilChanged()
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.rdb.toRemoteDataObservable(requestEntry$, responseCache$, payload$);
|
||||||
|
}
|
||||||
|
|
||||||
|
getBrowseEntriesFor(definitionID: string, options: {
|
||||||
|
pagination?: PaginationComponentOptions;
|
||||||
|
sort?: SortOptions;
|
||||||
|
} = {}): Observable<RemoteData<PaginatedList<BrowseEntry>>> {
|
||||||
|
const request$ = this.getBrowseDefinitions().pipe(
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
map((browseDefinitions: BrowseDefinition[]) => browseDefinitions
|
||||||
|
.find((def: BrowseDefinition) => def.id === definitionID && def.metadataBrowse === true)
|
||||||
|
),
|
||||||
|
map((def: BrowseDefinition) => {
|
||||||
|
if (isNotEmpty(def)) {
|
||||||
|
return def._links;
|
||||||
|
} else {
|
||||||
|
throw new Error(`No metadata browse definition could be found for id '${definitionID}'`);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
hasValueOperator(),
|
||||||
|
map((_links: any) => _links.entries),
|
||||||
|
hasValueOperator(),
|
||||||
|
map((href: string) => {
|
||||||
|
// TODO nearly identical to PaginatedSearchOptions => refactor
|
||||||
|
const args = [];
|
||||||
|
if (isNotEmpty(options.sort)) {
|
||||||
|
args.push(`sort=${options.sort.field},${options.sort.direction}`);
|
||||||
|
}
|
||||||
|
if (isNotEmpty(options.pagination)) {
|
||||||
|
args.push(`page=${options.pagination.currentPage - 1}`);
|
||||||
|
args.push(`size=${options.pagination.pageSize}`);
|
||||||
|
}
|
||||||
|
if (isNotEmpty(args)) {
|
||||||
|
href = new URLCombiner(href, `?${args.join('&')}`).toString();
|
||||||
|
}
|
||||||
|
return href;
|
||||||
|
}),
|
||||||
|
map((endpointURL: string) => new BrowseEntriesRequest(this.requestService.generateRequestId(), endpointURL)),
|
||||||
|
configureRequest(this.requestService)
|
||||||
|
);
|
||||||
|
|
||||||
|
const href$ = request$.pipe(map((request: RestRequest) => request.href));
|
||||||
|
|
||||||
|
const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService));
|
||||||
|
const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache));
|
||||||
|
|
||||||
|
const payload$ = responseCache$.pipe(
|
||||||
|
filterSuccessfulResponses(),
|
||||||
|
map((entry: ResponseCacheEntry) => entry.response),
|
||||||
|
map((response: GenericSuccessResponse<BrowseEntry[]>) => new PaginatedList(response.pageInfo, response.payload)),
|
||||||
|
distinctUntilChanged()
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.rdb.toRemoteDataObservable(requestEntry$, responseCache$, payload$);
|
||||||
}
|
}
|
||||||
|
|
||||||
getBrowseURLFor(metadatumKey: string, linkPath: string): Observable<string> {
|
getBrowseURLFor(metadatumKey: string, linkPath: string): Observable<string> {
|
||||||
const searchKeyArray = BrowseService.toSearchKeyArray(metadatumKey);
|
const searchKeyArray = BrowseService.toSearchKeyArray(metadatumKey);
|
||||||
return this.halService.getEndpoint(this.linkPath)
|
return this.getBrowseDefinitions().pipe(
|
||||||
.filter((href: string) => isNotEmpty(href))
|
getRemoteDataPayload(),
|
||||||
.distinctUntilChanged()
|
map((browseDefinitions: BrowseDefinition[]) => browseDefinitions
|
||||||
.map((endpointURL: string) => new BrowseEndpointRequest(this.requestService.generateRequestId(), endpointURL))
|
|
||||||
.do((request: RestRequest) => this.requestService.configure(request))
|
|
||||||
.flatMap((request: RestRequest) => {
|
|
||||||
const [successResponse, errorResponse] = this.responseCache.get(request.href)
|
|
||||||
.map((entry: ResponseCacheEntry) => entry.response)
|
|
||||||
.partition((response: RestResponse) => response.isSuccessful);
|
|
||||||
|
|
||||||
return Observable.merge(
|
|
||||||
errorResponse.flatMap((response: ErrorResponse) =>
|
|
||||||
Observable.throw(new Error(`Couldn't retrieve the browses endpoint`))),
|
|
||||||
successResponse
|
|
||||||
.filter((response: BrowseSuccessResponse) => isNotEmpty(response.browseDefinitions))
|
|
||||||
.map((response: BrowseSuccessResponse) => response.browseDefinitions)
|
|
||||||
.map((browseDefinitions: BrowseDefinition[]) => browseDefinitions
|
|
||||||
.find((def: BrowseDefinition) => {
|
.find((def: BrowseDefinition) => {
|
||||||
const matchingKeys = def.metadataKeys.find((key: string) => searchKeyArray.indexOf(key) >= 0);
|
const matchingKeys = def.metadataKeys.find((key: string) => searchKeyArray.indexOf(key) >= 0);
|
||||||
return isNotEmpty(matchingKeys);
|
return isNotEmpty(matchingKeys);
|
||||||
})
|
})
|
||||||
).map((def: BrowseDefinition) => {
|
),
|
||||||
|
map((def: BrowseDefinition) => {
|
||||||
if (isEmpty(def) || isEmpty(def._links) || isEmpty(def._links[linkPath])) {
|
if (isEmpty(def) || isEmpty(def._links) || isEmpty(def._links[linkPath])) {
|
||||||
throw new Error(`A browse endpoint for ${linkPath} on ${metadatumKey} isn't configured`);
|
throw new Error(`A browse endpoint for ${linkPath} on ${metadatumKey} isn't configured`);
|
||||||
} else {
|
} else {
|
||||||
return def._links[linkPath];
|
return def._links[linkPath];
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
startWith(undefined),
|
||||||
|
distinctUntilChanged()
|
||||||
);
|
);
|
||||||
}).startWith(undefined)
|
|
||||||
.distinctUntilChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,28 +1,25 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { map, tap } from 'rxjs/operators';
|
import { distinctUntilChanged, flatMap, map, startWith } from 'rxjs/operators';
|
||||||
import { NormalizedSearchResult } from '../../../+search-page/normalized-search-result.model';
|
import { hasValue, hasValueOperator, isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
||||||
import { SearchResult } from '../../../+search-page/search-result.model';
|
|
||||||
import { SearchQueryResponse } from '../../../+search-page/search-service/search-query-response.model';
|
|
||||||
import { hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
|
||||||
import { PaginatedList } from '../../data/paginated-list';
|
import { PaginatedList } from '../../data/paginated-list';
|
||||||
import { RemoteData } from '../../data/remote-data';
|
import { RemoteData } from '../../data/remote-data';
|
||||||
import { RemoteDataError } from '../../data/remote-data-error';
|
import { RemoteDataError } from '../../data/remote-data-error';
|
||||||
import { GetRequest, RestRequest } from '../../data/request.models';
|
import { GetRequest } from '../../data/request.models';
|
||||||
import { RequestEntry } from '../../data/request.reducer';
|
import { RequestEntry } from '../../data/request.reducer';
|
||||||
import { RequestService } from '../../data/request.service';
|
import { RequestService } from '../../data/request.service';
|
||||||
import { DSpaceObject } from '../../shared/dspace-object.model';
|
import { NormalizedObject } from '../models/normalized-object.model';
|
||||||
import { GenericConstructor } from '../../shared/generic-constructor';
|
|
||||||
import { NormalizedDSpaceObject } from '../models/normalized-dspace-object.model';
|
|
||||||
import { NormalizedObjectFactory } from '../models/normalized-object-factory';
|
|
||||||
|
|
||||||
import { CacheableObject } from '../object-cache.reducer';
|
|
||||||
import { ObjectCacheService } from '../object-cache.service';
|
import { ObjectCacheService } from '../object-cache.service';
|
||||||
import { DSOSuccessResponse, ErrorResponse, SearchSuccessResponse } from '../response-cache.models';
|
import { DSOSuccessResponse, ErrorResponse } from '../response-cache.models';
|
||||||
import { ResponseCacheEntry } from '../response-cache.reducer';
|
import { ResponseCacheEntry } from '../response-cache.reducer';
|
||||||
import { ResponseCacheService } from '../response-cache.service';
|
import { ResponseCacheService } from '../response-cache.service';
|
||||||
import { getMapsTo, getRelationMetadata, getRelationships } from './build-decorators';
|
import { getMapsTo, getRelationMetadata, getRelationships } from './build-decorators';
|
||||||
import { NormalizedObject } from '../models/normalized-object.model';
|
import {
|
||||||
|
getRequestFromSelflink,
|
||||||
|
getResourceLinksFromResponse,
|
||||||
|
getResponseFromSelflink,
|
||||||
|
filterSuccessfulResponses
|
||||||
|
} from '../../shared/operators';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RemoteDataBuildService {
|
export class RemoteDataBuildService {
|
||||||
@@ -31,43 +28,42 @@ export class RemoteDataBuildService {
|
|||||||
protected requestService: RequestService) {
|
protected requestService: RequestService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
buildSingle<TNormalized extends NormalizedObject, TDomain>(hrefObs: string | Observable<string>): Observable<RemoteData<TDomain>> {
|
buildSingle<TNormalized extends NormalizedObject, TDomain>(href$: string | Observable<string>): Observable<RemoteData<TDomain>> {
|
||||||
if (typeof hrefObs === 'string') {
|
if (typeof href$ === 'string') {
|
||||||
hrefObs = Observable.of(hrefObs);
|
href$ = Observable.of(href$);
|
||||||
}
|
}
|
||||||
const requestHrefObs = hrefObs.flatMap((href: string) =>
|
const requestHref$ = href$.pipe(flatMap((href: string) =>
|
||||||
this.objectCache.getRequestHrefBySelfLink(href));
|
this.objectCache.getRequestHrefBySelfLink(href)));
|
||||||
|
|
||||||
const requestEntryObs = Observable.race(
|
const requestEntry$ = Observable.race(
|
||||||
hrefObs.flatMap((href: string) => this.requestService.getByHref(href))
|
href$.pipe(getRequestFromSelflink(this.requestService)),
|
||||||
.filter((entry) => hasValue(entry)),
|
requestHref$.pipe(getRequestFromSelflink(this.requestService))
|
||||||
requestHrefObs.flatMap((requestHref) =>
|
|
||||||
this.requestService.getByHref(requestHref)).filter((entry) => hasValue(entry))
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const responseCacheObs = Observable.race(
|
const responseCache$ = Observable.race(
|
||||||
hrefObs.flatMap((href: string) => this.responseCache.get(href))
|
href$.pipe(getResponseFromSelflink(this.responseCache)),
|
||||||
.filter((entry) => hasValue(entry)),
|
requestHref$.pipe(getResponseFromSelflink(this.responseCache))
|
||||||
requestHrefObs.flatMap((requestHref) => this.responseCache.get(requestHref)).filter((entry) => hasValue(entry))
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// always use self link if that is cached, only if it isn't, get it via the response.
|
// always use self link if that is cached, only if it isn't, get it via the response.
|
||||||
const payloadObs =
|
const payload$ =
|
||||||
Observable.combineLatest(
|
Observable.combineLatest(
|
||||||
hrefObs.flatMap((href: string) => this.objectCache.getBySelfLink<TNormalized>(href))
|
href$.pipe(
|
||||||
.startWith(undefined),
|
flatMap((href: string) => this.objectCache.getBySelfLink<TNormalized>(href)),
|
||||||
responseCacheObs
|
startWith(undefined)
|
||||||
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
|
),
|
||||||
.map((entry: ResponseCacheEntry) => (entry.response as DSOSuccessResponse).resourceSelfLinks)
|
responseCache$.pipe(
|
||||||
.flatMap((resourceSelfLinks: string[]) => {
|
getResourceLinksFromResponse(),
|
||||||
|
flatMap((resourceSelfLinks: string[]) => {
|
||||||
if (isNotEmpty(resourceSelfLinks)) {
|
if (isNotEmpty(resourceSelfLinks)) {
|
||||||
return this.objectCache.getBySelfLink(resourceSelfLinks[0]);
|
return this.objectCache.getBySelfLink(resourceSelfLinks[0]);
|
||||||
} else {
|
} else {
|
||||||
return Observable.of(undefined);
|
return Observable.of(undefined);
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
.distinctUntilChanged()
|
distinctUntilChanged(),
|
||||||
.startWith(undefined),
|
startWith(undefined)
|
||||||
|
),
|
||||||
(fromSelfLink, fromResponse) => {
|
(fromSelfLink, fromResponse) => {
|
||||||
if (hasValue(fromSelfLink)) {
|
if (hasValue(fromSelfLink)) {
|
||||||
return fromSelfLink;
|
return fromSelfLink;
|
||||||
@@ -75,17 +71,19 @@ export class RemoteDataBuildService {
|
|||||||
return fromResponse;
|
return fromResponse;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
).filter((normalized) => hasValue(normalized))
|
).pipe(
|
||||||
.map((normalized: TNormalized) => {
|
hasValueOperator(),
|
||||||
|
map((normalized: TNormalized) => {
|
||||||
return this.build<TNormalized, TDomain>(normalized);
|
return this.build<TNormalized, TDomain>(normalized);
|
||||||
})
|
}),
|
||||||
.startWith(undefined)
|
startWith(undefined),
|
||||||
.distinctUntilChanged();
|
distinctUntilChanged()
|
||||||
return this.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs);
|
);
|
||||||
|
return this.toRemoteDataObservable(requestEntry$, responseCache$, payload$);
|
||||||
}
|
}
|
||||||
|
|
||||||
toRemoteDataObservable<T>(requestEntryObs: Observable<RequestEntry>, responseCacheObs: Observable<ResponseCacheEntry>, payloadObs: Observable<T>) {
|
toRemoteDataObservable<T>(requestEntry$: Observable<RequestEntry>, responseCache$: Observable<ResponseCacheEntry>, payload$: Observable<T>) {
|
||||||
return Observable.combineLatest(requestEntryObs, responseCacheObs.startWith(undefined), payloadObs,
|
return Observable.combineLatest(requestEntry$, responseCache$.startWith(undefined), payload$,
|
||||||
(reqEntry: RequestEntry, resEntry: ResponseCacheEntry, payload: T) => {
|
(reqEntry: RequestEntry, resEntry: ResponseCacheEntry, payload: T) => {
|
||||||
const requestPending = hasValue(reqEntry.requestPending) ? reqEntry.requestPending : true;
|
const requestPending = hasValue(reqEntry.requestPending) ? reqEntry.requestPending : true;
|
||||||
const responsePending = hasValue(reqEntry.responsePending) ? reqEntry.responsePending : false;
|
const responsePending = hasValue(reqEntry.responsePending) ? reqEntry.responsePending : false;
|
||||||
@@ -109,33 +107,31 @@ export class RemoteDataBuildService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
buildList<TNormalized extends NormalizedObject, TDomain>(hrefObs: string | Observable<string>): Observable<RemoteData<TDomain[] | PaginatedList<TDomain>>> {
|
buildList<TNormalized extends NormalizedObject, TDomain>(href$: string | Observable<string>): Observable<RemoteData<TDomain[] | PaginatedList<TDomain>>> {
|
||||||
if (typeof hrefObs === 'string') {
|
if (typeof href$ === 'string') {
|
||||||
hrefObs = Observable.of(hrefObs);
|
href$ = Observable.of(href$);
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestEntryObs = hrefObs.flatMap((href: string) => this.requestService.getByHref(href))
|
const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService));
|
||||||
.filter((entry) => hasValue(entry));
|
const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache));
|
||||||
const responseCacheObs = hrefObs.flatMap((href: string) => this.responseCache.get(href))
|
|
||||||
.filter((entry) => hasValue(entry));
|
|
||||||
|
|
||||||
const tDomainListObs = responseCacheObs
|
const tDomainList$ = responseCache$.pipe(
|
||||||
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
|
getResourceLinksFromResponse(),
|
||||||
.map((entry: ResponseCacheEntry) => (entry.response as DSOSuccessResponse).resourceSelfLinks)
|
flatMap((resourceUUIDs: string[]) => {
|
||||||
.flatMap((resourceUUIDs: string[]) => {
|
|
||||||
return this.objectCache.getList(resourceUUIDs)
|
return this.objectCache.getList(resourceUUIDs)
|
||||||
.map((normList: TNormalized[]) => {
|
.map((normList: TNormalized[]) => {
|
||||||
return normList.map((normalized: TNormalized) => {
|
return normList.map((normalized: TNormalized) => {
|
||||||
return this.build<TNormalized, TDomain>(normalized);
|
return this.build<TNormalized, TDomain>(normalized);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
}),
|
||||||
.startWith([])
|
startWith([]),
|
||||||
.distinctUntilChanged();
|
distinctUntilChanged()
|
||||||
|
);
|
||||||
|
|
||||||
const pageInfoObs = responseCacheObs
|
const pageInfo$ = responseCache$.pipe(
|
||||||
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
|
filterSuccessfulResponses(),
|
||||||
.map((entry: ResponseCacheEntry) => {
|
map((entry: ResponseCacheEntry) => {
|
||||||
if (hasValue((entry.response as DSOSuccessResponse).pageInfo)) {
|
if (hasValue((entry.response as DSOSuccessResponse).pageInfo)) {
|
||||||
const resPageInfo = (entry.response as DSOSuccessResponse).pageInfo;
|
const resPageInfo = (entry.response as DSOSuccessResponse).pageInfo;
|
||||||
if (isNotEmpty(resPageInfo) && resPageInfo.currentPage >= 0) {
|
if (isNotEmpty(resPageInfo) && resPageInfo.currentPage >= 0) {
|
||||||
@@ -144,9 +140,10 @@ export class RemoteDataBuildService {
|
|||||||
return resPageInfo;
|
return resPageInfo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const payloadObs = Observable.combineLatest(tDomainListObs, pageInfoObs, (tDomainList, pageInfo) => {
|
const payload$ = Observable.combineLatest(tDomainList$, pageInfo$, (tDomainList, pageInfo) => {
|
||||||
if (hasValue(pageInfo)) {
|
if (hasValue(pageInfo)) {
|
||||||
return new PaginatedList(pageInfo, tDomainList);
|
return new PaginatedList(pageInfo, tDomainList);
|
||||||
} else {
|
} else {
|
||||||
@@ -154,7 +151,7 @@ export class RemoteDataBuildService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs);
|
return this.toRemoteDataObservable(requestEntry$, responseCache$, payload$);
|
||||||
}
|
}
|
||||||
|
|
||||||
build<TNormalized, TDomain>(normalized: TNormalized): TDomain {
|
build<TNormalized, TDomain>(normalized: TNormalized): TDomain {
|
||||||
|
25
src/app/core/cache/response-cache.models.ts
vendored
25
src/app/core/cache/response-cache.models.ts
vendored
@@ -1,5 +1,6 @@
|
|||||||
import { SearchQueryResponse } from '../../+search-page/search-service/search-query-response.model';
|
import { SearchQueryResponse } from '../../+search-page/search-service/search-query-response.model';
|
||||||
import { RequestError } from '../data/request.models';
|
import { RequestError } from '../data/request.models';
|
||||||
|
import { BrowseEntry } from '../shared/browse-entry.model';
|
||||||
import { PageInfo } from '../shared/page-info.model';
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
import { BrowseDefinition } from '../shared/browse-definition.model';
|
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||||
import { ConfigObject } from '../shared/config/config.model';
|
import { ConfigObject } from '../shared/config/config.model';
|
||||||
@@ -9,12 +10,16 @@ import { RegistryMetadataschemasResponse } from '../registry/registry-metadatasc
|
|||||||
import { MetadataSchema } from '../metadata/metadataschema.model';
|
import { MetadataSchema } from '../metadata/metadataschema.model';
|
||||||
import { RegistryMetadatafieldsResponse } from '../registry/registry-metadatafields-response.model';
|
import { RegistryMetadatafieldsResponse } from '../registry/registry-metadatafields-response.model';
|
||||||
import { RegistryBitstreamformatsResponse } from '../registry/registry-bitstreamformats-response.model';
|
import { RegistryBitstreamformatsResponse } from '../registry/registry-bitstreamformats-response.model';
|
||||||
|
import { AuthTokenInfo } from '../auth/models/auth-token-info.model';
|
||||||
|
import { NormalizedAuthStatus } from '../auth/models/normalized-auth-status.model';
|
||||||
|
import { AuthStatus } from '../auth/models/auth-status.model';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
export class RestResponse {
|
export class RestResponse {
|
||||||
|
public toCache = true;
|
||||||
constructor(
|
constructor(
|
||||||
public isSuccessful: boolean,
|
public isSuccessful: boolean,
|
||||||
public statusCode: string
|
public statusCode: string,
|
||||||
) { }
|
) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,10 +126,11 @@ export class EndpointMapSuccessResponse extends RestResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BrowseSuccessResponse extends RestResponse {
|
export class GenericSuccessResponse<T> extends RestResponse {
|
||||||
constructor(
|
constructor(
|
||||||
public browseDefinitions: BrowseDefinition[],
|
public payload: T,
|
||||||
public statusCode: string
|
public statusCode: string,
|
||||||
|
public pageInfo?: PageInfo
|
||||||
) {
|
) {
|
||||||
super(true, statusCode);
|
super(true, statusCode);
|
||||||
}
|
}
|
||||||
@@ -149,4 +155,15 @@ export class ConfigSuccessResponse extends RestResponse {
|
|||||||
super(true, statusCode);
|
super(true, statusCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class AuthStatusResponse extends RestResponse {
|
||||||
|
public toCache = false;
|
||||||
|
constructor(
|
||||||
|
public response: AuthStatus,
|
||||||
|
public statusCode: string
|
||||||
|
) {
|
||||||
|
super(true, statusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* tslint:enable:max-classes-per-file */
|
/* tslint:enable:max-classes-per-file */
|
||||||
|
5
src/app/core/cache/response-cache.service.ts
vendored
5
src/app/core/cache/response-cache.service.ts
vendored
@@ -65,6 +65,11 @@ export class ResponseCacheService {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
remove(key: string): void {
|
||||||
|
if (this.has(key)) {
|
||||||
|
this.store.dispatch(new ResponseCacheRemoveAction(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Check whether a ResponseCacheEntry should still be cached
|
* Check whether a ResponseCacheEntry should still be cached
|
||||||
*
|
*
|
||||||
|
@@ -3,10 +3,12 @@ import { ObjectCacheEffects } from './cache/object-cache.effects';
|
|||||||
import { ResponseCacheEffects } from './cache/response-cache.effects';
|
import { ResponseCacheEffects } from './cache/response-cache.effects';
|
||||||
import { UUIDIndexEffects } from './index/index.effects';
|
import { UUIDIndexEffects } from './index/index.effects';
|
||||||
import { RequestEffects } from './data/request.effects';
|
import { RequestEffects } from './data/request.effects';
|
||||||
|
import { AuthEffects } from './auth/auth.effects';
|
||||||
|
|
||||||
export const coreEffects = [
|
export const coreEffects = [
|
||||||
ResponseCacheEffects,
|
ResponseCacheEffects,
|
||||||
RequestEffects,
|
RequestEffects,
|
||||||
ObjectCacheEffects,
|
ObjectCacheEffects,
|
||||||
UUIDIndexEffects
|
UUIDIndexEffects,
|
||||||
|
AuthEffects
|
||||||
];
|
];
|
||||||
|
@@ -14,7 +14,8 @@ import { coreReducers } from './core.reducers';
|
|||||||
|
|
||||||
import { isNotEmpty } from '../shared/empty.util';
|
import { isNotEmpty } from '../shared/empty.util';
|
||||||
|
|
||||||
import { ApiService } from '../shared/api.service';
|
import { ApiService } from '../shared/services/api.service';
|
||||||
|
import { BrowseEntriesResponseParsingService } from './data/browse-entries-response-parsing.service';
|
||||||
import { CollectionDataService } from './data/collection-data.service';
|
import { CollectionDataService } from './data/collection-data.service';
|
||||||
import { CommunityDataService } from './data/community-data.service';
|
import { CommunityDataService } from './data/community-data.service';
|
||||||
import { DebugResponseParsingService } from './data/debug-response-parsing.service';
|
import { DebugResponseParsingService } from './data/debug-response-parsing.service';
|
||||||
@@ -30,16 +31,21 @@ import { RemoteDataBuildService } from './cache/builders/remote-data-build.servi
|
|||||||
import { RequestService } from './data/request.service';
|
import { RequestService } from './data/request.service';
|
||||||
import { ResponseCacheService } from './cache/response-cache.service';
|
import { ResponseCacheService } from './cache/response-cache.service';
|
||||||
import { EndpointMapResponseParsingService } from './data/endpoint-map-response-parsing.service';
|
import { EndpointMapResponseParsingService } from './data/endpoint-map-response-parsing.service';
|
||||||
import { ServerResponseService } from '../shared/server-response.service';
|
import { ServerResponseService } from '../shared/services/server-response.service';
|
||||||
import { NativeWindowFactory, NativeWindowService } from '../shared/window.service';
|
import { NativeWindowFactory, NativeWindowService } from '../shared/services/window.service';
|
||||||
import { BrowseService } from './browse/browse.service';
|
import { BrowseService } from './browse/browse.service';
|
||||||
import { BrowseResponseParsingService } from './data/browse-response-parsing.service';
|
import { BrowseResponseParsingService } from './data/browse-response-parsing.service';
|
||||||
import { ConfigResponseParsingService } from './data/config-response-parsing.service';
|
import { ConfigResponseParsingService } from './data/config-response-parsing.service';
|
||||||
import { RouteService } from '../shared/route.service';
|
import { RouteService } from '../shared/services/route.service';
|
||||||
import { SubmissionDefinitionsConfigService } from './config/submission-definitions-config.service';
|
import { SubmissionDefinitionsConfigService } from './config/submission-definitions-config.service';
|
||||||
import { SubmissionFormsConfigService } from './config/submission-forms-config.service';
|
import { SubmissionFormsConfigService } from './config/submission-forms-config.service';
|
||||||
import { SubmissionSectionsConfigService } from './config/submission-sections-config.service';
|
import { SubmissionSectionsConfigService } from './config/submission-sections-config.service';
|
||||||
import { UUIDService } from './shared/uuid.service';
|
import { UUIDService } from './shared/uuid.service';
|
||||||
|
import { AuthenticatedGuard } from './auth/authenticated.guard';
|
||||||
|
import { AuthRequestService } from './auth/auth-request.service';
|
||||||
|
import { AuthResponseParsingService } from './auth/auth-response-parsing.service';
|
||||||
|
import { HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||||
|
import { AuthInterceptor } from './auth/auth.interceptor';
|
||||||
import { HALEndpointService } from './shared/hal-endpoint.service';
|
import { HALEndpointService } from './shared/hal-endpoint.service';
|
||||||
import { FacetValueResponseParsingService } from './data/facet-value-response-parsing.service';
|
import { FacetValueResponseParsingService } from './data/facet-value-response-parsing.service';
|
||||||
import { FacetValueMapResponseParsingService } from './data/facet-value-map-response-parsing.service';
|
import { FacetValueMapResponseParsingService } from './data/facet-value-map-response-parsing.service';
|
||||||
@@ -67,6 +73,9 @@ const EXPORTS = [
|
|||||||
|
|
||||||
const PROVIDERS = [
|
const PROVIDERS = [
|
||||||
ApiService,
|
ApiService,
|
||||||
|
AuthenticatedGuard,
|
||||||
|
AuthRequestService,
|
||||||
|
AuthResponseParsingService,
|
||||||
CommunityDataService,
|
CommunityDataService,
|
||||||
CollectionDataService,
|
CollectionDataService,
|
||||||
DSOResponseParsingService,
|
DSOResponseParsingService,
|
||||||
@@ -93,6 +102,7 @@ const PROVIDERS = [
|
|||||||
SearchResponseParsingService,
|
SearchResponseParsingService,
|
||||||
ServerResponseService,
|
ServerResponseService,
|
||||||
BrowseResponseParsingService,
|
BrowseResponseParsingService,
|
||||||
|
BrowseEntriesResponseParsingService,
|
||||||
BrowseService,
|
BrowseService,
|
||||||
ConfigResponseParsingService,
|
ConfigResponseParsingService,
|
||||||
RouteService,
|
RouteService,
|
||||||
@@ -100,6 +110,12 @@ const PROVIDERS = [
|
|||||||
SubmissionFormsConfigService,
|
SubmissionFormsConfigService,
|
||||||
SubmissionSectionsConfigService,
|
SubmissionSectionsConfigService,
|
||||||
UUIDService,
|
UUIDService,
|
||||||
|
// register AuthInterceptor as HttpInterceptor
|
||||||
|
{
|
||||||
|
provide: HTTP_INTERCEPTORS,
|
||||||
|
useClass: AuthInterceptor,
|
||||||
|
multi: true
|
||||||
|
},
|
||||||
NotificationsService,
|
NotificationsService,
|
||||||
{ provide: NativeWindowService, useFactory: NativeWindowFactory }
|
{ provide: NativeWindowService, useFactory: NativeWindowFactory }
|
||||||
];
|
];
|
||||||
|
@@ -4,19 +4,22 @@ import { responseCacheReducer, ResponseCacheState } from './cache/response-cache
|
|||||||
import { objectCacheReducer, ObjectCacheState } from './cache/object-cache.reducer';
|
import { objectCacheReducer, ObjectCacheState } from './cache/object-cache.reducer';
|
||||||
import { indexReducer, IndexState } from './index/index.reducer';
|
import { indexReducer, IndexState } from './index/index.reducer';
|
||||||
import { requestReducer, RequestState } from './data/request.reducer';
|
import { requestReducer, RequestState } from './data/request.reducer';
|
||||||
|
import { authReducer, AuthState } from './auth/auth.reducer';
|
||||||
|
|
||||||
export interface CoreState {
|
export interface CoreState {
|
||||||
'data/object': ObjectCacheState,
|
'data/object': ObjectCacheState,
|
||||||
'data/response': ResponseCacheState,
|
'data/response': ResponseCacheState,
|
||||||
'data/request': RequestState,
|
'data/request': RequestState,
|
||||||
'index': IndexState
|
'index': IndexState,
|
||||||
|
'auth': AuthState,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const coreReducers: ActionReducerMap<CoreState> = {
|
export const coreReducers: ActionReducerMap<CoreState> = {
|
||||||
'data/object': objectCacheReducer,
|
'data/object': objectCacheReducer,
|
||||||
'data/response': responseCacheReducer,
|
'data/response': responseCacheReducer,
|
||||||
'data/request': requestReducer,
|
'data/request': requestReducer,
|
||||||
'index': indexReducer
|
'index': indexReducer,
|
||||||
|
'auth': authReducer
|
||||||
};
|
};
|
||||||
|
|
||||||
export const coreSelector = createFeatureSelector<CoreState>('core');
|
export const coreSelector = createFeatureSelector<CoreState>('core');
|
||||||
|
@@ -0,0 +1,146 @@
|
|||||||
|
import { getMockObjectCacheService } from '../../shared/mocks/mock-object-cache.service';
|
||||||
|
import { ErrorResponse, GenericSuccessResponse } from '../cache/response-cache.models';
|
||||||
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
|
import { BrowseEntriesResponseParsingService } from './browse-entries-response-parsing.service';
|
||||||
|
import { BrowseEntriesRequest } from './request.models';
|
||||||
|
|
||||||
|
describe('BrowseEntriesResponseParsingService', () => {
|
||||||
|
let service: BrowseEntriesResponseParsingService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = new BrowseEntriesResponseParsingService(undefined, getMockObjectCacheService());
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parse', () => {
|
||||||
|
const request = new BrowseEntriesRequest('client/f5b4ccb8-fbb0-4548-b558-f234d9fdfad6', 'https://rest.api/discover/browses/author/entries');
|
||||||
|
|
||||||
|
const validResponse = {
|
||||||
|
payload: {
|
||||||
|
_embedded: {
|
||||||
|
browseEntries: [
|
||||||
|
{
|
||||||
|
authority: null,
|
||||||
|
value: 'Arulmozhiyal, Ramaswamy',
|
||||||
|
valueLang: null,
|
||||||
|
count: 1,
|
||||||
|
type: 'browseEntry',
|
||||||
|
_links: {
|
||||||
|
items: {
|
||||||
|
href: 'https://rest.api/discover/browses/author/items?filterValue=Arulmozhiyal, Ramaswamy'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authority: null,
|
||||||
|
value: 'Bastida-Jumilla, Ma Consuelo',
|
||||||
|
valueLang: null,
|
||||||
|
count: 1,
|
||||||
|
type: 'browseEntry',
|
||||||
|
_links: {
|
||||||
|
items: {
|
||||||
|
href: 'https://rest.api/discover/browses/author/items?filterValue=Bastida-Jumilla, Ma Consuelo'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authority: null,
|
||||||
|
value: 'Cao, Binggang',
|
||||||
|
valueLang: null,
|
||||||
|
count: 1,
|
||||||
|
type: 'browseEntry',
|
||||||
|
_links: {
|
||||||
|
items: {
|
||||||
|
href: 'https://rest.api/discover/browses/author/items?filterValue=Cao, Binggang'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authority: null,
|
||||||
|
value: 'Castelli, Mauro',
|
||||||
|
valueLang: null,
|
||||||
|
count: 1,
|
||||||
|
type: 'browseEntry',
|
||||||
|
_links: {
|
||||||
|
items: {
|
||||||
|
href: 'https://rest.api/discover/browses/author/items?filterValue=Castelli, Mauro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authority: null,
|
||||||
|
value: 'Cat, Lily',
|
||||||
|
valueLang: null,
|
||||||
|
count: 1,
|
||||||
|
type: 'browseEntry',
|
||||||
|
_links: {
|
||||||
|
items: {
|
||||||
|
href: 'https://rest.api/discover/browses/author/items?filterValue=Cat, Lily'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
_links: {
|
||||||
|
first: {
|
||||||
|
href: 'https://rest.api/discover/browses/author/entries?page=0&size=5'
|
||||||
|
},
|
||||||
|
self: {
|
||||||
|
href: 'https://rest.api/discover/browses/author/entries'
|
||||||
|
},
|
||||||
|
next: {
|
||||||
|
href: 'https://rest.api/discover/browses/author/entries?page=1&size=5'
|
||||||
|
},
|
||||||
|
last: {
|
||||||
|
href: 'https://rest.api/discover/browses/author/entries?page=9&size=5'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
page: {
|
||||||
|
size: 5,
|
||||||
|
totalElements: 50,
|
||||||
|
totalPages: 10,
|
||||||
|
number: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
statusCode: '200'
|
||||||
|
} as DSpaceRESTV2Response;
|
||||||
|
|
||||||
|
const invalidResponseNotAList = {
|
||||||
|
payload: {
|
||||||
|
authority: null,
|
||||||
|
value: 'Arulmozhiyal, Ramaswamy',
|
||||||
|
valueLang: null,
|
||||||
|
count: 1,
|
||||||
|
type: 'browseEntry',
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'https://rest.api/discover/browses/author/entries'
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
href: 'https://rest.api/discover/browses/author/items?filterValue=Arulmozhiyal, Ramaswamy'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
statusCode: '200'
|
||||||
|
} as DSpaceRESTV2Response;
|
||||||
|
|
||||||
|
const invalidResponseStatusCode = {
|
||||||
|
payload: {}, statusCode: '500'
|
||||||
|
} as DSpaceRESTV2Response;
|
||||||
|
|
||||||
|
it('should return a GenericSuccessResponse if data contains a valid browse entries response', () => {
|
||||||
|
const response = service.parse(request, validResponse);
|
||||||
|
expect(response.constructor).toBe(GenericSuccessResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an ErrorResponse if data contains an invalid browse entries response', () => {
|
||||||
|
const response = service.parse(request, invalidResponseNotAList);
|
||||||
|
expect(response.constructor).toBe(ErrorResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an ErrorResponse if data contains a statuscode other than 200', () => {
|
||||||
|
const response = service.parse(request, invalidResponseStatusCode);
|
||||||
|
expect(response.constructor).toBe(ErrorResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
48
src/app/core/data/browse-entries-response-parsing.service.ts
Normal file
48
src/app/core/data/browse-entries-response-parsing.service.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { Inject, Injectable } from '@angular/core';
|
||||||
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import {
|
||||||
|
ErrorResponse,
|
||||||
|
GenericSuccessResponse,
|
||||||
|
RestResponse
|
||||||
|
} from '../cache/response-cache.models';
|
||||||
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
|
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
||||||
|
import { BrowseEntry } from '../shared/browse-entry.model';
|
||||||
|
import { BaseResponseParsingService } from './base-response-parsing.service';
|
||||||
|
import { ResponseParsingService } from './parsing.service';
|
||||||
|
import { RestRequest } from './request.models';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BrowseEntriesResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
|
||||||
|
|
||||||
|
protected objectFactory = {
|
||||||
|
getConstructor: () => BrowseEntry
|
||||||
|
};
|
||||||
|
protected toCache = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
) { super();
|
||||||
|
}
|
||||||
|
|
||||||
|
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||||
|
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._embedded)
|
||||||
|
&& Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) {
|
||||||
|
const serializer = new DSpaceRESTv2Serializer(BrowseEntry);
|
||||||
|
const browseEntries = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]);
|
||||||
|
return new GenericSuccessResponse(browseEntries, data.statusCode, this.processPageInfo(data.payload));
|
||||||
|
} else {
|
||||||
|
return new ErrorResponse(
|
||||||
|
Object.assign(
|
||||||
|
new Error('Unexpected response from browse endpoint'),
|
||||||
|
{ statusText: data.statusCode }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1,6 +1,6 @@
|
|||||||
import { BrowseResponseParsingService } from './browse-response-parsing.service';
|
import { BrowseResponseParsingService } from './browse-response-parsing.service';
|
||||||
import { BrowseEndpointRequest } from './request.models';
|
import { BrowseEndpointRequest } from './request.models';
|
||||||
import { BrowseSuccessResponse, ErrorResponse } from '../cache/response-cache.models';
|
import { GenericSuccessResponse, ErrorResponse } from '../cache/response-cache.models';
|
||||||
import { BrowseDefinition } from '../shared/browse-definition.model';
|
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ describe('BrowseResponseParsingService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('parse', () => {
|
describe('parse', () => {
|
||||||
const validRequest = new BrowseEndpointRequest('clients/b186e8ce-e99c-4183-bc9a-42b4821bdb78', 'https://rest.api/discover/browses');
|
const validRequest = new BrowseEndpointRequest('client/b186e8ce-e99c-4183-bc9a-42b4821bdb78', 'https://rest.api/discover/browses');
|
||||||
|
|
||||||
const validResponse = {
|
const validResponse = {
|
||||||
payload: {
|
payload: {
|
||||||
@@ -138,9 +138,9 @@ describe('BrowseResponseParsingService', () => {
|
|||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
it('should return a BrowseSuccessResponse if data contains a valid browse endpoint response', () => {
|
it('should return a GenericSuccessResponse if data contains a valid browse endpoint response', () => {
|
||||||
const response = service.parse(validRequest, validResponse);
|
const response = service.parse(validRequest, validResponse);
|
||||||
expect(response.constructor).toBe(BrowseSuccessResponse);
|
expect(response.constructor).toBe(GenericSuccessResponse);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return an ErrorResponse if data contains an invalid browse endpoint response', () => {
|
it('should return an ErrorResponse if data contains an invalid browse endpoint response', () => {
|
||||||
@@ -155,9 +155,9 @@ describe('BrowseResponseParsingService', () => {
|
|||||||
expect(response.constructor).toBe(ErrorResponse);
|
expect(response.constructor).toBe(ErrorResponse);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return a BrowseSuccessResponse with the BrowseDefinitions in data', () => {
|
it('should return a GenericSuccessResponse with the BrowseDefinitions in data', () => {
|
||||||
const response = service.parse(validRequest, validResponse);
|
const response = service.parse(validRequest, validResponse);
|
||||||
expect((response as BrowseSuccessResponse).browseDefinitions).toEqual(definitions);
|
expect((response as GenericSuccessResponse<BrowseDefinition[]>).payload).toEqual(definitions);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
|
|||||||
import { ResponseParsingService } from './parsing.service';
|
import { ResponseParsingService } from './parsing.service';
|
||||||
import { RestRequest } from './request.models';
|
import { RestRequest } from './request.models';
|
||||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
import { BrowseSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models';
|
import { GenericSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models';
|
||||||
import { isNotEmpty } from '../../shared/empty.util';
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
||||||
import { BrowseDefinition } from '../shared/browse-definition.model';
|
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||||
@@ -15,7 +15,7 @@ export class BrowseResponseParsingService implements ResponseParsingService {
|
|||||||
&& Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) {
|
&& Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) {
|
||||||
const serializer = new DSpaceRESTv2Serializer(BrowseDefinition);
|
const serializer = new DSpaceRESTv2Serializer(BrowseDefinition);
|
||||||
const browseDefinitions = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]);
|
const browseDefinitions = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]);
|
||||||
return new BrowseSuccessResponse(browseDefinitions, data.statusCode);
|
return new GenericSuccessResponse(browseDefinitions, data.statusCode);
|
||||||
} else {
|
} else {
|
||||||
return new ErrorResponse(
|
return new ErrorResponse(
|
||||||
Object.assign(
|
Object.assign(
|
||||||
|
@@ -33,9 +33,9 @@ export class RequestEffects {
|
|||||||
let body;
|
let body;
|
||||||
if (isNotEmpty(request.body)) {
|
if (isNotEmpty(request.body)) {
|
||||||
const serializer = new DSpaceRESTv2Serializer(NormalizedObjectFactory.getConstructor(request.body.type));
|
const serializer = new DSpaceRESTv2Serializer(NormalizedObjectFactory.getConstructor(request.body.type));
|
||||||
body = JSON.stringify(serializer.serialize(request.body));
|
body = serializer.serialize(request.body);
|
||||||
}
|
}
|
||||||
return this.restApi.request(request.method, request.href, body)
|
return this.restApi.request(request.method, request.href, body, request.options)
|
||||||
.map((data: DSpaceRESTV2Response) =>
|
.map((data: DSpaceRESTV2Response) =>
|
||||||
this.injector.get(request.getResponseParser()).parse(request, data))
|
this.injector.get(request.getResponseParser()).parse(request, data))
|
||||||
.do((response: RestResponse) => this.responseCache.add(request.href, response, this.EnvConfig.cache.msToLive))
|
.do((response: RestResponse) => this.responseCache.add(request.href, response, this.EnvConfig.cache.msToLive))
|
||||||
|
@@ -2,11 +2,15 @@ import { SortOptions } from '../cache/models/sort-options.model';
|
|||||||
import { GenericConstructor } from '../shared/generic-constructor';
|
import { GenericConstructor } from '../shared/generic-constructor';
|
||||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
|
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
|
||||||
|
import { BrowseEntriesResponseParsingService } from './browse-entries-response-parsing.service';
|
||||||
import { DSOResponseParsingService } from './dso-response-parsing.service';
|
import { DSOResponseParsingService } from './dso-response-parsing.service';
|
||||||
import { ResponseParsingService } from './parsing.service';
|
import { ResponseParsingService } from './parsing.service';
|
||||||
import { EndpointMapResponseParsingService } from './endpoint-map-response-parsing.service';
|
import { EndpointMapResponseParsingService } from './endpoint-map-response-parsing.service';
|
||||||
import { BrowseResponseParsingService } from './browse-response-parsing.service';
|
import { BrowseResponseParsingService } from './browse-response-parsing.service';
|
||||||
import { ConfigResponseParsingService } from './config-response-parsing.service';
|
import { ConfigResponseParsingService } from './config-response-parsing.service';
|
||||||
|
import { AuthResponseParsingService } from '../auth/auth-response-parsing.service';
|
||||||
|
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||||
|
import { HttpHeaders } from '@angular/common/http';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
|
||||||
@@ -34,7 +38,8 @@ export abstract class RestRequest {
|
|||||||
public uuid: string,
|
public uuid: string,
|
||||||
public href: string,
|
public href: string,
|
||||||
public method: RestRequestMethod = RestRequestMethod.Get,
|
public method: RestRequestMethod = RestRequestMethod.Get,
|
||||||
public body?: any
|
public body?: any,
|
||||||
|
public options?: HttpOptions
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +52,8 @@ export class GetRequest extends RestRequest {
|
|||||||
constructor(
|
constructor(
|
||||||
public uuid: string,
|
public uuid: string,
|
||||||
public href: string,
|
public href: string,
|
||||||
public body?: any
|
public body?: any,
|
||||||
|
public options?: HttpOptions
|
||||||
) {
|
) {
|
||||||
super(uuid, href, RestRequestMethod.Get, body)
|
super(uuid, href, RestRequestMethod.Get, body)
|
||||||
}
|
}
|
||||||
@@ -57,7 +63,8 @@ export class PostRequest extends RestRequest {
|
|||||||
constructor(
|
constructor(
|
||||||
public uuid: string,
|
public uuid: string,
|
||||||
public href: string,
|
public href: string,
|
||||||
public body?: any
|
public body?: any,
|
||||||
|
public options?: HttpOptions
|
||||||
) {
|
) {
|
||||||
super(uuid, href, RestRequestMethod.Post, body)
|
super(uuid, href, RestRequestMethod.Post, body)
|
||||||
}
|
}
|
||||||
@@ -67,7 +74,8 @@ export class PutRequest extends RestRequest {
|
|||||||
constructor(
|
constructor(
|
||||||
public uuid: string,
|
public uuid: string,
|
||||||
public href: string,
|
public href: string,
|
||||||
public body?: any
|
public body?: any,
|
||||||
|
public options?: HttpOptions
|
||||||
) {
|
) {
|
||||||
super(uuid, href, RestRequestMethod.Put, body)
|
super(uuid, href, RestRequestMethod.Put, body)
|
||||||
}
|
}
|
||||||
@@ -77,7 +85,8 @@ export class DeleteRequest extends RestRequest {
|
|||||||
constructor(
|
constructor(
|
||||||
public uuid: string,
|
public uuid: string,
|
||||||
public href: string,
|
public href: string,
|
||||||
public body?: any
|
public body?: any,
|
||||||
|
public options?: HttpOptions
|
||||||
) {
|
) {
|
||||||
super(uuid, href, RestRequestMethod.Delete, body)
|
super(uuid, href, RestRequestMethod.Delete, body)
|
||||||
}
|
}
|
||||||
@@ -87,7 +96,8 @@ export class OptionsRequest extends RestRequest {
|
|||||||
constructor(
|
constructor(
|
||||||
public uuid: string,
|
public uuid: string,
|
||||||
public href: string,
|
public href: string,
|
||||||
public body?: any
|
public body?: any,
|
||||||
|
public options?: HttpOptions
|
||||||
) {
|
) {
|
||||||
super(uuid, href, RestRequestMethod.Options, body)
|
super(uuid, href, RestRequestMethod.Options, body)
|
||||||
}
|
}
|
||||||
@@ -97,7 +107,8 @@ export class HeadRequest extends RestRequest {
|
|||||||
constructor(
|
constructor(
|
||||||
public uuid: string,
|
public uuid: string,
|
||||||
public href: string,
|
public href: string,
|
||||||
public body?: any
|
public body?: any,
|
||||||
|
public options?: HttpOptions
|
||||||
) {
|
) {
|
||||||
super(uuid, href, RestRequestMethod.Head, body)
|
super(uuid, href, RestRequestMethod.Head, body)
|
||||||
}
|
}
|
||||||
@@ -107,7 +118,8 @@ export class PatchRequest extends RestRequest {
|
|||||||
constructor(
|
constructor(
|
||||||
public uuid: string,
|
public uuid: string,
|
||||||
public href: string,
|
public href: string,
|
||||||
public body?: any
|
public body?: any,
|
||||||
|
public options?: HttpOptions
|
||||||
) {
|
) {
|
||||||
super(uuid, href, RestRequestMethod.Patch, body)
|
super(uuid, href, RestRequestMethod.Patch, body)
|
||||||
}
|
}
|
||||||
@@ -134,7 +146,7 @@ export class FindAllRequest extends GetRequest {
|
|||||||
constructor(
|
constructor(
|
||||||
uuid: string,
|
uuid: string,
|
||||||
href: string,
|
href: string,
|
||||||
public options?: FindAllOptions,
|
public body?: FindAllOptions,
|
||||||
) {
|
) {
|
||||||
super(uuid, href);
|
super(uuid, href);
|
||||||
}
|
}
|
||||||
@@ -164,6 +176,12 @@ export class BrowseEndpointRequest extends GetRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class BrowseEntriesRequest extends GetRequest {
|
||||||
|
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||||
|
return BrowseEntriesResponseParsingService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class ConfigRequest extends GetRequest {
|
export class ConfigRequest extends GetRequest {
|
||||||
constructor(uuid: string, href: string) {
|
constructor(uuid: string, href: string) {
|
||||||
super(uuid, href);
|
super(uuid, href);
|
||||||
@@ -174,6 +192,26 @@ export class ConfigRequest extends GetRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class AuthPostRequest extends PostRequest {
|
||||||
|
constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) {
|
||||||
|
super(uuid, href, body, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||||
|
return AuthResponseParsingService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AuthGetRequest extends GetRequest {
|
||||||
|
constructor(uuid: string, href: string, public options?: HttpOptions) {
|
||||||
|
super(uuid, href, null, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||||
|
return AuthResponseParsingService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class RequestError extends Error {
|
export class RequestError extends Error {
|
||||||
statusText: string;
|
statusText: string;
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { cold, hot } from 'jasmine-marbles';
|
import { cold, hot } from 'jasmine-marbles';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import 'rxjs/add/observable/of';
|
||||||
import { getMockObjectCacheService } from '../../shared/mocks/mock-object-cache.service';
|
import { getMockObjectCacheService } from '../../shared/mocks/mock-object-cache.service';
|
||||||
import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service';
|
import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service';
|
||||||
import { getMockStore } from '../../shared/mocks/mock-store';
|
import { getMockStore } from '../../shared/mocks/mock-store';
|
||||||
@@ -229,6 +230,11 @@ describe('RequestService', () => {
|
|||||||
request = testGetRequest;
|
request = testGetRequest;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should track it on it\'s way to the store', () => {
|
||||||
|
spyOn(serviceAsAny, 'trackRequestsOnTheirWayToTheStore');
|
||||||
|
service.configure(request);
|
||||||
|
expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).toHaveBeenCalledWith(request);
|
||||||
|
});
|
||||||
describe('and it isn\'t cached or pending', () => {
|
describe('and it isn\'t cached or pending', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(serviceAsAny, 'isCachedOrPending').and.returnValue(false);
|
spyOn(serviceAsAny, 'isCachedOrPending').and.returnValue(false);
|
||||||
@@ -271,6 +277,28 @@ describe('RequestService', () => {
|
|||||||
service.configure(testPatchRequest);
|
service.configure(testPatchRequest);
|
||||||
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testPatchRequest);
|
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testPatchRequest);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shouldn\'t track it on it\'s way to the store', () => {
|
||||||
|
spyOn(serviceAsAny, 'trackRequestsOnTheirWayToTheStore');
|
||||||
|
|
||||||
|
serviceAsAny.dispatchRequest(testPostRequest);
|
||||||
|
expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
serviceAsAny.dispatchRequest(testPutRequest);
|
||||||
|
expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
serviceAsAny.dispatchRequest(testDeleteRequest);
|
||||||
|
expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
serviceAsAny.dispatchRequest(testOptionsRequest);
|
||||||
|
expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
serviceAsAny.dispatchRequest(testHeadRequest);
|
||||||
|
expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
serviceAsAny.dispatchRequest(testPatchRequest);
|
||||||
|
expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -381,43 +409,6 @@ describe('RequestService', () => {
|
|||||||
serviceAsAny.dispatchRequest(request);
|
serviceAsAny.dispatchRequest(request);
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(new RequestExecuteAction(request.uuid));
|
expect(store.dispatch).toHaveBeenCalledWith(new RequestExecuteAction(request.uuid));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when it\'s a GET request', () => {
|
|
||||||
let request: RestRequest;
|
|
||||||
beforeEach(() => {
|
|
||||||
request = testGetRequest;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should track it on it\'s way to the store', () => {
|
|
||||||
spyOn(serviceAsAny, 'trackRequestsOnTheirWayToTheStore');
|
|
||||||
serviceAsAny.dispatchRequest(request);
|
|
||||||
expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).toHaveBeenCalledWith(request);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when it\'s not a GET request', () => {
|
|
||||||
it('shouldn\'t track it', () => {
|
|
||||||
spyOn(serviceAsAny, 'trackRequestsOnTheirWayToTheStore');
|
|
||||||
|
|
||||||
serviceAsAny.dispatchRequest(testPostRequest);
|
|
||||||
expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
serviceAsAny.dispatchRequest(testPutRequest);
|
|
||||||
expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
serviceAsAny.dispatchRequest(testDeleteRequest);
|
|
||||||
expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
serviceAsAny.dispatchRequest(testOptionsRequest);
|
|
||||||
expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
serviceAsAny.dispatchRequest(testHeadRequest);
|
|
||||||
expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
serviceAsAny.dispatchRequest(testPatchRequest);
|
|
||||||
expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('trackRequestsOnTheirWayToTheStore', () => {
|
describe('trackRequestsOnTheirWayToTheStore', () => {
|
||||||
|
@@ -17,6 +17,7 @@ import { RequestConfigureAction, RequestExecuteAction } from './request.actions'
|
|||||||
import { GetRequest, RestRequest, RestRequestMethod } from './request.models';
|
import { GetRequest, RestRequest, RestRequestMethod } from './request.models';
|
||||||
|
|
||||||
import { RequestEntry, RequestState } from './request.reducer';
|
import { RequestEntry, RequestState } from './request.reducer';
|
||||||
|
import { ResponseCacheRemoveAction } from '../cache/response-cache.actions';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RequestService {
|
export class RequestService {
|
||||||
@@ -66,9 +67,14 @@ export class RequestService {
|
|||||||
.flatMap((uuid: string) => this.getByUUID(uuid));
|
.flatMap((uuid: string) => this.getByUUID(uuid));
|
||||||
}
|
}
|
||||||
|
|
||||||
configure<T extends CacheableObject>(request: RestRequest): void {
|
// TODO to review "overrideRequest" param when https://github.com/DSpace/dspace-angular/issues/217 will be fixed
|
||||||
if (request.method !== RestRequestMethod.Get || !this.isCachedOrPending(request)) {
|
configure<T extends CacheableObject>(request: RestRequest, forceBypassCache: boolean = false): void {
|
||||||
|
const isGetRequest = request.method === RestRequestMethod.Get;
|
||||||
|
if (!isGetRequest || !this.isCachedOrPending(request) || forceBypassCache) {
|
||||||
this.dispatchRequest(request);
|
this.dispatchRequest(request);
|
||||||
|
if (isGetRequest && !forceBypassCache) {
|
||||||
|
this.trackRequestsOnTheirWayToTheStore(request);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,9 +110,6 @@ export class RequestService {
|
|||||||
private dispatchRequest(request: RestRequest) {
|
private dispatchRequest(request: RestRequest) {
|
||||||
this.store.dispatch(new RequestConfigureAction(request));
|
this.store.dispatch(new RequestConfigureAction(request));
|
||||||
this.store.dispatch(new RequestExecuteAction(request.uuid));
|
this.store.dispatch(new RequestExecuteAction(request.uuid));
|
||||||
if (request.method === RestRequestMethod.Get) {
|
|
||||||
this.trackRequestsOnTheirWayToTheStore(request);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
import { HttpHeaders } from '@angular/common/http';
|
||||||
|
|
||||||
export interface DSpaceRESTV2Response {
|
export interface DSpaceRESTV2Response {
|
||||||
payload: {
|
payload: {
|
||||||
[name: string]: any;
|
[name: string]: any;
|
||||||
@@ -5,5 +7,6 @@ export interface DSpaceRESTV2Response {
|
|||||||
_links?: any;
|
_links?: any;
|
||||||
page?: any;
|
page?: any;
|
||||||
},
|
},
|
||||||
|
headers?: HttpHeaders,
|
||||||
statusCode: string
|
statusCode: string
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,21 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Request } from '@angular/http';
|
import { Request } from '@angular/http';
|
||||||
import { HttpClient, HttpResponse } from '@angular/common/http'
|
import { HttpClient, HttpHeaders, HttpParams, HttpResponse } from '@angular/common/http'
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { RestRequestMethod } from '../data/request.models';
|
import { RestRequestMethod } from '../data/request.models';
|
||||||
|
|
||||||
import { DSpaceRESTV2Response } from './dspace-rest-v2-response.model';
|
import { DSpaceRESTV2Response } from './dspace-rest-v2-response.model';
|
||||||
|
import { HttpObserve } from '@angular/common/http/src/client';
|
||||||
|
|
||||||
|
export interface HttpOptions {
|
||||||
|
body?: any;
|
||||||
|
headers?: HttpHeaders;
|
||||||
|
params?: HttpParams;
|
||||||
|
observe?: HttpObserve;
|
||||||
|
reportProgress?: boolean;
|
||||||
|
responseType?: 'arraybuffer' | 'blob' | 'json' | 'text';
|
||||||
|
withCredentials?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service to access DSpace's REST API
|
* Service to access DSpace's REST API
|
||||||
@@ -45,9 +56,18 @@ export class DSpaceRESTv2Service {
|
|||||||
* @return {Observable<string>}
|
* @return {Observable<string>}
|
||||||
* An Observable<string> containing the response from the server
|
* An Observable<string> containing the response from the server
|
||||||
*/
|
*/
|
||||||
request(method: RestRequestMethod, url: string, body?: any): Observable<DSpaceRESTV2Response> {
|
request(method: RestRequestMethod, url: string, body?: any, options?: HttpOptions): Observable<DSpaceRESTV2Response> {
|
||||||
return this.http.request(method, url, { body, observe: 'response' })
|
const requestOptions: HttpOptions = {};
|
||||||
.map((res) => ({ payload: res.body, statusCode: res.statusText }))
|
requestOptions.body = body;
|
||||||
|
requestOptions.observe = 'response';
|
||||||
|
if (options && options.headers) {
|
||||||
|
requestOptions.headers = Object.assign(new HttpHeaders(), options.headers);
|
||||||
|
}
|
||||||
|
if (options && options.responseType) {
|
||||||
|
requestOptions.responseType = options.responseType;
|
||||||
|
}
|
||||||
|
return this.http.request(method, url, requestOptions)
|
||||||
|
.map((res) => ({ payload: res.body, headers: res.headers, statusCode: res.statusText }))
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log('Error: ', err);
|
console.log('Error: ', err);
|
||||||
return Observable.throw(err);
|
return Observable.throw(err);
|
||||||
|
37
src/app/core/eperson/models/NormalizedEperson.model.ts
Normal file
37
src/app/core/eperson/models/NormalizedEperson.model.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { autoserialize, inheritSerialization } from 'cerialize';
|
||||||
|
import { CacheableObject } from '../../cache/object-cache.reducer';
|
||||||
|
import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model';
|
||||||
|
import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model';
|
||||||
|
import { Eperson } from './eperson.model';
|
||||||
|
import { mapsTo, relationship } from '../../cache/builders/build-decorators';
|
||||||
|
import { ResourceType } from '../../shared/resource-type';
|
||||||
|
|
||||||
|
@mapsTo(Eperson)
|
||||||
|
@inheritSerialization(NormalizedDSpaceObject)
|
||||||
|
export class NormalizedEpersonModel extends NormalizedDSpaceObject implements CacheableObject, ListableObject {
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
public handle: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
@relationship(ResourceType.Group, true)
|
||||||
|
groups: string[];
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
public netid: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
public lastActive: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
public canLogIn: boolean;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
public email: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
public requireCertificate: boolean;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
public selfRegistered: boolean;
|
||||||
|
}
|
18
src/app/core/eperson/models/NormalizedGroup.model.ts
Normal file
18
src/app/core/eperson/models/NormalizedGroup.model.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { autoserialize, inheritSerialization } from 'cerialize';
|
||||||
|
import { CacheableObject } from '../../cache/object-cache.reducer';
|
||||||
|
import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model';
|
||||||
|
import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model';
|
||||||
|
import { Eperson } from './eperson.model';
|
||||||
|
import { mapsTo } from '../../cache/builders/build-decorators';
|
||||||
|
import { Group } from './group.model';
|
||||||
|
|
||||||
|
@mapsTo(Group)
|
||||||
|
@inheritSerialization(NormalizedDSpaceObject)
|
||||||
|
export class NormalizedGroupModel extends NormalizedDSpaceObject implements CacheableObject, ListableObject {
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
public handle: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
public permanent: boolean;
|
||||||
|
}
|
22
src/app/core/eperson/models/eperson.model.ts
Normal file
22
src/app/core/eperson/models/eperson.model.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { DSpaceObject } from '../../shared/dspace-object.model';
|
||||||
|
import { Group } from './group.model';
|
||||||
|
|
||||||
|
export class Eperson extends DSpaceObject {
|
||||||
|
|
||||||
|
public handle: string;
|
||||||
|
|
||||||
|
public groups: Group[];
|
||||||
|
|
||||||
|
public netid: string;
|
||||||
|
|
||||||
|
public lastActive: string;
|
||||||
|
|
||||||
|
public canLogIn: boolean;
|
||||||
|
|
||||||
|
public email: string;
|
||||||
|
|
||||||
|
public requireCertificate: boolean;
|
||||||
|
|
||||||
|
public selfRegistered: boolean;
|
||||||
|
|
||||||
|
}
|
8
src/app/core/eperson/models/group.model.ts
Normal file
8
src/app/core/eperson/models/group.model.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { DSpaceObject } from '../../shared/dspace-object.model';
|
||||||
|
|
||||||
|
export class Group extends DSpaceObject {
|
||||||
|
|
||||||
|
public handle: string;
|
||||||
|
|
||||||
|
public permanent: boolean;
|
||||||
|
}
|
@@ -2,6 +2,9 @@ import { autoserialize, autoserializeAs } from 'cerialize';
|
|||||||
import { SortOption } from './sort-option.model';
|
import { SortOption } from './sort-option.model';
|
||||||
|
|
||||||
export class BrowseDefinition {
|
export class BrowseDefinition {
|
||||||
|
@autoserialize
|
||||||
|
id: string;
|
||||||
|
|
||||||
@autoserialize
|
@autoserialize
|
||||||
metadataBrowse: boolean;
|
metadataBrowse: boolean;
|
||||||
|
|
||||||
|
20
src/app/core/shared/browse-entry.model.ts
Normal file
20
src/app/core/shared/browse-entry.model.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { autoserialize, autoserializeAs } from 'cerialize';
|
||||||
|
|
||||||
|
export class BrowseEntry {
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
authority: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
value: string;
|
||||||
|
|
||||||
|
@autoserializeAs('valueLang')
|
||||||
|
language: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
count: number;
|
||||||
|
|
||||||
|
}
|
152
src/app/core/shared/operators.spec.ts
Normal file
152
src/app/core/shared/operators.spec.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
||||||
|
import { TestScheduler } from '../../../../node_modules/rxjs';
|
||||||
|
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
||||||
|
import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service';
|
||||||
|
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
||||||
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
|
import { GetRequest, RestRequest } from '../data/request.models';
|
||||||
|
import { RequestEntry } from '../data/request.reducer';
|
||||||
|
import { RequestService } from '../data/request.service';
|
||||||
|
import {
|
||||||
|
configureRequest,
|
||||||
|
filterSuccessfulResponses, getRemoteDataPayload,
|
||||||
|
getRequestFromSelflink, getResourceLinksFromResponse,
|
||||||
|
getResponseFromSelflink
|
||||||
|
} from './operators';
|
||||||
|
|
||||||
|
describe('Core Module - RxJS Operators', () => {
|
||||||
|
let scheduler: TestScheduler;
|
||||||
|
let requestService: RequestService;
|
||||||
|
const testSelfLink = 'https://rest.api/';
|
||||||
|
|
||||||
|
const testRCEs = {
|
||||||
|
a: { response: { isSuccessful: true, resourceSelfLinks: ['a', 'b', 'c', 'd'] } },
|
||||||
|
b: { response: { isSuccessful: false, resourceSelfLinks: ['e', 'f'] } },
|
||||||
|
c: { response: { isSuccessful: undefined, resourceSelfLinks: ['g', 'h', 'i'] } },
|
||||||
|
d: { response: { isSuccessful: true, resourceSelfLinks: ['j', 'k', 'l', 'm', 'n'] } },
|
||||||
|
e: { response: { isSuccessful: 1, resourceSelfLinks: [] } }
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getRequestFromSelflink', () => {
|
||||||
|
|
||||||
|
it('should return the RequestEntry corresponding to the self link in the source', () => {
|
||||||
|
requestService = getMockRequestService();
|
||||||
|
|
||||||
|
const source = hot('a', { a: testSelfLink });
|
||||||
|
const result = source.pipe(getRequestFromSelflink(requestService));
|
||||||
|
const expected = cold('a', { a: new RequestEntry()});
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected)
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use the requestService to fetch the request by its self link', () => {
|
||||||
|
requestService = getMockRequestService();
|
||||||
|
|
||||||
|
const source = hot('a', { a: testSelfLink });
|
||||||
|
scheduler.schedule(() => source.pipe(getRequestFromSelflink(requestService)).subscribe());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(requestService.getByHref).toHaveBeenCalledWith(testSelfLink)
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shouldn\'t return anything if there is no request matching the self link', () => {
|
||||||
|
requestService = getMockRequestService(cold('a', { a: undefined }));
|
||||||
|
|
||||||
|
const source = hot('a', { a: testSelfLink });
|
||||||
|
const result = source.pipe(getRequestFromSelflink(requestService));
|
||||||
|
const expected = cold('-');
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getResponseFromSelflink', () => {
|
||||||
|
let responseCacheService: ResponseCacheService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the ResponseCacheEntry corresponding to the self link in the source', () => {
|
||||||
|
responseCacheService = getMockResponseCacheService();
|
||||||
|
|
||||||
|
const source = hot('a', { a: testSelfLink });
|
||||||
|
const result = source.pipe(getResponseFromSelflink(responseCacheService));
|
||||||
|
const expected = cold('a', { a: new ResponseCacheEntry()});
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected)
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use the responseCacheService to fetch the response by the request\'s link', () => {
|
||||||
|
responseCacheService = getMockResponseCacheService();
|
||||||
|
|
||||||
|
const source = hot('a', { a: testSelfLink });
|
||||||
|
scheduler.schedule(() => source.pipe(getResponseFromSelflink(responseCacheService)).subscribe());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(responseCacheService.get).toHaveBeenCalledWith(testSelfLink)
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shouldn\'t return anything if there is no response matching the request\'s link', () => {
|
||||||
|
responseCacheService = getMockResponseCacheService(undefined, cold('a', { a: undefined }));
|
||||||
|
|
||||||
|
const source = hot('a', { a: testSelfLink });
|
||||||
|
const result = source.pipe(getResponseFromSelflink(responseCacheService));
|
||||||
|
const expected = cold('-');
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('filterSuccessfulResponses', () => {
|
||||||
|
it('should only return responses for which isSuccessful === true', () => {
|
||||||
|
const source = hot('abcde', testRCEs);
|
||||||
|
const result = source.pipe(filterSuccessfulResponses());
|
||||||
|
const expected = cold('a--d-', testRCEs);
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getResourceLinksFromResponse', () => {
|
||||||
|
it('should return the resourceSelfLinks for all successful responses', () => {
|
||||||
|
const source = hot('abcde', testRCEs);
|
||||||
|
const result = source.pipe(getResourceLinksFromResponse());
|
||||||
|
const expected = cold('a--d-', {
|
||||||
|
a: testRCEs.a.response.resourceSelfLinks,
|
||||||
|
d: testRCEs.d.response.resourceSelfLinks
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('configureRequest', () => {
|
||||||
|
it('should call requestService.configure with the source request', () => {
|
||||||
|
requestService = getMockRequestService();
|
||||||
|
const testRequest = new GetRequest('6b789e31-f026-4ff8-8993-4eb3b730c841', testSelfLink);
|
||||||
|
const source = hot('a', { a: testRequest });
|
||||||
|
scheduler.schedule(() => source.pipe(configureRequest(requestService)).subscribe());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(requestService.configure).toHaveBeenCalledWith(testRequest)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getRemoteDataPayload', () => {
|
||||||
|
it('should return the payload of the source RemoteData', () => {
|
||||||
|
const testRD = { a: { payload: 'a' } };
|
||||||
|
const source = hot('a', testRD);
|
||||||
|
const result = source.pipe(getRemoteDataPayload());
|
||||||
|
const expected = cold('a', {
|
||||||
|
a: testRD.a.payload,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
47
src/app/core/shared/operators.ts
Normal file
47
src/app/core/shared/operators.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { filter, flatMap, map, tap } from 'rxjs/operators';
|
||||||
|
import { hasValueOperator } from '../../shared/empty.util';
|
||||||
|
import { DSOSuccessResponse } from '../cache/response-cache.models';
|
||||||
|
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
||||||
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
import { RestRequest } from '../data/request.models';
|
||||||
|
import { RequestEntry } from '../data/request.reducer';
|
||||||
|
import { RequestService } from '../data/request.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file contains custom RxJS operators that can be used in multiple places
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const getRequestFromSelflink = (requestService: RequestService) =>
|
||||||
|
(source: Observable<string>): Observable<RequestEntry> =>
|
||||||
|
source.pipe(
|
||||||
|
flatMap((href: string) => requestService.getByHref(href)),
|
||||||
|
hasValueOperator()
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getResponseFromSelflink = (responseCache: ResponseCacheService) =>
|
||||||
|
(source: Observable<string>): Observable<ResponseCacheEntry> =>
|
||||||
|
source.pipe(
|
||||||
|
flatMap((href: string) => responseCache.get(href)),
|
||||||
|
hasValueOperator()
|
||||||
|
);
|
||||||
|
|
||||||
|
export const filterSuccessfulResponses = () =>
|
||||||
|
(source: Observable<ResponseCacheEntry>): Observable<ResponseCacheEntry> =>
|
||||||
|
source.pipe(filter((entry: ResponseCacheEntry) => entry.response.isSuccessful === true));
|
||||||
|
|
||||||
|
export const getResourceLinksFromResponse = () =>
|
||||||
|
(source: Observable<ResponseCacheEntry>): Observable<string[]> =>
|
||||||
|
source.pipe(
|
||||||
|
filterSuccessfulResponses(),
|
||||||
|
map((entry: ResponseCacheEntry) => (entry.response as DSOSuccessResponse).resourceSelfLinks),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const configureRequest = (requestService: RequestService) =>
|
||||||
|
(source: Observable<RestRequest>): Observable<RestRequest> =>
|
||||||
|
source.pipe(tap((request: RestRequest) => requestService.configure(request)));
|
||||||
|
|
||||||
|
export const getRemoteDataPayload = () =>
|
||||||
|
<T>(source: Observable<RemoteData<T>>): Observable<T> =>
|
||||||
|
source.pipe(map((remoteData: RemoteData<T>) => remoteData.payload));
|
@@ -6,4 +6,6 @@ export enum ResourceType {
|
|||||||
Item = 'item',
|
Item = 'item',
|
||||||
Collection = 'collection',
|
Collection = 'collection',
|
||||||
Community = 'community',
|
Community = 'community',
|
||||||
|
Eperson = 'eperson',
|
||||||
|
Group = 'group',
|
||||||
}
|
}
|
||||||
|
@@ -12,6 +12,7 @@
|
|||||||
<a class="nav-link" routerLink="/home" routerLinkActive="active"><i class="fa fa-home fa-fw" aria-hidden="true"></i> {{ 'nav.home' | translate }}<span class="sr-only">(current)</span></a>
|
<a class="nav-link" routerLink="/home" routerLinkActive="active"><i class="fa fa-home fa-fw" aria-hidden="true"></i> {{ 'nav.home' | translate }}<span class="sr-only">(current)</span></a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<ds-auth-nav-menu></ds-auth-nav-menu>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
@@ -9,6 +9,16 @@ import { Observable } from 'rxjs/Observable';
|
|||||||
import { HeaderComponent } from './header.component';
|
import { HeaderComponent } from './header.component';
|
||||||
import { HeaderState } from './header.reducer';
|
import { HeaderState } from './header.reducer';
|
||||||
import { HeaderToggleAction } from './header.actions';
|
import { HeaderToggleAction } from './header.actions';
|
||||||
|
import { AuthNavMenuComponent } from '../shared/auth-nav-menu/auth-nav-menu.component';
|
||||||
|
import { LogInComponent } from '../shared/log-in/log-in.component';
|
||||||
|
import { LogOutComponent } from '../shared/log-out/log-out.component';
|
||||||
|
import { LoadingComponent } from '../shared/loading/loading.component';
|
||||||
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { HostWindowService } from '../shared/host-window.service';
|
||||||
|
import { HostWindowServiceStub } from '../shared/testing/host-window-service-stub';
|
||||||
|
import { RouterStub } from '../shared/testing/router-stub';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
|
||||||
let comp: HeaderComponent;
|
let comp: HeaderComponent;
|
||||||
let fixture: ComponentFixture<HeaderComponent>;
|
let fixture: ComponentFixture<HeaderComponent>;
|
||||||
@@ -19,8 +29,17 @@ describe('HeaderComponent', () => {
|
|||||||
// async beforeEach
|
// async beforeEach
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [StoreModule.forRoot({}), TranslateModule.forRoot(), NgbCollapseModule.forRoot()],
|
imports: [
|
||||||
declarations: [HeaderComponent]
|
StoreModule.forRoot({}),
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
NgbCollapseModule.forRoot(),
|
||||||
|
NoopAnimationsModule,
|
||||||
|
ReactiveFormsModule],
|
||||||
|
declarations: [HeaderComponent, AuthNavMenuComponent, LoadingComponent, LogInComponent, LogOutComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
|
||||||
|
{ provide: Router, useClass: RouterStub },
|
||||||
|
]
|
||||||
})
|
})
|
||||||
.compileComponents(); // compile template and css
|
.compileComponents(); // compile template and css
|
||||||
}));
|
}));
|
||||||
|
@@ -1,10 +1,12 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { createSelector, Store } from '@ngrx/store';
|
import { createSelector, Store } from '@ngrx/store';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { RouterReducerState } from '@ngrx/router-store';
|
||||||
|
|
||||||
import { HeaderState } from './header.reducer';
|
import { HeaderState } from './header.reducer';
|
||||||
import { HeaderToggleAction } from './header.actions';
|
import { HeaderToggleAction } from './header.actions';
|
||||||
import { AppState } from '../app.reducer';
|
import { AppState } from '../app.reducer';
|
||||||
|
import { HostWindowService } from '../shared/host-window.service';
|
||||||
|
|
||||||
const headerStateSelector = (state: AppState) => state.header;
|
const headerStateSelector = (state: AppState) => state.header;
|
||||||
const navCollapsedSelector = createSelector(headerStateSelector, (header: HeaderState) => header.navCollapsed);
|
const navCollapsedSelector = createSelector(headerStateSelector, (header: HeaderState) => header.navCollapsed);
|
||||||
@@ -12,17 +14,25 @@ const navCollapsedSelector = createSelector(headerStateSelector, (header: Header
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-header',
|
selector: 'ds-header',
|
||||||
styleUrls: ['header.component.scss'],
|
styleUrls: ['header.component.scss'],
|
||||||
templateUrl: 'header.component.html'
|
templateUrl: 'header.component.html',
|
||||||
})
|
})
|
||||||
export class HeaderComponent implements OnInit {
|
export class HeaderComponent implements OnInit {
|
||||||
|
/**
|
||||||
|
* Whether user is authenticated.
|
||||||
|
* @type {Observable<string>}
|
||||||
|
*/
|
||||||
|
public isAuthenticated: Observable<boolean>;
|
||||||
public isNavBarCollapsed: Observable<boolean>;
|
public isNavBarCollapsed: Observable<boolean>;
|
||||||
|
public showAuth = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private store: Store<AppState>
|
private store: Store<AppState>,
|
||||||
|
private windowService: HostWindowService
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
// set loading
|
||||||
this.isNavBarCollapsed = this.store.select(navCollapsedSelector);
|
this.isNavBarCollapsed = this.store.select(navCollapsedSelector);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { ServerResponseService } from '../shared/server-response.service';
|
import { ServerResponseService } from '../shared/services/server-response.service';
|
||||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
26
src/app/shared/auth-nav-menu/auth-nav-menu.component.html
Normal file
26
src/app/shared/auth-nav-menu/auth-nav-menu.component.html
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<ul class="navbar-nav" [ngClass]="{'mr-auto': (isXsOrSm$ | async)}">
|
||||||
|
<li *ngIf="!(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item dropdown" (click)="$event.stopPropagation();">
|
||||||
|
<div ngbDropdown placement="bottom-right" class="d-inline-block float-right" @fadeInOut>
|
||||||
|
<a href="#" id="dropdownLogin" class="nav-link" (click)="$event.preventDefault()" ngbDropdownToggle><i class="fa fa-sign-in fa-fw" aria-hidden="true"></i> {{ 'nav.login' | translate }}<span class="caret"></span></a>
|
||||||
|
<div id="loginDropdownMenu" [ngClass]="{'pl-3 pr-3': (loading | async)}" ngbDropdownMenu aria-labelledby="dropdownLogin">
|
||||||
|
<ds-log-in></ds-log-in>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li *ngIf="!(isAuthenticated | async) && (isXsOrSm$ | async)" class="nav-item">
|
||||||
|
<a id="loginLink" class="nav-link" routerLink="/login" routerLinkActive="active"><i class="fa fa-sign-in fa-fw" aria-hidden="true"></i> {{ 'nav.login' | translate }}<span class="sr-only">(current)</span></a>
|
||||||
|
</li>
|
||||||
|
<li *ngIf="(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item">
|
||||||
|
<div ngbDropdown placement="bottom-right" class="d-inline-block" [ngClass]="{'float-right': !(isXsOrSm$ | async)}" @fadeInOut>
|
||||||
|
<a href="#" id="dropdownUser" class="nav-link" (click)="$event.preventDefault()" ngbDropdownToggle><i class="fa fa-user fa-fw" aria-hidden="true"></i>Hello {{(user | async).name}}<span class="caret"></span></a>
|
||||||
|
<div id="logoutDropdownMenu" ngbDropdownMenu aria-labelledby="dropdownUser">
|
||||||
|
<ds-log-out></ds-log-out>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li *ngIf="(isAuthenticated | async) && (isXsOrSm$ | async)" class="nav-item">
|
||||||
|
<a id="logoutLink" class="nav-link" routerLink="/logout" routerLinkActive="active"><i class="fa fa-sign-out fa-fw" aria-hidden="true"></i> {{ 'nav.logout' | translate }}<span class="sr-only">(current)</span></a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
@@ -0,0 +1,8 @@
|
|||||||
|
#loginDropdownMenu, #logoutDropdownMenu {
|
||||||
|
min-width: 330px;
|
||||||
|
z-index: 1002;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loginDropdownMenu {
|
||||||
|
min-height: 260px;
|
||||||
|
}
|
297
src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts
Normal file
297
src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core';
|
||||||
|
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { Store, StoreModule } from '@ngrx/store';
|
||||||
|
|
||||||
|
import { authReducer, AuthState } from '../../core/auth/auth.reducer';
|
||||||
|
import { EpersonMock } from '../testing/eperson-mock';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { AppState } from '../../app.reducer';
|
||||||
|
import { AuthNavMenuComponent } from './auth-nav-menu.component';
|
||||||
|
import { HostWindowServiceStub } from '../testing/host-window-service-stub';
|
||||||
|
import { HostWindowService } from '../host-window.service';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model';
|
||||||
|
|
||||||
|
describe('AuthNavMenuComponent', () => {
|
||||||
|
|
||||||
|
let component: AuthNavMenuComponent;
|
||||||
|
let deNavMenu: DebugElement;
|
||||||
|
let deNavMenuItem: DebugElement;
|
||||||
|
let fixture: ComponentFixture<AuthNavMenuComponent>;
|
||||||
|
|
||||||
|
const notAuthState: AuthState = {
|
||||||
|
authenticated: false,
|
||||||
|
loaded: false,
|
||||||
|
loading: false
|
||||||
|
};
|
||||||
|
const authState: AuthState = {
|
||||||
|
authenticated: true,
|
||||||
|
loaded: true,
|
||||||
|
loading: false,
|
||||||
|
authToken: new AuthTokenInfo('test_token'),
|
||||||
|
user: EpersonMock
|
||||||
|
};
|
||||||
|
let routerState = {
|
||||||
|
url: '/home'
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('when is a not mobile view', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
const window = new HostWindowServiceStub(800);
|
||||||
|
|
||||||
|
// refine the test module by declaring the test component
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
NoopAnimationsModule,
|
||||||
|
StoreModule.forRoot(authReducer),
|
||||||
|
TranslateModule.forRoot()
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
AuthNavMenuComponent
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{provide: HostWindowService, useValue: window},
|
||||||
|
],
|
||||||
|
schemas: [
|
||||||
|
CUSTOM_ELEMENTS_SCHEMA
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('when route is /login and user is not authenticated', () => {
|
||||||
|
routerState = {
|
||||||
|
url: '/login'
|
||||||
|
};
|
||||||
|
beforeEach(inject([Store], (store: Store<AppState>) => {
|
||||||
|
store
|
||||||
|
.subscribe((state) => {
|
||||||
|
(state as any).router = Object.create({});
|
||||||
|
(state as any).router.state = routerState;
|
||||||
|
(state as any).core = Object.create({});
|
||||||
|
(state as any).core.auth = notAuthState;
|
||||||
|
});
|
||||||
|
|
||||||
|
// create component and test fixture
|
||||||
|
fixture = TestBed.createComponent(AuthNavMenuComponent);
|
||||||
|
|
||||||
|
// get test component from the fixture
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const navMenuSelector = '.navbar-nav';
|
||||||
|
deNavMenu = fixture.debugElement.query(By.css(navMenuSelector));
|
||||||
|
|
||||||
|
const navMenuItemSelector = 'li';
|
||||||
|
deNavMenuItem = deNavMenu.query(By.css(navMenuItemSelector));
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should not render', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
expect(deNavMenu.nativeElement).toBeDefined();
|
||||||
|
expect(deNavMenuItem).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when route is /logout and user is authenticated', () => {
|
||||||
|
routerState = {
|
||||||
|
url: '/logout'
|
||||||
|
};
|
||||||
|
beforeEach(inject([Store], (store: Store<AppState>) => {
|
||||||
|
store
|
||||||
|
.subscribe((state) => {
|
||||||
|
(state as any).router = Object.create({});
|
||||||
|
(state as any).router.state = routerState;
|
||||||
|
(state as any).core = Object.create({});
|
||||||
|
(state as any).core.auth = authState;
|
||||||
|
});
|
||||||
|
|
||||||
|
// create component and test fixture
|
||||||
|
fixture = TestBed.createComponent(AuthNavMenuComponent);
|
||||||
|
|
||||||
|
// get test component from the fixture
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const navMenuSelector = '.navbar-nav';
|
||||||
|
deNavMenu = fixture.debugElement.query(By.css(navMenuSelector));
|
||||||
|
|
||||||
|
const navMenuItemSelector = 'li';
|
||||||
|
deNavMenuItem = deNavMenu.query(By.css(navMenuItemSelector));
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should not render', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
expect(deNavMenu.nativeElement).toBeDefined();
|
||||||
|
expect(deNavMenuItem).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when route is not /login neither /logout', () => {
|
||||||
|
describe('when user is not authenticated', () => {
|
||||||
|
|
||||||
|
beforeEach(inject([Store], (store: Store<AppState>) => {
|
||||||
|
routerState = {
|
||||||
|
url: '/home'
|
||||||
|
};
|
||||||
|
store
|
||||||
|
.subscribe((state) => {
|
||||||
|
(state as any).router = Object.create({});
|
||||||
|
(state as any).router.state = routerState;
|
||||||
|
(state as any).core = Object.create({});
|
||||||
|
(state as any).core.auth = notAuthState;
|
||||||
|
});
|
||||||
|
|
||||||
|
// create component and test fixture
|
||||||
|
fixture = TestBed.createComponent(AuthNavMenuComponent);
|
||||||
|
|
||||||
|
// get test component from the fixture
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const navMenuSelector = '.navbar-nav';
|
||||||
|
deNavMenu = fixture.debugElement.query(By.css(navMenuSelector));
|
||||||
|
|
||||||
|
const navMenuItemSelector = 'li';
|
||||||
|
deNavMenuItem = deNavMenu.query(By.css(navMenuItemSelector));
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should render login dropdown menu', () => {
|
||||||
|
const loginDropdownMenu = deNavMenuItem.query(By.css('div[id=loginDropdownMenu]'));
|
||||||
|
expect(loginDropdownMenu.nativeElement).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when user is authenticated', () => {
|
||||||
|
beforeEach(inject([Store], (store: Store<AppState>) => {
|
||||||
|
routerState = {
|
||||||
|
url: '/home'
|
||||||
|
};
|
||||||
|
store
|
||||||
|
.subscribe((state) => {
|
||||||
|
(state as any).router = Object.create({});
|
||||||
|
(state as any).router.state = routerState;
|
||||||
|
(state as any).core = Object.create({});
|
||||||
|
(state as any).core.auth = authState;
|
||||||
|
});
|
||||||
|
|
||||||
|
// create component and test fixture
|
||||||
|
fixture = TestBed.createComponent(AuthNavMenuComponent);
|
||||||
|
|
||||||
|
// get test component from the fixture
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const navMenuSelector = '.navbar-nav';
|
||||||
|
deNavMenu = fixture.debugElement.query(By.css(navMenuSelector));
|
||||||
|
|
||||||
|
const navMenuItemSelector = 'li';
|
||||||
|
deNavMenuItem = deNavMenu.query(By.css(navMenuItemSelector));
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should render logout dropdown menu', () => {
|
||||||
|
const logoutDropdownMenu = deNavMenuItem.query(By.css('div[id=logoutDropdownMenu]'));
|
||||||
|
expect(logoutDropdownMenu.nativeElement).toBeDefined();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when is a mobile view', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
const window = new HostWindowServiceStub(300);
|
||||||
|
|
||||||
|
// refine the test module by declaring the test component
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
NoopAnimationsModule,
|
||||||
|
StoreModule.forRoot(authReducer),
|
||||||
|
TranslateModule.forRoot()
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
AuthNavMenuComponent
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{provide: HostWindowService, useValue: window},
|
||||||
|
],
|
||||||
|
schemas: [
|
||||||
|
CUSTOM_ELEMENTS_SCHEMA
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('when user is not authenticated', () => {
|
||||||
|
|
||||||
|
beforeEach(inject([Store], (store: Store<AppState>) => {
|
||||||
|
store
|
||||||
|
.subscribe((state) => {
|
||||||
|
(state as any).router = Object.create({});
|
||||||
|
(state as any).router.state = routerState;
|
||||||
|
(state as any).core = Object.create({});
|
||||||
|
(state as any).core.auth = notAuthState;
|
||||||
|
});
|
||||||
|
|
||||||
|
// create component and test fixture
|
||||||
|
fixture = TestBed.createComponent(AuthNavMenuComponent);
|
||||||
|
|
||||||
|
// get test component from the fixture
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const navMenuSelector = '.navbar-nav';
|
||||||
|
deNavMenu = fixture.debugElement.query(By.css(navMenuSelector));
|
||||||
|
|
||||||
|
const navMenuItemSelector = 'li';
|
||||||
|
deNavMenuItem = deNavMenu.query(By.css(navMenuItemSelector));
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should render login link', () => {
|
||||||
|
const loginDropdownMenu = deNavMenuItem.query(By.css('a[id=loginLink]'));
|
||||||
|
expect(loginDropdownMenu.nativeElement).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when user is authenticated', () => {
|
||||||
|
beforeEach(inject([Store], (store: Store<AppState>) => {
|
||||||
|
store
|
||||||
|
.subscribe((state) => {
|
||||||
|
(state as any).router = Object.create({});
|
||||||
|
(state as any).router.state = routerState;
|
||||||
|
(state as any).core = Object.create({});
|
||||||
|
(state as any).core.auth = authState;
|
||||||
|
});
|
||||||
|
|
||||||
|
// create component and test fixture
|
||||||
|
fixture = TestBed.createComponent(AuthNavMenuComponent);
|
||||||
|
|
||||||
|
// get test component from the fixture
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const navMenuSelector = '.navbar-nav';
|
||||||
|
deNavMenu = fixture.debugElement.query(By.css(navMenuSelector));
|
||||||
|
|
||||||
|
const navMenuItemSelector = 'li';
|
||||||
|
deNavMenuItem = deNavMenu.query(By.css(navMenuItemSelector));
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should render logout link', inject([Store], (store: Store<AppState>) => {
|
||||||
|
const logoutDropdownMenu = deNavMenuItem.query(By.css('a[id=logoutLink]'));
|
||||||
|
expect(logoutDropdownMenu.nativeElement).toBeDefined();
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
59
src/app/shared/auth-nav-menu/auth-nav-menu.component.ts
Normal file
59
src/app/shared/auth-nav-menu/auth-nav-menu.component.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { RouterReducerState } from '@ngrx/router-store';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
|
||||||
|
import { fadeInOut, fadeOut } from '../animations/fade';
|
||||||
|
import { HostWindowService } from '../host-window.service';
|
||||||
|
import { AppState, routerStateSelector } from '../../app.reducer';
|
||||||
|
import { isNotUndefined } from '../empty.util';
|
||||||
|
import { getAuthenticatedUser, isAuthenticated, isAuthenticationLoading } from '../../core/auth/selectors';
|
||||||
|
import { Eperson } from '../../core/eperson/models/eperson.model';
|
||||||
|
import { LOGIN_ROUTE, LOGOUT_ROUTE } from '../../core/auth/auth.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-auth-nav-menu',
|
||||||
|
templateUrl: './auth-nav-menu.component.html',
|
||||||
|
styleUrls: ['./auth-nav-menu.component.scss'],
|
||||||
|
animations: [fadeInOut, fadeOut]
|
||||||
|
})
|
||||||
|
export class AuthNavMenuComponent implements OnInit {
|
||||||
|
/**
|
||||||
|
* Whether user is authenticated.
|
||||||
|
* @type {Observable<string>}
|
||||||
|
*/
|
||||||
|
public isAuthenticated: Observable<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the authentication is loading.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
public loading: Observable<boolean>;
|
||||||
|
|
||||||
|
public isXsOrSm$: Observable<boolean>;
|
||||||
|
|
||||||
|
public showAuth = Observable.of(false);
|
||||||
|
|
||||||
|
public user: Observable<Eperson>;
|
||||||
|
|
||||||
|
constructor(private store: Store<AppState>,
|
||||||
|
private windowService: HostWindowService) {
|
||||||
|
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
// set isAuthenticated
|
||||||
|
this.isAuthenticated = this.store.select(isAuthenticated);
|
||||||
|
|
||||||
|
// set loading
|
||||||
|
this.loading = this.store.select(isAuthenticationLoading);
|
||||||
|
|
||||||
|
this.user = this.store.select(getAuthenticatedUser);
|
||||||
|
|
||||||
|
this.showAuth = this.store.select(routerStateSelector)
|
||||||
|
.filter((router: RouterReducerState) => isNotUndefined(router) && isNotUndefined(router.state))
|
||||||
|
.map((router: RouterReducerState) => {
|
||||||
|
return !router.state.url.startsWith(LOGIN_ROUTE) && !router.state.url.startsWith(LOGOUT_ROUTE);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -1,6 +1,16 @@
|
|||||||
|
import { cold, hot } from 'jasmine-marbles';
|
||||||
import {
|
import {
|
||||||
isEmpty, hasNoValue, hasValue, isNotEmpty, isNull, isNotNull,
|
ensureArrayHasValue,
|
||||||
isUndefined, isNotUndefined
|
hasNoValue,
|
||||||
|
hasValue,
|
||||||
|
hasValueOperator,
|
||||||
|
isEmpty,
|
||||||
|
isNotEmpty,
|
||||||
|
isNotEmptyOperator,
|
||||||
|
isNotNull,
|
||||||
|
isNotUndefined,
|
||||||
|
isNull,
|
||||||
|
isUndefined
|
||||||
} from './empty.util';
|
} from './empty.util';
|
||||||
|
|
||||||
describe('Empty Utils', () => {
|
describe('Empty Utils', () => {
|
||||||
@@ -274,6 +284,25 @@ describe('Empty Utils', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('hasValueOperator', () => {
|
||||||
|
it('should only include items from the source observable for which hasValue is true, and omit all others', () => {
|
||||||
|
const testData = {
|
||||||
|
a: null,
|
||||||
|
b: 'test',
|
||||||
|
c: true,
|
||||||
|
d: undefined,
|
||||||
|
e: 1,
|
||||||
|
f: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const source$ = hot('abcdef', testData);
|
||||||
|
const expected$ = cold('-bc-ef', testData);
|
||||||
|
const result$ = source$.pipe(hasValueOperator());
|
||||||
|
|
||||||
|
expect(result$).toBeObservable(expected$);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('isEmpty', () => {
|
describe('isEmpty', () => {
|
||||||
it('should return true for null', () => {
|
it('should return true for null', () => {
|
||||||
expect(isEmpty(null)).toBe(true);
|
expect(isEmpty(null)).toBe(true);
|
||||||
@@ -393,4 +422,56 @@ describe('Empty Utils', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('isNotEmptyOperator', () => {
|
||||||
|
it('should only include items from the source observable for which isNotEmpty is true, and omit all others', () => {
|
||||||
|
const testData = {
|
||||||
|
a: null,
|
||||||
|
b: 'test',
|
||||||
|
c: true,
|
||||||
|
d: undefined,
|
||||||
|
e: 1,
|
||||||
|
f: {},
|
||||||
|
g: '',
|
||||||
|
h: ' '
|
||||||
|
};
|
||||||
|
|
||||||
|
const source$ = hot('abcdefgh', testData);
|
||||||
|
const expected$ = cold('-bc-e--h', testData);
|
||||||
|
const result$ = source$.pipe(isNotEmptyOperator());
|
||||||
|
|
||||||
|
expect(result$).toBeObservable(expected$);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ensureArrayHasValue', () => {
|
||||||
|
it('should let all arrays pass unchanged, and turn everything else in to empty arrays', () => {
|
||||||
|
const sourceData = {
|
||||||
|
a: { a: 'b' },
|
||||||
|
b: ['a', 'b', 'c'],
|
||||||
|
c: null,
|
||||||
|
d: [1],
|
||||||
|
e: undefined,
|
||||||
|
f: [],
|
||||||
|
g: () => true,
|
||||||
|
h: {},
|
||||||
|
i: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectedData = Object.assign({}, sourceData, {
|
||||||
|
a: [],
|
||||||
|
c: [],
|
||||||
|
e: [],
|
||||||
|
g: [],
|
||||||
|
h: [],
|
||||||
|
i: []
|
||||||
|
});
|
||||||
|
|
||||||
|
const source$ = hot('abcdefghi', sourceData);
|
||||||
|
const expected$ = cold('abcdefghi', expectedData);
|
||||||
|
const result$ = source$.pipe(ensureArrayHasValue());
|
||||||
|
|
||||||
|
expect(result$).toBeObservable(expected$);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,3 +1,6 @@
|
|||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { filter, map } from 'rxjs/operators';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the passed value is null.
|
* Returns true if the passed value is null.
|
||||||
* isNull(); // false
|
* isNull(); // false
|
||||||
@@ -82,6 +85,14 @@ export function hasValue(obj?: any): boolean {
|
|||||||
return isNotUndefined(obj) && isNotNull(obj);
|
return isNotUndefined(obj) && isNotNull(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter items emitted by the source Observable by only emitting those for
|
||||||
|
* which hasValue is true
|
||||||
|
*/
|
||||||
|
export const hasValueOperator = () =>
|
||||||
|
<T>(source: Observable<T>): Observable<T> =>
|
||||||
|
source.pipe(filter((obj: T) => hasValue(obj)));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifies that a value is `null` or an empty string, empty array,
|
* Verifies that a value is `null` or an empty string, empty array,
|
||||||
* or empty function.
|
* or empty function.
|
||||||
@@ -148,3 +159,21 @@ export function isEmpty(obj?: any): boolean {
|
|||||||
export function isNotEmpty(obj?: any): boolean {
|
export function isNotEmpty(obj?: any): boolean {
|
||||||
return !isEmpty(obj);
|
return !isEmpty(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter items emitted by the source Observable by only emitting those for
|
||||||
|
* which isNotEmpty is true
|
||||||
|
*/
|
||||||
|
export const isNotEmptyOperator = () =>
|
||||||
|
<T>(source: Observable<T>): Observable<T> =>
|
||||||
|
source.pipe(filter((obj: T) => isNotEmpty(obj)));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests each value emitted by the source Observable,
|
||||||
|
* let's arrays pass through, turns other values in to
|
||||||
|
* empty arrays. Used to be able to chain array operators
|
||||||
|
* on something that may not have a value
|
||||||
|
*/
|
||||||
|
export const ensureArrayHasValue = () =>
|
||||||
|
<T>(source: Observable<T[]>): Observable<T[]> =>
|
||||||
|
source.pipe(map((arr: T[]): T[] => Array.isArray(arr) ? arr : []));
|
||||||
|
@@ -92,4 +92,12 @@ export class HostWindowService {
|
|||||||
distinctUntilChanged()
|
distinctUntilChanged()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isXsOrSm(): Observable<boolean> {
|
||||||
|
return Observable.combineLatest(
|
||||||
|
this.isXs(),
|
||||||
|
this.isSm(),
|
||||||
|
((isXs, isSm) => isXs || isSm)
|
||||||
|
).distinctUntilChanged();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
28
src/app/shared/log-in/log-in.component.html
Normal file
28
src/app/shared/log-in/log-in.component.html
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<ds-loading *ngIf="(loading | async) || (isAuthenticated | async)" class="m-5"></ds-loading>
|
||||||
|
<form *ngIf="!(loading | async) && !(isAuthenticated | async)" class="form-login px-4 py-3" (ngSubmit)="submit()" [formGroup]="form" novalidate>
|
||||||
|
<label for="inputEmail" class="sr-only">{{"login.form.email" | translate}}</label>
|
||||||
|
<input id="inputEmail"
|
||||||
|
autocomplete="off"
|
||||||
|
autofocus
|
||||||
|
class="form-control form-control-lg position-relative"
|
||||||
|
formControlName="email"
|
||||||
|
placeholder="{{'login.form.email' | translate}}"
|
||||||
|
required
|
||||||
|
type="email">
|
||||||
|
<label for="inputPassword" class="sr-only">{{"login.form.password" | translate}}</label>
|
||||||
|
<input id="inputPassword"
|
||||||
|
autocomplete="off"
|
||||||
|
class="form-control form-control-lg position-relative mb-3"
|
||||||
|
placeholder="{{'login.form.password' | translate}}"
|
||||||
|
formControlName="password"
|
||||||
|
required
|
||||||
|
type="password">
|
||||||
|
<div *ngIf="(error | async) && hasError" class="alert alert-danger" role="alert" @fadeOut>{{ (error | async) | translate }}</div>
|
||||||
|
<div *ngIf="(message | async) && hasMessage" class="alert alert-info" role="alert" @fadeOut>{{ (message | async) | translate }}</div>
|
||||||
|
<button class="btn btn-lg btn-primary btn-block mt-3" type="submit" [disabled]="!form.valid">{{"login.form.submit" | translate}}</button>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<a class="dropdown-item" href="#">{{"login.form.new-user" | translate}}</a>
|
||||||
|
<a class="dropdown-item" href="#">{{"login.form.forgot-password" | translate}}</a>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
15
src/app/shared/log-in/log-in.component.scss
Normal file
15
src/app/shared/log-in/log-in.component.scss
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
@import '../../../styles/variables.scss';
|
||||||
|
|
||||||
|
.form-login .form-control:focus {
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.form-login input[type="email"] {
|
||||||
|
margin-bottom: -1px;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
|
.form-login input[type="password"] {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
127
src/app/shared/log-in/log-in.component.spec.ts
Normal file
127
src/app/shared/log-in/log-in.component.spec.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
|
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||||
|
import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { Store, StoreModule } from '@ngrx/store';
|
||||||
|
|
||||||
|
import { LogInComponent } from './log-in.component';
|
||||||
|
import { authReducer } from '../../core/auth/auth.reducer';
|
||||||
|
import { EpersonMock } from '../testing/eperson-mock';
|
||||||
|
import { Eperson } from '../../core/eperson/models/eperson.model';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
import { AuthServiceStub } from '../testing/auth-service-stub';
|
||||||
|
import { AppState } from '../../app.reducer';
|
||||||
|
|
||||||
|
describe('LogInComponent', () => {
|
||||||
|
|
||||||
|
let component: LogInComponent;
|
||||||
|
let fixture: ComponentFixture<LogInComponent>;
|
||||||
|
let page: Page;
|
||||||
|
let user: Eperson;
|
||||||
|
|
||||||
|
const authState = {
|
||||||
|
authenticated: false,
|
||||||
|
loaded: false,
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
user = EpersonMock;
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
// refine the test module by declaring the test component
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
StoreModule.forRoot(authReducer),
|
||||||
|
TranslateModule.forRoot()
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
LogInComponent
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{provide: AuthService, useClass: AuthServiceStub}
|
||||||
|
],
|
||||||
|
schemas: [
|
||||||
|
CUSTOM_ELEMENTS_SCHEMA
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(inject([Store], (store: Store<AppState>) => {
|
||||||
|
store
|
||||||
|
.subscribe((state) => {
|
||||||
|
(state as any).core = Object.create({});
|
||||||
|
(state as any).core.auth = authState;
|
||||||
|
});
|
||||||
|
|
||||||
|
// create component and test fixture
|
||||||
|
fixture = TestBed.createComponent(LogInComponent);
|
||||||
|
|
||||||
|
// get test component from the fixture
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
|
||||||
|
// create page
|
||||||
|
page = new Page(component, fixture);
|
||||||
|
|
||||||
|
// verify the fixture is stable (no pending tasks)
|
||||||
|
fixture.whenStable().then(() => {
|
||||||
|
page.addPageElements();
|
||||||
|
});
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should create a FormGroup comprised of FormControls', () => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(component.form instanceof FormGroup).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should authenticate', () => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
// set FormControl values
|
||||||
|
component.form.controls.email.setValue('user');
|
||||||
|
component.form.controls.password.setValue('password');
|
||||||
|
|
||||||
|
// submit form
|
||||||
|
component.submit();
|
||||||
|
|
||||||
|
// verify Store.dispatch() is invoked
|
||||||
|
expect(page.navigateSpy.calls.any()).toBe(true, 'Store.dispatch not invoked');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* I represent the DOM elements and attach spies.
|
||||||
|
*
|
||||||
|
* @class Page
|
||||||
|
*/
|
||||||
|
class Page {
|
||||||
|
|
||||||
|
public emailInput: HTMLInputElement;
|
||||||
|
public navigateSpy: jasmine.Spy;
|
||||||
|
public passwordInput: HTMLInputElement;
|
||||||
|
|
||||||
|
constructor(private component: LogInComponent, private fixture: ComponentFixture<LogInComponent>) {
|
||||||
|
// use injector to get services
|
||||||
|
const injector = fixture.debugElement.injector;
|
||||||
|
const store = injector.get(Store);
|
||||||
|
|
||||||
|
// add spies
|
||||||
|
this.navigateSpy = spyOn(store, 'dispatch');
|
||||||
|
}
|
||||||
|
|
||||||
|
public addPageElements() {
|
||||||
|
const emailInputSelector = 'input[formcontrolname=\'email\']';
|
||||||
|
this.emailInput = this.fixture.debugElement.query(By.css(emailInputSelector)).nativeElement;
|
||||||
|
|
||||||
|
const passwordInputSelector = 'input[formcontrolname=\'password\']';
|
||||||
|
this.passwordInput = this.fixture.debugElement.query(By.css(passwordInputSelector)).nativeElement;
|
||||||
|
}
|
||||||
|
}
|
184
src/app/shared/log-in/log-in.component.ts
Normal file
184
src/app/shared/log-in/log-in.component.ts
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||||
|
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import 'rxjs/add/operator/filter';
|
||||||
|
import 'rxjs/add/operator/takeWhile';
|
||||||
|
|
||||||
|
import { AuthenticateAction, ResetAuthenticationMessagesAction } from '../../core/auth/auth.actions';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getAuthenticationError,
|
||||||
|
getAuthenticationInfo,
|
||||||
|
isAuthenticated,
|
||||||
|
isAuthenticationLoading,
|
||||||
|
} from '../../core/auth/selectors';
|
||||||
|
import { CoreState } from '../../core/core.reducers';
|
||||||
|
|
||||||
|
import { isNotEmpty } from '../empty.util';
|
||||||
|
import { fadeOut } from '../animations/fade';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* /users/sign-in
|
||||||
|
* @class LogInComponent
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-log-in',
|
||||||
|
templateUrl: './log-in.component.html',
|
||||||
|
styleUrls: ['./log-in.component.scss'],
|
||||||
|
animations: [fadeOut]
|
||||||
|
})
|
||||||
|
export class LogInComponent implements OnDestroy, OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The error if authentication fails.
|
||||||
|
* @type {Observable<string>}
|
||||||
|
*/
|
||||||
|
public error: Observable<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Has authentication error.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
public hasError = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The authentication info message.
|
||||||
|
* @type {Observable<string>}
|
||||||
|
*/
|
||||||
|
public message: Observable<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Has authentication message.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
public hasMessage = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether user is authenticated.
|
||||||
|
* @type {Observable<string>}
|
||||||
|
*/
|
||||||
|
public isAuthenticated: Observable<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the authentication is loading.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
public loading: Observable<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The authentication form.
|
||||||
|
* @type {FormGroup}
|
||||||
|
*/
|
||||||
|
public form: FormGroup;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component state.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
private alive = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
* @param {AuthService} authService
|
||||||
|
* @param {FormBuilder} formBuilder
|
||||||
|
* @param {Store<State>} store
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
private authService: AuthService,
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private store: Store<CoreState>
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lifecycle hook that is called after data-bound properties of a directive are initialized.
|
||||||
|
* @method ngOnInit
|
||||||
|
*/
|
||||||
|
public ngOnInit() {
|
||||||
|
// set isAuthenticated
|
||||||
|
this.isAuthenticated = this.store.select(isAuthenticated);
|
||||||
|
|
||||||
|
// set formGroup
|
||||||
|
this.form = this.formBuilder.group({
|
||||||
|
email: ['', Validators.required],
|
||||||
|
password: ['', Validators.required]
|
||||||
|
});
|
||||||
|
|
||||||
|
// set error
|
||||||
|
this.error = this.store.select(getAuthenticationError)
|
||||||
|
.map((error) => {
|
||||||
|
this.hasError = (isNotEmpty(error));
|
||||||
|
return error;
|
||||||
|
});
|
||||||
|
|
||||||
|
// set error
|
||||||
|
this.message = this.store.select(getAuthenticationInfo)
|
||||||
|
.map((message) => {
|
||||||
|
this.hasMessage = (isNotEmpty(message));
|
||||||
|
return message;
|
||||||
|
});
|
||||||
|
|
||||||
|
// set loading
|
||||||
|
this.loading = this.store.select(isAuthenticationLoading);
|
||||||
|
|
||||||
|
// subscribe to success
|
||||||
|
this.store.select(isAuthenticated)
|
||||||
|
.takeWhile(() => this.alive)
|
||||||
|
.filter((authenticated) => authenticated)
|
||||||
|
.subscribe(() => {
|
||||||
|
this.authService.redirectToPreviousUrl();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lifecycle hook that is called when a directive, pipe or service is destroyed.
|
||||||
|
* @method ngOnDestroy
|
||||||
|
*/
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.alive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset error or message.
|
||||||
|
*/
|
||||||
|
public resetErrorOrMessage() {
|
||||||
|
if (this.hasError || this.hasMessage) {
|
||||||
|
this.store.dispatch(new ResetAuthenticationMessagesAction());
|
||||||
|
this.hasError = false;
|
||||||
|
this.hasMessage = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To the registration page.
|
||||||
|
* @method register
|
||||||
|
*/
|
||||||
|
public register() {
|
||||||
|
// TODO enable after registration process is done
|
||||||
|
// this.router.navigate(['/register']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit the authentication form.
|
||||||
|
* @method submit
|
||||||
|
*/
|
||||||
|
public submit() {
|
||||||
|
this.resetErrorOrMessage();
|
||||||
|
// get email and password values
|
||||||
|
const email: string = this.form.get('email').value;
|
||||||
|
const password: string = this.form.get('password').value;
|
||||||
|
|
||||||
|
// trim values
|
||||||
|
email.trim();
|
||||||
|
password.trim();
|
||||||
|
|
||||||
|
// dispatch AuthenticationAction
|
||||||
|
this.store.dispatch(new AuthenticateAction(email, password));
|
||||||
|
|
||||||
|
// clear form
|
||||||
|
this.form.reset();
|
||||||
|
}
|
||||||
|
}
|
7
src/app/shared/log-out/log-out.component.html
Normal file
7
src/app/shared/log-out/log-out.component.html
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<ds-loading *ngIf="(loading | async)"></ds-loading>
|
||||||
|
<div *ngIf="!(loading | async)" class="form-login px-4 py-3">
|
||||||
|
|
||||||
|
<div *ngIf="(error | async) && hasError" class="alert alert-danger" role="alert" @fadeOut>{{ error | async }}</div>
|
||||||
|
|
||||||
|
<button class="btn btn-lg btn-primary btn-block mt-3" (click)="logOut()">{{"logout.form.submit" | translate}}</button>
|
||||||
|
</div>
|
1
src/app/shared/log-out/log-out.component.scss
Normal file
1
src/app/shared/log-out/log-out.component.scss
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@import '../log-in/log-in.component.scss';
|
108
src/app/shared/log-out/log-out.component.spec.ts
Normal file
108
src/app/shared/log-out/log-out.component.spec.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
|
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
|
||||||
|
import { Store, StoreModule } from '@ngrx/store';
|
||||||
|
|
||||||
|
import { authReducer } from '../../core/auth/auth.reducer';
|
||||||
|
import { EpersonMock } from '../testing/eperson-mock';
|
||||||
|
import { Eperson } from '../../core/eperson/models/eperson.model';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { AppState } from '../../app.reducer';
|
||||||
|
import { LogOutComponent } from './log-out.component';
|
||||||
|
import { RouterStub } from '../testing/router-stub';
|
||||||
|
|
||||||
|
describe('LogOutComponent', () => {
|
||||||
|
|
||||||
|
let component: LogOutComponent;
|
||||||
|
let fixture: ComponentFixture<LogOutComponent>;
|
||||||
|
let page: Page;
|
||||||
|
let user: Eperson;
|
||||||
|
|
||||||
|
const authState = {
|
||||||
|
authenticated: false,
|
||||||
|
loaded: false,
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
const routerStub = new RouterStub();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
user = EpersonMock;
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
// refine the test module by declaring the test component
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
StoreModule.forRoot(authReducer),
|
||||||
|
TranslateModule.forRoot()
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
LogOutComponent
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{provide: Router, useValue: routerStub},
|
||||||
|
],
|
||||||
|
schemas: [
|
||||||
|
CUSTOM_ELEMENTS_SCHEMA
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(inject([Store], (store: Store<AppState>) => {
|
||||||
|
store
|
||||||
|
.subscribe((state) => {
|
||||||
|
(state as any).core = Object.create({});
|
||||||
|
(state as any).core.auth = authState;
|
||||||
|
});
|
||||||
|
|
||||||
|
// create component and test fixture
|
||||||
|
fixture = TestBed.createComponent(LogOutComponent);
|
||||||
|
|
||||||
|
// get test component from the fixture
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
|
||||||
|
// create page
|
||||||
|
page = new Page(component, fixture);
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should create an instance', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log out', () => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
// submit form
|
||||||
|
component.logOut();
|
||||||
|
|
||||||
|
// verify Store.dispatch() is invoked
|
||||||
|
expect(page.navigateSpy.calls.any()).toBe(true, 'Store.dispatch not invoked');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* I represent the DOM elements and attach spies.
|
||||||
|
*
|
||||||
|
* @class Page
|
||||||
|
*/
|
||||||
|
class Page {
|
||||||
|
|
||||||
|
public navigateSpy: jasmine.Spy;
|
||||||
|
|
||||||
|
constructor(private component: LogOutComponent, private fixture: ComponentFixture<LogOutComponent>) {
|
||||||
|
// use injector to get services
|
||||||
|
const injector = fixture.debugElement.injector;
|
||||||
|
const store = injector.get(Store);
|
||||||
|
|
||||||
|
// add spies
|
||||||
|
this.navigateSpy = spyOn(store, 'dispatch');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
82
src/app/shared/log-out/log-out.component.ts
Normal file
82
src/app/shared/log-out/log-out.component.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
// @ngrx
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
|
||||||
|
// actions
|
||||||
|
import { LogOutAction } from '../../core/auth/auth.actions';
|
||||||
|
|
||||||
|
// reducers
|
||||||
|
import {
|
||||||
|
getLogOutError,
|
||||||
|
isAuthenticated,
|
||||||
|
isAuthenticationLoading,
|
||||||
|
} from '../../core/auth/selectors';
|
||||||
|
|
||||||
|
import { AppState } from '../../app.reducer';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { fadeOut } from '../animations/fade';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-log-out',
|
||||||
|
templateUrl: './log-out.component.html',
|
||||||
|
styleUrls: ['./log-out.component.scss'],
|
||||||
|
animations: [fadeOut]
|
||||||
|
})
|
||||||
|
export class LogOutComponent implements OnDestroy, OnInit {
|
||||||
|
/**
|
||||||
|
* The error if authentication fails.
|
||||||
|
* @type {Observable<string>}
|
||||||
|
*/
|
||||||
|
public error: Observable<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the logout is loading.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
public loading: Observable<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component state.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
private alive = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
* @param {Store<State>} store
|
||||||
|
*/
|
||||||
|
constructor(private router: Router,
|
||||||
|
private store: Store<AppState>) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lifecycle hook that is called when a directive, pipe or service is destroyed.
|
||||||
|
*/
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.alive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lifecycle hook that is called after data-bound properties of a directive are initialized.
|
||||||
|
*/
|
||||||
|
ngOnInit() {
|
||||||
|
// set error
|
||||||
|
this.error = this.store.select(getLogOutError);
|
||||||
|
|
||||||
|
// set loading
|
||||||
|
this.loading = this.store.select(isAuthenticationLoading);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Go to the home page.
|
||||||
|
*/
|
||||||
|
public home() {
|
||||||
|
this.router.navigate(['/home']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public logOut() {
|
||||||
|
this.store.dispatch(new LogOutAction());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
6
src/app/shared/mocks/mock-auth.service.ts
Normal file
6
src/app/shared/mocks/mock-auth.service.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/* tslint:disable:no-empty */
|
||||||
|
export class AuthServiceMock {
|
||||||
|
public checksAuthenticationToken() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
23
src/app/shared/mocks/mock-remote-data-build.service.ts
Normal file
23
src/app/shared/mocks/mock-remote-data-build.service.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { map, take } from 'rxjs/operators';
|
||||||
|
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
|
||||||
|
import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { RequestEntry } from '../../core/data/request.reducer';
|
||||||
|
import { hasValue } from '../empty.util';
|
||||||
|
|
||||||
|
export function getMockRemoteDataBuildService(toRemoteDataObservable$?: Observable<RemoteData<any>>): RemoteDataBuildService {
|
||||||
|
return {
|
||||||
|
toRemoteDataObservable: (requestEntry$: Observable<RequestEntry>, responseCache$: Observable<ResponseCacheEntry>, payload$: Observable<any>) => {
|
||||||
|
|
||||||
|
if (hasValue(toRemoteDataObservable$)) {
|
||||||
|
return toRemoteDataObservable$;
|
||||||
|
} else {
|
||||||
|
return payload$.pipe(map((payload) => ({
|
||||||
|
payload
|
||||||
|
} as RemoteData<any>)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as RemoteDataBuildService;
|
||||||
|
|
||||||
|
}
|
@@ -1,10 +1,11 @@
|
|||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { RequestService } from '../../core/data/request.service';
|
import { RequestService } from '../../core/data/request.service';
|
||||||
import { RequestEntry } from '../../core/data/request.reducer';
|
import { RequestEntry } from '../../core/data/request.reducer';
|
||||||
|
|
||||||
export function getMockRequestService(): RequestService {
|
export function getMockRequestService(getByHref$: Observable<RequestEntry> = Observable.of(new RequestEntry())): RequestService {
|
||||||
return jasmine.createSpyObj('requestService', {
|
return jasmine.createSpyObj('requestService', {
|
||||||
configure: () => false,
|
configure: false,
|
||||||
generateRequestId: () => 'clients/b186e8ce-e99c-4183-bc9a-42b4821bdb78',
|
generateRequestId: 'clients/b186e8ce-e99c-4183-bc9a-42b4821bdb78',
|
||||||
getByHref: (uuid: string) => new RequestEntry()
|
getByHref: getByHref$
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,16 @@
|
|||||||
import { ResponseCacheService } from '../../core/cache/response-cache.service';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer';
|
import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer';
|
||||||
import { RestResponse } from '../../core/cache/response-cache.models';
|
import { ResponseCacheService } from '../../core/cache/response-cache.service';
|
||||||
|
|
||||||
export function getMockResponseCacheService(): ResponseCacheService {
|
export function getMockResponseCacheService(
|
||||||
|
add$: Observable<ResponseCacheEntry> = Observable.of(new ResponseCacheEntry()),
|
||||||
|
get$: Observable<ResponseCacheEntry> = Observable.of(new ResponseCacheEntry()),
|
||||||
|
has: boolean = false
|
||||||
|
): ResponseCacheService {
|
||||||
return jasmine.createSpyObj('ResponseCacheService', {
|
return jasmine.createSpyObj('ResponseCacheService', {
|
||||||
add: (key: string, response: RestResponse, msToLive: number) => new ResponseCacheEntry(),
|
add: add$,
|
||||||
get: (key: string) => new ResponseCacheEntry(),
|
get: get$,
|
||||||
has: (key: string) => false,
|
has,
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user