diff --git a/README.md b/README.md
index 466e736de2..dceb415d58 100644
--- a/README.md
+++ b/README.md
@@ -23,9 +23,6 @@ git clone https://github.com/DSpace/dspace-angular.git
# change directory to our repo
cd dspace-angular
-# install the global dependencies
-yarn run global
-
# install the local dependencies
yarn install
diff --git a/package.json b/package.json
index 2878daf1c8..76c1f07a63 100644
--- a/package.json
+++ b/package.json
@@ -99,8 +99,10 @@
"font-awesome": "4.7.0",
"http-server": "0.11.1",
"https": "1.0.0",
+ "js-cookie": "2.2.0",
"js.clone": "0.0.3",
"jsonschema": "1.2.2",
+ "jwt-decode": "^2.2.0",
"methods": "1.1.2",
"morgan": "1.9.0",
"ngx-pagination": "3.0.3",
@@ -124,6 +126,7 @@
"@types/express-serve-static-core": "4.11.1",
"@types/hammerjs": "2.0.35",
"@types/jasmine": "^2.8.6",
+ "@types/js-cookie": "2.1.0",
"@types/memory-cache": "0.2.0",
"@types/mime": "2.0.0",
"@types/node": "^9.4.6",
diff --git a/resources/i18n/en.json b/resources/i18n/en.json
index 896c399835..3169dcbae9 100644
--- a/resources/i18n/en.json
+++ b/resources/i18n/en.json
@@ -46,7 +46,9 @@
}
},
"nav": {
- "home": "Home"
+ "home": "Home",
+ "login": "Log In",
+ "logout": "Log Out"
},
"pagination": {
"results-per-page": "Results Per Page",
@@ -203,5 +205,31 @@
"item": "Error fetching item",
"objects": "Error fetching objects",
"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."
+ }
}
}
diff --git a/src/app/+login-page/login-page-routing.module.ts b/src/app/+login-page/login-page-routing.module.ts
new file mode 100644
index 0000000000..4e932c50ce
--- /dev/null
+++ b/src/app/+login-page/login-page-routing.module.ts
@@ -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 { }
diff --git a/src/app/+login-page/login-page.component.html b/src/app/+login-page/login-page.component.html
new file mode 100644
index 0000000000..6dcb11fbb0
--- /dev/null
+++ b/src/app/+login-page/login-page.component.html
@@ -0,0 +1,9 @@
+
+
+
+

+
{{"login.form.header" | translate}}
+
+
+
+
diff --git a/src/app/+login-page/login-page.component.scss b/src/app/+login-page/login-page.component.scss
new file mode 100644
index 0000000000..38adf24671
--- /dev/null
+++ b/src/app/+login-page/login-page.component.scss
@@ -0,0 +1,6 @@
+@import '../../styles/variables.scss';
+
+.login-logo {
+ height: $login-logo-height;
+ width: $login-logo-width;
+}
diff --git a/src/app/+login-page/login-page.component.spec.ts b/src/app/+login-page/login-page.component.spec.ts
new file mode 100644
index 0000000000..609cf47794
--- /dev/null
+++ b/src/app/+login-page/login-page.component.spec.ts
@@ -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;
+
+ const store: Store = 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()
+ });
+
+});
diff --git a/src/app/+login-page/login-page.component.ts b/src/app/+login-page/login-page.component.ts
new file mode 100644
index 0000000000..2752973130
--- /dev/null
+++ b/src/app/+login-page/login-page.component.ts
@@ -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) {}
+
+ ngOnDestroy() {
+ // Clear all authentication messages when leaving login page
+ this.store.dispatch(new ResetAuthenticationMessagesAction());
+ }
+}
diff --git a/src/app/+login-page/login-page.module.ts b/src/app/+login-page/login-page.module.ts
new file mode 100644
index 0000000000..4d3f726c40
--- /dev/null
+++ b/src/app/+login-page/login-page.module.ts
@@ -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 {
+
+}
diff --git a/src/app/+logout-page/logout-page-routing.module.ts b/src/app/+logout-page/logout-page-routing.module.ts
new file mode 100644
index 0000000000..64894c1f87
--- /dev/null
+++ b/src/app/+logout-page/logout-page-routing.module.ts
@@ -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 { }
diff --git a/src/app/+logout-page/logout-page.component.html b/src/app/+logout-page/logout-page.component.html
new file mode 100644
index 0000000000..9c6185b665
--- /dev/null
+++ b/src/app/+logout-page/logout-page.component.html
@@ -0,0 +1,9 @@
+
+
+
+

+
{{"logout.form.header" | translate}}
+
+
+
+
diff --git a/src/app/+logout-page/logout-page.component.scss b/src/app/+logout-page/logout-page.component.scss
new file mode 100644
index 0000000000..7e594c0d9b
--- /dev/null
+++ b/src/app/+logout-page/logout-page.component.scss
@@ -0,0 +1 @@
+@import '../+login-page/login-page.component.scss';
diff --git a/src/app/+logout-page/logout-page.component.spec.ts b/src/app/+logout-page/logout-page.component.spec.ts
new file mode 100644
index 0000000000..5fd4e076f2
--- /dev/null
+++ b/src/app/+logout-page/logout-page.component.spec.ts
@@ -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;
+
+ 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()
+ });
+
+});
diff --git a/src/app/+logout-page/logout-page.component.ts b/src/app/+logout-page/logout-page.component.ts
new file mode 100644
index 0000000000..4fa4b9900a
--- /dev/null
+++ b/src/app/+logout-page/logout-page.component.ts
@@ -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 {
+
+}
diff --git a/src/app/+logout-page/logout-page.module.ts b/src/app/+logout-page/logout-page.module.ts
new file mode 100644
index 0000000000..b085a5117b
--- /dev/null
+++ b/src/app/+logout-page/logout-page.module.ts
@@ -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 {
+
+}
diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.service.ts b/src/app/+search-page/search-filters/search-filter/search-filter.service.ts
index 44d9c7e709..695e0204f2 100644
--- a/src/app/+search-page/search-filters/search-filter/search-filter.service.ts
+++ b/src/app/+search-page/search-filters/search-filter/search-filter.service.ts
@@ -14,7 +14,7 @@ import {
import { hasValue, isEmpty, isNotEmpty, } from '../../../shared/empty.util';
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
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 { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
diff --git a/src/app/+search-page/search-options.model.ts b/src/app/+search-page/search-options.model.ts
index df8d8e713a..4b4987a096 100644
--- a/src/app/+search-page/search-options.model.ts
+++ b/src/app/+search-page/search-options.model.ts
@@ -1,6 +1,6 @@
import { isNotEmpty } from '../shared/empty.util';
import { URLCombiner } from '../core/url-combiner/url-combiner';
-import 'core-js/fn/object/entries';
+import 'core-js/library/fn/object/entries';
export enum ViewMode {
List = 'list',
diff --git a/src/app/+search-page/search-page.component.html b/src/app/+search-page/search-page.component.html
index 1a1f379920..e8dee94139 100644
--- a/src/app/+search-page/search-page.component.html
+++ b/src/app/+search-page/search-page.component.html
@@ -1,8 +1,8 @@
-
+
-
diff --git a/src/app/header/header.component.spec.ts b/src/app/header/header.component.spec.ts
index 7c1a9601ac..87fa2995d6 100644
--- a/src/app/header/header.component.spec.ts
+++ b/src/app/header/header.component.spec.ts
@@ -9,6 +9,16 @@ import { Observable } from 'rxjs/Observable';
import { HeaderComponent } from './header.component';
import { HeaderState } from './header.reducer';
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 fixture: ComponentFixture
;
@@ -19,8 +29,17 @@ describe('HeaderComponent', () => {
// async beforeEach
beforeEach(async(() => {
TestBed.configureTestingModule({
- imports: [StoreModule.forRoot({}), TranslateModule.forRoot(), NgbCollapseModule.forRoot()],
- declarations: [HeaderComponent]
+ imports: [
+ 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
}));
diff --git a/src/app/header/header.component.ts b/src/app/header/header.component.ts
index 624ae209dd..93cb329f4f 100644
--- a/src/app/header/header.component.ts
+++ b/src/app/header/header.component.ts
@@ -1,10 +1,12 @@
import { Component, OnInit } from '@angular/core';
import { createSelector, Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
+import { RouterReducerState } from '@ngrx/router-store';
import { HeaderState } from './header.reducer';
import { HeaderToggleAction } from './header.actions';
import { AppState } from '../app.reducer';
+import { HostWindowService } from '../shared/host-window.service';
const headerStateSelector = (state: AppState) => state.header;
const navCollapsedSelector = createSelector(headerStateSelector, (header: HeaderState) => header.navCollapsed);
@@ -12,17 +14,25 @@ const navCollapsedSelector = createSelector(headerStateSelector, (header: Header
@Component({
selector: 'ds-header',
styleUrls: ['header.component.scss'],
- templateUrl: 'header.component.html'
+ templateUrl: 'header.component.html',
})
export class HeaderComponent implements OnInit {
+ /**
+ * Whether user is authenticated.
+ * @type {Observable}
+ */
+ public isAuthenticated: Observable;
public isNavBarCollapsed: Observable;
+ public showAuth = false;
constructor(
- private store: Store
+ private store: Store,
+ private windowService: HostWindowService
) {
}
ngOnInit(): void {
+ // set loading
this.isNavBarCollapsed = this.store.select(navCollapsedSelector);
}
diff --git a/src/app/pagenotfound/pagenotfound.component.ts b/src/app/pagenotfound/pagenotfound.component.ts
index bd119a4de9..e7923b3466 100644
--- a/src/app/pagenotfound/pagenotfound.component.ts
+++ b/src/app/pagenotfound/pagenotfound.component.ts
@@ -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';
@Component({
diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.html b/src/app/shared/auth-nav-menu/auth-nav-menu.component.html
new file mode 100644
index 0000000000..cc9b8c410b
--- /dev/null
+++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.html
@@ -0,0 +1,26 @@
+
+
+
diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.scss b/src/app/shared/auth-nav-menu/auth-nav-menu.component.scss
new file mode 100644
index 0000000000..a8c7b84f56
--- /dev/null
+++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.scss
@@ -0,0 +1,8 @@
+#loginDropdownMenu, #logoutDropdownMenu {
+ min-width: 330px;
+ z-index: 1002;
+}
+
+#loginDropdownMenu {
+ min-height: 260px;
+}
diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts b/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts
new file mode 100644
index 0000000000..8b9f7c8775
--- /dev/null
+++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts
@@ -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;
+
+ 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) => {
+ 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) => {
+ 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) => {
+ 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) => {
+ 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) => {
+ 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) => {
+ 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) => {
+ const logoutDropdownMenu = deNavMenuItem.query(By.css('a[id=logoutLink]'));
+ expect(logoutDropdownMenu.nativeElement).toBeDefined();
+ }));
+ })
+ })
+});
diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts b/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts
new file mode 100644
index 0000000000..1c376258fb
--- /dev/null
+++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts
@@ -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}
+ */
+ public isAuthenticated: Observable;
+
+ /**
+ * True if the authentication is loading.
+ * @type {boolean}
+ */
+ public loading: Observable;
+
+ public isXsOrSm$: Observable;
+
+ public showAuth = Observable.of(false);
+
+ public user: Observable;
+
+ constructor(private store: Store,
+ 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);
+ });
+ }
+}
diff --git a/src/app/shared/empty.util.spec.ts b/src/app/shared/empty.util.spec.ts
index 509f55f7f8..1112883c2a 100644
--- a/src/app/shared/empty.util.spec.ts
+++ b/src/app/shared/empty.util.spec.ts
@@ -1,6 +1,16 @@
+import { cold, hot } from 'jasmine-marbles';
import {
- isEmpty, hasNoValue, hasValue, isNotEmpty, isNull, isNotNull,
- isUndefined, isNotUndefined
+ ensureArrayHasValue,
+ hasNoValue,
+ hasValue,
+ hasValueOperator,
+ isEmpty,
+ isNotEmpty,
+ isNotEmptyOperator,
+ isNotNull,
+ isNotUndefined,
+ isNull,
+ isUndefined
} from './empty.util';
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', () => {
it('should return true for null', () => {
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$);
+ });
+ });
});
diff --git a/src/app/shared/empty.util.ts b/src/app/shared/empty.util.ts
index 1dc3f71871..c1498d11af 100644
--- a/src/app/shared/empty.util.ts
+++ b/src/app/shared/empty.util.ts
@@ -1,3 +1,6 @@
+import { Observable } from 'rxjs/Observable';
+import { filter, map } from 'rxjs/operators';
+
/**
* Returns true if the passed value is null.
* isNull(); // false
@@ -82,6 +85,14 @@ export function hasValue(obj?: any): boolean {
return isNotUndefined(obj) && isNotNull(obj);
}
+/**
+ * Filter items emitted by the source Observable by only emitting those for
+ * which hasValue is true
+ */
+export const hasValueOperator = () =>
+ (source: Observable): Observable =>
+ source.pipe(filter((obj: T) => hasValue(obj)));
+
/**
* Verifies that a value is `null` or an empty string, empty array,
* or empty function.
@@ -148,3 +159,21 @@ export function isEmpty(obj?: any): boolean {
export function isNotEmpty(obj?: any): boolean {
return !isEmpty(obj);
}
+
+/**
+ * Filter items emitted by the source Observable by only emitting those for
+ * which isNotEmpty is true
+ */
+export const isNotEmptyOperator = () =>
+ (source: Observable): Observable =>
+ 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 = () =>
+ (source: Observable): Observable =>
+ source.pipe(map((arr: T[]): T[] => Array.isArray(arr) ? arr : []));
diff --git a/src/app/shared/host-window.service.ts b/src/app/shared/host-window.service.ts
index 13ecbe7538..ecbee685f1 100644
--- a/src/app/shared/host-window.service.ts
+++ b/src/app/shared/host-window.service.ts
@@ -92,4 +92,12 @@ export class HostWindowService {
distinctUntilChanged()
);
}
+
+ isXsOrSm(): Observable {
+ return Observable.combineLatest(
+ this.isXs(),
+ this.isSm(),
+ ((isXs, isSm) => isXs || isSm)
+ ).distinctUntilChanged();
+ }
}
diff --git a/src/app/shared/log-in/log-in.component.html b/src/app/shared/log-in/log-in.component.html
new file mode 100644
index 0000000000..fe9a506e71
--- /dev/null
+++ b/src/app/shared/log-in/log-in.component.html
@@ -0,0 +1,28 @@
+
+
+
+
diff --git a/src/app/shared/log-in/log-in.component.scss b/src/app/shared/log-in/log-in.component.scss
new file mode 100644
index 0000000000..5e4393edaf
--- /dev/null
+++ b/src/app/shared/log-in/log-in.component.scss
@@ -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;
+}
+
diff --git a/src/app/shared/log-in/log-in.component.spec.ts b/src/app/shared/log-in/log-in.component.spec.ts
new file mode 100644
index 0000000000..dc4a0be1c6
--- /dev/null
+++ b/src/app/shared/log-in/log-in.component.spec.ts
@@ -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;
+ 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) => {
+ 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) {
+ // 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;
+ }
+}
diff --git a/src/app/shared/log-in/log-in.component.ts b/src/app/shared/log-in/log-in.component.ts
new file mode 100644
index 0000000000..3364b1067d
--- /dev/null
+++ b/src/app/shared/log-in/log-in.component.ts
@@ -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}
+ */
+ public error: Observable;
+
+ /**
+ * Has authentication error.
+ * @type {boolean}
+ */
+ public hasError = false;
+
+ /**
+ * The authentication info message.
+ * @type {Observable}
+ */
+ public message: Observable;
+
+ /**
+ * Has authentication message.
+ * @type {boolean}
+ */
+ public hasMessage = false;
+
+ /**
+ * Whether user is authenticated.
+ * @type {Observable}
+ */
+ public isAuthenticated: Observable;
+
+ /**
+ * True if the authentication is loading.
+ * @type {boolean}
+ */
+ public loading: Observable;
+
+ /**
+ * The authentication form.
+ * @type {FormGroup}
+ */
+ public form: FormGroup;
+
+ /**
+ * Component state.
+ * @type {boolean}
+ */
+ private alive = true;
+
+ /**
+ * @constructor
+ * @param {AuthService} authService
+ * @param {FormBuilder} formBuilder
+ * @param {Store} store
+ */
+ constructor(
+ private authService: AuthService,
+ private formBuilder: FormBuilder,
+ private store: Store
+ ) {
+ }
+
+ /**
+ * 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();
+ }
+}
diff --git a/src/app/shared/log-out/log-out.component.html b/src/app/shared/log-out/log-out.component.html
new file mode 100644
index 0000000000..f3ceae0087
--- /dev/null
+++ b/src/app/shared/log-out/log-out.component.html
@@ -0,0 +1,7 @@
+
+
diff --git a/src/app/shared/log-out/log-out.component.scss b/src/app/shared/log-out/log-out.component.scss
new file mode 100644
index 0000000000..dcd67e092f
--- /dev/null
+++ b/src/app/shared/log-out/log-out.component.scss
@@ -0,0 +1 @@
+@import '../log-in/log-in.component.scss';
diff --git a/src/app/shared/log-out/log-out.component.spec.ts b/src/app/shared/log-out/log-out.component.spec.ts
new file mode 100644
index 0000000000..ad609f0aea
--- /dev/null
+++ b/src/app/shared/log-out/log-out.component.spec.ts
@@ -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;
+ 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) => {
+ 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) {
+ // use injector to get services
+ const injector = fixture.debugElement.injector;
+ const store = injector.get(Store);
+
+ // add spies
+ this.navigateSpy = spyOn(store, 'dispatch');
+ }
+
+}
diff --git a/src/app/shared/log-out/log-out.component.ts b/src/app/shared/log-out/log-out.component.ts
new file mode 100644
index 0000000000..37d0b142f9
--- /dev/null
+++ b/src/app/shared/log-out/log-out.component.ts
@@ -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}
+ */
+ public error: Observable;
+
+ /**
+ * True if the logout is loading.
+ * @type {boolean}
+ */
+ public loading: Observable;
+
+ /**
+ * Component state.
+ * @type {boolean}
+ */
+ private alive = true;
+
+ /**
+ * @constructor
+ * @param {Store} store
+ */
+ constructor(private router: Router,
+ private store: Store) { }
+
+ /**
+ * 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());
+ }
+
+}
diff --git a/src/app/shared/mocks/mock-auth.service.ts b/src/app/shared/mocks/mock-auth.service.ts
new file mode 100644
index 0000000000..6258e4aa21
--- /dev/null
+++ b/src/app/shared/mocks/mock-auth.service.ts
@@ -0,0 +1,6 @@
+/* tslint:disable:no-empty */
+export class AuthServiceMock {
+ public checksAuthenticationToken() {
+ return
+ }
+}
diff --git a/src/app/shared/mocks/mock-remote-data-build.service.ts b/src/app/shared/mocks/mock-remote-data-build.service.ts
new file mode 100644
index 0000000000..c10032eb94
--- /dev/null
+++ b/src/app/shared/mocks/mock-remote-data-build.service.ts
@@ -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>): RemoteDataBuildService {
+ return {
+ toRemoteDataObservable: (requestEntry$: Observable, responseCache$: Observable, payload$: Observable) => {
+
+ if (hasValue(toRemoteDataObservable$)) {
+ return toRemoteDataObservable$;
+ } else {
+ return payload$.pipe(map((payload) => ({
+ payload
+ } as RemoteData)))
+ }
+ }
+ } as RemoteDataBuildService;
+
+}
diff --git a/src/app/shared/mocks/mock-request.service.ts b/src/app/shared/mocks/mock-request.service.ts
index 02d3e54282..b4d8c693e5 100644
--- a/src/app/shared/mocks/mock-request.service.ts
+++ b/src/app/shared/mocks/mock-request.service.ts
@@ -1,10 +1,11 @@
+import { Observable } from 'rxjs/Observable';
import { RequestService } from '../../core/data/request.service';
import { RequestEntry } from '../../core/data/request.reducer';
-export function getMockRequestService(): RequestService {
+export function getMockRequestService(getByHref$: Observable = Observable.of(new RequestEntry())): RequestService {
return jasmine.createSpyObj('requestService', {
- configure: () => false,
- generateRequestId: () => 'clients/b186e8ce-e99c-4183-bc9a-42b4821bdb78',
- getByHref: (uuid: string) => new RequestEntry()
+ configure: false,
+ generateRequestId: 'clients/b186e8ce-e99c-4183-bc9a-42b4821bdb78',
+ getByHref: getByHref$
});
}
diff --git a/src/app/shared/mocks/mock-response-cache.service.ts b/src/app/shared/mocks/mock-response-cache.service.ts
index ad1457c3eb..2c21b07777 100644
--- a/src/app/shared/mocks/mock-response-cache.service.ts
+++ b/src/app/shared/mocks/mock-response-cache.service.ts
@@ -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 { RestResponse } from '../../core/cache/response-cache.models';
+import { ResponseCacheService } from '../../core/cache/response-cache.service';
-export function getMockResponseCacheService(): ResponseCacheService {
+export function getMockResponseCacheService(
+ add$: Observable = Observable.of(new ResponseCacheEntry()),
+ get$: Observable = Observable.of(new ResponseCacheEntry()),
+ has: boolean = false
+): ResponseCacheService {
return jasmine.createSpyObj('ResponseCacheService', {
- add: (key: string, response: RestResponse, msToLive: number) => new ResponseCacheEntry(),
- get: (key: string) => new ResponseCacheEntry(),
- has: (key: string) => false,
+ add: add$,
+ get: get$,
+ has,
});
}
diff --git a/src/app/shared/api.service.ts b/src/app/shared/services/api.service.ts
similarity index 100%
rename from src/app/shared/api.service.ts
rename to src/app/shared/services/api.service.ts
diff --git a/src/app/shared/services/client-cookie.service.ts b/src/app/shared/services/client-cookie.service.ts
new file mode 100644
index 0000000000..4aa670ca78
--- /dev/null
+++ b/src/app/shared/services/client-cookie.service.ts
@@ -0,0 +1,25 @@
+import { Injectable } from '@angular/core'
+import { CookieAttributes, getJSON, remove, set } from 'js-cookie'
+import { CookieService, ICookieService } from './cookie.service';
+
+@Injectable()
+export class ClientCookieService extends CookieService implements ICookieService {
+
+ public set(name: string, value: any, options?: CookieAttributes): void {
+ set(name, value, options);
+ this.updateSource()
+ }
+
+ public remove(name: string, options?: CookieAttributes): void {
+ remove(name, options);
+ this.updateSource()
+ }
+
+ public get(name: string): any {
+ return getJSON(name)
+ }
+
+ public getAll(): any {
+ return getJSON()
+ }
+}
diff --git a/src/app/shared/services/cookie.service.spec.ts b/src/app/shared/services/cookie.service.spec.ts
new file mode 100644
index 0000000000..dddb8f095f
--- /dev/null
+++ b/src/app/shared/services/cookie.service.spec.ts
@@ -0,0 +1,28 @@
+import { CookieService, ICookieService } from './cookie.service'
+import { async, TestBed } from '@angular/core/testing'
+import { REQUEST } from '@nguniversal/express-engine/tokens'
+
+describe(CookieService.name, () => {
+ let service: ICookieService;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ providers: [
+ CookieService,
+ {provide: REQUEST, useValue: {}}
+ ]
+ })
+ }));
+
+ beforeEach(() => {
+ service = TestBed.get(CookieService)
+ });
+
+ afterEach(() => {
+ TestBed.resetTestingModule()
+ });
+
+ it('should construct', async(() => {
+ expect(service).toBeDefined()
+ }))
+});
diff --git a/src/app/shared/services/cookie.service.ts b/src/app/shared/services/cookie.service.ts
new file mode 100644
index 0000000000..8265651d81
--- /dev/null
+++ b/src/app/shared/services/cookie.service.ts
@@ -0,0 +1,40 @@
+import { Inject, Injectable } from '@angular/core'
+
+import { REQUEST } from '@nguniversal/express-engine/tokens'
+
+import { Subject } from 'rxjs/Subject'
+import { Observable } from 'rxjs/Observable'
+import { CookieAttributes } from 'js-cookie'
+
+export interface ICookieService {
+ readonly cookies$: Observable<{ readonly [key: string]: any }>
+
+ getAll(): any
+
+ get(name: string): any
+
+ set(name: string, value: any, options?: CookieAttributes): void
+
+ remove(name: string, options?: CookieAttributes): void
+}
+
+@Injectable()
+export abstract class CookieService implements ICookieService {
+ protected readonly cookieSource = new Subject<{ readonly [key: string]: any }>();
+ public readonly cookies$ = this.cookieSource.asObservable();
+
+ constructor(@Inject(REQUEST) protected req: any) {
+ }
+
+ public abstract set(name: string, value: any, options?: CookieAttributes): void
+
+ public abstract remove(name: string, options?: CookieAttributes): void
+
+ public abstract get(name: string): any
+
+ public abstract getAll(): any
+
+ protected updateSource() {
+ this.cookieSource.next(this.getAll());
+ }
+}
diff --git a/src/app/shared/route.service.spec.ts b/src/app/shared/services/route.service.spec.ts
similarity index 100%
rename from src/app/shared/route.service.spec.ts
rename to src/app/shared/services/route.service.spec.ts
diff --git a/src/app/shared/route.service.ts b/src/app/shared/services/route.service.ts
similarity index 96%
rename from src/app/shared/route.service.ts
rename to src/app/shared/services/route.service.ts
index 9c2b64ede1..aa683a6403 100644
--- a/src/app/shared/route.service.ts
+++ b/src/app/shared/services/route.service.ts
@@ -4,7 +4,7 @@ import {
ActivatedRoute, convertToParamMap, NavigationExtras, Params,
Router,
} from '@angular/router';
-import { isNotEmpty } from './empty.util';
+import { isNotEmpty } from '../empty.util';
@Injectable()
export class RouteService {
diff --git a/src/app/shared/services/server-cookie.service.ts b/src/app/shared/services/server-cookie.service.ts
new file mode 100644
index 0000000000..49cc738346
--- /dev/null
+++ b/src/app/shared/services/server-cookie.service.ts
@@ -0,0 +1,29 @@
+import { Injectable } from '@angular/core'
+import { CookieAttributes } from 'js-cookie'
+import { CookieService, ICookieService } from './cookie.service';
+
+@Injectable()
+export class ServerCookieService extends CookieService implements ICookieService {
+
+ public set(name: string, value: any, options?: CookieAttributes): void {
+ return
+ }
+
+ public remove(name: string, options?: CookieAttributes): void {
+ return
+ }
+
+ public get(name: string): any {
+ try {
+ return JSON.parse(this.req.cookies[name])
+ } catch (err) {
+ return this.req ? this.req.cookies[name] : undefined
+ }
+ }
+
+ public getAll(): any {
+ if (this.req) {
+ return this.req.cookies
+ }
+ }
+}
diff --git a/src/app/shared/server-response.service.ts b/src/app/shared/services/server-response.service.ts
similarity index 100%
rename from src/app/shared/server-response.service.ts
rename to src/app/shared/services/server-response.service.ts
diff --git a/src/app/shared/window.service.ts b/src/app/shared/services/window.service.ts
similarity index 100%
rename from src/app/shared/window.service.ts
rename to src/app/shared/services/window.service.ts
diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts
index 79ddd8680f..8e3d2149d9 100644
--- a/src/app/shared/shared.module.ts
+++ b/src/app/shared/shared.module.ts
@@ -40,6 +40,9 @@ import { SearchResultGridElementComponent } from './object-grid/search-result-gr
import { ViewModeSwitchComponent } from './view-mode-switch/view-mode-switch.component';
import { GridThumbnailComponent } from './object-grid/grid-thumbnail/grid-thumbnail.component';
import { VarDirective } from './utils/var.directive';
+import { LogInComponent } from './log-in/log-in.component';
+import { AuthNavMenuComponent } from './auth-nav-menu/auth-nav-menu.component';
+import { LogOutComponent } from './log-out/log-out.component';
import { NotificationComponent } from './notifications/notification/notification.component';
import { NotificationsBoardComponent } from './notifications/notifications-board/notifications-board.component';
import { DragClickDirective } from './utils/drag-click.directive';
@@ -70,11 +73,14 @@ const PIPES = [
const COMPONENTS = [
// put shared components here
+ AuthNavMenuComponent,
ComcolPageContentComponent,
ComcolPageHeaderComponent,
ComcolPageLogoComponent,
ErrorComponent,
LoadingComponent,
+ LogInComponent,
+ LogOutComponent,
ObjectListComponent,
AbstractListableElementComponent,
WrapperListElementComponent,
diff --git a/src/app/shared/testing/auth-request-service-stub.ts b/src/app/shared/testing/auth-request-service-stub.ts
new file mode 100644
index 0000000000..2c47068af4
--- /dev/null
+++ b/src/app/shared/testing/auth-request-service-stub.ts
@@ -0,0 +1,69 @@
+import { Observable } from 'rxjs/Observable';
+import { HttpOptions } from '../../core/dspace-rest-v2/dspace-rest-v2.service';
+import { AuthStatus } from '../../core/auth/models/auth-status.model';
+import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model';
+import { Eperson } from '../../core/eperson/models/eperson.model';
+import { isNotEmpty } from '../empty.util';
+import { EpersonMock } from './eperson-mock';
+
+export class AuthRequestServiceStub {
+ protected mockUser: Eperson = EpersonMock;
+ protected mockTokenInfo = new AuthTokenInfo('test_token');
+
+ public postToEndpoint(method: string, body: any, options?: HttpOptions): Observable {
+ const authStatusStub: AuthStatus = new AuthStatus();
+ if (isNotEmpty(body)) {
+ const parsedBody = this.parseQueryString(body);
+ authStatusStub.okay = true;
+ if (parsedBody.user === 'user' && parsedBody.password === 'password') {
+ authStatusStub.authenticated = true;
+ authStatusStub.token = this.mockTokenInfo;
+ } else {
+ authStatusStub.authenticated = false;
+ }
+ } else {
+ const token = (options.headers as any).lazyUpdate[1].value;
+ if (this.validateToken(token)) {
+ authStatusStub.authenticated = true;
+ authStatusStub.token = this.mockTokenInfo;
+ authStatusStub.eperson = [this.mockUser];
+ } else {
+ authStatusStub.authenticated = false;
+ }
+ }
+ return Observable.of(authStatusStub);
+ }
+
+ public getRequest(method: string, options?: HttpOptions): Observable {
+ const authStatusStub: AuthStatus = new AuthStatus();
+ switch (method) {
+ case 'logout':
+ authStatusStub.authenticated = false;
+ break;
+ case 'status':
+ const token = (options.headers as any).lazyUpdate[1].value;
+ if (this.validateToken(token)) {
+ authStatusStub.authenticated = true;
+ authStatusStub.token = this.mockTokenInfo;
+ authStatusStub.eperson = [this.mockUser];
+ } else {
+ authStatusStub.authenticated = false;
+ }
+ break;
+ }
+ return Observable.of(authStatusStub);
+ }
+
+ private validateToken(token): boolean {
+ return (token === 'Bearer test_token');
+ }
+ private parseQueryString(query): any {
+ const obj = Object.create({});
+ const vars = query.split('&');
+ for (const param of vars) {
+ const pair = param.split('=');
+ obj[pair[0]] = pair[1]
+ }
+ return obj;
+ }
+}
diff --git a/src/app/shared/testing/auth-service-stub.ts b/src/app/shared/testing/auth-service-stub.ts
new file mode 100644
index 0000000000..c7d5556910
--- /dev/null
+++ b/src/app/shared/testing/auth-service-stub.ts
@@ -0,0 +1,95 @@
+import { AuthStatus } from '../../core/auth/models/auth-status.model';
+import { Observable } from 'rxjs/Observable';
+import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model';
+import { EpersonMock } from './eperson-mock';
+import { Eperson } from '../../core/eperson/models/eperson.model';
+
+export class AuthServiceStub {
+
+ token: AuthTokenInfo = new AuthTokenInfo('token_test');
+ private _tokenExpired = false;
+
+ constructor() {
+ this.token.expires = Date.now() + (1000 * 60 * 60);
+ }
+
+ public authenticate(user: string, password: string): Observable {
+ if (user === 'user' && password === 'password') {
+ const authStatus = new AuthStatus();
+ authStatus.okay = true;
+ authStatus.authenticated = true;
+ authStatus.token = this.token;
+ authStatus.eperson = [EpersonMock];
+ return Observable.of(authStatus);
+ } else {
+ console.log('error');
+ throw(new Error('Message Error test'));
+ }
+ }
+
+ public authenticatedUser(token: AuthTokenInfo): Observable {
+ if (token.accessToken === 'token_test') {
+ return Observable.of(EpersonMock);
+ } else {
+ throw(new Error('Message Error test'));
+ }
+ }
+
+ public buildAuthHeader(token?: AuthTokenInfo): string {
+ return `Bearer ${token.accessToken}`;
+ }
+
+ public getToken(): AuthTokenInfo {
+ return this.token;
+ }
+
+ public hasValidAuthenticationToken(): Observable {
+ return Observable.of(this.token);
+ }
+
+ public logout(): Observable {
+ return Observable.of(true);
+ }
+
+ public isTokenExpired(token?: AuthTokenInfo): boolean {
+ return this._tokenExpired;
+ }
+
+ /**
+ * This method is used to ease testing
+ */
+ public setTokenAsExpired() {
+ this._tokenExpired = true
+ }
+
+ /**
+ * This method is used to ease testing
+ */
+ public setTokenAsNotExpired() {
+ this._tokenExpired = false
+ }
+
+ public isTokenExpiring(): Observable {
+ return Observable.of(false);
+ }
+
+ public refreshAuthenticationToken(token: AuthTokenInfo): Observable {
+ return Observable.of(this.token);
+ }
+
+ public redirectToPreviousUrl() {
+ return;
+ }
+
+ public removeToken() {
+ return;
+ }
+
+ setRedirectUrl(url: string) {
+ return;
+ }
+
+ public storeToken(token: AuthTokenInfo) {
+ return;
+ }
+}
diff --git a/src/app/shared/testing/eperson-mock.ts b/src/app/shared/testing/eperson-mock.ts
new file mode 100644
index 0000000000..9cf938fcf2
--- /dev/null
+++ b/src/app/shared/testing/eperson-mock.ts
@@ -0,0 +1,34 @@
+import { Eperson } from '../../core/eperson/models/eperson.model';
+
+export const EpersonMock: Eperson = Object.assign(new Eperson(),{
+ handle: null,
+ groups: [],
+ netid: 'test@test.com',
+ lastActive: '2018-05-14T12:25:42.411+0000',
+ canLogIn: true,
+ email: 'test@test.com',
+ requireCertificate: false,
+ selfRegistered: false,
+ self: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/epersons/testid',
+ id: 'testid',
+ uuid: 'testid',
+ type: 'eperson',
+ name: 'User Test',
+ metadata: [
+ {
+ key: 'eperson.firstname',
+ language: null,
+ value: 'User'
+ },
+ {
+ key: 'eperson.lastname',
+ language: null,
+ value: 'Test'
+ },
+ {
+ key: 'eperson.language',
+ language: null,
+ value: 'en'
+ }
+ ]
+});
diff --git a/src/app/shared/testing/host-window-service-stub.ts b/src/app/shared/testing/host-window-service-stub.ts
index 98af7fda18..2833415477 100644
--- a/src/app/shared/testing/host-window-service-stub.ts
+++ b/src/app/shared/testing/host-window-service-stub.ts
@@ -16,4 +16,8 @@ export class HostWindowServiceStub {
isXs(): Observable {
return Observable.of(this.width < 576);
}
+
+ isXsOrSm(): Observable {
+ return this.isXs();
+ }
}
diff --git a/src/app/shared/testing/platform-service-stub.ts b/src/app/shared/testing/platform-service-stub.ts
new file mode 100644
index 0000000000..39b9b5f4d3
--- /dev/null
+++ b/src/app/shared/testing/platform-service-stub.ts
@@ -0,0 +1,12 @@
+
+// declare a stub service
+export class PlatformServiceStub {
+
+ public get isBrowser(): boolean {
+ return true;
+ }
+
+ public get isServer(): boolean {
+ return false;
+ }
+}
diff --git a/src/app/shared/utils/encode-decode.util.spec.ts b/src/app/shared/utils/encode-decode.util.spec.ts
new file mode 100644
index 0000000000..c3039c482e
--- /dev/null
+++ b/src/app/shared/utils/encode-decode.util.spec.ts
@@ -0,0 +1,10 @@
+import { Base64EncodeUrl } from './encode-decode.util';
+
+describe('Encode/Decode Utils', () => {
+ const strng = '+string+/=t-';
+ const encodedStrng = '%2Bstring%2B%2F%3Dt-';
+
+ it('should return encoded string', () => {
+ expect(Base64EncodeUrl(strng)).toBe(encodedStrng);
+ });
+});
diff --git a/src/app/shared/utils/encode-decode.util.ts b/src/app/shared/utils/encode-decode.util.ts
new file mode 100644
index 0000000000..e21034b7bd
--- /dev/null
+++ b/src/app/shared/utils/encode-decode.util.ts
@@ -0,0 +1,10 @@
+/**
+ * use this to make a Base64 encoded string URL friendly,
+ * i.e. '+' and '/' are replaced with special percent-encoded hexadecimal sequences
+ *
+ * @param {String} str the encoded string
+ * @returns {String} the URL friendly encoded String
+ */
+export function Base64EncodeUrl(str): string {
+ return str.replace(/\+/g, '%2B').replace(/\//g, '%2F').replace(/\=/g, '%3D');
+}
diff --git a/src/modules/app/browser-app.module.ts b/src/modules/app/browser-app.module.ts
index 1112ebdb23..a7a59dc837 100644
--- a/src/modules/app/browser-app.module.ts
+++ b/src/modules/app/browser-app.module.ts
@@ -1,8 +1,9 @@
import { HttpClient, HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
-import { BrowserModule } from '@angular/platform-browser';
+import { BrowserModule, makeStateKey, TransferState } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule } from '@angular/router';
+import { REQUEST } from '@nguniversal/express-engine/tokens';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
@@ -14,13 +15,22 @@ import { AppComponent } from '../../app/app.component';
import { AppModule } from '../../app/app.module';
import { DSpaceBrowserTransferStateModule } from '../transfer-state/dspace-browser-transfer-state.module';
import { DSpaceTransferState } from '../transfer-state/dspace-transfer-state.service';
+import { ClientCookieService } from '../../app/shared/services/client-cookie.service';
+import { CookieService } from '../../app/shared/services/cookie.service';
+import { AuthService } from '../../app/core/auth/auth.service';
import { Angulartics2Module } from 'angulartics2';
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
+export const REQ_KEY = makeStateKey('req');
+
export function createTranslateLoader(http: HttpClient) {
return new TranslateHttpLoader(http, 'assets/i18n/', '.json');
}
+export function getRequest(transferState: TransferState): any {
+ return transferState.get(REQ_KEY, {})
+}
+
@NgModule({
bootstrap: [AppComponent],
imports: [
@@ -48,6 +58,21 @@ export function createTranslateLoader(http: HttpClient) {
}),
AppModule
],
+ providers: [
+ {
+ provide: REQUEST,
+ useFactory: getRequest,
+ deps: [TransferState]
+ },
+ {
+ provide: AuthService,
+ useClass: AuthService
+ },
+ {
+ provide: CookieService,
+ useClass: ClientCookieService
+ }
+ ]
})
export class BrowserAppModule {
constructor(
diff --git a/src/modules/app/server-app.module.ts b/src/modules/app/server-app.module.ts
index fac1b63ada..10285e75f5 100644
--- a/src/modules/app/server-app.module.ts
+++ b/src/modules/app/server-app.module.ts
@@ -15,6 +15,10 @@ import { DSpaceServerTransferStateModule } from '../transfer-state/dspace-server
import { DSpaceTransferState } from '../transfer-state/dspace-transfer-state.service';
import { TranslateUniversalLoader } from '../translate-universal-loader';
+import { CookieService } from '../../app/shared/services/cookie.service';
+import { ServerCookieService } from '../../app/shared/services/server-cookie.service';
+import { AuthService } from '../../app/core/auth/auth.service';
+import { ServerAuthService } from '../../app/core/auth/server-auth.service';
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
import { AngularticsMock } from '../../app/shared/mocks/mock-angulartics.service';
@@ -45,7 +49,15 @@ export function createTranslateLoader() {
AppModule
],
providers: [
- { provide: Angulartics2GoogleAnalytics, useClass: AngularticsMock }
+ { provide: Angulartics2GoogleAnalytics, useClass: AngularticsMock },
+ {
+ provide: AuthService,
+ useClass: ServerAuthService
+ },
+ {
+ provide: CookieService,
+ useClass: ServerCookieService
+ }
]
})
export class ServerAppModule {
diff --git a/src/routes.ts b/src/routes.ts
index f051b16198..392d3925a5 100644
--- a/src/routes.ts
+++ b/src/routes.ts
@@ -3,6 +3,8 @@ export const ROUTES: string[] = [
'items/:id',
'collections/:id',
'communities/:id',
+ 'login',
+ 'logout',
'search',
'**'
];
diff --git a/src/styles/_custom_variables.scss b/src/styles/_custom_variables.scss
index 4dcc161cc4..d04e9e8c83 100644
--- a/src/styles/_custom_variables.scss
+++ b/src/styles/_custom_variables.scss
@@ -3,4 +3,6 @@ $content-spacing: $spacer * 1.5;
$button-height: $input-btn-padding-y * 2 + $input-btn-line-height + calculateRem($input-btn-border-width*2);
$card-height-percentage:98%;
$card-thumbnail-height:240px;
+$login-logo-height:72px;
+$login-logo-width:72px;
diff --git a/tsconfig.json b/tsconfig.json
index 8ab72a4327..8037c659ed 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -26,8 +26,7 @@
"es6",
"es2015",
"es2016",
- "es2017",
- "es2017.object"
+ "es2017"
]
},
"exclude": [
diff --git a/yarn.lock b/yarn.lock
index bb571e8d6c..28091288cb 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -203,6 +203,10 @@
version "2.8.6"
resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-2.8.6.tgz#14445b6a1613cf4e05dd61c3c3256d0e95c0421e"
+"@types/js-cookie@2.1.0":
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.1.0.tgz#a8916246aa994db646c66d54c854916213300a51"
+
"@types/lodash@4.14.74":
version "4.14.74"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.74.tgz#ac3bd8db988e7f7038e5d22bd76a7ba13f876168"
@@ -4476,6 +4480,10 @@ js-base64@^2.1.8, js-base64@^2.1.9:
version "2.3.2"
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.3.2.tgz#a79a923666372b580f8e27f51845c6f7e8fbfbaf"
+js-cookie@2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.0.tgz#1b2c279a6eece380a12168b92485265b35b1effb"
+
js-tokens@^3.0.0, js-tokens@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
@@ -4597,6 +4605,10 @@ jszip@^3.1.3:
pako "~1.0.2"
readable-stream "~2.0.6"
+jwt-decode@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-2.2.0.tgz#7d86bd56679f58ce6a84704a657dd392bba81a79"
+
karma-chrome-launcher@2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-2.2.0.tgz#cf1b9d07136cc18fe239327d24654c3dbc368acf"